Search by

uxf / hydrator

uxf

Package info

gitlab.com/uxf/hydrator

Issues

pkg:composer/uxf/hydrator

Statistics

Installs: 50 099

Dependents: 1

Suggesters: 0

Stars: 1

3.85.1 2026-10-09 16:39 UTC

README

The fastest PHP object hydrator.....

Install

$ composer req uxf/hydrator

Config

// config/packages/uxf.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use UXF\Core\Http\Request\NotSet;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->extension('uxf_hydrator', [       
        'ignored_types' => [NotSet::class], // optional
        'collection_types' => ['array'],    // optional
        'map_sources' => [__DIR__],         // optional
        'overwrite' => true,                // optional (default %kernel.debug%)
        'warmup' => true,                   // optional (default null = enabled when overwrite is false)
        'default_options' => [
            'allow_lax_string'  => true,     // default false
            'allow_trim_string' => true,     // default false
            'allow_fallback'    => true,     // default false
            'nullable_optional' => true,     // default false
            'xml_mode'          => true,     // default false
        ],
    ]);
};

Warmup

bin/console cache:warmup pre-generates type casters, so production (e.g. a Docker image) never generates them at runtime:

  • uxf/core - classes used by #[FromBody], #[FromQuery], #[FromForm] and #[FromHeader] controller arguments
  • uxf/graphql - GraphQL inputs

Nested classes are generated too. A class which cannot be generated fails the warmup.

warmup: null (default) runs only when overwrite is disabled (prod), true / false forces it on / off.

Tests (paratest)

With overwrite: true (default in debug) every process regenerates each type caster it uses and waits for the file lock, so parallel test workers repeat the same work. For faster tests disable overwrite in the test environment (warmup is then enabled automatically) and warm the cache before running tests:

// config/packages/uxf.php
if ($containerConfigurator->env() === 'test') {
    $containerConfigurator->extension('uxf_hydrator', [
        'overwrite' => false,
    ]);
}
bin/console cache:warmup -e test
vendor/bin/paratest

With overwrite: false the runtime never checks whether a class changed. Run cache:warmup (or cache:clear) after changing a hydrated class, otherwise tests run with an outdated type caster (e.g. a single test started from IDE). Classes not covered by the warmup are still generated at runtime once; files are written atomically, so parallel workers never include a partially written file.

Basic usage

ObjectHydrator create object from

class Person
{
    public function __construct(
        public readonly string $name,
        public readonly int $age,
        public readonly Sex $sex, // enum
        public readonly DateTimeImmutable $createdAt,
        public readonly ?string $note = null, // optional
    ) {
    }
}

$hydrator = $container->get(UXF\Hydrator\ObjectHydrator::class);

$data = [
    'name' => 'Joe Doe',
    'age' => 55,
    'sex' => 'MALE',
    'createdAt' => '2000-01-02T13:03:01+00:00',
];

// single object
/** @var Person $person */
$person = $this->hydrator->hydrateArray($data, Person::class);

var_dump($person);
/*
Person {
    name: "Joe Doe" (string)
    age: 55 (int)
    sex: Sex::MALE, (enum)
    createdAt: "2000-01-02T13:03:01+00:00" (DateTimeImmutable)
}
*/

// array of objects
/** @var Person[] $people */
$people = $this->hydrator->hydrateArrays([$data], Person::class);

Exceptions

try {
    $badPerson = $this->hydrator->hydrateArray($badData, Person::class);        
} catch (HydratorException $e) {
    /** @var array<string, array<string>> $errors */
    $errors = $e->errors;
}

Options

  • You can set default_options in bundle configuration or as ObjectHydrator constructor parameter.
  • Or pass Options parameter with unique name to hydrateArray/hydrateArrays method.
  • Use unique option name!!! Same option name with different values can lead to unexpected results...

allow_lax_string: true

Convert all scalars or stringable objects to string.

class Test
{
    public function __construct(public readonly string $helloWorld) {}
}

$a = ['helloWorld' => new Uuid()];   // Uuid is Stringable
$b = ['helloWorld' => 1];            // 1 => '1'

allow_trim_string: true

Trim all strings.

class Test
{
    public function __construct(public readonly string $helloWorld) {}
}

$a = ['helloWorld' => ' A ']; // helloWorld => 'A'

allow_fallback: true

Use FallbackParameterGenerator as fallback hydrator variant.

class Test
{
    public function __construct(public readonly mixed $helloWorld) {}
}

$a = ['helloWorld' => ['every value is possible', 1, new DateTime(), ['hello' => 'kitty']];

nullable_optional: true

final readonly class Test
{
    public function __construct(
        public ?string $helloWorld,
    ) {
    }
}

$a = []; // helloWorld => null

xml_mode: true

Convert scalars from string + enable parse empty/one-item/multi-item collections

HydratorProperty

use UXF\Hydrator\Attribute\HydratorProperty;

class Cart
{
    public function __construct(
        #[HydratorProperty('@id')]
        public readonly int $id,
    ) {
    }
}
{
    "@id": 1
}

HydratorXml

With xml_mode: true you can use HydratorXml PHP attribute for XML objects with optional XML attributes. Compatible with Symfony XmlEncoder.

use UXF\Hydrator\Attribute\HydratorProperty;
use UXF\Hydrator\Attribute\HydratorXml;

#[HydratorXml('text')] // "text is PHP property name with #"
final readonly class CatDto
{
    public function __construct(
        #[HydratorProperty('#')]
        public string $text,
        #[HydratorProperty('@ID')]
        public ?int $id = null,
    ) {
    }
}

Accept

<cat ID="2">CAT</cat>

OR

<cat>CAT</cat>

HydratorMap

Interface

use UXF\Hydrator\Attribute\HydratorMap;

// interface
#[HydratorMap(property: 'type', matrix: [
    'o' => Orienteering::class,
    'p' => Paragliding::class,
])]
interface Activity
{
}

// children
class Orienteering implements Activity
{
    public function __construct(
        public readonly int $card,
    ) {
    }
}

class Paragliding implements Activity
{
    public function __construct(
        public readonly string $glider,
    ) {
    }
}

// usage
class Club
{
    /**
     * @param Activity[] $activities
     */
    public function __construct(
        public readonly array $activities,
        public readonly Activity $activity,
    ) {
    }
}

Abstract class

use UXF\Hydrator\Attribute\HydratorMap;

// abstract class
#[HydratorMap(property: 'type', matrix: [
    'c' => CrossCountrySkiing::class,
    'o' => Orienteering::class,
    'p' => Paragliding::class,
])]
abstract class Sport
{
}

// children
class CrossCountrySkiing extends Sport
{
    public function __construct(
        public readonly string $ski,
    ) {
    }
}

class Orienteering extends Sport
{
    public function __construct(
        public readonly int $card,
    ) {
    }
}

class Paragliding extends Sport
{
    public function __construct(
        public readonly string $glider,
    ) {
    }
}

// usage
class Club
{
    /**
     * @param Sport[] $sports
     */
    public function __construct(
        public readonly array $sports,
        public readonly Sport $sport,
    ) {
    }
}

HydratorMapKey

use UXF\Hydrator\Attribute\HydratorMapKey;

#[HydratorMapKey('r')]
final class Running extends Sport
{
    public function __construct(
        public readonly string $type,
        public readonly string $shoes,
    ) {
    }
}