zero-to-prod/data-model
Type-safe PHP data models that hydrate from arrays/JSON via a single from($data) call. Uses reflection, type hints, and #[Describe] attributes for defaults, required/nullable rules, casting, and assignment—ideal for APIs, DB rows, and user input.
Installation:
composer require zero-to-prod/data-model
Basic Usage:
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 in API responses or form submissions:
$apiResponse = json_decode($request->getContent(), true);
$user = User::from($apiResponse['user']);
DataModel trait: Core functionality for type-safe hydration.#[Describe] attribute: Centralized property configuration (validation, casting, defaults).from() method: Static entry point for hydration.API Response Handling:
$dto = Order::from($request->json()->all());
return new JsonResponse($dto);
Form Request Validation:
$data = $request->validate([
'name' => 'required|string',
'age' => 'required|integer'
]);
$user = User::from($data);
Nested Object Hydration:
class Order {
use DataModel;
public string $id;
public User $customer;
}
Laravel Eloquent:
$model = User::from($this->validated());
$model->save();
Collection Mapping:
$users = collect($apiResponse['users'])
->map(fn($user) => User::from($user))
->values();
Custom Casting:
#[Describe(['cast' => [User::class, 'formatDate']])]
public string $created_at;
public static function formatDate(string $date): string {
return (new DateTime($date))->format('Y-m-d');
}
Union Types:
#[Describe(['cast' => fn($v) => is_string($v) ? $v : (int)$v])]
public string|int $variant;
Recursive Arrays:
class Product {
use DataModel;
#[Describe(['via' => [Product::class, 'fromArray']])]
public array $items;
}
Missing required Properties:
// Throws PropertyRequiredException
User::from(['name' => 'Alice']); // Missing 'age'
Fix: Add #[Describe(['required' => false])] or provide defaults.
Circular References:
class A { use DataModel; public B $b; }
class B { use DataModel; public A $a; }
Fix: Use #[Describe(['via' => fn($data) => new B()])] with manual assignment.
Attribute Overrides:
// Only the last attribute applies
#[Describe(['cast' => 'strtoupper'])]
#[Describe(['cast' => 'strtolower'])]
public string $name;
Inspect Resolution Order:
dd(DataModel::resolveProperty($user, 'name', ['name' => 'Alice']));
Custom Exceptions:
try {
$user = User::from([]);
} catch (PropertyRequiredException $e) {
report($e);
}
Callable Signatures:
pre/post hooks receive $value even if null (check for null explicitly).cast must return a value matching the property type.Default Values:
// Overrides missing keys
#[Describe(['default' => fn() => now()->format('Y-m-d')])]
public string $date;
Performance:
cast logic in tight loops (cache results if needed).#[Describe(['ignore' => true])] for properties you won’t hydrate.Custom Attributes:
#[Describe(['my_key' => 'value'])]
public string $name;
Access via $attribute->getArguments()['my_key'].
Global Defaults:
class BaseModel {
use DataModel;
#[Describe(['default' => null])]
public ?string $optional;
}
Laravel Integration:
// Override from() for model binding
public static function from(array $data): static {
return parent::from($data)->setAttribute('guard', 'api');
}
Form Requests:
public function rules() {
return [
'name' => 'required|string',
'age' => 'required|integer|min:18'
];
}
public function prepareForValidation() {
$this->merge([
'age' => $this->age ?? 0
]);
}
public function validated() {
return User::from(parent::validated());
}
API Resources:
public function toArray($request) {
return User::from($this->resource)->toArray();
}
How can I help you explore Laravel packages today?