Framework-agnostic DTOs with hydration, validation, and serialization for PHP 8.2+.
Spatie's laravel-data is excellent but tightly coupled to Laravel. datapack gives you the same power — hydration from arrays/JSON, attribute-based transformers, typed collections, validation adapters — without any framework dependency. It works with Laravel, Symfony, Slim, or vanilla PHP.
composer require kexxt/datapackRequires PHP 8.2+.
use Kexxt\Datapack\Data;
use Kexxt\Datapack\Attributes\CastToDate;
use Kexxt\Datapack\Attributes\Trim;
class UserData extends Data
{
public function __construct(
#[Trim]
public readonly string $name,
#[CastToDate]
public readonly DateTimeImmutable $birthDate,
public readonly Role $role, // backed enum — auto-cast
public readonly AddressData $address, // nested DTO — auto-hydrated
) {}
}
// Hydrate from array
$user = UserData::from([
'name' => ' Alice ',
'birthDate' => '1990-01-15',
'role' => 'admin',
'address' => ['street' => '123 Main', 'city' => 'Springfield', 'zip' => '62701'],
]);
$user->name; // "Alice" (trimmed)
$user->role; // Role::Admin (enum)
$user->toArray(); // serialized array
$user->toJson(); // JSON stringCreate DTOs from arrays, JSON strings, stdClass objects, or anything with a toArray() method:
$user = UserData::from(['name' => 'Alice', 'age' => 30]);
$user = UserData::from('{"name":"Alice","age":30}');
$user = UserData::from($stdClassObj);Features:
- Nested DTOs are hydrated automatically
- Backed enums are cast automatically
- Nullable properties and defaults are handled
- Type coercion for scalars (string "30" → int 30)
| Attribute | Target | Description |
|---|---|---|
#[CastToDate] |
Property | Cast string to DateTimeImmutable |
#[CastWith(CasterClass)] |
Property | Use a custom caster |
#[Trim] |
Property | Trim whitespace |
#[Uppercase] |
Property | Convert to uppercase |
#[Lowercase] |
Property | Convert to lowercase |
#[Hidden] |
Property | Exclude from serialization |
#[Computed] |
Property | Exclude from hydration |
#[MapFrom('key')] |
Property | Map from different input key |
#[MapTo('key')] |
Property | Map to different output key |
Implement CastInterface:
use Kexxt\Datapack\Contracts\CastInterface;
class MoneyCast implements CastInterface
{
public function cast(mixed $value, string $propertyName, array $context = []): Money
{
return new Money((int) ($value * 100));
}
}
class OrderData extends Data
{
public function __construct(
#[CastWith(MoneyCast::class)]
public readonly Money $total,
) {}
}$user->toArray(); // associative array
$user->toJson(); // JSON string
json_encode($user); // implements JsonSerializable#[Hidden]fields are excluded#[MapTo]renames keys in output- Nested DTOs, enums, and DateTimes are serialized recursively
$updated = $user->with('name', 'Bob');
// $user->name is still "Alice"
// $updated->name is "Bob"$users = UserData::collection([
['name' => 'Alice', 'age' => 30],
['name' => 'Bob', 'age' => 25],
]);
$users->count(); // 2
$users->first(); // UserData instance
$users->toArray(); // list of arrays
$users->toJson(); // JSON array
// Filter and map
$seniors = $users->filter(fn(UserData $u) => $u->age >= 30);
$names = $users->map(fn(UserData $u) => $u->name); // ['Alice', 'Bob']
// Iterable
foreach ($users as $user) { ... }Bring your own validator by implementing ValidatorInterface:
use Kexxt\Datapack\Contracts\ValidatorInterface;
use Kexxt\Datapack\Data;
class MyValidator implements ValidatorInterface
{
public function validate(Data $data): array
{
$errors = [];
// your validation logic
return $errors; // ['field' => ['error message']]
}
}
$errors = $user->validate(new MyValidator());
$user->validateOrFail(new MyValidator()); // throws ValidationExceptioncomposer test # Run tests
composer analyse # Run PHPStan (level 8)
composer lint # Check code style
composer fix # Fix code style- Fork the repository
- Create a feature branch
- Write tests for your changes
- Ensure
composer test && composer analyse && composer lintall pass - Submit a pull request
MIT — see LICENSE.