hydrastat / dto-hydrator
Framework-agnostic DTO hydrator with Laravel, CodeIgniter, Symfony, Yii 2, and CakePHP integrations
Requires
- php: ^8.2
- psr/container: ^2.0
- psr/http-message: ^2.0
Requires (Dev)
- cakephp/cakephp: ^4.5|^5.0
- codeigniter4/framework: ^4.5
- doctrine/annotations: ^2.0
- laravel/framework: ^10|^11
- phpunit/phpunit: ^10.5
- symfony/framework-bundle: ^6|^7
- yiisoft/yii2: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Framework-agnostic DTO hydrator with Laravel, CodeIgniter, Symfony, Yii 2, and CakePHP integrations.
Installation
composer require hydrastat/dto-hydrator
Quick Start
1. Define your DTO with attributes
<?php namespace App\DTO; use Hydrastat\DtoHydrator\Mapping\Attribute\MapFrom; use Hydrastat\DtoHydrator\Mapping\Attribute\Validate; class UserCreateDTO { #[MapFrom('name', caster: 'trim')] #[Validate('required')] #[Validate('min:2')] #[Validate('max:100')] public string $name; #[MapFrom('email_address', caster: 'email')] #[Validate('required')] #[Validate('email')] public string $email; #[MapFrom('password', caster: 'string', writeonly: true)] #[Validate('required')] #[Validate('min:8')] public string $password; #[MapFrom('age', caster: 'int', required: false, default: 18)] #[Validate('integer')] #[Validate('min:13')] public int $age = 18; #[MapFrom('is_active', caster: 'bool', required: false, default: true)] public bool $isActive = true; #[MapFrom('created_at', caster: 'datetime', readonly: true)] public \DateTimeImmutable $createdAt; }
2. Use in your framework
Laravel
// config/app.php - auto-discovered via service provider use Hydrastat\DtoHydrator\Contracts\HydratorInterface; class UserController { public function __construct(private HydratorInterface $hydrator) {} public function store(Request $request) { $dto = $this->hydrator->hydrateAndValidate( UserCreateDTO::class, $request->validated() ); $user = $this->userService->create($dto); return response()->json($this->hydrator->extract($user)); } } // Or via Facade use Hydrastat\DtoHydrator\Integrations\Laravel\Facade as Hydrator; $dto = Hydrator::hydrateAndValidate(UserCreateDTO::class, $data);
CodeIgniter 4
// app/Config/Services.php - auto-registered use Hydrastat\DtoHydrator\Contracts\HydratorInterface; class UserController extends BaseController { private HydratorInterface $hydrator; public function __construct() { $this->hydrator = service('hydrator'); } public function create() { $dto = $this->hydrator->hydrateAndValidate( UserCreateDTO::class, $this->request->getJSON(true) ); // ... } }
Symfony
// config/bundles.php - auto-registered // Hydrastat\DtoHydrator\Integrations\Symfony\Bundle::class => ['all' => true], use Hydrastat\DtoHydrator\Contracts\HydratorInterface; class UserController { public function __construct(private HydratorInterface $hydrator) {} #[Route('/api/users', methods: ['POST'])] public function create(Request $request): JsonResponse { $data = json_decode($request->getContent(), true); $dto = $this->hydrator->hydrateAndValidate(UserCreateDTO::class, $data); $user = $this->userService->create($dto); return $this->json($this->hydrator->extract($user)); } }
Yii 2
// config/main.php return [ 'bootstrap' => [\Hydrastat\DtoHydrator\Integrations\Yii\Bootstrap::class], 'params' => [ 'hydrator' => [ 'allowPrivateAccess' => true, 'defaultOptions' => ['groups' => ['default']], 'customValidators' => [], 'customCasters' => [], ], ], ];
The bootstrap registers HydratorInterface::class and the hydrator alias in Yii::$container:
use Hydrastat\DtoHydrator\Contracts\HydratorInterface; final class UserController extends \yii\web\Controller { public function actionCreate(): array { $hydrator = \Yii::$container->get(HydratorInterface::class); $dto = $hydrator->hydrateAndValidate(UserCreateDTO::class, \Yii::$app->request->post()); return $hydrator->extract($this->userService->create($dto)); } }
You can also build one directly with \Hydrastat\DtoHydrator\Integrations\Yii\Service::hydrator().
CakePHP
// config/bootstrap.php use Cake\Core\Configure; Configure::write('Hydrator', [ 'allowPrivateAccess' => true, 'defaultOptions' => ['groups' => ['default']], 'customValidators' => [], 'customCasters' => [], ]);
use Hydrastat\DtoHydrator\Contracts\HydratorInterface; use Hydrastat\DtoHydrator\Integrations\CakePHP\Service; class UsersController extends AppController { public function add() { $hydrator = Service::hydrator(); $dto = $hydrator->hydrateAndValidate( UserCreateDTO::class, (array) $this->request->getData() ); // ... } }
For dependency injection, register shared definitions in a CakePHP container:
$container = new \Cake\Core\Container(); Service::register($container); $hydrator = $container->get(HydratorInterface::class);
Framework-agnostic (standalone)
use Hydrastat\DtoHydrator\Core\Hydrator; use Hydrastat\DtoHydrator\Core\MappingResolver; use Hydrastat\DtoHydrator\Core\PropertyAccessor; use Hydrastat\DtoHydrator\Core\TypeCaster; use Hydrastat\DtoHydrator\Core\Validator; $hydrator = new Hydrator( new MappingResolver(new PropertyAccessor()), new PropertyAccessor(), new TypeCaster(), new Validator() ); $dto = $hydrator->hydrateAndValidate(UserCreateDTO::class, $_POST);
Mapping Attributes
#[MapFrom]
| Parameter | Type | Default | Description |
|---|---|---|---|
source |
string | - | Source key in input data |
caster |
string | null | null |
required |
bool | true | Whether field is required |
default |
mixed | null | Default value if missing (supports Closure) |
groups |
array | ['default'] | Extraction groups |
readonly |
bool | false | Skip during hydration, include in extraction |
writeonly |
bool | true | Include in hydration, skip during extraction |
validator |
string | null | null |
#[Validate]
#[Validate('required')] #[Validate('email')] #[Validate('min:2')] #[Validate('max:100')] #[Validate('between:1,100')] #[Validate('in:admin,user,guest')] #[Validate('regex:/^[a-z]+$/')] #[Validate('confirmed')] // expects {field}_confirmation public string $field;
Built-in Casters
int, integer, float, double, bool, boolean, string, trim,
lower, upper, email, array, json, datetime, date, timestamp
Built-in Validators
required, email, min, max, between, numeric, integer, boolean,
date, in, regex, unique, confirmed
Custom Casters & Validators
Laravel, CodeIgniter, Yii 2, and CakePHP expose the same four settings through their own config convention: allowPrivateAccess, defaultOptions, customValidators, and customCasters. Laravel uses snake_case keys in config/hydrator.php; CodeIgniter uses app/Config/Hydrator.php, Yii 2 uses params.hydrator, and CakePHP uses Configure::write('Hydrator', ...) with the camelCase names.
// Laravel: config/hydrator.php 'custom_casters' => [ 'uuid' => fn($v) => (string) $v, 'carbon' => fn($v) => \Carbon\Carbon::parse($v), ], 'custom_validators' => [ 'unique' => function($value, $dto, $params) { [$table, $column] = $params; $exists = DB::table($table)->where($column, $value)->exists(); return $exists ? 'Already exists' : null; }, ],
Extraction Groups
// Only extract 'api' group $hydrator->extract($dto, ['groups' => ['api']]); // Exclude null values $hydrator->extract($dto, ['exclude_null' => true]);
Testing
use Hydrastat\DtoHydrator\Core\Hydrator; use Hydrastat\DtoHydrator\Core\MappingResolver; use Hydrastat\DtoHydrator\Core\PropertyAccessor; use Hydrastat\DtoHydrator\Core\TypeCaster; use Hydrastat\DtoHydrator\Core\Validator; $hydrator = new Hydrator( new MappingResolver(new PropertyAccessor()), new PropertyAccessor(), new TypeCaster(), new Validator() ); $dto = $hydrator->hydrate(UserCreateDTO::class, [ 'name' => ' John Doe ', 'email_address' => 'JOHN@EXAMPLE.COM', 'password' => 'secret123', ]); assert($dto->name === 'John Doe'); assert($dto->email === 'john@example.com'); assert($dto->age === 18); // default assert($dto->isActive === true); // default
License
MIT
Why Local?
This library has zero external HTTP calls — no telemetry, no license checks, no update pings. Everything runs in-process. The "external calls: zero" badge reflects this design principle.