zero-to-prod/data-model
Reflection-based PHP data models that hydrate typed objects from arrays with a single from($data) call. Use #[Describe] attributes to define casting, validation, defaults, nullable/required rules, and assignments—keeping mapping logic predictable, readable, and verifiable.
Installation:
composer require zero-to-prod/data-model
Basic Usage:
Add the DataModel trait to any class and define properties with type hints.
use Zerotoprod\DataModel\DataModel;
class User {
use DataModel;
public string $name;
public int $age;
}
$user = User::from(['name' => 'Alice', 'age' => 30]);
First Use Case: Replace manual array-to-object mapping with type-safe hydration:
$user = User::from($request->all()); // Automatically casts and validates
Describe Attribute: Core configuration for property hydration.from() Method: Static factory for instantiation.class User {
use DataModel;
#[Describe(['required' => true])]
public string $name;
#[Describe(['default' => 18])]
public int $age;
}
$user = User::from($apiResponse);
class Profile {
use DataModel;
public User $user;
}
$profile = Profile::from($data); // Automatically hydrates nested `User`
$user = User::from($request->validated());
$user = User::from(json_decode($response, true));
ValidatesWhen or custom pre/post hooks.#[Describe(['default' => fn() => now()])]
public Carbon $createdAt;
#[Describe(['cast' => [User::class, 'hashPassword']])]
public string $password;
class Team {
use DataModel;
public array $members; // Automatically hydrates array of `User`
}
Circular References:
Avoid bidirectional relationships (e.g., User->Team and Team->User) without custom via logic.
#[Describe(['via' => [Team::class, 'fromArray']])]
public Team $team;
Type Mismatches:
Ensure cast methods return the exact property type (e.g., string for public string $name).
Attribute Conflicts:
Duplicate #[Describe] on the same property throws DuplicateDescribeAttributeException.
Describe::$extra to log debug info:
#[Describe(['my_key' => 'debug'])]
public string $name;
null:
Test edge cases by passing null or empty arrays to from().Custom Attributes:
Subclass Describe to add domain-specific keys:
#[Attribute]
class CustomDescribe extends Describe {
public function __construct(array $options = []) {
parent::__construct($options + ['custom_key' => 'value']);
}
}
Override from():
Extend hydration logic for complex cases:
class User extends BaseUser {
public static function from(mixed $context): static {
$user = parent::from($context);
$user->normalize();
return $user;
}
}
Laravel Integration:
Use DataModelFactory to integrate with Eloquent:
$user = (new DataModelFactory(User::class))->from($request->all());
$value, $context, $Attribute, $Property) for full control.from() (treated as empty context) for default-value testing:
$user = User::from(''); // Uses `default` values
ReflectionProperty instances if hydrating the same class repeatedly.DataModelHelper for collections:
$users = DataModelHelper::mapOf(User::class, $apiData['users']);
How can I help you explore Laravel packages today?