PackageTrack
Sign in Get early access

cuyz/valinor

Dependency free PHP library that helps to map any input into a strongly-typed structure.

2.6.0 16M downloads/mo #522 most downloaded on Packagist CuyZ/Valinor

What this package is like to depend on

Last release 12 days ago

11 Aug 2026

Release timing varies

gaps range from 8 days to 3 months

Nearly every release is documented

notes for 58 of 60 stable releases

Nothing withdrawn

no release was ever pulled

5 years old

60 releases · first in 2021

10 releases in the last 12 months

see the full history below

Release timeline

60 releases · Nov 2021 to Aug 2026
2022 2023 2024 2025 2026
Release Pre-release

Releases

latest 60
  1. 2.6.0 11 Aug 2026
    Release notes

    Notable changes

    This release brings a set of new features to the library:

    Enjoy! 🎉


    Provided mapper configurators

    A set of configurators is now available out-of-the-box for the mapper, mirroring the normalizer configurators introduced in the previous release. Each one can be used either globally through the configureWith() method or locally as an attribute targeting a specific property.

    The MapToDateTimeFromFormat configurator parses the input string using the given date format, which must follow the syntax supported by DateTimeImmutable::createFromFormat():

    use CuyZ\Valinor\Mapper\Configurator\MapToDateTimeFromFormat;
    use CuyZ\Valinor\MapperBuilder;
    use DateTimeInterface;
    
    final readonly class Event
    {
        public function __construct(
            public string $name,
    
            #[MapToDateTimeFromFormat('d/m/Y')]
            public DateTimeInterface $date,
        ) {}
    }
    
    $event = (new MapperBuilder())
        ->mapper()
        ->map(Event::class, [
            'name' => 'Release of legendary album',
            'date' => '08/11/1971', // mapped to a `DateTimeImmutable`
        ]);

    The MapExplodedStringToList configurator explodes a string into a list using the given separator, which is useful when the input carries a list as a single delimited string, for instance a value coming from a CSV file or a query parameter:

    use CuyZ\Valinor\Mapper\Configurator\MapExplodedStringToList;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class Product
    {
        public function __construct(
            public string $name,
    
            /** @var list<string> */
            #[MapExplodedStringToList(separator: ',')]
            public array $sizes,
        ) {}
    }
    
    $product = (new MapperBuilder())
        ->mapper()
        ->map(Product::class, [
            'name' => 'T-Shirt',
            'sizes' => 'XS,S,M,L,XL', // mapped to `['XS', 'S', 'M', 'L', 'XL']`
        ]);

    The MapArrayToList configurator discards the keys of an array and maps its values to a list, for cases where the input is an associative array, or a sparse list with missing or out-of-order indices, that should be handled as a sequential list:

    use CuyZ\Valinor\Mapper\Configurator\MapArrayToList;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class Basket
    {
        public function __construct(
            /** @var list<string> */
            #[MapArrayToList]
            public array $products,
        ) {}
    }
    
    $basket = (new MapperBuilder())
        ->mapper()
        ->map(Basket::class, [
            'a' => 'Coffee',
            'b' => 'Tea',
        ]); // mapped to `['Coffee', 'Tea']`

    Finally, the MapFromJson configurator decodes a JSON string and hands the result over to the mapper, so that the usual validation and error reporting still apply to the decoded value:

    use CuyZ\Valinor\Mapper\Configurator\MapFromJson;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            /** @var list<string> */
            #[MapFromJson]
            public array $roles,
        ) {}
    }
    
    $user = (new MapperBuilder())
        ->mapper()
        ->map(User::class, [
            'name' => 'John Doe',
            'roles' => '["admin", "editor"]', // mapped to `['admin', 'editor']`
        ]);

    Scalar value casting

    Four configurators convert a scalar value to a specific type before mapping: MapAsBool, MapAsInt, MapAsFloat and MapAsString. They are useful when the input data carries values in a different representation than the targeted type, for instance numbers or booleans encoded as strings in a form submission, a CSV file or a JSON payload.

    Used as an attribute, a single property is cast, leaving the strictness rules untouched for every other value:

    use CuyZ\Valinor\Mapper\Configurator\MapAsBool;
    use CuyZ\Valinor\Mapper\Configurator\MapAsInt;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            #[MapAsInt]
            public int $age,
    
            #[MapAsBool(true: ['on', 'yes'], false: ['off', 'no'])]
            public bool $isActive,
        ) {}
    }
    
    $user = (new MapperBuilder())
        ->mapper()
        ->map(User::class, [
            'name' => 'John Doe',
            'age' => '42', // mapped to `42`
            'isActive' => 'on', // mapped to `true`
        ]);

    Casting can also be enabled for every value of a given type with the new allowCastingToBoolean(), allowCastingToInteger(), allowCastingToFloat() and allowCastingToString() methods of the mapper builder. They offer a finer control than allowScalarValueCasting(), which relaxes strictness for all scalar types at once:

    use CuyZ\Valinor\MapperBuilder;
    
    $age = (new MapperBuilder())
        ->allowCastingToInteger()
        ->mapper()
        ->map('int', '42'); // mapped to `42`

    Mapping a property from a specific key

    The new MapFromKey attribute feeds a class property, or a constructor/method argument, from a specific source key instead of matching it against the property name:

    use CuyZ\Valinor\Mapper\Configurator\MapFromKey;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class Person
    {
        public function __construct(
            public string $name,
    
            #[MapFromKey('zipCode')]
            public string $postalCode,
        ) {}
    }
    
    $person = (new MapperBuilder())
        ->mapper()
        ->map(Person::class, [
            'name' => 'John Doe',
            'zipCode' => '75001', // mapped to `$postalCode`
        ]);

    This attribute is built on a lightweight protocol that is open to userland: any attribute class declaring a mapKey(string $key): string method and carrying the #[AsConverter] attribute can remap the key of the element it is placed on. This is handy to factor out a recurring transformation, such as a prefix shared by several properties:

    #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
    #[\CuyZ\Valinor\Mapper\AsConverter]
    final class MapWithPrefix
    {
        public function __construct(private string $prefix) {}
    
        public function mapKey(string $key): string
        {
            return $this->prefix . $key;
        }
    }
    
    final readonly class Configuration
    {
        public function __construct(
            #[MapWithPrefix('app_')] // reads from `app_host`
            public string $host,
            #[MapWithPrefix('app_')] // reads from `app_port`
            public int $port,
        ) {}
    }

    New normalizer configurators

    Three configurators join the ones introduced in the previous release.

    The NormalizeKeyTo attribute renames the key of a property during normalization, when the name used in the data format differs from the one used in the PHP codebase:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeyTo;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    final readonly class Address
    {
        public function __construct(
            public string $street,
    
            #[NormalizeKeyTo('town')]
            public string $city,
        ) {}
    }
    
    $addressAsArray = (new NormalizerBuilder())
        ->normalizer(Format::array())
        ->normalize(new Address('221B Baker Street', 'London'));
    
    // [
    //     'street' => '221B Baker Street',
    //     'town' => 'London',
    // ]

    The NormalizeToSingleValue class flattens an object holding a single property, so that instead of ['someProperty' => 'value'] the normalized result is simply 'value'. It can be used either as a configurator, applying to every object with a single property, or as an attribute targeting a specific class or property:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeToSingleValue;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    final readonly class Email
    {
        public function __construct(
            public string $email,
        ) {}
    }
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            #[NormalizeToSingleValue]
            public Email $email,
        ) {}
    }
    
    $userAsArray = (new NormalizerBuilder())
        ->normalizer(Format::array())
        ->normalize(new User('John Doe', new Email('[email protected]')));
    
    // [
    //     'name' => 'John Doe',
    //     'email' => '[email protected]',
    // ]

    The IgnoreOnNormalization attribute excludes a property from the normalized output, for instance to hide sensitive data such as a password. For the attribute to take effect, an instance of this class must also be registered on the builder via configureWith():

    use CuyZ\Valinor\Normalizer\Configurator\IgnoreOnNormalization;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            #[IgnoreOnNormalization]
            public string $password,
        ) {}
    }
    
    $userAsArray = (new NormalizerBuilder())
        ->configureWith(new IgnoreOnNormalization())
        ->normalizer(Format::array())
        ->normalize(new User('John Doe', 's3cr3t'));
    
    // ['name' => 'John Doe']

    Generics of PHP internal classes

    Generics used to be limited to userland classes, because classes internal to PHP or provided by an extension cannot declare @template annotations in their own source code. The library now ships generic signatures for a wide range of them, including ArrayObject, ArrayIterator, the SPL data structures and the Ds collection classes, so they can be parameterized like any other class:

    use CuyZ\Valinor\MapperBuilder;
    
    $sizes = (new MapperBuilder())
        ->mapper()
        ->map('ArrayObject<string, int>', [
            'S' => 36,
            'M' => 38,
            'L' => 40,
        ]);

    Every one of these templates declares a default type, so bare references like ArrayObject keep resolving as before.


    Default types for templates

    A @template annotation can now declare a default type with =. A template that declares a default type may be omitted when the class is referenced, in which case the default type is used:

    /**
     * @template TValue
     * @template TMeta of array<string, mixed> = array<string, string>
     */
    final readonly class Page
    {
        public function __construct(
            /** @var list<TValue> */
            public array $items,
    
            /** @var TMeta */
            public array $meta,
        ) {}
    }
    
    final readonly class SomeClass
    {
        public function __construct(
            // `TMeta` is not filled in, its default type is used
            /** @var Page<string> */
            public Page $pageWithDefaultMeta,
    
            // `TMeta` is filled in, overriding its default type
            /** @var Page<string, array{cursor: int}> */
            public Page $pageWithCursorMeta,
        ) {}
    }

    A default type is what makes it possible to add a template to a class that is already referenced elsewhere: the existing references, which do not fill the new template in, keep resolving to its default type and can be made more precise later on.


    Overriding an unparseable type

    When a property, parameter or return type uses a PHPStan or Psalm syntax that the library cannot parse yet, for instance a conditional type like ($a is 1 ? int : null), the dedicated @valinor-var, @valinor-param and @valinor-return annotations can be used to give the library a type it understands. They take precedence over every other annotation, so the static analysis tools keep using their own type while the library uses the override:

    final class SomeClass
    {
        /**
         * @phpstan-param ($a is 1 ? int : null) $b
         * @valinor-param int|null $b
         */
        public function __construct(
            public readonly int $a,
            public readonly ?int $b,
        ) {}
    }

    Features

    • Add @valinor-* annotations to override an unparseable type (11938c)
    • Add default value support for @template annotations (0d6efe)
    • Add mapper builder methods to cast to scalar types (cdca3f)
    • Add mapper configurator MapArrayToList (1f81fa)
    • Add mapper configurator MapAsBool (65dfed)
    • Add mapper configurator MapAsFloat (84eea9)
    • Add mapper configurator MapAsInt (ea28a7)
    • Add mapper configurator MapAsString (6b0528)
    • Add mapper configurator MapExplodedStringToList (beb4db)
    • Add mapper configurator MapFromJson (469863)
    • Add mapper configurator MapToDateTimeFromFormat (d6e53b)
    • Add normalizer configurator IgnoreOnNormalization (9769f2)
    • Add normalizer configurator NormalizeKeyTo (947127)
    • Add normalizer configurator NormalizeToSingleValue (7c5f13)
    • Allow mapping source keys with attributes (631f66)
    • Support generics of PHP internal classes (5419b4)

    Bug Fixes

    • Bind the templates a constructor declares to the type being mapped (0629d8)

    Internal

    • Refactor HTTP request mapping (578bd5)
    • Remove canCast() and cast() from scalar types (eae3f0)
    • Unify shaped array and HTTP request node building (3bb83b)
    Open source →
    Release notes

    Changelog 2.6.0 — 11th of August 2026

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.6.0

    Notable changes

    This release brings a set of new features to the library:

    Enjoy! 🎉


    Provided mapper configurators

    A set of configurators is now available out-of-the-box for the mapper, mirroring the normalizer configurators introduced in the previous release. Each one can be used either globally through the configureWith() method or locally as an attribute targeting a specific property.

    The MapToDateTimeFromFormat configurator parses the input string using the given date format, which must follow the syntax supported by DateTimeImmutable::createFromFormat():

    use CuyZ\Valinor\Mapper\Configurator\MapToDateTimeFromFormat;
    use CuyZ\Valinor\MapperBuilder;
    use DateTimeInterface;
    
    final readonly class Event
    {
        public function __construct(
            public string $name,
    
            #[MapToDateTimeFromFormat('d/m/Y')]
            public DateTimeInterface $date,
        ) {}
    }
    
    $event = (new MapperBuilder())
        ->mapper()
        ->map(Event::class, [
            'name' => 'Release of legendary album',
            'date' => '08/11/1971', // mapped to a `DateTimeImmutable`
        ]);
    

    The MapExplodedStringToList configurator explodes a string into a list using the given separator, which is useful when the input carries a list as a single delimited string, for instance a value coming from a CSV file or a query parameter:

    use CuyZ\Valinor\Mapper\Configurator\MapExplodedStringToList;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class Product
    {
        public function __construct(
            public string $name,
    
            /** @var list<string> */
            #[MapExplodedStringToList(separator: ',')]
            public array $sizes,
        ) {}
    }
    
    $product = (new MapperBuilder())
        ->mapper()
        ->map(Product::class, [
            'name' => 'T-Shirt',
            'sizes' => 'XS,S,M,L,XL', // mapped to `['XS', 'S', 'M', 'L', 'XL']`
        ]);
    

    The MapArrayToList configurator discards the keys of an array and maps its values to a list, for cases where the input is an associative array, or a sparse list with missing or out-of-order indices, that should be handled as a sequential list:

    use CuyZ\Valinor\Mapper\Configurator\MapArrayToList;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class Basket
    {
        public function __construct(
            /** @var list<string> */
            #[MapArrayToList]
            public array $products,
        ) {}
    }
    
    $basket = (new MapperBuilder())
        ->mapper()
        ->map(Basket::class, [
            'a' => 'Coffee',
            'b' => 'Tea',
        ]); // mapped to `['Coffee', 'Tea']`
    

    Finally, the MapFromJson configurator decodes a JSON string and hands the result over to the mapper, so that the usual validation and error reporting still apply to the decoded value:

    use CuyZ\Valinor\Mapper\Configurator\MapFromJson;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            /** @var list<string> */
            #[MapFromJson]
            public array $roles,
        ) {}
    }
    
    $user = (new MapperBuilder())
        ->mapper()
        ->map(User::class, [
            'name' => 'John Doe',
            'roles' => '["admin", "editor"]', // mapped to `['admin', 'editor']`
        ]);
    

    Scalar value casting

    Four configurators convert a scalar value to a specific type before mapping: MapAsBool, MapAsInt, MapAsFloat and MapAsString. They are useful when the input data carries values in a different representation than the targeted type, for instance numbers or booleans encoded as strings in a form submission, a CSV file or a JSON payload.

    Used as an attribute, a single property is cast, leaving the strictness rules untouched for every other value:

    use CuyZ\Valinor\Mapper\Configurator\MapAsBool;
    use CuyZ\Valinor\Mapper\Configurator\MapAsInt;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            #[MapAsInt]
            public int $age,
    
            #[MapAsBool(true: ['on', 'yes'], false: ['off', 'no'])]
            public bool $isActive,
        ) {}
    }
    
    $user = (new MapperBuilder())
        ->mapper()
        ->map(User::class, [
            'name' => 'John Doe',
            'age' => '42', // mapped to `42`
            'isActive' => 'on', // mapped to `true`
        ]);
    

    Casting can also be enabled for every value of a given type with the new allowCastingToBoolean(), allowCastingToInteger(), allowCastingToFloat() and allowCastingToString() methods of the mapper builder. They offer a finer control than allowScalarValueCasting(), which relaxes strictness for all scalar types at once:

    use CuyZ\Valinor\MapperBuilder;
    
    $age = (new MapperBuilder())
        ->allowCastingToInteger()
        ->mapper()
        ->map('int', '42'); // mapped to `42`
    

    Mapping a property from a specific key

    The new MapFromKey attribute feeds a class property, or a constructor/method argument, from a specific source key instead of matching it against the property name:

    use CuyZ\Valinor\Mapper\Configurator\MapFromKey;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class Person
    {
        public function __construct(
            public string $name,
    
            #[MapFromKey('zipCode')]
            public string $postalCode,
        ) {}
    }
    
    $person = (new MapperBuilder())
        ->mapper()
        ->map(Person::class, [
            'name' => 'John Doe',
            'zipCode' => '75001', // mapped to `$postalCode`
        ]);
    

    This attribute is built on a lightweight protocol that is open to userland: any attribute class declaring a mapKey(string $key): string method and carrying the #[AsConverter] attribute can remap the key of the element it is placed on. This is handy to factor out a recurring transformation, such as a prefix shared by several properties:

    #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
    #[\CuyZ\Valinor\Mapper\AsConverter]
    final class MapWithPrefix
    {
        public function __construct(private string $prefix) {}
    
        public function mapKey(string $key): string
        {
            return $this->prefix . $key;
        }
    }
    
    final readonly class Configuration
    {
        public function __construct(
            #[MapWithPrefix('app_')] // reads from `app_host`
            public string $host,
            #[MapWithPrefix('app_')] // reads from `app_port`
            public int $port,
        ) {}
    }
    

    New normalizer configurators

    Three configurators join the ones introduced in the previous release.

    The NormalizeKeyTo attribute renames the key of a property during normalization, when the name used in the data format differs from the one used in the PHP codebase:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeyTo;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    final readonly class Address
    {
        public function __construct(
            public string $street,
    
            #[NormalizeKeyTo('town')]
            public string $city,
        ) {}
    }
    
    $addressAsArray = (new NormalizerBuilder())
        ->normalizer(Format::array())
        ->normalize(new Address('221B Baker Street', 'London'));
    
    // [
    //     'street' => '221B Baker Street',
    //     'town' => 'London',
    // ]
    

    The NormalizeToSingleValue class flattens an object holding a single property, so that instead of ['someProperty' => 'value'] the normalized result is simply 'value'. It can be used either as a configurator, applying to every object with a single property, or as an attribute targeting a specific class or property:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeToSingleValue;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    final readonly class Email
    {
        public function __construct(
            public string $email,
        ) {}
    }
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            #[NormalizeToSingleValue]
            public Email $email,
        ) {}
    }
    
    $userAsArray = (new NormalizerBuilder())
        ->normalizer(Format::array())
        ->normalize(new User('John Doe', new Email('[email protected]')));
    
    // [
    //     'name' => 'John Doe',
    //     'email' => '[email protected]',
    // ]
    

    The IgnoreOnNormalization attribute excludes a property from the normalized output, for instance to hide sensitive data such as a password. For the attribute to take effect, an instance of this class must also be registered on the builder via configureWith():

    use CuyZ\Valinor\Normalizer\Configurator\IgnoreOnNormalization;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            #[IgnoreOnNormalization]
            public string $password,
        ) {}
    }
    
    $userAsArray = (new NormalizerBuilder())
        ->configureWith(new IgnoreOnNormalization())
        ->normalizer(Format::array())
        ->normalize(new User('John Doe', 's3cr3t'));
    
    // ['name' => 'John Doe']
    

    Generics of PHP internal classes

    Generics used to be limited to userland classes, because classes internal to PHP or provided by an extension cannot declare @template annotations in their own source code. The library now ships generic signatures for a wide range of them, including ArrayObject, ArrayIterator, the SPL data structures and the Ds collection classes, so they can be parameterized like any other class:

    use CuyZ\Valinor\MapperBuilder;
    
    $sizes = (new MapperBuilder())
        ->mapper()
        ->map('ArrayObject<string, int>', [
            'S' => 36,
            'M' => 38,
            'L' => 40,
        ]);
    

    Every one of these templates declares a default type, so bare references like ArrayObject keep resolving as before.


    Default types for templates

    A @template annotation can now declare a default type with =. A template that declares a default type may be omitted when the class is referenced, in which case the default type is used:

    /**
     * @template TValue
     * @template TMeta of array<string, mixed> = array<string, string>
     */
    final readonly class Page
    {
        public function __construct(
            /** @var list<TValue> */
            public array $items,
    
            /** @var TMeta */
            public array $meta,
        ) {}
    }
    
    final readonly class SomeClass
    {
        public function __construct(
            // `TMeta` is not filled in, its default type is used
            /** @var Page<string> */
            public Page $pageWithDefaultMeta,
    
            // `TMeta` is filled in, overriding its default type
            /** @var Page<string, array{cursor: int}> */
            public Page $pageWithCursorMeta,
        ) {}
    }
    

    A default type is what makes it possible to add a template to a class that is already referenced elsewhere: the existing references, which do not fill the new template in, keep resolving to its default type and can be made more precise later on.


    Overriding an unparseable type

    When a property, parameter or return type uses a PHPStan or Psalm syntax that the library cannot parse yet, for instance a conditional type like ($a is 1 ? int : null), the dedicated @valinor-var, @valinor-param and @valinor-return annotations can be used to give the library a type it understands. They take precedence over every other annotation, so the static analysis tools keep using their own type while the library uses the override:

    final class SomeClass
    {
        /**
         * @phpstan-param ($a is 1 ? int : null) $b
         * @valinor-param int|null $b
         */
        public function __construct(
            public readonly int $a,
            public readonly ?int $b,
        ) {}
    }
    

    Features

    • Add @valinor-* annotations to override an unparseable type (11938c)
    • Add default value support for @template annotations (0d6efe)
    • Add mapper builder methods to cast to scalar types (cdca3f)
    • Add mapper configurator MapArrayToList (1f81fa)
    • Add mapper configurator MapAsBool (65dfed)
    • Add mapper configurator MapAsFloat (84eea9)
    • Add mapper configurator MapAsInt (ea28a7)
    • Add mapper configurator MapAsString (6b0528)
    • Add mapper configurator MapExplodedStringToList (beb4db)
    • Add mapper configurator MapFromJson (469863)
    • Add mapper configurator MapToDateTimeFromFormat (d6e53b)
    • Add normalizer configurator IgnoreOnNormalization (9769f2)
    • Add normalizer configurator NormalizeKeyTo (947127)
    • Add normalizer configurator NormalizeToSingleValue (7c5f13)
    • Allow mapping source keys with attributes (631f66)
    • Support generics of PHP internal classes (5419b4)

    Bug Fixes

    • Bind the templates a constructor declares to the type being mapped (0629d8)

    Internal

    • Refactor HTTP request mapping (578bd5)
    • Remove canCast() and cast() from scalar types (eae3f0)
    • Unify shaped array and HTTP request node building (3bb83b)
    Open source →
  2. 2.5.1 28 Jul 2026
    Release notes

    Bug Fixes

    • Prevent union collision caused by absent optional elements (81b098)
    • Resolve docblock types of anonymous classes (7fc482)
    • Resolve members declared in parent interfaces (d46acf)

    Internal

    • Refactor compiler node usage (166cde)
    Open source →
    Release notes

    Changelog 2.5.1 — 28th of July 2026

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.5.1

    Bug Fixes

    • Prevent union collision caused by absent optional elements (81b098)
    • Resolve docblock types of anonymous classes (7fc482)
    • Resolve members declared in parent interfaces (d46acf)

    Internal

    • Refactor compiler node usage (166cde)
    Open source →
  3. 2.5.0 28 Jun 2026
    Release notes

    Notable changes

    This release brings a set of new features to the library:

    Enjoy! 🎉


    Normalizer configurators support

    A set of configurators is now available for the normalizer, mirroring the mapper configurators introduced in the previous release. Each one can be used either globally through the configureWith() method or locally as an attribute targeting a specific class or property.

    Keys case normalization

    Four configurators normalize the keys of a normalized object to a given case. This is useful to expose data following a naming convention that differs from the one used in the PHP codebase.

    Configurator Example
    new NormalizeKeysToCamelCase() first_namefirstName
    new NormalizeKeysToPascalCase() first_nameFirstName
    new NormalizeKeysToSnakeCase() firstNamefirst_name
    new NormalizeKeysToKebabCase() firstNamefirst-name

    Used globally, the keys of every normalized object are converted:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeysToSnakeCase;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    $userAsArray = (new NormalizerBuilder())
        ->configureWith(new NormalizeKeysToSnakeCase())
        ->normalizer(Format::array())
        ->normalize($user);
    
    // ['first_name' => 'John']

    Used as an attribute, only the keys of the targeted class are converted:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeysToSnakeCase;
    
    #[NormalizeKeysToSnakeCase]
    final readonly class User
    {
        public function __construct(
            public string $firstName,
        ) {}
    }
    
    // ['first_name' => 'John']

    Date and time normalization

    The NormalizeDateTimeFormat configurator normalizes any DateTimeInterface instance to a string using the given format.

    Used globally, every date and time encountered during normalization is formatted:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeDateTimeFormat;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    $userAsArray = (new NormalizerBuilder())
        ->configureWith(new NormalizeDateTimeFormat(DATE_ATOM))
        ->normalizer(Format::array())
        ->normalize($user);
    
    // [
    //     'name' => 'Jane Doe',
    //     'createdAt' => '2000-01-01T00:00:00+00:00',
    // ]

    Used as an attribute, only the targeted property is formatted:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeDateTimeFormat;
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            #[NormalizeDateTimeFormat(DATE_ATOM)]
            public DateTimeInterface $createdAt,
        ) {}
    }

    Shaped list type support

    The shaped list type list{…} is now supported. It works like a shaped array but enforces sequential integer keys starting at 0, making it the right type to describe a tuple-like list of values.

    final readonly class SomeClass
    {
        public function __construct(
            /** @var list{string, int, float} */
            public array $shapedList,
    
            /** @var list{0: string, 1: int} */
            public array $shapedListWithExplicitKeys,
    
            /** @var list{0: string, 1?: int} */
            public array $shapedListWithOptionalElement,
    
            /** @var list{string, int, ...} */
            public array $unsealedShapedList,
    
            /** @var list{string, int, ...list<float>} */
            public array $unsealedShapedListWithExplicitType,
    
            /** @var list{string, ...<float>} */
            public array $unsealedShapedListWithShorthandType,
        ) {}
    }

    key-of type support

    The key-of<T> type is now supported. It extracts the key types from enums, arrays, lists, and shaped arrays, including array constants. It is compatible with the same syntax as accepted by PHPStan and Psalm.

    enum SomeBackedEnum: string
    {
        case FOO = 'foo';
        case BAR = 'bar';
    }
    
    final readonly class SomeClassWithConstants
    {
        public const SOME_ARRAY = ['foo' => 1, 'bar' => 2];
    }
    
    final readonly class SomeClass
    {
        public function __construct(
            // Accepts 'FOO' or 'BAR' (the case names of the enum)
            /** @var key-of<SomeBackedEnum> */
            public string $enumKey,
    
            // Accepts 'foo' or 'bar' (the keys of the shaped array)
            /** @var key-of<array{foo: string, bar: int}> */
            public string $shapedArrayKey,
    
            // Accepts the key type of the array (string here)
            /** @var key-of<array<string, int>> */
            public string $arrayKey,
    
            // Accepts 'foo' or 'bar' (the keys of the class constant array)
            /** @var key-of<SomeClassWithConstants::SOME_ARRAY> */
            public string $constantArrayKey,
        ) {}
    }

    Features

    • Add normalizer configurator ConvertDateTime (bf688b)
    • Add normalizer configurator NormalizeKeysToCamelCase (b0d38f)
    • Add normalizer configurator NormalizeKeysToKebabCase (9826ca)
    • Add normalizer configurator NormalizeKeysToPascalCase (53bff2)
    • Add normalizer configurator NormalizeKeysToSnakeCase (c831e0)
    • Add support for key-of type mapping (ff16b2)
    • Add support for covariant templates (c31f24)
    • Add support for shaped list type (eeeb5c)
    • Support local alias types referencing other local aliases (757256)
    • Support null values for class constants (5cd356)
    • Support parenthesized union types (21a04b)

    Bug Fixes

    • Prevent memory leak with functions' reflection (6d36a0)
    • Rank union candidates by matching arguments (126cf7)

    Internal

    • Add security vulnerability reporting guidelines (b80e2a)
    • Memoize parent class definitions (17d8cf)
    • Move int to float casting outside Shell (f84e78)

    Other

    • Rename ConvertDateTime configurator to NormalizeDateTimeFormat (b7683a)
    • Rename ConvertKeysTo*Case configurators to MapKeysTo*Case (e38e06)
    Open source →
    Release notes

    Changelog 2.5.0 — 28th of June 2026

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.5.0

    Notable changes

    This release brings a set of new features to the library:

    Enjoy! 🎉


    Normalizer configurators support

    A set of configurators is now available for the normalizer, mirroring the mapper configurators introduced in the previous release. Each one can be used either globally through the configureWith() method or locally as an attribute targeting a specific class or property.

    Keys case normalization

    Four configurators normalize the keys of a normalized object to a given case. This is useful to expose data following a naming convention that differs from the one used in the PHP codebase.

    Configurator Example
    new NormalizeKeysToCamelCase() first_namefirstName
    new NormalizeKeysToPascalCase() first_nameFirstName
    new NormalizeKeysToSnakeCase() firstNamefirst_name
    new NormalizeKeysToKebabCase() firstNamefirst-name

    Used globally, the keys of every normalized object are converted:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeysToSnakeCase;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    $userAsArray = (new NormalizerBuilder())
        ->configureWith(new NormalizeKeysToSnakeCase())
        ->normalizer(Format::array())
        ->normalize($user);
    
    // ['first_name' => 'John']
    

    Used as an attribute, only the keys of the targeted class are converted:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeysToSnakeCase;
    
    #[NormalizeKeysToSnakeCase]
    final readonly class User
    {
        public function __construct(
            public string $firstName,
        ) {}
    }
    
    // ['first_name' => 'John']
    

    Date and time normalization

    The NormalizeDateTimeFormat configurator normalizes any DateTimeInterface instance to a string using the given format.

    Used globally, every date and time encountered during normalization is formatted:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeDateTimeFormat;
    use CuyZ\Valinor\Normalizer\Format;
    use CuyZ\Valinor\NormalizerBuilder;
    
    $userAsArray = (new NormalizerBuilder())
        ->configureWith(new NormalizeDateTimeFormat(DATE_ATOM))
        ->normalizer(Format::array())
        ->normalize($user);
    
    // [
    //     'name' => 'Jane Doe',
    //     'createdAt' => '2000-01-01T00:00:00+00:00',
    // ]
    

    Used as an attribute, only the targeted property is formatted:

    use CuyZ\Valinor\Normalizer\Configurator\NormalizeDateTimeFormat;
    
    final readonly class User
    {
        public function __construct(
            public string $name,
    
            #[NormalizeDateTimeFormat(DATE_ATOM)]
            public DateTimeInterface $createdAt,
        ) {}
    }
    

    Shaped list type support

    The shaped list type list{…} is now supported. It works like a shaped array but enforces sequential integer keys starting at 0, making it the right type to describe a tuple-like list of values.

    final readonly class SomeClass
    {
        public function __construct(
            /** @var list{string, int, float} */
            public array $shapedList,
    
            /** @var list{0: string, 1: int} */
            public array $shapedListWithExplicitKeys,
    
            /** @var list{0: string, 1?: int} */
            public array $shapedListWithOptionalElement,
    
            /** @var list{string, int, ...} */
            public array $unsealedShapedList,
    
            /** @var list{string, int, ...list<float>} */
            public array $unsealedShapedListWithExplicitType,
    
            /** @var list{string, ...<float>} */
            public array $unsealedShapedListWithShorthandType,
        ) {}
    }
    

    key-of type support

    The key-of<T> type is now supported. It extracts the key types from enums, arrays, lists, and shaped arrays, including array constants. It is compatible with the same syntax as accepted by [PHPStan] and [Psalm].

    enum SomeBackedEnum: string
    {
        case FOO = 'foo';
        case BAR = 'bar';
    }
    
    final readonly class SomeClassWithConstants
    {
        public const SOME_ARRAY = ['foo' => 1, 'bar' => 2];
    }
    
    final readonly class SomeClass
    {
        public function __construct(
            // Accepts 'FOO' or 'BAR' (the case names of the enum)
            /** @var key-of<SomeBackedEnum> */
            public string $enumKey,
    
            // Accepts 'foo' or 'bar' (the keys of the shaped array)
            /** @var key-of<array{foo: string, bar: int}> */
            public string $shapedArrayKey,
    
            // Accepts the key type of the array (string here)
            /** @var key-of<array<string, int>> */
            public string $arrayKey,
    
            // Accepts 'foo' or 'bar' (the keys of the class constant array)
            /** @var key-of<SomeClassWithConstants::SOME_ARRAY> */
            public string $constantArrayKey,
        ) {}
    }
    

    Features

    • Add normalizer configurator ConvertDateTime (bf688b)
    • Add normalizer configurator NormalizeKeysToCamelCase (b0d38f)
    • Add normalizer configurator NormalizeKeysToKebabCase (9826ca)
    • Add normalizer configurator NormalizeKeysToPascalCase (53bff2)
    • Add normalizer configurator NormalizeKeysToSnakeCase (c831e0)
    • Add support for key-of type mapping (ff16b2)
    • Add support for covariant templates (c31f24)
    • Add support for shaped list type (eeeb5c)
    • Support local alias types referencing other local aliases (757256)
    • Support null values for class constants (5cd356)
    • Support parenthesized union types (21a04b)

    Bug Fixes

    • Prevent memory leak with functions' reflection (6d36a0)
    • Rank union candidates by matching arguments (126cf7)

    Internal

    • Add security vulnerability reporting guidelines (b80e2a)
    • Memoize parent class definitions (17d8cf)
    • Move int to float casting outside Shell (f84e78)

    Other

    • Rename ConvertDateTime configurator to NormalizeDateTimeFormat (b7683a)
    • Rename ConvertKeysTo*Case configurators to MapKeysTo*Case (e38e06)
    Open source →
  4. 2.4.0 23 Mar 2026
    Release notes

    Notable changes

    This release brings a whole set of new features to the library:

    Enjoy! 🎉


    HTTP request mapping support

    This library now provides a way to map an HTTP request to controller action parameters or object properties. Parameters can be mapped from route, query and body values.

    Three attributes are available to explicitly bind a parameter to a single source, ensuring the value is never resolved from the wrong source:

    • #[FromRoute] — for parameters extracted from the URL path by router
    • #[FromQuery] — for query string parameters
    • #[FromBody] — for request body values

    Those attributes can be omitted entirely if the parameter is not bound to a specific source, in which case a collision error is raised if the same key is found in more than one source.

    This gives controllers a clean, type-safe signature without coupling to a framework's request object, while benefiting from the library's validation and error handling.

    Normal mapping rules apply there: parameters are required unless they have a default value.

    Route and query parameter values coming from an HTTP request are typically strings. The mapper automatically handles scalar value casting for these parameters: a string "42" will be properly mapped to an int parameter.

    Mapping a request using attributes

    Consider an API that lists articles for a given author. The author identifier comes from the URL path, while filtering and pagination come from the query string.

    use CuyZ\Valinor\Mapper\Http\FromQuery;
    use CuyZ\Valinor\Mapper\Http\FromRoute;
    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    
    final class ListArticles
    {
        /**
         * GET /api/authors/{authorId}/articles?status=X&page=X&limit=X
         *
         * @param non-empty-string $page
         * @param positive-int $page
         * @param int<10, 100> $limit
         */
        public function __invoke(
            // Comes from the route
            #[FromRoute] string $authorId,
    
            // All come from query parameters
            #[FromQuery] string $status,
            #[FromQuery] int $page = 1,
            #[FromQuery] int $limit = 10,
        ): ResponseInterface { … }
    }
    
    // GET /api/authors/42/articles?status=published&page=2
    $request = new HttpRequest(
        routeParameters: ['authorId' => 42],
        queryParameters: [
            'status' => 'published',
            'page' => 2,
        ],
    );
    
    $controller = new ListArticles();
    
    $arguments = (new MapperBuilder())
        ->argumentsMapper()
        ->mapArguments($controller, $request);
    
    $response = $controller(...$arguments);

    Mapping a request without using attributes

    When it is unnecessary to distinguish which source a parameter comes from, the attribute can be omitted entirely — the mapper will resolve each parameter from whichever source contains the matching key.

    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    
    final class PostComment
    {
        /**
         * POST /api/posts/{postId}/comments
         *
         * @param non-empty-string $author
         * @param non-empty-string $content
         */
        public function __invoke(
            int $postId,
            string $author,
            string $content,
        ): ResponseInterface { … }
    }
    
    // POST /api/posts/1337/comments
    $request = new HttpRequest(
        routeParameters: ['postId' => 1337],
        bodyValues: [
            'author' => '[email protected]',
            'content' => 'Great article, thanks for sharing!',
        ],
    );
    
    $controller = new PostComment();
    
    $arguments = (new MapperBuilder())
        ->argumentsMapper()
        ->mapArguments($controller, $request);
    
    $response = $controller(...$arguments);

    Note

    If the same key is found in more than one source for a parameter that has no attribute, a collision error is raised.

    Mapping all parameters at once

    Instead of mapping individual query parameters or body values to separate parameters, the asRoot option can be used to map all of them at once to a single parameter. This is useful when working with complex data structures or when the number of parameters is large.

    use CuyZ\Valinor\Mapper\Http\FromQuery;
    use CuyZ\Valinor\Mapper\Http\FromRoute;
    
    final readonly class ArticleFilters
    {
        public function __construct(
            /** @var non-empty-string */
            public string $status,
            /** @var positive-int */
            public int $page = 1,
            /** @var int<10, 100> */
            public int $limit = 10,
        ) {}
    }
    
    final class ListArticles
    {
        /**
         * GET /api/authors/{authorId}/articles?status=X&page=X&limit=X
         */
        public function __invoke(
            #[FromRoute] string $authorId,
            #[FromQuery(asRoot: true)] ArticleFilters $filters,
        ): ResponseInterface { … }
    }

    The same approach works with #[FromBody(asRoot: true)] for body values.

    Tip

    A shaped array can be used alongside asRoot to map all values to a single parameter:

    use CuyZ\Valinor\Mapper\Http\FromQuery;
    use CuyZ\Valinor\Mapper\Http\FromRoute;
    
    final class ListArticles
    {
        /**
         * GET /api/authors/{authorId}/articles?status=X&&page=X&limit=X
         *
         * @param array{
         *     status: non-empty-string,
         *     page?: positive-int,
         *     limit?: int<10, 100>,
         * } $filters
         */
        public function __invoke(
            #[FromRoute] string $authorId,
            #[FromQuery(asRoot: true)] array $filters,
        ): ResponseInterface { … }
    }

    Mapping to an object

    Instead of mapping to a callable's arguments, an HttpRequest can be mapped directly to an object. The attributes work the same way on constructor parameters or promoted properties.

    use CuyZ\Valinor\Mapper\Http\FromBody;
    use CuyZ\Valinor\Mapper\Http\FromRoute;
    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class PostComment
    {
        public function __construct(
            #[FromRoute] public int $postId,
            /** @var non-empty-string */
            #[FromBody] public string $author,
            /** @var non-empty-string */
            #[FromBody] public string $content,
        ) {}
    }
    
    $request = new HttpRequest(
        routeParameters: ['postId' => 1337],
        bodyValues: [
            'author' => '[email protected]',
            'content' => 'Great article, thanks for sharing!',
        ],
    );
    
    $comment = (new MapperBuilder())
        ->mapper()
        ->map(PostComment::class, $request);
    
    // $comment->postId  === 1337
    // $comment->author  === '[email protected]'
    // $comment->content === 'Great article, thanks for sharing!'

    Using PSR-7 requests

    An HttpRequest instance can be built directly from a PSR-7 ServerRequestInterface. This is the recommended approach when integrating with frameworks that use PSR-7.

    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    
    // `$psrRequest` is a PSR-7 `ServerRequestInterface` instance
    // `$routeParameters` are the parameters extracted by the router
    $request = HttpRequest::fromPsr($psrRequest, $routeParameters);
    
    $arguments = (new MapperBuilder())
        ->argumentsMapper()
        ->mapArguments($controller, $request);

    The factory method extracts query parameters from getQueryParams() and body values from getParsedBody(). It also passes the original PSR-7 request object through, so it can be injected into controller parameters if needed (see below).

    Accessing the original request object

    When building an HttpRequest, an original request object can be provided. If a controller parameter's type matches this object, it will be injected automatically; no attribute is needed.

    use CuyZ\Valinor\Mapper\Http\FromRoute;
    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    use Psr\Http\Message\ServerRequestInterface;
    
    final class ListArticles
    {
        /**
         * GET /api/authors/{authorId}/articles
         */
        public function __invoke(
            // Request object injected automatically
            ServerRequestInterface $request,
    
            #[FromRoute] string $authorId,
        ): ResponseInterface {
            $acceptHeader = $request->getHeaderLine('Accept');
    
            // …
        }
    }
    
    $request = HttpRequest::fromPsr($psrRequest, $routeParameters);
    
    $arguments = (new MapperBuilder())
        ->argumentsMapper()
        ->mapArguments(new ListArticles(), $request);
    
    // $arguments['request'] is the original PSR-7 request instance

    Error handling

    When the mapping fails — for instance because a required query parameter is missing or a body value has the wrong type — a MappingError is thrown, just like with regular mapping.

    Read the validation and error handling chapter for more information.


    Mapper/Normalizer configurators support

    Introduce MapperBuilderConfigurator and NormalizerBuilderConfigurator interfaces along with a configureWith() method on both builders.

    A configurator is a reusable piece of configuration logic that can be applied to a MapperBuilder or a NormalizerBuilder instance. This is useful when the same configuration needs to be applied in multiple places across an application, or when configuration logic needs to be distributed as a package.

    In the example below, we apply two configuration settings to a MapperBuilder inside a single class, but this could contain any number of customizations, depending on the needs of the application.

    namespace My\App;
    
    use CuyZ\Valinor\MapperBuilder;
    use CuyZ\Valinor\Mapper\Configurator\MapperBuilderConfigurator;
    
    final class ApplicationMappingConfigurator implements MapperBuilderConfigurator
    {
        public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
        {
            return $builder
                ->allowSuperfluousKeys()
                ->registerConstructor(
                    \My\App\CustomerId::fromString(...),
                );
        }
    }

    This configurator can be registered within the MapperBuilder instance:

    $result = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(new \My\App\ApplicationMappingConfigurator())
        ->mapper()
        ->map(\My\App\User::class, [
            'id' => '604e4b36-5b76-4b1a-9e6c-02d5acb53a4d',
            'name' => 'John Doe',
            'extraField' => 'ignored because superfluous keys are allowed',
        ]);

    Composing multiple configurators

    Multiple configurators can be combined to compose the final configuration. Each configurator is applied in order, allowing layered and modular configuration.

    namespace My\App;
    
    use CuyZ\Valinor\MapperBuilder;
    use CuyZ\Valinor\Mapper\Configurator\MapperBuilderConfigurator;
    
    final class FlexibleMappingConfigurator implements MapperBuilderConfigurator
    {
        public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
        {
            return $builder
                ->allowScalarValueCasting()
                ->allowSuperfluousKeys();
        }
    }
    
    final class DomainConstructorsConfigurator implements MapperBuilderConfigurator
    {
        public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
        {
            return $builder
                ->registerConstructor(
                    \My\App\CustomerId::fromString(...),
                    \My\App\Email::fromString(...),
                );
        }
    }
    
    $result = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(
            new \My\App\FlexibleMappingConfigurator(),
            new \My\App\DomainConstructorsConfigurator(),
        )
        ->mapper()
        ->map(\My\App\User::class, $someData);

    This approach keeps each configurator focused on a single concern, making them easier to test and reuse independently.

    Using NormalizerBuilderConfigurator

    The same configurator logic can be applied on NormalizerBuilder:

    namespace My\App;
    
    use CuyZ\Valinor\NormalizerBuilder;
    use CuyZ\Valinor\Normalizer\Configurator\NormalizerBuilderConfigurator;
    
    final class DomainObjectConfigurator implements NormalizerBuilderConfigurator
    {
        public function configureNormalizerBuilder(NormalizerBuilder $builder): NormalizerBuilder
        {
            return $builder
                ->registerTransformer(
                    fn (\DateTimeInterface $date) => $date->format('Y-m-d')
                )
                ->registerTransformer(
                    fn (\My\App\Money $money) => [
                        'amount' => $money->amount,
                        'currency' => $money->currency->value,
                    ]
                );
        }
    }
    
    final class SensitiveDataConfigurator implements NormalizerBuilderConfigurator
    {
        public function configureNormalizerBuilder(NormalizerBuilder $builder): NormalizerBuilder
        {
            return $builder
                ->registerTransformer(
                    fn (\My\App\EmailAddress $email) => '***@' . $email->domain()
                );
        }
    }
    
    $json = (new \CuyZ\Valinor\NormalizerBuilder())
        ->configureWith(
            new \My\App\DomainObjectConfigurator(),
            new \My\App\SensitiveDataConfigurator(),
        )
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
        ->normalize($someObject);

    CamelCase/snake_case keys conversion support

    Two configurators are available to convert the keys of input data before mapping them to object properties or shaped array keys. This allows accepting data with a different naming convention than the one used in the PHP codebase.

    ConvertKeysToCamelCase

    Conversion
    first_namefirstName
    FirstNamefirstName
    first-namefirstName
    $user = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(
            new \CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase()
        )
        ->mapper()
        ->map(\My\App\User::class, [
            'first_name' => 'John', // mapped to `$firstName`
            'last_name' => 'Doe',   // mapped to `$lastName`
        ]);

    ConvertKeysToSnakeCase

    Conversion
    firstNamefirst_name
    FirstNamefirst_name
    first-namefirst_name
    $user = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(
            new \CuyZ\Valinor\Mapper\Configurator\ConvertKeysToSnakeCase()
        )
        ->mapper()
        ->map(\My\App\User::class, [
            'firstName' => 'John', // mapped to `$first_name`
            'lastName' => 'Doe',   // mapped to `$last_name`
        ]);

    This configurator can be combined with a key restriction configurator to both validate and convert keys in a single step. The restriction configurator must be registered before the conversion so that the validation runs on the original input keys.

    use CuyZ\Valinor\MapperBuilder;
    use CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase;
    use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToSnakeCase;
    
    $user = (new MapperBuilder())
        ->configureWith(
            new RestrictKeysToSnakeCase(),
            new ConvertKeysToCamelCase(),
        )
        ->mapper()
        ->map(User::class, [
            'first_name' => 'John',
            'last_name' => 'Doe',
        ]);

    Keys case restriction support

    Four configurators restrict which key case is accepted when mapping input data to objects or shaped arrays. If a key does not match the expected case, a mapping error will be raised.

    This is useful, for instance, to enforce a consistent naming convention across an API's input to ensure that a JSON payload only contains camelCase, snake_case, PascalCase or kebab-case keys.

    Available configurators:

    Configurator Example
    new RestrictKeysToCamelCase() firstName
    new RestrictKeysToPascalCase() FirstName
    new RestrictKeysToSnakeCase() first_name
    new RestrictKeysToKebabCase() first-name
    $user = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(
            new \CuyZ\Valinor\Mapper\Configurator\RestrictKeysToCamelCase()
        )
        ->mapper()
        ->map(\My\App\User::class, [
            'firstName' => 'John', // Ok
            'last_name' => 'Doe',  // Error
        ]);

    Features

    • Add HTTP request mapping support (385f0c)
    • Add configurator support for mapper and normalizer builders (49dd0a)
    • Add mapper configurators to convert keys to camelCase/snake_case (a92bd3)
    • Add mapper configurators to restrict keys cases (0be7dc)
    • Introduce key converters to transform source keys (bfd4ab)

    Bug Fixes

    • Allow mapping a single value to a list type (7241a6)
    • Disallow duplicate converted keys (498dcf)
    • Handle concurrent cache directory race condition (13f06d)
    • Properly invalidate cache entries when using FileWatchingCache (d445e4)

    Internal

    Deps
    • Update dependencies (73a1cb)
    • Update mkdocs dependencies (67a6b4)
    Open source →
    Release notes

    Changelog 2.4.0 — 23rd of March 2026

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.4.0

    Notable changes

    This release brings a whole set of new features to the library:

    Enjoy! 🎉


    HTTP request mapping support

    This library now provides a way to map an HTTP request to controller action parameters or object properties. Parameters can be mapped from route, query and body values.

    Three attributes are available to explicitly bind a parameter to a single source, ensuring the value is never resolved from the wrong source:

    • #[FromRoute] — for parameters extracted from the URL path by router
    • #[FromQuery] — for query string parameters
    • #[FromBody] — for request body values

    Those attributes can be omitted entirely if the parameter is not bound to a specific source, in which case a collision error is raised if the same key is found in more than one source.

    This gives controllers a clean, type-safe signature without coupling to a framework's request object, while benefiting from the library's validation and error handling.

    Normal mapping rules apply there: parameters are required unless they have a default value.

    Route and query parameter values coming from an HTTP request are typically strings. The mapper automatically handles scalar value casting for these parameters: a string "42" will be properly mapped to an int parameter.

    Mapping a request using attributes

    Consider an API that lists articles for a given author. The author identifier comes from the URL path, while filtering and pagination come from the query string.

    use CuyZ\Valinor\Mapper\Http\FromQuery;
    use CuyZ\Valinor\Mapper\Http\FromRoute;
    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    
    final class ListArticles
    {
        /**
         * GET /api/authors/{authorId}/articles?status=X&page=X&limit=X
         *
         * @param non-empty-string $page
         * @param positive-int $page
         * @param int<10, 100> $limit
         */
        public function __invoke(
            // Comes from the route
            #[FromRoute] string $authorId,
    
            // All come from query parameters
            #[FromQuery] string $status,
            #[FromQuery] int $page = 1,
            #[FromQuery] int $limit = 10,
        ): ResponseInterface { … }
    }
    
    // GET /api/authors/42/articles?status=published&page=2
    $request = new HttpRequest(
        routeParameters: ['authorId' => 42],
        queryParameters: [
            'status' => 'published',
            'page' => 2,
        ],
    );
    
    $controller = new ListArticles();
    
    $arguments = (new MapperBuilder())
        ->argumentsMapper()
        ->mapArguments($controller, $request);
    
    $response = $controller(...$arguments);
    

    Mapping a request without using attributes

    When it is unnecessary to distinguish which source a parameter comes from, the attribute can be omitted entirely — the mapper will resolve each parameter from whichever source contains the matching key.

    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    
    final class PostComment
    {
        /**
         * POST /api/posts/{postId}/comments
         *
         * @param non-empty-string $author
         * @param non-empty-string $content
         */
        public function __invoke(
            int $postId,
            string $author,
            string $content,
        ): ResponseInterface { … }
    }
    
    // POST /api/posts/1337/comments
    $request = new HttpRequest(
        routeParameters: ['postId' => 1337],
        bodyValues: [
            'author' => '[email protected]',
            'content' => 'Great article, thanks for sharing!',
        ],
    );
    
    $controller = new PostComment();
    
    $arguments = (new MapperBuilder())
        ->argumentsMapper()
        ->mapArguments($controller, $request);
    
    $response = $controller(...$arguments);
    

    !!! note

    If the same key is found in more than one source for a parameter 
    that has no attribute, a collision error is raised.
    

    Mapping all parameters at once

    Instead of mapping individual query parameters or body values to separate parameters, the asRoot option can be used to map all of them at once to a single parameter. This is useful when working with complex data structures or when the number of parameters is large.

    use CuyZ\Valinor\Mapper\Http\FromQuery;
    use CuyZ\Valinor\Mapper\Http\FromRoute;
    
    final readonly class ArticleFilters
    {
        public function __construct(
            /** @var non-empty-string */
            public string $status,
            /** @var positive-int */
            public int $page = 1,
            /** @var int<10, 100> */
            public int $limit = 10,
        ) {}
    }
    
    final class ListArticles
    {
        /**
         * GET /api/authors/{authorId}/articles?status=X&page=X&limit=X
         */
        public function __invoke(
            #[FromRoute] string $authorId,
            #[FromQuery(asRoot: true)] ArticleFilters $filters,
        ): ResponseInterface { … }
    }
    

    The same approach works with #[FromBody(asRoot: true)] for body values.

    !!! hint

    A shaped array can be used alongside `asRoot` to map all values to a
    single parameter:
    
    ```php
    use CuyZ\Valinor\Mapper\Http\FromQuery;
    use CuyZ\Valinor\Mapper\Http\FromRoute;
    
    final class ListArticles
    {
        /**
         * GET /api/authors/{authorId}/articles?status=X&&page=X&limit=X
         *
         * @param array{
         *     status: non-empty-string,
         *     page?: positive-int,
         *     limit?: int<10, 100>,
         * } $filters
         */
        public function __invoke(
            #[FromRoute] string $authorId,
            #[FromQuery(asRoot: true)] array $filters,
        ): ResponseInterface { … }
    }
    ```
    

    Mapping to an object

    Instead of mapping to a callable's arguments, an HttpRequest can be mapped directly to an object. The attributes work the same way on constructor parameters or promoted properties.

    use CuyZ\Valinor\Mapper\Http\FromBody;
    use CuyZ\Valinor\Mapper\Http\FromRoute;
    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    
    final readonly class PostComment
    {
        public function __construct(
            #[FromRoute] public int $postId,
            /** @var non-empty-string */
            #[FromBody] public string $author,
            /** @var non-empty-string */
            #[FromBody] public string $content,
        ) {}
    }
    
    $request = new HttpRequest(
        routeParameters: ['postId' => 1337],
        bodyValues: [
            'author' => '[email protected]',
            'content' => 'Great article, thanks for sharing!',
        ],
    );
    
    $comment = (new MapperBuilder())
        ->mapper()
        ->map(PostComment::class, $request);
    
    // $comment->postId  === 1337
    // $comment->author  === '[email protected]'
    // $comment->content === 'Great article, thanks for sharing!'
    

    Using PSR-7 requests

    An HttpRequest instance can be built directly from a PSR-7 ServerRequestInterface. This is the recommended approach when integrating with frameworks that use PSR-7.

    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    
    // `$psrRequest` is a PSR-7 `ServerRequestInterface` instance
    // `$routeParameters` are the parameters extracted by the router
    $request = HttpRequest::fromPsr($psrRequest, $routeParameters);
    
    $arguments = (new MapperBuilder())
        ->argumentsMapper()
        ->mapArguments($controller, $request);
    

    The factory method extracts query parameters from getQueryParams() and body values from getParsedBody(). It also passes the original PSR-7 request object through, so it can be injected into controller parameters if needed (see below).

    Accessing the original request object

    When building an HttpRequest, an original request object can be provided. If a controller parameter's type matches this object, it will be injected automatically; no attribute is needed.

    use CuyZ\Valinor\Mapper\Http\FromRoute;
    use CuyZ\Valinor\Mapper\Http\HttpRequest;
    use CuyZ\Valinor\MapperBuilder;
    use Psr\Http\Message\ServerRequestInterface;
    
    final class ListArticles
    {
        /**
         * GET /api/authors/{authorId}/articles
         */
        public function __invoke(
            // Request object injected automatically
            ServerRequestInterface $request,
    
            #[FromRoute] string $authorId,
        ): ResponseInterface {
            $acceptHeader = $request->getHeaderLine('Accept');
    
            // …
        }
    }
    
    $request = HttpRequest::fromPsr($psrRequest, $routeParameters);
    
    $arguments = (new MapperBuilder())
        ->argumentsMapper()
        ->mapArguments(new ListArticles(), $request);
    
    // $arguments['request'] is the original PSR-7 request instance
    

    Error handling

    When the mapping fails — for instance because a required query parameter is missing or a body value has the wrong type — a MappingError is thrown, just like with regular mapping.

    Read the validation and error handling chapter for more information.


    Mapper/Normalizer configurators support

    Introduce MapperBuilderConfigurator and NormalizerBuilderConfigurator interfaces along with a configureWith() method on both builders.

    A configurator is a reusable piece of configuration logic that can be applied to a MapperBuilder or a NormalizerBuilder instance. This is useful when the same configuration needs to be applied in multiple places across an application, or when configuration logic needs to be distributed as a package.

    In the example below, we apply two configuration settings to a MapperBuilder inside a single class, but this could contain any number of customizations, depending on the needs of the application.

    namespace My\App;
    
    use CuyZ\Valinor\MapperBuilder;
    use CuyZ\Valinor\Mapper\Configurator\MapperBuilderConfigurator;
    
    final class ApplicationMappingConfigurator implements MapperBuilderConfigurator
    {
        public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
        {
            return $builder
                ->allowSuperfluousKeys()
                ->registerConstructor(
                    \My\App\CustomerId::fromString(...),
                );
        }
    }
    

    This configurator can be registered within the MapperBuilder instance:

    $result = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(new \My\App\ApplicationMappingConfigurator())
        ->mapper()
        ->map(\My\App\User::class, [
            'id' => '604e4b36-5b76-4b1a-9e6c-02d5acb53a4d',
            'name' => 'John Doe',
            'extraField' => 'ignored because superfluous keys are allowed',
        ]);
    

    Composing multiple configurators

    Multiple configurators can be combined to compose the final configuration. Each configurator is applied in order, allowing layered and modular configuration.

    namespace My\App;
    
    use CuyZ\Valinor\MapperBuilder;
    use CuyZ\Valinor\Mapper\Configurator\MapperBuilderConfigurator;
    
    final class FlexibleMappingConfigurator implements MapperBuilderConfigurator
    {
        public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
        {
            return $builder
                ->allowScalarValueCasting()
                ->allowSuperfluousKeys();
        }
    }
    
    final class DomainConstructorsConfigurator implements MapperBuilderConfigurator
    {
        public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
        {
            return $builder
                ->registerConstructor(
                    \My\App\CustomerId::fromString(...),
                    \My\App\Email::fromString(...),
                );
        }
    }
    
    $result = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(
            new \My\App\FlexibleMappingConfigurator(),
            new \My\App\DomainConstructorsConfigurator(),
        )
        ->mapper()
        ->map(\My\App\User::class, $someData);
    

    This approach keeps each configurator focused on a single concern, making them easier to test and reuse independently.

    Using NormalizerBuilderConfigurator

    The same configurator logic can be applied on NormalizerBuilder:

    namespace My\App;
    
    use CuyZ\Valinor\NormalizerBuilder;
    use CuyZ\Valinor\Normalizer\Configurator\NormalizerBuilderConfigurator;
    
    final class DomainObjectConfigurator implements NormalizerBuilderConfigurator
    {
        public function configureNormalizerBuilder(NormalizerBuilder $builder): NormalizerBuilder
        {
            return $builder
                ->registerTransformer(
                    fn (\DateTimeInterface $date) => $date->format('Y-m-d')
                )
                ->registerTransformer(
                    fn (\My\App\Money $money) => [
                        'amount' => $money->amount,
                        'currency' => $money->currency->value,
                    ]
                );
        }
    }
    
    final class SensitiveDataConfigurator implements NormalizerBuilderConfigurator
    {
        public function configureNormalizerBuilder(NormalizerBuilder $builder): NormalizerBuilder
        {
            return $builder
                ->registerTransformer(
                    fn (\My\App\EmailAddress $email) => '***@' . $email->domain()
                );
        }
    }
    
    $json = (new \CuyZ\Valinor\NormalizerBuilder())
        ->configureWith(
            new \My\App\DomainObjectConfigurator(),
            new \My\App\SensitiveDataConfigurator(),
        )
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
        ->normalize($someObject);
    

    CamelCase/snake_case keys conversion support

    Two configurators are available to convert the keys of input data before mapping them to object properties or shaped array keys. This allows accepting data with a different naming convention than the one used in the PHP codebase.

    ConvertKeysToCamelCase

    Conversion
    first_namefirstName
    FirstNamefirstName
    first-namefirstName
    $user = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(
            new \CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase()
        )
        ->mapper()
        ->map(\My\App\User::class, [
            'first_name' => 'John', // mapped to `$firstName`
            'last_name' => 'Doe',   // mapped to `$lastName`
        ]);
    

    ConvertKeysToSnakeCase

    Conversion
    firstNamefirst_name
    FirstNamefirst_name
    first-namefirst_name
    $user = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(
            new \CuyZ\Valinor\Mapper\Configurator\ConvertKeysToSnakeCase()
        )
        ->mapper()
        ->map(\My\App\User::class, [
            'firstName' => 'John', // mapped to `$first_name`
            'lastName' => 'Doe',   // mapped to `$last_name`
        ]);
    

    This configurator can be combined with a key restriction configurator to both validate and convert keys in a single step. The restriction configurator must be registered before the conversion so that the validation runs on the original input keys.

    use CuyZ\Valinor\MapperBuilder;
    use CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase;
    use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToSnakeCase;
    
    $user = (new MapperBuilder())
        ->configureWith(
            new RestrictKeysToSnakeCase(),
            new ConvertKeysToCamelCase(),
        )
        ->mapper()
        ->map(User::class, [
            'first_name' => 'John',
            'last_name' => 'Doe',
        ]);
    

    Keys case restriction support

    Four configurators restrict which key case is accepted when mapping input data to objects or shaped arrays. If a key does not match the expected case, a mapping error will be raised.

    This is useful, for instance, to enforce a consistent naming convention across an API's input to ensure that a JSON payload only contains camelCase, snake_case, PascalCase or kebab-case keys.

    Available configurators:

    Configurator Example
    new RestrictKeysToCamelCase() firstName
    new RestrictKeysToPascalCase() FirstName
    new RestrictKeysToSnakeCase() first_name
    new RestrictKeysToKebabCase() first-name
    $user = (new \CuyZ\Valinor\MapperBuilder())
        ->configureWith(
            new \CuyZ\Valinor\Mapper\Configurator\RestrictKeysToCamelCase()
        )
        ->mapper()
        ->map(\My\App\User::class, [
            'firstName' => 'John', // Ok
            'last_name' => 'Doe',  // Error
        ]);
    

    Features

    • Add HTTP request mapping support (385f0c)
    • Add configurator support for mapper and normalizer builders (49dd0a)
    • Add mapper configurators to convert keys to camelCase/snake_case (a92bd3)
    • Add mapper configurators to restrict keys cases (0be7dc)
    • Introduce key converters to transform source keys (bfd4ab)

    Bug Fixes

    • Allow mapping a single value to a list type (7241a6)
    • Disallow duplicate converted keys (498dcf)
    • Handle concurrent cache directory race condition (13f06d)
    • Properly invalidate cache entries when using FileWatchingCache (d445e4)

    Internal

    Deps
    • Update dependencies (73a1cb)
    • Update mkdocs dependencies (67a6b4)
    Open source →
  5. 2.3.2 23 Jan 2026
    Release notes

    Notable changes

    End of PHP 8.1 support

    PHP 8.1 security support has ended on the 31st of December 2025.

    See: https://www.php.net/supported-versions.php

    Removal of composer-runtime-api package dependency

    Using the composer-runtime-api library leads to unnecessary IO everytime the library is used; therefore, we prefer to use a basic constant that contains the package version.

    This change slightly increases performance and makes the package completely dependency free. 🎉

    Bug Fixes

    • Properly handle attribute transformers compilation (747414)
    • Properly handle imported function's namespace resolution (7757bd)
    • Properly handle large string integer casting (b4d9a4)
    • Simplify circular dependency handling (a7d8e2)
    • Use native type if advanced type unresolvable in normalizer compile (121798)
    Cache
    • Only unlink temp file if still exists (58b89c)

    Internal

    • Remove unused exception (aad781)
    • Replace composer-runtime-api requirement by PHP constant usage (8152be)
    • Standardize documentation comments (274207)
    • Use internal interface for mapping logical exception (8e00d3)

    Other

    • Drop support for PHP 8.1 (fec22a)
    • Separate unexpected mapped keys in own errors (332ef6)
    Open source →
    Release notes

    Changelog 2.3.2 — 23rd of January 2026

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.3.2

    Notable changes

    End of PHP 8.1 support

    PHP 8.1 security support has ended on the 31st of December 2025.

    See: https://www.php.net/supported-versions.php

    Removal of composer-runtime-api package dependency

    Using the composer-runtime-api library leads to unnecessary IO everytime the library is used; therefore, we prefer to use a basic constant that contains the package version.

    This change slightly increases performance and makes the package completely dependency free. 🎉

    Bug Fixes

    • Properly handle attribute transformers compilation (747414)
    • Properly handle imported function's namespace resolution (7757bd)
    • Properly handle large string integer casting (b4d9a4)
    • Simplify circular dependency handling (a7d8e2)
    • Use native type if advanced type unresolvable in normalizer compile (121798)
    Cache
    • Only unlink temp file if still exists (58b89c)

    Internal

    • Remove unused exception (aad781)
    • Replace composer-runtime-api requirement by PHP constant usage (8152be)
    • Standardize documentation comments (274207)
    • Use internal interface for mapping logical exception (8e00d3)

    Other

    • Drop support for PHP 8.1 (fec22a)
    • Separate unexpected mapped keys in own errors (332ef6)
    Open source →
  6. 2.3.1 21 Oct 2025
    Release notes

    Bug Fixes

    • Handle default value retrieval for properties (45b9de)
    Open source →
    Release notes

    Changelog 2.3.1 — 21st of October 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.3.1

    Bug Fixes

    • Handle default value retrieval for properties (45b9de)
    Open source →
  7. 2.3.0 21 Oct 2025
    Release notes

    Notable new features

    PHP 8.5 support 🐘

    Enjoy the upcoming PHP 8.5 version before it is even officially released!

    Performance improvements

    The awesome @staabm has identified some performance bottlenecks in the codebase, leading to changes that improved the execution time of the mapper by ~50% in his case (and probably some of yours)!

    Incoming HTTP request mapping

    There is an ongoing discussion to add support for HTTP request mapping, if that's something you're interested in, please join the discussion!

    Features

    • Add support for closures in attributes (d25d6f)
    • Add support for PHP 8.5 (7c34e7)

    Other

    • Support empty shaped array (a3eec8)

    Internal

    • Change compiled transformer method hashing algo (cf112b)
    • Micro-optimize arguments conversion to shaped array (33346d)
    • Use memoization for ShapedArrayType::toString() (4fcfb6)
    • Use memoization for arguments' conversion to shaped array (0f83be)
    • Use memoization for type dumping (f47613)
    Open source →
    Release notes

    Changelog 2.3.0 — 21st of October 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.3.0

    Notable new features

    PHP 8.5 support 🐘

    Enjoy the upcoming PHP 8.5 version before it is even officially released!

    Performance improvements

    The awesome Markus Staab has identified some performance bottlenecks in the codebase, leading to changes that improved the execution time of the mapper by ~50% in his case (and probably some of yours)!

    Incoming HTTP request mapping

    There is an ongoing discussion to add support for HTTP request mapping, if that's something you're interested in, please join the discussion!

    Features

    • Add support for closures in attributes (d25d6f)
    • Add support for PHP 8.5 (7c34e7)

    Other

    • Support empty shaped array (a3eec8)

    Internal

    • Change compiled transformer method hashing algo (cf112b)
    • Micro-optimize arguments conversion to shaped array (33346d)
    • Use memoization for ShapedArrayType::toString() (4fcfb6)
    • Use memoization for arguments' conversion to shaped array (0f83be)
    • Use memoization for type dumping (f47613)
    Open source →
  8. 2.2.2 13 Oct 2025
    Release notes

    Bug Fixes

    • Handle object arguments default value (c2cee2)
    Open source →
    Release notes

    Changelog 2.2.2 — 13th of October 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.2.2

    Bug Fixes

    • Handle object arguments default value (c2cee2)
    Open source →
  9. 2.2.1 12 Oct 2025
    Release notes

    ⚠️ Important changes ⚠️

    This release contains a lot of internal refactorings that were needed to fix an important bug regarding converters. Although we made our best to provide a stable release, bugs can have slipped through the cracks. If that's the case, please open an issue describing the issue and we will try to fix it as soon as possible.

    ⚠️ This fix is not backward-compatible in some cases, which are explained below. If you use mapper converters in your application, you should definitely read the following changes carefully.


    The commit d9e3cf0 is the result of a long journey whose goal was to fix a very upsetting bug that would make mapper converters being called when they shouldn't be. This could result in unexpected behaviors and could even lead to invalid data being mapped.

    Take the following example below:

    We register a converter that will return null if the string length is lower than 5. For this converter to be called, the target type should match the string|null type, because that is what the converter can return.

    In this example, we want to map a value to string, which is not matched by the converter return type because it does not contain null. This means that the converter should never be called, because it could return an invalid value (null will never be a valid string).

     (new \CuyZ\Valinor\MapperBuilder())
        ->registerConverter(
            // If the string length is lower than 5, we return `null`
            fn (string $val): ?string => strlen($val) < 5 ? null : $val
        )
        ->mapper()
        ->map('string', 'foo');

    Before this commit, the converter would be called and return null, which would raise an unexpected error:

    An error occurred at path root: value null is not a valid string.

    This error was caused by the following line:

    if (! $shell->type->matches($converter->returnType)) {
        continue;
    }

    It should have been:

    if (! $converter->returnType->matches($shell->type)) {
        continue;
    }

    Easy fix, isn't it?

    Well… actually no. Because changing this completely modifies the behavior of the converters, and the library is now missing a lot of information to properly infer the return type of the converter.

    In some cases this change was enough, but in some more complex cases we now would need more information.

    For instance, let's take the CamelCaseKeys example as it was written in the documentation before this commit:

    final class CamelCaseKeys
    {
        /**
         * @param array<mixed> $value
         * @param callable(array<mixed>): object $next
         */
        public function map(array $value, callable $next): object { … }
    }

    There is a big issue in the types signature of this converter: the object return type means that the converter can return anything, as long as this is an object. This breaks the type matching contract and the converter should never be called. But it was.

    This is the new way of writing this converter:

    final class CamelCaseKeys
    {
        /**
         * @template T of object
         * @param array<mixed> $value
         * @param callable(array<mixed>): T $next
         * @return T
         */
        public function map(array $value, callable $next): object { … }

    Now, the type matching contract is respected because of the @template annotation, and the converter is called when mapping to any object.

    To be able to properly infer the return type of the converter, we needed to:

    1. Be able to understand @template annotations inside functions
    2. Be able to statically infer the generics using these annotations
    3. Assign the inferred generics to the whole converter
    4. Let the system call the converter pipeline properly

    This was a huge amount of work, which required several small changes during the last month, as well as b7f3e5f and d9e3cf0. A lot of work for an error in a single line of code, right? T_T

    The good news is: the library is now more powerful than ever, as it is now able to statically infer generic types, which could bring new possibilities in the future.

    Now the bad news is: this commit can break backwards compatibility promise in some cases. But as this is still a (huge) bug fix, we will not release a new major version, although it can break some existing code. Instead, converters should be adapted to use proper type signatures.

    To help with that, here are the list of the diff that should be applied to converter examples that were written in the documentation:

    CamelCaseKeys

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_CLASS)]
    final class CamelCaseKeys
    {
        /**
    +    * @template T of object
         * @param array<mixed> $value
    -    * @param callable(array<mixed>): object $next
    +    * @param callable(array<mixed>): T $next
    +    * @return T
         */
        public function map(array $value, callable $next): object
        {
            …
        }
    }

    RenameKeys

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_CLASS)]
    final class RenameKeys
    {
        public function __construct(
            /** @var non-empty-array<non-empty-string, non-empty-string> */
            private array $mapping,
        ) {}
    
        /**
    +    * @template T of object
         * @param array<mixed> $value
    -    * @param callable(array<mixed>): object $next
    +    * @param callable(array<mixed>): T $next
    +    * @return T
         */
        public function map(array $value, callable $next): object
        {
            …
        }
    }

    Explode

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    final class Explode
    {
        public function __construct(
            /** @var non-empty-string */
            private string $separator,
        ) {}
    
        /**
    -    * @return array<mixed>
    +    * @return list<string>
         */
        public function map(string $value): array
        {
            return explode($this->separator, $value);
        }
    }

    ArrayToList

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    final class ArrayToList
    {
        /**
         * @template T
    -    * @param array<mixed> $value
    +    * @param non-empty-array<T> $value
    -    * @return list<mixed>
    +    * @return non-empty-list<T>
         */
        public function map(array $value): array
        {
            return array_values($value);
        }
    }

    JsonDecode

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    final class JsonDecode
    {
         /**
    +    * @template T
    -    * @param callable(mixed): mixed $next
    +    * @param callable(mixed): T $next
    +    * @return T
         */
        public function map(string $value, callable $next): mixed
        {
            $decoded = json_decode($value, associative: true);
    
            return $next($decoded);
        }
    }

    Bug Fixes

    • Make iterable type not match array types (27f2e3)
    • Prevent undefined object type to match invalid types (4ae98a)
    • Properly handle union and array-key types matching (71787a)
    • Use converter only if its return type matches the current node (d9e3cf)

    Internal

    • Detect converter argument value using native functions (81b4e5)
    • Refactor class and interface mapping process (ab1350)
    • Refactor definition type assignments to handle generic types (b7f3e5)
    • Refactor shell responsibilities and node builders API (63624c)
    • Remove exception code timestamps from codebase (460bb2)
    • Use INF constant to detect default converter value (72079b)

    Other

    • Enhance callable type parsing (2563a3)
    Open source →
    Release notes

    Changelog 2.2.1 — 13th of October 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.2.1

    ⚠️ Important changes ⚠️

    This release contains a lot of internal refactorings that were needed to fix an important bug regarding converters. Although we made our best to provide a stable release, bugs can have slipped through the cracks. If that's the case, please open an issue describing the issue and we will try to fix it as soon as possible.

    ⚠️ This fix is not backward-compatible in some cases, which are explained below. If you use mapper converters in your application, you should definitely read the following changes carefully.


    The commit d9e3cf0 is the result of a long journey whose goal was to fix a very upsetting bug that would make mapper converters being called when they shouldn't be. This could result in unexpected behaviors and could even lead to invalid data being mapped.

    Take the following example below:

    We register a converter that will return null if the string length is lower than 5. For this converter to be called, the target type should match the string|null type, because that is what the converter can return.

    In this example, we want to map a value to string, which is not matched by the converter return type because it does not contain null. This means that the converter should never be called, because it could return an invalid value (null will never be a valid string).

     (new \CuyZ\Valinor\MapperBuilder())
        ->registerConverter(
            // If the string length is lower than 5, we return `null`
            fn (string $val): ?string => strlen($val) < 5 ? null : $val
        )
        ->mapper()
        ->map('string', 'foo');
    

    Before this commit, the converter would be called and return null, which would raise an unexpected error:

    An error occurred at path root: value null is not a valid string.

    This error was caused by the following line:

    if (! $shell->type->matches($converter->returnType)) {
        continue;
    }
    

    It should have been:

    if (! $converter->returnType->matches($shell->type)) {
        continue;
    }
    

    Easy fix, isn't it?

    Well… actually no. Because changing this completely modifies the behavior of the converters, and the library is now missing a lot of information to properly infer the return type of the converter.

    In some cases this change was enough, but in some more complex cases we now would need more information.

    For instance, let's take the CamelCaseKeys example as it was written in the documentation before this commit:

    final class CamelCaseKeys
    {
        /**
         * @param array<mixed> $value
         * @param callable(array<mixed>): object $next
         */
        public function map(array $value, callable $next): object { … }
    }
    

    There is a big issue in the types signature of this converter: the object return type means that the converter can return anything, as long as this is an object. This breaks the type matching contract and the converter should never be called. But it was.

    This is the new way of writing this converter:

    final class CamelCaseKeys
    {
        /**
         * @template T of object
         * @param array<mixed> $value
         * @param callable(array<mixed>): T $next
         * @return T
         */
        public function map(array $value, callable $next): object { … }
    

    Now, the type matching contract is respected because of the @template annotation, and the converter is called when mapping to any object.

    To be able to properly infer the return type of the converter, we needed to:

    1. Be able to understand @template annotations inside functions
    2. Be able to statically infer the generics using these annotations
    3. Assign the inferred generics to the whole converter
    4. Let the system call the converter pipeline properly

    This was a huge amount of work, which required several small changes during the last month, as well as b7f3e5f and d9e3cf0. A lot of work for an error in a single line of code, right? T_T

    The good news is: the library is now more powerful than ever, as it is now able to statically infer generic types, which could bring new possibilities in the future.

    Now the bad news is: this commit can break backwards compatibility promise in some cases. But as this is still a (huge) bug fix, we will not release a new major version, although it can break some existing code. Instead, converters should be adapted to use proper type signatures.

    To help with that, here are the list of the diff that should be applied to converter examples that were written in the documentation:

    CamelCaseKeys

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_CLASS)]
    final class CamelCaseKeys
    {
        /**
    +    * @template T of object
         * @param array<mixed> $value
    -    * @param callable(array<mixed>): object $next
    +    * @param callable(array<mixed>): T $next
    +    * @return T
         */
        public function map(array $value, callable $next): object
        {
            …
        }
    }
    

    RenameKeys

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_CLASS)]
    final class RenameKeys
    {
        public function __construct(
            /** @var non-empty-array<non-empty-string, non-empty-string> */
            private array $mapping,
        ) {}
    
        /**
    +    * @template T of object
         * @param array<mixed> $value
    -    * @param callable(array<mixed>): object $next
    +    * @param callable(array<mixed>): T $next
    +    * @return T
         */
        public function map(array $value, callable $next): object
        {
            …
        }
    }
    

    Explode

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    final class Explode
    {
        public function __construct(
            /** @var non-empty-string */
            private string $separator,
        ) {}
    
        /**
    -    * @return array<mixed>
    +    * @return list<string>
         */
        public function map(string $value): array
        {
            return explode($this->separator, $value);
        }
    }
    

    ArrayToList

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    final class ArrayToList
    {
        /**
         * @template T
    -    * @param array<mixed> $value
    +    * @param non-empty-array<T> $value
    -    * @return list<mixed>
    +    * @return non-empty-list<T>
         */
        public function map(array $value): array
        {
            return array_values($value);
        }
    }
    

    JsonDecode

    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    final class JsonDecode
    {
         /**
    +    * @template T
    -    * @param callable(mixed): mixed $next
    +    * @param callable(mixed): T $next
    +    * @return T
         */
        public function map(string $value, callable $next): mixed
        {
            $decoded = json_decode($value, associative: true);
    
            return $next($decoded);
        }
    }
    

    Bug Fixes

    • Make iterable type not match array types (27f2e3)
    • Prevent undefined object type to match invalid types (4ae98a)
    • Properly handle union and array-key types matching (71787a)
    • Use converter only if its return type matches the current node (d9e3cf)

    Internal

    • Detect converter argument value using native functions (81b4e5)
    • Refactor class and interface mapping process (ab1350)
    • Refactor definition type assignments to handle generic types (b7f3e5)
    • Refactor shell responsibilities and node builders API (63624c)
    • Remove exception code timestamps from codebase (460bb2)
    • Use INF constant to detect default converter value (72079b)

    Other

    • Enhance callable type parsing (2563a3)
    Open source →
  10. 2.2.0 29 Sep 2025
    Release notes

    Notable new features

    Mapping error messages improvements

    Feedback has been improved in mapping error messages, especially the expected signature of the failing nodes.

    This gets rid of the infamous ? that was used whenever an object was present in a type, leading to incomplete and misleading messages.

    Example of a new message:

    final class User
    {
        public function __construct(
            public string $name,
            public int $age,
        ) {}
    }
    
    (new MapperBuilder())
        ->mapper()
        ->map(User::class, 'invalid value');
    
    // Could not map type `User`. An error occurred at path *root*: Value
    // 'invalid value' does not match `array{name: string, age: int}`.

    Features

    • Improve mapping error messages types signatures (ce1b0a)

    Bug Fixes

    • Prevent undefined values in non-empty-list (9739cd)
    • Properly detect nested invalid types during mapping (ad756a)
    • Use proper error message for invalid nullable scalar value (b84cbe)

    Other

    • Add safeguard in type parsing when reading next type (da0de0)
    • Improve type parsing error when an unexpected token is found (5ae904)
    • Lighten types initialization (6f0b3f)
    • Parse iterable type the same way it is done with array (6291a7)
    • Rework how type traversing is used (20f17f)
    • Set default exception error code to unknown (c8ef49)
    Open source →
    Release notes

    Changelog 2.2.0 — 29th of September 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.2.0

    Notable new features

    Mapping error messages improvements

    Feedback has been improved in mapping error messages, especially the expected signature of the failing nodes.

    This gets rid of the infamous ? that was used whenever an object was present in a type, leading to incomplete and misleading messages.

    Example of a new message:

    final class User
    {
        public function __construct(
            public string $name,
            public int $age,
        ) {}
    }
    
    (new MapperBuilder())
        ->mapper()
        ->map(User::class, 'invalid value');
    
    // Could not map type `User`. An error occurred at path *root*: Value
    // 'invalid value' does not match `array{name: string, age: int}`.
    

    Features

    • Improve mapping error messages types signatures (ce1b0a)

    Bug Fixes

    • Prevent undefined values in non-empty-list (9739cd)
    • Properly detect nested invalid types during mapping (ad756a)
    • Use proper error message for invalid nullable scalar value (b84cbe)

    Other

    • Add safeguard in type parsing when reading next type (da0de0)
    • Improve type parsing error when an unexpected token is found (5ae904)
    • Lighten types initialization (6f0b3f)
    • Parse iterable type the same way it is done with array (6291a7)
    • Rework how type traversing is used (20f17f)
    • Set default exception error code to unknown (c8ef49)
    Open source →
  11. 2.1.2 28 Aug 2025
    Release notes

    Changelog 2.1.2 — 28th of August 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.1.2

    Bug Fixes

    • Prevent converters from being called several times on same node (15be9e)

    Other

    • Add missing @pure annotations (c3871f)
    • Exclude unneeded methods when building class definition (0cf9f8)
    Open source →
  12. 2.1.1 23 Jul 2025
    Release notes

    Changelog 2.1.1 — 23rd of July 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.1.1

    Bug Fixes

    • Handle errors priorities when mapping to a union type (42cd02)
    • Properly flatten node path when single value objects are used (5b0bf2)
    Open source →
  13. 2.1.0 23 Jul 2025
    Release notes

    Changelog 2.1.0 — 23rd of July 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.1.0

    Notable changes

    Attribute converters

    !!! info

    Fetch common examples of mapping converters [in the documentation].
    
    [in the documentation]: ../../how-to/convert-input.md
    

    Callable converters allow targeting any value during mapping, whereas attribute converters allow targeting a specific class or property for a more granular control.

    To be detected by the mapper, an attribute class must be registered first by adding the AsConverter attribute to it.

    Attributes must declare a method named map that follows the same rules as callable converters: a mandatory first parameter and an optional second callable parameter.

    Below is an example of an attribute converter that converts string inputs to boolean values based on specific string inputs:

    namespace My\App;
    
    #[\CuyZ\Valinor\Mapper\AsConverter]
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    final class CastToBool
    {
        /**
         * @param callable(mixed): bool $next
         */
        public function map(string $value, callable $next): bool
        {
            $value = match ($value) {
                'yes', 'on' => true,
                'no', 'off' => false,
                default => $value,
            };
            
            return $next($value);
        }
    }
    
    final class User
    {
        public string $name;
        
        #[\My\App\CastToBool]
        public bool $isActive;
    }
    
    $user = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(User::class, [
            'name' => 'John Doe',
            'isActive' => 'yes',
        ]);
    
    $user->name === 'John Doe';
    $user->isActive === true;
    

    Attribute converters can also be used on function parameters when mapping arguments:

    function someFunction(string $name, #[\My\App\CastToBool] bool $isActive) {
        // …
    };
    
    $arguments = (new \CuyZ\Valinor\MapperBuilder())
        ->argumentsMapper()
        ->mapArguments(someFunction(...), [
            'name' => 'John Doe',
            'isActive' => 'yes',
        ]);
    
    $arguments['name'] === 'John Doe';
    $arguments['isActive'] === true;
    

    When there is no control over the converter attribute class, it is possible to register it using the registerConverter method.

    (new \CuyZ\Valinor\MapperBuilder())
        ->registerConverter(\Some\External\ConverterAttribute::class)
        ->mapper()
        ->map(…);
    

    It is also possible to register attributes that share a common interface by giving the interface name to the registration method.

    namespace My\App;
    
    interface SomeAttributeInterface {}
    
    #[\Attribute]
    final class SomeAttribute implements \My\App\SomeAttributeInterface {}
    
    #[\Attribute]
    final class SomeOtherAttribute implements \My\App\SomeAttributeInterface {}
    
    (new \CuyZ\Valinor\MapperBuilder())
        // Registers both `SomeAttribute` and `SomeOtherAttribute` attributes
        ->registerConverter(\My\App\SomeAttributeInterface::class)
        ->mapper()
        ->map(…);
    

    Features

    • Introduce attribute converters for granular control during mapping (0a8c0d)

    Bug Fixes

    • Properly detect invalid values returned by mapping converters (e80de7)
    • Properly extract = token when reading types (9a511d)
    • Use polyfill for array_find (540741)

    Other

    • Mark exception as @internal (f3eace)
    Open source →
  14. 2.0.0 27 Jun 2025
    Release notes

    Changelog 2.0.0 — 27th of June 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.0.0

    First release of the v2 series! 🎉

    This release introduces some new features but also backward compatibility breaks that are detailed in the upgrading chapter: it is strongly recommended to read it carefully before upgrading.

    Notable new features

    Mapper converters introduction

    A mapper converter allows users to hook into the mapping process and apply custom logic to the input, by defining a callable signature that properly describes when it should be called:

    • A first argument with a type matching the expected input being mapped
    • A return type representing the targeted mapped type

    These two types are enough for the library to know when to call the converter and can contain advanced type annotations for more specific use cases.

    Below is a basic example of a converter that converts string inputs to uppercase:

    (new \CuyZ\Valinor\MapperBuilder())
        ->registerConverter(
            fn (string $value): string => strtoupper($value)
        )
        ->mapper()
        ->map('string', 'hello world'); // 'HELLO WORLD'
    

    Converters can be chained, allowing multiple transformations to be applied to a value. A second callable parameter can be declared, allowing the current converter to call the next one in the chain.

    A priority can be given to a converter to control the order in which converters are applied. The higher the priority, the earlier the converter will be executed. The default priority is 0.

    (new \CuyZ\Valinor\MapperBuilder())
        ->registerConverter(
            function(string $value, callable $next): string {
                return $next(strtoupper($value));
            }
        )
        ->registerConverter(
            function(string $value, callable $next): string {
                return $next($value . '!');
            },
            priority: -10,
        )
        ->registerConverter(
            function(string $value, callable $next): string {
                return $next($value . '?');
            },
            priority: 10,
        )
        ->mapper()
        ->map('string', 'hello world'); // 'HELLO WORLD?!'
    

    More information can be found in the mapper converter chapter.

    NormalizerBuilder introduction

    The NormalizerBuilder class has been introduced and will now be the main entry to instantiate normalizers. Therefore, the methods inMapperBuilder that used to configure and return normalizers have been removed.

    This decision aims to make a clear distinction between the mapper and the normalizer configuration API, where confusion could arise when using both.

    The NormalizerBuilder can be used like this:

    $normalizer = (new \CuyZ\Valinor\NormalizerBuilder())
        ->registerTransformer(
            fn (\DateTimeInterface $date) => $date->format('Y/m/d')
        )
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
        ->normalize($someData);
    

    Changes to messages/errors handling

    Some changes have been made to the way messages and errors are handled.

    It is now easier to fetch messages when error(s) occur during mapping:

    try {
        (new \CuyZ\Valinor\MapperBuilder())->mapper()->map(/* … */);
    } catch (\CuyZ\Valinor\Mapper\MappingError $error) {
        // Before (1.x):
        $messages = \CuyZ\Valinor\Mapper\Tree\Message\Messages::flattenFromNode(
            $error->node()
        );
        
        // After (2.x):
        $messages = $error->messages();
    }
    

    Upgrading from 1.x to 2.x

    See the upgrading chapter.

    ⚠ BREAKING CHANGES

    • Add purity markers in MapperBuilder and NormalizerBuilder (123058)
    • Add type and source accessors to MappingError (378141)
    • Change exposed error messages codes (15bb11)
    • Introduce NormalizerBuilder as the main entry for normalizers (f79ce2)
    • Introduce internal cache interface and remove PSR-16 dependency (dfdf40)
    • Mark some class constructors as @internal (7fe5fe)
    • Remove MapperBuilder::alter() in favor of mapper converters (bee098)
    • Remove MapperBuilder::enableFlexibleCasting() (f8f16d)
    • Remove unused class PrioritizedList (0b8c89)
    • Remove unused interface IdentifiableSource (aefb20)
    • Rename MapperBuilder::warmup() method to warmupCacheFor() (963156)
    • Rework mapper node and messages handling (14d5ca)

    Features

    • Allow MapperBuilder and NormalizerBuilder to clear cache (fe318c)
    • Introduce mapper converters to apply custom logic during mapping (46c823)

    Bug Fixes

    • Update file system cache entries permissions (6ffb0f)

    Other

    • Remove Throwable inheritance from ErrorMessage (dbd731)
    • Remove old class doc block (4c2194)
    • Remove unused property (53841a)
    Open source →
  15. 1.17.0 20 Jun 2025
    Release notes

    Changelog 1.17.0 — 20th of June 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.17.0

    Notable changes

    Flexible casting setting split

    The mapper setting enableFlexibleCasting is (softly) deprecated in favor of three distinct modes, which guarantee the same functionalities as before.

    Allowing scalar value casting:

    With this setting enabled, scalar types will accept castable values:

    • Integer types will accept any valid numeric value, for instance the string value "42".

    • Float types will accept any valid numeric value, for instance the string value "1337.42".

    • String types will accept any integer, float or object implementing the Stringable interface.

    • Boolean types will accept any truthy or falsy value:

      • (string) "true", (string) "1" and (int) 1 will be cast to true
      • (string) "false", (string) "0" and (int) 0 will be cast to false
    (new \CuyZ\Valinor\MapperBuilder())
        ->allowScalarValueCasting()
        ->mapper()
        ->map('array{id: string, price: float, active: bool}', [
            'id' => 549465210, // Will be cast to string
            'price' => '42.39', // Will be cast to float
            'active' => 1, // Will be cast to bool
        ]);
    

    Allowing non-sequential lists:

    By default, list types will only accept sequential keys starting from 0.

    This setting allows the mapper to convert associative arrays to a list with sequential keys.

    (new \CuyZ\Valinor\MapperBuilder())
        ->allowNonSequentialList()
        ->mapper()
        ->map('list<int>', [
            'foo' => 42,
            'bar' => 1337,
        ]);
    
    // => [0 => 42, 1 => 1337]
    

    Allowing undefined values:

    Allows the mapper to accept undefined values (missing from the input), by converting them to null (if the current type is nullable) or an empty array (if the current type is an object or an iterable).

    (new \CuyZ\Valinor\MapperBuilder())
        ->allowUndefinedValues()
        ->mapper()
        ->map('array{name: string, age: int|null}', [
            'name' => 'John Doe',
            // 'age' is not defined
        ]);
    
    // => ['name' => 'John Doe', 'age' => null]
    

    Features

    • Split flexible casting setting in three distinct modes (02ef8e)

    Other

    • Simplify ValueNode implementation (7e6ccf)
    Open source →
  16. 1.16.1 19 May 2025
    Release notes

    Changelog 1.16.1 — 19th of May 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.16.1

    Bug Fixes

    • Handle mapping of argument of type object with names shared (771696)
    Open source →
  17. 1.16.0 19 May 2025
    Release notes

    Changelog 1.16.0 — 19th of May 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.16.0

    Features

    • Add support for scalar type (eaebe1)
    • Handle flattened values when mapping to a single object argument (6ca0ee)
    Open source →
  18. 1.15.0 30 Mar 2025
    Release notes

    Changelog 1.15.0 — 31st of March 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.15.0

    Notable changes

    Normalizer compilation

    A new compilation step for the normalizer has been implemented, which aims to bring huge performance gains when normalizing any value. It works by adding a static analysis pass to the process, which will recursively analyse how the normalizer should perform for every value it can meet. This process results in a native PHP code entry, that can then be cached for further usage.

    This compilation cache feature is automatically enabled when adding the cache in the mapper builder. This should be transparent for most users, but as this is a major change in the code (see Pull Request #500), some bugs may have slipped through. If you encounter such issues that look related to this change, please open an issue and we will try to fix it as soon as possible.

    !!! note "Sponsoring and notes about the future"

    My goal remains to provide users of this library with the best possible 
    experience. To that end, motivational messages and financial support are
    greatly appreciated. If you use this library and find it useful, please
    consider [sponsoring the project on GitHub] 🤗
    
    The development of this feature took nearly two years, mainly due to limited
    spare time to work on it. I hope you enjoy this feature as much as I enjoyed
    building it!
    
    On a side note, the next major project will be adding a compiled cache entry
    feature for mappers, similar to how it was implemented for normalizers. Stay
    tuned…
    

    Features

    • Introduce compiled normalizer cache (a4b2a7)

    Bug Fixes

    • Accept an object implementing an interface without infer setting (edd488)
    • Handle self-referential types in object constructors (dc7b6a)
    • Properly handle interface with no implementation in union type (f3f98d)
    • Properly match class-string type with no subtype (c8fe90)

    Other

    • Add methods to fetch native types (7213eb)
    • Improve integer value type match algorithm (048a48)
    • Update default error message for invalid value for union type (d1ab6a)
    Open source →
  19. 1.14.4 23 Feb 2025
    Release notes

    Changelog 1.14.4 — 23rd of February 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.4

    Bug Fixes

    • Properly handle superfluous keys when source is an iterable (33ec7e)
    Open source →
  20. 1.14.3 17 Feb 2025
    Release notes

    Changelog 1.14.3 — 17th of February 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.3

    Bug Fixes

    • Normalize empty iterable object as empty array in JSON (a22a53)
    • Properly handle full namespaced enum type in docblock (eb8816)
    • Support PHPStan extension for PHPStan v1 and v2 (9f043b)

    Other

    • Handle trailing comma in shaped array declaration (1cb4e9)
    • Make sure a child shell is not root (ef0b5c)
    • Refactor node builders stack (040b90)
    • Remove ErrorCatcherNodeBuilder (f8eedc)
    • Remove IterableNodeBuilder (339f10)
    • Remove the need to keep a reference to the Shell parent node (070db3)
    Open source →
  21. 1.14.2 09 Jan 2025
    Release notes

    Changelog 1.14.2 — 9th of January 2025

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.2

    Bug Fixes

    • Properly handle array arguments during attributes compilation (abfca1)

    Other

    • Fix PHP 8.4 deprecations (b698de)
    • Remove "pure" requirements from several methods (b15d1a)
    • Update dependencies (564691)
    • Update PHPStan to version 2 (9ef3cf)
    Open source →
  22. 1.14.1 06 Nov 2024
    Release notes

    Changelog 1.14.1 — 6th of November 2024

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.1

    Bug Fixes

    • Properly handle partial namespace signature (9b58f2)
    Open source →
  23. 1.14.0 04 Nov 2024
    Release notes

    Changelog 1.14.0 — 4th of November 2024

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.0

    Notable changes

    PHP 8.4 support 🐘

    Enjoy the upcoming PHP 8.4 version before it is even officially released!

    Pretty JSON output

    The JSON_PRETTY_PRINT option is now supported by the JSON normalizer and will format the ouput with whitespaces and line breaks:

    $input = [
        'value' => 'foo',
        'list' => [
            'foo',
            42,
            ['sub']
        ],
        'associative' => [
            'value' => 'foo',
            'sub' => [
                'string' => 'foo',
                'integer' => 42,
            ],
        ],
    ];
    
    (new \CuyZ\Valinor\MapperBuilder())
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
        ->withOptions(\JSON_PRETTY_PRINT)
        ->normalize($input);
    
    // Result:
    // {
    //     "value": "foo",
    //     "list": [
    //         "foo",
    //         42,
    //         [
    //             "sub"
    //         ]
    //     ],
    //     "associative": {
    //         "value": "foo",
    //         "sub": {
    //             "string": "foo",
    //             "integer": 42
    //         }
    //     }
    // }
    

    Force array as object in JSON output

    The JSON_FORCE_OBJECT option is now supported by the JSON normalizer and will force the output of an array to be an object:

    (new \CuyZ\Valinor\MapperBuilder())
        ->normalizer(Format::json())
        ->withOptions(JSON_FORCE_OBJECT)
        ->normalize(['foo', 'bar']);
    
    // {"0":"foo","1":"bar"}
    

    Features

    • Add support for JSON_FORCE_OBJECT option in JSON normalizer (f3e8c1)
    • Add support for PHP 8.4 (07a06a)
    • Handle JSON_PRETTY_PRINT option with the JSON normalizer (950395)

    Bug Fixes

    • Handle float type casting properly (8742b2)
    • Handle namespace for Closure without class scope (7a0fc2)
    • Prevent cache corruption when normalizing and mapping to enum (e695b2)
    • Properly handle class sharing class name and namespace group name (6e68d6)

    Other

    • Change implicitly nullable parameter types (304db3)
    • Fix typo in property type annotation (b9c6ad)
    • Use xxh128 hash algorithm for cache keys (546c45)
    Open source →
  24. 1.13.0 02 Sep 2024
    Release notes

    Changelog 1.13.0 — 2nd of September 2024

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.13.0

    Notable changes

    Microseconds support for timestamp format

    Prior to this patch, this would require a custom constructor in the form of:

    static fn(float | int $timestamp): DateTimeImmutable => new
        DateTimeImmutable(sprintf("@%d", $timestamp)),
    

    This bypasses the datetime format support of Valinor entirely. This is required because the library does not support floats as valid DateTimeInterface input values.

    This commit adds support for floats and registers timestamp.microseconds (U.u) as a valid default format.

    Support for value-of<BackedEnum> type

    This type can be used as follows:

    enum Suit: string
    {
        case Hearts = 'H';
        case Diamonds = 'D';
        case Clubs = 'C';
        case Spades = 'S';
    }
    
    $suit = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map('value-of<Suit>', 'D');
    
    // $suit === 'D'
    

    Object constructors parameters types inferring improvements

    The collision system that checks object constructors parameters types is now way more clever, as it no longer checks for parameters' names only. Types are now also checked, and only true collision will be detected, for instance when two constructors share a parameter with the same name and type.

    Note that when two parameters share the same name, the following type priority operates:

    1. Non-scalar type
    2. Integer type
    3. Float type
    4. String type
    5. Boolean type

    With this change, the code below is now valid:

    final readonly class Money
    {
        private function __construct(
            public int $value,
        ) {}
    
        #[\CuyZ\Valinor\Mapper\Object\Constructor]
        public static function fromInt(int $value): self
        {
            return new self($value);
        }
    
        #[\CuyZ\Valinor\Mapper\Object\Constructor]
        public static function fromString(string $value): self
        {
            if (! preg_match('/^\d+€$/', $value)) {
                throw new \InvalidArgumentException('Invalid money format');
            }
    
            return new self((int)rtrim($value, '€'));
        }
    }
    
    $mapper = (new \CuyZ\Valinor\MapperBuilder())->mapper();
    
    $mapper->map(Money::class, 42); // ✅
    $mapper->map(Money::class, '42€'); // ✅
    

    Features

    • Add microseconds support to timestamp format (02bd2e)
    • Add support for value-of<BackedEnum> type (b1017c)
    • Improve object constructors parameters types inferring (2150dc)

    Bug Fixes

    • Allow any constant in class constant type (694275)
    • Allow docblock for transformer callable type (69e0e3)
    • Do not override invalid variadic parameter type (c5860f)
    • Handle interface generics (40e6fa)
    • Handle iterable objects as iterable during normalization (436e3c)
    • Properly format empty object with JSON normalizer (ba22b5)
    • Properly handle nested local type aliases (127839)

    Other

    • Exclude unneeded attributes in class/function definitions (1803d0)
    • Improve mapping performance for nullable union type (6fad94)
    • Move "float type accepting integer value" logic in Shell (047953)
    • Move setting values in shell (84b1ff)
    • Reorganize type resolver services (86fb7b)
    Open source →
  25. 1.12.0 04 Apr 2024
    Release notes

    Changelog 1.12.0 — 4th of April 2024

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.12.0

    Notable changes

    Introduce unsealed shaped array syntax

    This syntax enables an extension of the shaped array type by allowing additional values that must respect a certain type.

    $mapper = (new \CuyZ\Valinor\MapperBuilder())->mapper();
    
    // Default syntax can be used like this:
    $mapper->map(
        'array{foo: string, ...array<string>}',
        [
            'foo' => 'foo',
            'bar' => 'bar', // ✅ valid additional value
        ]
    );
    
    $mapper->map(
        'array{foo: string, ...array<string>}',
        [
            'foo' => 'foo',
            'bar' => 1337, // ❌ invalid value 1337
        ]
    );
    
    // Key type can be added as well:
    $mapper->map(
        'array{foo: string, ...array<int, string>}',
        [
            'foo' => 'foo',
            42 => 'bar', // ✅ valid additional key
        ]
    );
    
    $mapper->map(
        'array{foo: string, ...array<int, string>}',
        [
            'foo' => 'foo',
            'bar' => 'bar' // ❌ invalid key
        ]
    );
    
    // Advanced types can be used:
    $mapper->map(
        "array{
            'en_US': non-empty-string,
            ...array<non-empty-string, non-empty-string>
        }",
        [
            'en_US' => 'Hello',
            'fr_FR' => 'Salut', // ✅ valid additional value
        ]
    );
    
    $mapper->map(
        "array{
            'en_US': non-empty-string,
            ...array<non-empty-string, non-empty-string>
        }",
        [
            'en_US' => 'Hello',
            'fr_FR' => '', // ❌ invalid value
        ]
    );
    
    // If the permissive type is enabled, the following will work:
    (new \CuyZ\Valinor\MapperBuilder())
        ->allowPermissiveTypes()
        ->mapper()
        ->map(
            'array{foo: string, ...}',
            ['foo' => 'foo', 'bar' => 'bar', 42 => 1337]
        ); // ✅
    

    Interface constructor registration

    By default, the mapper cannot instantiate an interface, as it does not know which implementation to use. To do so, the MapperBuilder::infer() method can be used, but it is cumbersome in most cases.

    It is now also possible to register a constructor for an interface, in the same way as for a class.

    Because the mapper cannot automatically guess which implementation can be used for an interface, it is not possible to use the Constructor attribute, the MapperBuilder::registerConstructor() method must be used instead.

    In the example below, the mapper is taught how to instantiate an implementation of UuidInterface from package ramsey/uuid:

    (new \CuyZ\Valinor\MapperBuilder())
        ->registerConstructor(
            // The static method below has return type `UuidInterface`;
            // therefore, the mapper will build an instance of `Uuid` when
            // it needs to instantiate an implementation of `UuidInterface`.
            Ramsey\Uuid\Uuid::fromString(...)
        )
        ->mapper()
        ->map(
            Ramsey\Uuid\UuidInterface::class,
            '663bafbf-c3b5-4336-b27f-1796be8554e0'
        );
    

    JSON normalizer formatting options

    By default, the JSON normalizer will only use JSON_THROW_ON_ERROR to encode non-boolean scalar values. There might be use-cases where projects will need flags like JSON_JSON_PRESERVE_ZERO_FRACTION.

    This can be achieved by passing these flags to the new JsonNormalizer::withOptions() method:

    namespace My\App;
    
    $normalizer = (new \CuyZ\Valinor\MapperBuilder())
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
        ->withOptions(\JSON_PRESERVE_ZERO_FRACTION);
    
    $lowerManhattanAsJson = $normalizer->normalize(
        new \My\App\Coordinates(
            longitude: 40.7128,
            latitude: -74.0000
        )
    );
    
    // `$lowerManhattanAsJson` is a valid JSON string representing the data:
    // {"longitude":40.7128,"latitude":-74.0000}
    

    The method accepts an int-mask of the following JSON_* constant representations:

    • JSON_HEX_QUOT
    • JSON_HEX_TAG
    • JSON_HEX_AMP
    • JSON_HEX_APOS
    • JSON_INVALID_UTF8_IGNORE
    • JSON_INVALID_UTF8_SUBSTITUTE
    • JSON_NUMERIC_CHECK
    • JSON_PRESERVE_ZERO_FRACTION
    • JSON_UNESCAPED_LINE_TERMINATORS
    • JSON_UNESCAPED_SLASHES
    • JSON_UNESCAPED_UNICODE

    JSON_THROW_ON_ERROR is always enforced and thus is not accepted.

    See official doc for more information: https://www.php.net/manual/en/json.constants.php

    Features

    • Allow JSON normalizer to set JSON formatting options (cd5df9)
    • Allow mapping to array-key type (5020d6)
    • Handle interface constructor registration (13f69a)
    • Handle type importation from interface (3af22d)
    • Introduce unsealed shaped array syntax (fa8bb0)

    Bug Fixes

    • Handle class tokens only when needed during lexing (c4be75)
    • Load needed information only during interface inferring (c8e204)

    Other

    • Rename internal class (4c62d8)
    Open source →
  26. 1.11.0 27 Mar 2024
    Release notes

    Changelog 1.11.0 — 27th of March 2024

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.11.0

    Notable changes

    Improvement of union types narrowing

    The algorithm used by the mapper to narrow a union type has been greatly improved, and should cover more edge-cases that would previously prevent the mapper from performing well.

    If an interface, a class or a shaped array is matched by the input, it will take precedence over arrays or scalars.

    (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(
            signature: 'array<int>|' . Color::class,
            source: [
                'red' => 255,
                'green' => 128,
                'blue' => 64,
            ],
        ); // Returns an instance of `Color`
    

    When superfluous keys are allowed, if the input matches several interfaces, classes or shaped array, the one with the most children node will be prioritized, as it is considered the most specific type:

    (new \CuyZ\Valinor\MapperBuilder())
        ->allowSuperfluousKeys()
        ->mapper()
        ->map(
            // Even if the first shaped array matches the input, the second one is
            // used because it's more specific.
            signature: 'array{foo: int}|array{foo: int, bar: int}',
            source: [
                'foo' => 42,
                'bar' => 1337,
            ],
        );
    

    If the input matches several types within the union, a collision will occur and cause the mapper to fail:

    (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(
            // Even if the first shaped array matches the input, the second one is
            // used because it's more specific.
            signature: 'array{red: int, green: int, blue: int}|' . Color::class,
            source: [
                'red' => 255,
                'green' => 128,
                'blue' => 64,
            ],
        );
    
    // ⚠️ Invalid value array{red: 255, green: 128, blue: 64}, it matches at
    //    least two types from union.
    

    Introducing AsTransformer attribute

    After the introduction of the Constructor attribute used for the mapper, the new AsTransformer attribute is now available for the normalizer to ease the registration of a transformer.

    namespace My\App;
    
    #[\CuyZ\Valinor\Normalizer\AsTransformer]
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    final class DateTimeFormat
    {
        public function __construct(private string $format) {}
    
        public function normalize(\DateTimeInterface $date): string
        {
            return $date->format($this->format);
        }
    }
    
    final readonly class Event
    {
        public function __construct(
            public string $eventName,
            #[\My\App\DateTimeFormat('Y/m/d')]
            public \DateTimeInterface $date,
        ) {}
    }
    
    (new \CuyZ\Valinor\MapperBuilder())
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
        ->normalize(new \My\App\Event(
            eventName: 'Release of legendary album',
            date: new \DateTimeImmutable('1971-11-08'),
        ));
    
    // [
    //     'eventName' => 'Release of legendary album',
    //     'date' => '1971/11/08',
    // ]
    

    Features

    • Improve union type narrowing during mapping (f73158)
    • Introduce AsTransformer attribute (13b6d0)

    Bug Fixes

    • Handle single array mapping when a superfluous value is present (86d021)
    • Properly handle ArrayObject normalization (4f555d)
    • Properly handle class type with matching name and namespace (0f5e96)
    • Properly handle nested unresolvable type during mapping (194706)
    • Strengthen type tokens extraction (c9dc97)

    Other

    • Reduce number of calls to class autoloader during type parsing (0f0e35)
    • Refactor generic types parsing and checking (ba6770)
    • Separate native type and docblock type for property and parameter (37993b)
    Open source →
  27. 1.10.0 12 Mar 2024
    Release notes

    Changelog 1.10.0 — 12th of March 2024

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.10.0

    Notable changes

    Dropping support for PHP 8.0

    PHP 8.0 security support has ended on the 26th of November 2023. Therefore, we are dropping support for PHP 8.0 in this version.

    If any security issue was to be found, we might consider backporting the fix to the 1.9.x version if people need it, but we strongly recommend upgrading your application to a supported PHP version.

    Introducing Constructor attribute

    A long awaited feature has landed in the library!

    The Constructor attribute can be assigned to any method inside an object, to automatically mark the method as a constructor for the class. This is a more convenient way of registering constructors than using the MapperBuilder::registerConstructor method, although it does not replace it.

    The method targeted by a Constructor attribute must be public, static and return an instance of the class it is part of.

    final readonly class Email
    {
        // When another constructor is registered for the class, the native
        // constructor is disabled. To enable it again, it is mandatory to
        // explicitly register it again.
        #[\CuyZ\Valinor\Mapper\Object\Constructor]
        public function __construct(public string $value) {}
    
        #[\CuyZ\Valinor\Mapper\Object\Constructor]
        public static function createFrom(
            string $userName, string $domainName
        ): self {
            return new self($userName . '@' . $domainName);
        }
    }
    
    (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(Email::class, [
            'userName' => 'john.doe',
            'domainName' => 'example.com',
        ]); // [email protected]
    

    Features

    • Introduce Constructor attribute (d86295)

    Bug Fixes

    • Properly encode scalar value in JSON normalization (2107ea)
    • Properly handle list type when input contains superfluous keys (1b8efa)

    Other

    • Drop support for PHP 8.0 (dafcc8)
    • Improve internal definitions string types (105281)
    • Refactor file system cache to improve performance (e692f0)
    • Remove unneeded closure conversion (972e65)
    • Update dependencies (c5627f)
    Open source →
  28. 1.9.0 02 Feb 2024
    Release notes

    Changelog 1.9.0 — 2nd of February 2024

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.9.0

    Notable changes

    JSON normalizer

    The normalizer is able to normalize a data structure to JSON without using the native json_encode() function.

    Using the normalizer instead of the native json_encode() function offers some benefits:

    • Values will be recursively normalized using the default transformations
    • All registered transformers will be applied to the data before it is formatted
    • The JSON can be streamed to a PHP resource in a memory-efficient way

    Basic usage:

    namespace My\App;
    
    $normalizer = (new \CuyZ\Valinor\MapperBuilder())
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::json());
    
    $userAsJson = $normalizer->normalize(
        new \My\App\User(
            name: 'John Doe',
            age: 42,
            country: new \My\App\Country(
                name: 'France',
                code: 'FR',
            ),
        )
    );
    
    // `$userAsJson` is a valid JSON string representing the data:
    // {"name":"John Doe","age":42,"country":{"name":"France","code":"FR"}}
    

    By default, the JSON normalizer will return a JSON string representing the data it was given. Instead of getting a string, it is possible to stream the JSON data to a PHP resource:

    $file = fopen('path/to/some_file.json', 'w');
    
    $normalizer = (new \CuyZ\Valinor\MapperBuilder())
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
        ->streamTo($file);
    
    $normalizer->normalize(/* … */);
    
    // The file now contains the JSON data
    

    Another benefit of streaming the data to a PHP resource is that it may be more memory-efficient when using generators — for instance when querying a database:

    // In this example, we assume that the result of the query below is a
    // generator, every entry will be yielded one by one, instead of
    // everything being loaded in memory at once.
    $users = $database->execute('SELECT * FROM users');
    
    $file = fopen('path/to/some_file.json', 'w');
    
    $normalizer = (new \CuyZ\Valinor\MapperBuilder())
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
        ->streamTo($file);
    
    // Even if there are thousands of users, memory usage will be kept low
    // when writing JSON into the file.
    $normalizer->normalize($users);
    

    Features

    • Introduce JSON normalizer (959740)

    Bug Fixes

    • Add default transformer for DateTimeZone (acf097)
    • Detect circular references linearly through objects (36aead)

    Other

    • Refactor attribute definition to include class definition (4b8cf6)
    Open source →
  29. 1.8.2 08 Jan 2024
    Release notes

    Changelog 1.8.2 — 8th of January 2024

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.8.2

    Bug Fixes

    • Allow callable type to be compiled (4a9771f)
    Open source →
  30. 1.8.1 08 Jan 2024
    Release notes

    Changelog 1.8.1 — 8th of January 2024

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.8.1

    Bug Fixes

    • Properly detect namespaced class in docblock (6f7c77)
    Open source →
  31. 1.8.0 26 Dec 2023
    Release notes

    Changelog 1.8.0 — 26th of December 2023

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.8.0

    Notable changes

    Normalizer service (serialization)

    This new service can be instantiated with the MapperBuilder. It allows transformation of a given input into scalar and array values, while preserving the original structure.

    This feature can be used to share information with other systems that use a data format (JSON, CSV, XML, etc.). The normalizer will take care of recursively transforming the data into a format that can be serialized.

    Below is a basic example, showing the transformation of objects into an array of scalar values.

    namespace My\App;
    
    $normalizer = (new \CuyZ\Valinor\MapperBuilder())
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::array());
    
    $userAsArray = $normalizer->normalize(
        new \My\App\User(
            name: 'John Doe',
            age: 42,
            country: new \My\App\Country(
                name: 'France',
                countryCode: 'FR',
            ),
        )
    );
    
    // `$userAsArray` is now an array and can be manipulated much more
    // easily, for instance to be serialized to the wanted data format.
    //
    // [
    //     'name' => 'John Doe',
    //     'age' => 42,
    //     'country' => [
    //         'name' => 'France',
    //         'countryCode' => 'FR',
    //     ],
    // ];
    

    A normalizer can be extended by using so-called transformers, which can be either an attribute or any callable object.

    In the example below, a global transformer is used to format any date found by the normalizer.

    (new \CuyZ\Valinor\MapperBuilder())
        ->registerTransformer(
            fn (\DateTimeInterface $date) => $date->format('Y/m/d')
        )
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
        ->normalize(
            new \My\App\Event(
                eventName: 'Release of legendary album',
                date: new \DateTimeImmutable('1971-11-08'),
            )
        );
    
    // [
    //     'eventName' => 'Release of legendary album',
    //     'date' => '1971/11/08',
    // ]
    

    This date transformer could have been an attribute for a more granular control, as shown below.

    namespace My\App;
    
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    final class DateTimeFormat
    {
        public function __construct(private string $format) {}
    
        public function normalize(\DateTimeInterface $date): string
        {
            return $date->format($this->format);
        }
    }
    
    final readonly class Event
    {
        public function __construct(
            public string $eventName,
            #[\My\App\DateTimeFormat('Y/m/d')]
            public \DateTimeInterface $date,
        ) {}
    }
    
    (new \CuyZ\Valinor\MapperBuilder())
        ->registerTransformer(\My\App\DateTimeFormat::class)
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
        ->normalize(
            new \My\App\Event(
                eventName: 'Release of legendary album',
                date: new \DateTimeImmutable('1971-11-08'),
            )
        );
    
    // [
    //     'eventName' => 'Release of legendary album',
    //     'date' => '1971/11/08',
    // ]
    

    More features are available, details about it can be found in the documentation.

    Features

    • Introduce normalizer service (1c9368)

    Bug Fixes

    • Allow leading zeros in numeric string in flexible mode (f000c1)
    • Allow mapping union of scalars and classes (4f4af0)
    • Properly handle single-namespaced classes (a53ef9)
    • Properly parse class name in same single-namespace (a462fe)
    Open source →
  32. 1.7.0 23 Oct 2023
    Release notes

    Changelog 1.7.0 — 23rd of October 2023

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.7.0

    Notable changes

    Non-positive integer

    Non-positive integer can be used as below. It will accept any value equal to or lower than zero.

    final class SomeClass
    {
        /** @var non-positive-int */
        public int $nonPositiveInteger;
    }
    

    Non-negative integer

    Non-negative integer can be used as below. It will accept any value equal to or greater than zero.

    final class SomeClass
    {
        /** @var non-negative-int */
        public int $nonNegativeInteger;
    }
    

    Features

    • Handle non-negative integer type (f444ea)
    • Handle non-positive integer type (53e404)

    Bug Fixes

    • Add missing @psalm-pure annotation to pure methods (004eb1)
    • Handle comments in classes when parsing types imports (3b663a)

    Other

    • Add comment for future PHP version change (461898)
    • Fix some typos (5cf8ae)
    • Make NativeBooleanType a BooleanType (d57ffa)
    Open source →
  33. 1.6.1 11 Oct 2023
    Release notes

    Changelog 1.6.1 — 11th of October 2023

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.6.1

    Bug Fixes

    • Correctly handle multiline type alias in classes (c23102)
    • Handle integer key in path mapping modifier (9419f6)
    • Handle variadic parameters declared in docblock (f4884c)
    Open source →
  34. 1.6.0 25 Aug 2023
    Release notes

    Changelog 1.6.0 — 25th of August 2023

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.6.0

    Notable changes

    Symfony Bundle

    A bundle is now available for Symfony applications, it will ease the integration and usage of the Valinor library in the framework. The documentation can be found in the CuyZ/Valinor-Bundle repository.

    Note that the documentation has been updated to add information about the bundle as well as tips on how to integrate the library in other frameworks.

    PHP 8.3 support

    Thanks to @TimWolla, the library now supports PHP 8.3, which entered its beta phase. Do not hesitate to test the library with this new version, and report any encountered issue on the repository.

    Better type parsing

    The first layer of the type parser has been completely rewritten. The previous one would use regex to split a raw type in tokens, but that led to limitations — mostly concerning quoted strings — that are now fixed.

    Although this change should not impact the end user, it is a major change in the library, and it is possible that some edge cases were not covered by tests. If that happens, please report any encountered issue on the repository.

    Example of previous limitations, now solved:

    // Union of strings containing space chars
    (new MapperBuilder())
        ->mapper()
        ->map(
            "'foo bar'|'baz fiz'",
            'baz fiz'
        );
    
    // Shaped array with special chars in the key
    (new MapperBuilder())
        ->mapper()
        ->map(
            "array{'some & key': string}",
            ['some & key' => 'value']
        );
    

    More advanced array-key handling

    It is now possible to use any string or integer as an array key. The following types are now accepted and will work properly with the mapper:

    $mapper->map("array<'foo'|'bar', string>", ['foo' => 'foo']);
    
    $mapper->map('array<42|1337, string>', [42 => 'foo']);
    
    $mapper->map('array<positive-int, string>', [42 => 'foo']);
    
    $mapper->map('array<negative-int, string>', [-42 => 'foo']);
    
    $mapper->map('array<int<-42, 1337>, string>', [42 => 'foo']);
    
    $mapper->map('array<non-empty-string, string>', ['foo' => 'foo']);
    
    $mapper->map('array<class-string, string>', ['SomeClass' => 'foo']);
    

    Features

    • Add support for PHP 8.3 (5c44f8)
    • Allow any string or integer in array key (12af3e)
    • Support microseconds in the Atom / RFC 3339 / ISO 8601 format (c25721)

    Bug Fixes

    • Correctly handle type inferring for method coming from interface (2657f8)
    • Detect missing closing bracket after comma in shaped array type (2aa4b6)
    • Handle class name collision while parsing types inside a class (044072)
    • Handle invalid Intl formats with intl.use_exceptions=1 (29da9a)
    • Improve cache warmup by creating required directories (a3341a)
    • Load attributes lazily during runtime and cache access (3e7c63)
    • Properly handle class/enum name in shaped array key (1964d4)

    Other

    • Improve attributes arguments compilation (c4acb1)
    • Replace regex-based type parser with character-based one (ae8303)
    • Simplify symbol parsing algorithm (f260cf)
    • Update Rector dependency (669ff9)
    Open source →
  35. 1.5.0 07 Aug 2023
    Release notes

    Changelog 1.5.0 — 7th of August 2023

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.5.0

    Features

    • Introduce method to get date formats supported during mapping (873961)

    Bug Fixes

    • Allow filesystem cache to be cleared when directory does not exist (782408)
    • Allow negative timestamp to be mapped to a datetime (d358e8)
    • Allow overriding of supported datetime formats (1c70c2)
    • Correctly handle message formatting for long truncated UTF8 strings (0a8f37)
    • Make serialization of attributes possible (e8ca2f)
    • Remove exception inheritance from UnresolvableType (eaa128)
    • Remove previous exception from UnresolvableType (5c89c6)

    Other

    • Avoid using unserialize when caching NULL default values (5e9b4c)
    • Catch json_encode exception to help identifying parsing errors (861c3b)
    • Update dependencies (c31e5c, 5fa107)
    Open source →
  36. 1.4.0 17 Apr 2023
    Release notes

    Changelog 1.4.0 — 17th of April 2023

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.4.0

    Notable changes

    Exception thrown when source is invalid

    JSON or YAML given to a source may be invalid, in which case an exception can now be caught and manipulated.

    try {
        $source = \CuyZ\Valinor\Mapper\Source\Source::json('invalid JSON');
    } catch (\CuyZ\Valinor\Mapper\Source\Exception\InvalidSource $error) {
        // Let the application handle the exception in the desired way.
        // It is possible to get the original source with `$error->source()`
    }
    

    Features

    • Introduce InvalidSource thrown when using invalid JSON/YAML (0739d1)

    Bug Fixes

    • Allow integer values in float types (c6df24)
    • Make array-key type match mixed (ccebf7)
    • Prevent infinite loop when class has parent class with same name (83eb05)

    Other

    • Add previous exception in various custom exceptions (b9e381)
    Open source →
  37. 1.3.1 13 Feb 2023
    Release notes

    Changelog 1.3.1 — 13th of February 2023

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.3.1

    Bugfix release.

    Bug Fixes

    • Check if temporary cache file exists before deletion (3177bf)
    • Display useful error message for invalid constructor return type (dc7f5c)
    • Keep input path when error occurs in single node (d70257)
    • Properly handle class static constructor for other class (d34974)
    • Properly handle union of null and objects (8f03a7)

    Other

    • Update dependencies (f7e7f2)
    Open source →
  38. 1.3.0 08 Feb 2023
    Release notes

    Changelog 1.3.0 — 8th of February 2023

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.3.0

    Notable changes

    Handle custom enum constructors registration

    It is now possible to register custom constructors for enum, the same way it could be done for classes.

    (new \CuyZ\Valinor\MapperBuilder())
        ->registerConstructor(
            // Allow the native constructor to be used
            SomeEnum::class,
    
            // Register a named constructor
            SomeEnum::fromMatrix(...)
        )
        ->mapper()
        ->map(SomeEnum::class, [
            'type' => 'FOO',
            'number' => 'BAR',
        ]);
    
    enum SomeEnum: string
    {
        case CASE_A = 'FOO_VALUE_1';
        case CASE_B = 'FOO_VALUE_2';
        case CASE_C = 'BAR_VALUE_1';
        case CASE_D = 'BAR_VALUE_2';
    
        /**
         * @param 'FOO'|'BAR' $type
         * @param int<1, 2> $number
         * /
        public static function fromMatrix(string $type, int $number): self
        {
            return self::from("{$type}_VALUE_{$number}");
        }
    }
    

    An enum constructor can be for a specific pattern:

    enum SomeEnum
    {
        case FOO;
        case BAR;
        case BAZ;
    }
    
    (new \CuyZ\Valinor\MapperBuilder())
        ->registerConstructor(
            /**
             * This constructor will be called only when pattern
             * `SomeEnum::BA*` is requested during mapping.
             *
             * @return SomeEnum::BA*
             */
            fn (string $value): SomeEnum => /* Some custom domain logic */
        )
        ->mapper()
        ->map(SomeEnum::class . '::BA*', 'some custom value');
    

    Note that this commit required heavy refactoring work, leading to a regression for union types containing enums and other types. As these cases are considered marginal, this change is considered non-breaking.

    Features

    • Handle custom enum constructors registration (217e12)

    Other

    • Handle enum type as class type (5a3caf)
    Open source →
  39. 1.2.0 09 Jan 2023
    Release notes

    Changelog 1.2.0 — 9th of January 2023

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.2.0

    Notable changes

    Handle single property/constructor argument with array input

    It is now possible, again, to use an array for a single node (single class property or single constructor argument), if this array has one value with a key matching the argument/property name.

    This is a revert of a change that was introduced in a previous commit: see hash 72cba320f582c7cda63865880a1cbf7ea292d2b1

    Features

    • Allow usage of array input for single node during mapping (686186)

    Bug Fixes

    • Do not re-validate single node with existing error (daaaac)

    Other

    • Remove unneeded internal check (86cca5)
    • Remove unneeded internal checks and exceptions (157723)
    Open source →
  40. 1.1.0 20 Dec 2022
    Release notes

    Changelog 1.1.0 — 20th of December 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.1.0

    Notable changes

    Handle class generic types inheritance

    It is now possible to use the @extends tag (already handled by PHPStan and Psalm) to declare the type of a parent class generic. This logic is recursively applied to all parents.

    /**
     * @template FirstTemplate
     * @template SecondTemplate
     */
    abstract class FirstClassWithGenerics
    {
        /** @var FirstTemplate */
        public $valueA;
    
        /** @var SecondTemplate */
        public $valueB;
    }
    
    /**
     * @template FirstTemplate
     * @extends FirstClassWithGenerics<FirstTemplate, int>
     */
    abstract class SecondClassWithGenerics extends FirstClassWithGenerics
    {
        /** @var FirstTemplate */
        public $valueC;
    }
    
    /**
     * @extends SecondClassWithGenerics<string>
     */
    final class ChildClass extends SecondClassWithGenerics
    {
    }
    
    $object = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(ChildClass::class, [
            'valueA' => 'foo',
            'valueB' => 1337,
            'valueC' => 'bar',
        ]);
    
    echo $object->valueA; // 'foo'
    echo $object->valueB; // 1337
    echo $object->valueC; // 'bar'
    

    Added support for class inferring

    It is now possible to infer abstract or parent classes the same way it can be done for interfaces.

    Example with an abstract class:

    abstract class SomeAbstractClass
    {
        public string $foo;
    
        public string $bar;
    }
    
    final class SomeChildClass extends SomeAbstractClass
    {
        public string $baz;
    }
    
    $result = (new \CuyZ\Valinor\MapperBuilder())
        ->infer(
            SomeAbstractClass::class,
            fn () => SomeChildClass::class
        )
        ->mapper()
        ->map(SomeAbstractClass::class, [
            'foo' => 'foo',
            'bar' => 'bar',
            'baz' => 'baz',
        ]);
    
    assert($result instanceof SomeChildClass);
    assert($result->foo === 'foo');
    assert($result->bar === 'bar');
    assert($result->baz === 'baz');
    

    Features

    • Add support for class inferring (5a90ad)
    • Handle class generic types inheritance (6506b7)

    Bug Fixes

    • Handle object return type in PHPStan extension (201728)
    • Import plugin class file in PHPStan configuration (58d540)
    • Keep nested errors when superfluous keys are detected (813b3b)

    Other

    • Adapt code with PHP 8.0 syntax (3fac3e)
    • Add isAbstract flag in class definition (ad0c06)
    • Add isFinal flag in class definition (25da31)
    • Enhance TreeMapper::map() return type signature (dc32d3)
    • Improve return type signature for TreeMapper (c8f362)
    • Prevent multiple cache round-trip (13b620)
    Open source →
  41. 1.0.0 27 Nov 2022
    Release notes

    Changelog 1.0.0 — 28th of November 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.0.0

    First stable version! 🥳 🎉

    This release marks the end of the initial development phase. The library has been live for exactly one year at this date and is stable enough to start following the semantic versioning — it means that any backward incompatible change (aka breaking change) will lead to a bump of the major version.

    This is the biggest milestone achieved by this project (yet™); I want to thank everyone who has been involved to make it possible, especially the contributors who submitted high-quality pull requests to improve the library.

    There is also one person that I want to thank even more: my best friend Nathan, who has always been so supportive with my side-projects. Thanks, bro! 🙌

    The last year marked a bigger investment of my time in OSS contributions; I've proven to myself that I am able to follow a stable way of managing my engagement to this community, and this is why I enabled sponsorship on my profile to allow people to ❤️ sponsor my work on GitHub — if you use this library in your applications, please consider offering me a 🍺 from time to time! 🤗

    Notable changes

    End of PHP 7.4 support

    PHP 7.4 security support has ended on the 28th of November 2022; the minimum version supported by this library is now PHP 8.0.

    New mapper to map arguments of a callable

    This new mapper can be used to ensure a source has the right shape before calling a function/method.

    The mapper builder can be configured the same way it would be with a tree mapper, for instance to customize the type strictness.

    $someFunction = function(string $foo, int $bar): string {
        return "$foo / $bar";
    };
    
    try {
        $arguments = (new \CuyZ\Valinor\MapperBuilder())
            ->argumentsMapper()
            ->mapArguments($someFunction, [
                'foo' => 'some value',
                'bar' => 42,
            ]);
    
        // some value / 42
        echo $someFunction(...$arguments);
    } catch (\CuyZ\Valinor\Mapper\MappingError $error) {
        // Do something…
    }
    

    Support for TimeZone objects

    Native TimeZone objects construction is now supported with a proper error handling.

    try {
        (new \CuyZ\Valinor\MapperBuilder())
            ->mapper()
            ->map(DateTimeZone::class, 'Jupiter/Europa');
    } catch (MappingError $exception) {
        $error = $exception->node()->messages()[0];
    
        // Value 'Jupiter/Europa' is not a valid timezone.
        echo $error->toString();
    }
    

    Mapping object with one property

    When a class needs only one value, the source given to the mapper must match the type of the single property/parameter.

    This change aims to bring consistency on how the mapper behaves when mapping an object that needs one argument. Before this change, the source could either match the needed type, or be an array with a single entry and a key named after the argument.

    See example below:

    final class Identifier
    {
        public readonly string $value;
    }
    
    final class SomeClass
    {
        public readonly Identifier $identifier;
    
        public readonly string $description;
    }
    
    (new \CuyZ\Valinor\MapperBuilder())->mapper()->map(SomeClass::class, [
        'identifier' => ['value' => 'some-identifier'], // ❌
        'description' => 'Lorem ipsum…',
    ]);
    
    (new \CuyZ\Valinor\MapperBuilder())->mapper()->map(SomeClass::class, [
        'identifier' => 'some-identifier', // ✅
        'description' => 'Lorem ipsum…',
    ]);
    

    Upgrading from 0.x to 1.0

    As this is a major release, all deprecated features have been removed, leading to an important number of breaking changes.

    You can click on the entries below to get advice on available replacements.

    ??? tip "Doctrine annotations support removal"

    Doctrine annotations cannot be used anymore, [PHP attributes] must be used.
    

    ??? tip "BackwardCompatibilityDateTimeConstructor class removal"

    You must use the method available in the mapper builder, see [dealing 
    with dates chapter].
    

    ??? tip "Mapper builder flexible method removal"

    The flexible has been split in three disctint modes, see [type strictness
    & flexibility chapter].
    

    ??? tip "Mapper builder withCacheDir method removal"

    You must now register a cache instance directly, see [performance & 
    caching chapter].
    

    ??? tip "StaticMethodConstructor class removal"

    You must now register the constructors using the mapper builder, see [custom
    object constructors chapter].
    

    ??? tip "Mapper builder bind method removal"

    You must now register the constructors using the mapper builder, see [custom
    object constructors chapter].
    

    ??? tip "ThrowableMessage class removal"

    You must now use the `MessageBuilder` class, see [error handling chapter].
    

    ??? tip "MessagesFlattener class removal"

    You must now use the `Messages` class, see [error handling chapter].
    

    ??? tip "TranslatableMessage class removal"

    You must now use the `HasParameters` class, see [custom exception chapter].
    

    ??? tip "Message methods removal"

    The following methods have been removed:
    
    - `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::name()`
    - `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::path()`
    - `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::type()`
    - `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::value()`
    - `\CuyZ\Valinor\Mapper\Tree\Node::value()`
    
    It is still possible to get the wanted values using the method
    `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::node()`.
    
    The placeholder `{original_value}` has also been removed, the same value can
    be fetched with `{source_value}`.
    

    ??? tip "PlaceHolderMessageFormatter class removal"

    Other features are available to format message, see [error messages
    customization chapter].
    

    ??? tip "Identifier attribute removal"

    This feature has been part of the library since its first public release,
    but it was never documented because it did not fit one of the library's main 
    philosophy which is to be almost entirely decoupled from an application's
    domain layer.
    
    The feature is entirely removed and not planned to be replaced by an
    alternative, unless the community really feels like there is a need for
    something alike.
    

    ⚠ BREAKING CHANGES

    • Disallow array when mapping to object with one argument (72cba3)
    • Mark tree mapper and arguments mapper as @pure (0d9855)
    • Remove deprecated backward compatibility datetime constructor (a65e8d)
    • Remove deprecated class ThrowableMessage (d36ca9)
    • Remove deprecated class to flatten messages (f9ed93)
    • Remove deprecated interface TranslatableMessage (ceb197)
    • Remove deprecated message methods (e6557d)
    • Remove deprecated method constructor attribute (d76467)
    • Remove deprecated method to enable flexible mode (a2bef3)
    • Remove deprecated method to set cache directory (b0d6d2)
    • Remove deprecated method used to bind a callback (b79ed8)
    • Remove deprecated placeholder message formatter (c2723d)
    • Remove Doctrine annotations support (66c182)
    • Remove identifier attribute (8a7486)
    • Remove PHP 7.4 support (5f5a50)
    • Remove support for strict-array type (22c3b4)

    Features

    • Add constructor for DateTimeZone with error support (a0a4d6)
    • Introduce mapper to map arguments of a callable (9c7e88)

    Bug Fixes

    • Allow mapping null to single node nullable type (0a98ec)
    • Handle single argument mapper properly (d7bf6a)
    • Handle tree mapper call without argument in PHPStan extension (3f3a01)
    • Handle tree mapper call without argument in Psalm plugin (b425af)

    Other

    • Activate value altering feature only when callbacks are registered (0f33a5)
    • Bump psr/simple-cache supported version (e4059a)
    • Remove @ from comments for future PHP versions changes (68774c)
    • Update dependencies (4afcda)
    Open source →
  42. 0.17.1 18 Jan 2023

    Nothing published for this version

  43. 0.17.0 08 Nov 2022
    Release notes

    Changelog 0.17.0 — 8th of November 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.17.0

    Notable changes

    The main feature introduced in this release is the split of the flexible mode in three distinct modes:

    1. The flexible casting

      Changes the behaviours explained below:

      $flexibleMapper = (new \CuyZ\Valinor\MapperBuilder())
          ->enableFlexibleCasting()
          ->mapper();
      
      // ---
      // Scalar types will accept non-strict values; for instance an
      // integer type will accept any valid numeric value like the
      // *string* "42".
      
      $flexibleMapper->map('int', '42');
      // => 42
      
      // ---
      // List type will accept non-incremental keys.
      
      $flexibleMapper->map('list<int>', ['foo' => 42, 'bar' => 1337]);
      // => [0 => 42, 1 => 1338]
      
      // ---
      // If a value is missing in a source for a node that accepts `null`,
      // the node will be filled with `null`.
      
      $flexibleMapper->map(
          'array{foo: string, bar: null|string}',
          ['foo' => 'foo'] // `bar` is missing
      );
      // => ['foo' => 'foo', 'bar' => null]
      
      // ---
      // Array and list types will convert `null` or missing values to an
      // empty array.
      
      $flexibleMapper->map(
          'array{foo: string, bar: array<string>}',
          ['foo' => 'foo'] // `bar` is missing
      );
      // => ['foo' => 'foo', 'bar' => []]
      
    2. The superfluous keys

      Superfluous keys in source arrays will be allowed, preventing errors when a value is not bound to any object property/parameter or shaped array element.

      (new \CuyZ\Valinor\MapperBuilder())
          ->allowSuperfluousKeys()
          ->mapper()
          ->map(
              'array{foo: string, bar: int}',
              [
                  'foo' => 'foo',
                  'bar' => 42,
                  'baz' => 1337.404, // `baz` will be ignored
              ]
          );
      
    3. The permissive types

      Allows permissive types mixed and object to be used during mapping.

      (new \CuyZ\Valinor\MapperBuilder())
          ->allowPermissiveTypes()
          ->mapper()
          ->map(
              'array{foo: string, bar: mixed}',
              [
                  'foo' => 'foo',
                  'bar' => 42, // Could be any value
              ]
          );
      

    Features

    • Add support for strict-array type (d456eb)
    • Introduce new callback message formatter (93f898)
    • Introduce new helper class to list messages (513827)
    • Split mapper flexible mode in three distinct modes (549e5f)

    Bug Fixes

    • Allow missing and null value for array node in flexible mode (034f1c)
    • Allow missing value for shaped array nullable node in flexible mode (08fb0e)
    • Handle scalar value casting in union types only in flexible mode (752ad9)

    Other

    • Do not use uniqid() (b81847)
    • Transform missing source value to null in flexible mode (92a41a)
    Open source →
  44. 0.16.0 19 Oct 2022
    Release notes

    Changelog 0.16.0 — 19th of October 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.16.0

    Features

    • Add support for PHP 8.2 (a92360)

    Bug Fixes

    • Properly handle quote char in type definition (c71d6a)

    Other

    • Update dependencies (c2de32)
    Open source →
  45. 0.15.0 06 Oct 2022
    Release notes

    Changelog 0.15.0 — 6th of October 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.15.0

    Notable changes

    Two similar features are introduced in this release: constants and enums wildcard notations. This is mainly useful when several cases of an enum or class constants share a common prefix.

    Example for class constants:

    final class SomeClassWithConstants
    {
        public const FOO = 1337;
    
        public const BAR = 'bar';
    
        public const BAZ = 'baz';
    }
    
    $mapper = (new MapperBuilder())->mapper();
    
    $mapper->map('SomeClassWithConstants::BA*', 1337); // error
    $mapper->map('SomeClassWithConstants::BA*', 'bar'); // ok
    $mapper->map('SomeClassWithConstants::BA*', 'baz'); // ok
    

    Example for enum:

    enum SomeEnum: string
    {
        case FOO = 'foo';
        case BAR = 'bar';
        case BAZ = 'baz';
    }
    
    $mapper = (new MapperBuilder())->mapper();
    
    $mapper->map('SomeEnum::BA*', 'foo'); // error
    $mapper->map('SomeEnum::BA*', 'bar'); // ok
    $mapper->map('SomeEnum::BA*', 'baz'); // ok
    

    Features

    • Add support for class constant type (1244c2)
    • Add support for wildcard in enumeration type (69ebd1)
    • Introduce utility class to build messages (cb8792)

    Bug Fixes

    • Add return types for cache implementations (0e8f12)
    • Correctly handle type inferring during mapping (37f96f)
    • Fetch correct node value for children (3ee526)
    • Improve scalar values casting (212b77)
    • Properly handle static anonymous functions (c009ab)

    Other

    • Import namespace token parser inside library (0b8ca9)
    • Remove unused code (b2889a, de8aa9)
    • Save type token symbols during lexing (ad0f8f)
    Open source →
  46. 0.14.0 01 Sep 2022
    Release notes

    Changelog 0.14.0 — 1st of September 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.14.0

    Notable changes

    Until this release, the behaviour of the date objects creation was very opinionated: a huge list of date formats were tested out, and if one was working it was used to create the date.

    This approach resulted in two problems. First, it led to (minor) performance issues, because a lot of date formats were potentially tested for nothing. More importantly, it was not possible to define which format(s) were to be allowed (and in result deny other formats).

    A new method can now be used in the MapperBuilder:

    (new \CuyZ\Valinor\MapperBuilder())
        // Both `Cookie` and `ATOM` formats will be accepted
        ->supportDateFormats(DATE_COOKIE, DATE_ATOM)
        ->mapper()
        ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');
    

    Please note that the old behaviour has been removed. From now on, only valid timestamp or ATOM-formatted value will be accepted by default.

    If needed and to help with the migration, the following deprecated constructor can be registered to reactivate the previous behaviour:

    (new \CuyZ\Valinor\MapperBuilder())
        ->registerConstructor(
            new \CuyZ\Valinor\Mapper\Object\BackwardCompatibilityDateTimeConstructor()
        )
        ->mapper()
        ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');
    

    ⚠ BREAKING CHANGES

    • Introduce constructor for custom date formats (f232cc)

    Features

    • Handle abstract constructor registration (c37ac1)
    • Introduce attribute DynamicConstructor (e437d9)
    • Introduce helper method to describe supported date formats (11a7ea)

    Bug Fixes

    • Allow trailing comma in shaped array (bf445b)
    • Correctly fetch file system cache entries (48208c)
    • Detect invalid constructor handle type (b3cb59)
    • Handle classes in a case-sensitive way in type parser (254074)
    • Handle concurrent cache file creation (fd39ae)
    • Handle inherited private constructor in class definition (73b622)
    • Handle invalid nodes recursively (a401c2)
    • Prevent illegal characters in PSR-16 cache keys (3c4d29)
    • Properly handle callable objects of the same class (ae7ddc)

    Other

    • Add singleton usage of ClassStringType (4bc50e)
    • Change ObjectBuilderFactory::for return signature (57849c)
    • Extract native constructor object builder (2b46a6)
    • Fetch attributes for function definition (ec494c)
    • Refactor arguments instantiation (6414e9)
    Open source →
  47. 0.13.0 31 Jul 2022
    Release notes

    Changelog 0.13.0 — 31st of July 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.13.0

    Notable changes

    Reworking of messages body and parameters features

    The \CuyZ\Valinor\Mapper\Tree\Message\Message interface is no longer a Stringable, however it defines a new method body that must return the body of the message, which can contain placeholders that will be replaced by parameters.

    These parameters can now be defined by implementing the interface \CuyZ\Valinor\Mapper\Tree\Message\HasParameters.

    This leads to the deprecation of the no longer needed interface \CuyZ\Valinor\Mapper\Tree\Message\TranslatableMessage which had a confusing name.

    final class SomeException
        extends DomainException 
        implements ErrorMessage, HasParameters, HasCode
    {
        private string $someParameter;
    
        public function __construct(string $someParameter)
        {
            parent::__construct();
    
            $this->someParameter = $someParameter;
        }
    
        public function body() : string
        {
            return 'Some message / {some_parameter} / {source_value}';
        }
    
        public function parameters(): array
        {
            return [
                'some_parameter' => $this->someParameter,
            ];
        }
    
        public function code() : string
        {
            // A unique code that can help to identify the error
            return 'some_unique_code';
        }
    }
    

    Handle numeric-string type

    The new numeric-string type can be used in docblocks.

    It will accept any string value that is also numeric.

    (new MapperBuilder())->mapper()->map('numeric-string', '42'); // ✅
    (new MapperBuilder())->mapper()->map('numeric-string', 'foo'); // ❌
    

    Better mapping error message

    The message of the exception will now contain more information, especially the total number of errors and the source that was given to the mapper. This change aims to have a better understanding of what is wrong when debugging.

    Before:

    Could not map type `array{foo: string, bar: int}` with the given source.

    After:

    Could not map type `array{foo: string, bar: int}`. An error occurred at path bar: Value 'some other string' does not match type `int`.

    ⚠ BREAKING CHANGES

    • Rework messages body and parameters features (ad1207)

    Features

    • Allow to declare parameter for message (f61eb5)
    • Display more information in mapping error message (9c1e7c)
    • Handle numeric string type (96a493)
    • Make MessagesFlattener countable (2c1c7c)

    Bug Fixes

    • Handle native attribute on promoted parameter (897ca9)

    Other

    • Add fixed value for root node path (0b37b4)
    • Remove types stringable behavior (b47a1b)
    Open source →
  48. 0.12.0 10 Jul 2022
    Release notes

    Changelog 0.12.0 — 10th of July 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.12.0

    Notable changes

    SECURITY — Userland exception filtering

    See advisory GHSA-5pgm-3j3g-2rc7 for more information.

    Userland exception thrown in a constructor will not be automatically caught by the mapper anymore. This prevents messages with sensible information from reaching the final user — for instance an SQL exception showing a part of a query.

    To allow exceptions to be considered as safe, the new method MapperBuilder::filterExceptions() must be used, with caution.

    final class SomeClass
    {
        public function __construct(private string $value)
        {
            \Webmozart\Assert\Assert::startsWith($value, 'foo_');
        }
    }
    
    try {
        (new \CuyZ\Valinor\MapperBuilder())
            ->filterExceptions(function (Throwable $exception) {
                if ($exception instanceof \Webmozart\Assert\InvalidArgumentException) {
                    return \CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage::from($exception);
                }
    
                // If the exception should not be caught by this library, it
                // must be thrown again.
                throw $exception;
            })
            ->mapper()
            ->map(SomeClass::class, 'bar_baz');
    } catch (\CuyZ\Valinor\Mapper\MappingError $exception) {
        // Should print something similar to:
        // > Expected a value to start with "foo_". Got: "bar_baz"
        echo $exception->node()->messages()[0];
    }
    

    Tree node API rework

    The class \CuyZ\Valinor\Mapper\Tree\Node has been refactored to remove access to unwanted methods that were not supposed to be part of the public API. Below are a list of all changes:

    • New methods $node->sourceFilled() and $node->sourceValue() allow accessing the source value.

    • The method $node->value() has been renamed to $node->mappedValue() and will throw an exception if the node is not valid.

    • The method $node->type() now returns a string.

    • The methods $message->name(), $message->path(), $message->type() and $message->value() have been deprecated in favor of the new method $message->node().

    • The message parameter {original_value} has been deprecated in favor of {source_value}.

    Access removal of several parts of the library public API

    The access to class/function definition, types and exceptions did not add value to the actual goal of the library. Keeping these features under the public API flag causes more maintenance burden whereas revoking their access allows more flexibility with the overall development of the library.

    ⚠ BREAKING CHANGES

    • Filter userland exceptions to hide potential sensible data (6ce1a4)
    • Refactor tree node API (d3b1dc)
    • Remove API access from several parts of library (316d91)
    • Remove node visitor feature (63c87a)

    Bug Fixes

    • Handle inferring methods with same names properly (dc45dd)
    • Process invalid type default value as unresolvable type (7c9ac1)
    • Properly display unresolvable type (3020db)

    Other

    • Ignore .idea folder (84ead0)
    Open source →
  49. 0.11.0 23 Jun 2022
    Release notes

    Changelog 0.11.0 — 23rd of June 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.11.0

    Notable changes

    Strict mode

    The mapper is now more type-sensitive and will fail in the following situations:

    • When a value does not match exactly the awaited scalar type, for instance a string "42" given to a node that awaits an integer.

    • When unnecessary array keys are present, for instance mapping an array ['foo' => …, 'bar' => …, 'baz' => …] to an object that needs only foo and bar.

    • When permissive types like mixed or object are encountered.

    These limitations can be bypassed by enabling the flexible mode:

    (new \CuyZ\Valinor\MapperBuilder())
        ->flexible()
        ->mapper();
        ->map('array{foo: int, bar: bool}', [
            'foo' => '42', // Will be cast from `string` to `int`
            'bar' => 'true', // Will be cast from `string` to `bool`
            'baz' => '…', // Will be ignored
        ]);
    

    When using this library for a provider application — for instance an API endpoint that can be called with a JSON payload — it is recommended to use the strict mode. This ensures that the consumers of the API provide the exact awaited data structure, and prevents unknown values to be passed.

    When using this library as a consumer of an external source, it can make sense to enable the flexible mode. This allows for instance to convert string numeric values to integers or to ignore data that is present in the source but not needed in the application.

    Interface inferring

    It is now mandatory to list all possible class-types that can be inferred by the mapper. This change is a step towards the library being able to deliver powerful new features such as compiling a mapper for better performance.

    The existing calls to MapperBuilder::infer that could return several class-names must now add a signature to the callback. The callbacks that require no parameter and always return the same class-name can remain unchanged.

    For instance:

    $builder = (new \CuyZ\Valinor\MapperBuilder())
        // Can remain unchanged
        ->infer(SomeInterface::class, fn () => SomeImplementation::class);
    
    $builder = (new \CuyZ\Valinor\MapperBuilder())
        ->infer(
            SomeInterface::class,
            fn (string $type) => match($type) {
                'first' => ImplementationA::class,
                'second' => ImplementationB::class,
                default => throw new DomainException("Unhandled `$type`.")
            }
        )
        // …should be modified with:
        ->infer(
            SomeInterface::class,
            /** @return class-string<ImplementationA|ImplementationB> */
            fn (string $type) => match($type) {
                'first' => ImplementationA::class,
                'second' => ImplementationB::class,
                default => throw new DomainException("Unhandled `$type`.")
            }
        );
    

    Object constructors collision

    All these changes led to a new check that runs on all registered object constructors. If a collision is found between several constructors that have the same signature (the same parameter names), an exception will be thrown.

    final class SomeClass
    {
        public static function constructorA(string $foo, string $bar): self
        {
            // …
        }
    
        public static function constructorB(string $foo, string $bar): self
        {
            // …
        }
    }
    
    (new \CuyZ\Valinor\MapperBuilder())
        ->registerConstructor(
            SomeClass::constructorA(...),
            SomeClass::constructorB(...),
        )
        ->mapper();
        ->map(SomeClass::class, [
            'foo' => 'foo',
            'bar' => 'bar',
        ]);
    
    // Exception: A collision was detected […]
    

    ⚠ BREAKING CHANGES

    • Handle exhaustive list of interface inferring (1b0ff3)
    • Make mapper more strict and allow flexible mode (90dc58)

    Features

    • Improve cache warmup (44c5f1)
    Open source →
  50. 0.10.0 10 Jun 2022
    Release notes

    Changelog 0.10.0 — 10th of June 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.10.0

    Notable changes

    Documentation is now available at valinor.cuyz.io.

    Features

    • Support mapping to dates with no time (e0a529)

    Bug Fixes

    • Allow declaring promoted parameter type with @var annotation (d8eb4d)
    • Allow mapping iterable to shaped array (628baf)
    Open source →
  51. 0.9.0 23 May 2022
    Release notes

    Changelog 0.9.0 — 23rd of May 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.9.0

    Notable changes

    Cache injection and warmup

    The cache feature has been revisited, to give more control to the user on how and when to use it.

    The method MapperBuilder::withCacheDir() has been deprecated in favor of a new method MapperBuilder::withCache() which accepts any PSR-16 compliant implementation.

    Warning

    These changes lead up to the default cache not being automatically registered anymore. If you still want to enable the cache (which you should), you will have to explicitly inject it (see below).

    A default implementation is provided out of the box, which saves cache entries into the file system.

    When the application runs in a development environment, the cache implementation should be decorated with FileWatchingCache, which will watch the files of the application and invalidate cache entries when a PHP file is modified by a developer — preventing the library not behaving as expected when the signature of a property or a method changes.

    The cache can be warmed up, for instance in a pipeline during the build and deployment of the application — kudos to @boesing for the feature!

    Note The cache has to be registered first, otherwise the warmup will end up being useless.

    $cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-directory');
    
    if ($isApplicationInDevelopmentEnvironment) {
        $cache = new \CuyZ\Valinor\Cache\FileWatchingCache($cache);
    }
    
    $mapperBuilder = (new \CuyZ\Valinor\MapperBuilder())->withCache($cache);
    
    // During the build:
    $mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class);
    
    // In the application:
    $mapperBuilder->mapper()->map(SomeClass::class, [/* … */]);
    

    Message formatting & translation

    Major changes have been made to the messages being returned in case of a mapping error: the actual texts are now more accurate and show better information.

    Warning

    The method NodeMessage::format has been removed, message formatters should be used instead. If needed, the old behaviour can be retrieved with the formatter PlaceHolderMessageFormatter, although it is strongly advised to use the new placeholders feature (see below).

    The signature of the method MessageFormatter::format has changed as well.

    It is now also easier to format the messages, for instance when they need to be translated. Placeholders can now be used in a message body, and will be replaced with useful information.

    Placeholder Description
    {message_code} the code of the message
    {node_name} name of the node to which the message is bound
    {node_path} path of the node to which the message is bound
    {node_type} type of the node to which the message is bound
    {original_value} the source value that was given to the node
    {original_message} the original message before being customized
    try {
        (new \CuyZ\Valinor\MapperBuilder())
            ->mapper()
            ->map(SomeClass::class, [/* … */]);
    } catch (\CuyZ\Valinor\Mapper\MappingError $error) {
        $node = $error->node();
        $messages = new \CuyZ\Valinor\Mapper\Tree\Message\MessagesFlattener($node);
    
        foreach ($messages as $message) {
            if ($message->code() === 'some_code') {
                $message = $message->withBody('new message / {original_message}');
            }
    
            echo $message;
        }
    }
    

    The messages are formatted using the ICU library, enabling the placeholders to use advanced syntax to perform proper translations, for instance currency support.

    try {
        (new \CuyZ\Valinor\MapperBuilder())->mapper()->map('int<0, 100>', 1337);
    } catch (\CuyZ\Valinor\Mapper\MappingError $error) {
        $message = $error->node()->messages()[0];
    
        if (is_numeric($message->value())) {
            $message = $message->withBody(
                'Invalid amount {original_value, number, currency}'
            );    
        } 
    
        // Invalid amount: $1,337.00
        echo $message->withLocale('en_US');
        
        // Invalid amount: £1,337.00
        echo $message->withLocale('en_GB');
        
        // Invalid amount: 1 337,00 €
        echo $message->withLocale('fr_FR');
    }
    

    See ICU documentation for more information on available syntax.

    Warning If the intl extension is not installed, a shim will be available to replace the placeholders, but it won't handle advanced syntax as described above.

    The formatter TranslationMessageFormatter can be used to translate the content of messages.

    The library provides a list of all messages that can be returned; this list can be filled or modified with custom translations.

    \CuyZ\Valinor\Mapper\Tree\Message\Formatter\TranslationMessageFormatter::default()
        // Create/override a single entry…
        ->withTranslation('fr', 'some custom message', 'un message personnalisé')
        // …or several entries.
        ->withTranslations([
            'some custom message' => [
                'en' => 'Some custom message',
                'fr' => 'Un message personnalisé',
                'es' => 'Un mensaje personalizado',
            ], 
            'some other message' => [
                // …
            ], 
        ])
        ->format($message);
    

    It is possible to join several formatters into one formatter by using the AggregateMessageFormatter. This instance can then easily be injected in a service that will handle messages.

    The formatters will be called in the same order they are given to the aggregate.

    (new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\AggregateMessageFormatter(
        new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\LocaleMessageFormatter('fr'),
        new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\MessageMapFormatter([
            // …
        ],
        \CuyZ\Valinor\Mapper\Tree\Message\Formatter\TranslationMessageFormatter::default(),
    ))->format($message)
    

    ⚠ BREAKING CHANGES

    • Improve message customization with formatters (60a665)
    • Revoke ObjectBuilder API access (11e126)

    Features

    • Allow injecting a cache implementation that is used by the mapper (69ad3f)
    • Extract file watching feature in own cache implementation (2d70ef)
    • Improve mapping error messages (05cf4a)
    • Introduce method to warm the cache up (ccf09f)

    Bug Fixes

    • Make interface type match undefined object type (105eef)

    Other

    • Change InvalidParameterIndex exception inheritance type (b75adb)
    • Introduce layer for object builder arguments (48f936)
    Open source →
  52. 0.8.0 09 May 2022
    Release notes

    Changelog 0.8.0 — 9th of May 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.8.0

    Notable changes

    Float values handling

    Allows the usage of float values, as follows:

    class Foo
    {
        /** @var 404.42|1337.42 */
        public readonly float $value;
    }
    

    Literal boolean true / false values handling

    Thanks @danog for this feature!

    Allows the usage of boolean values, as follows:

    class Foo
    {
        /** @var int|false */
        public readonly int|bool $value;
    }
    

    Class string of union of object handling

    Allows to declare several class names in a class-string:

    class Foo
    {
        /** @var class-string<SomeClass|SomeOtherClass> */
        public readonly string $className;
    }
    

    Allow psalm and phpstan prefix in docblocks

    Thanks @boesing for this feature!

    The following annotations are now properly handled: @psalm-param, @phpstan-param, @psalm-return and @phpstan-return.

    If one of those is found along with a basic @param or @return annotation, it will take precedence over the basic value.

    Features

    • Allow psalm and phpstan prefix in docblocks (64e0a2)
    • Handle class string of union of object (b7923b)
    • Handle filename in function definition (0b042b)
    • Handle float value type (790df8)
    • Handle literal boolean true / false types (afcedf)
    • Introduce composite types (892f38)

    Bug Fixes

    • Call value altering function only if value is accepted (2f08e1)
    • Handle function definition cache invalidation when file is modified (511a0d)

    Other

    • Add configuration for Composer allowed plugins (2f310c)
    • Add Psalm configuration file to .gitattributes (979272)
    • Bump dev-dependencies (844384)
    • Declare code type in docblocks (03c84a)
    • Ignore Polyfill coverage (c08fe5)
    • Remove symfony/polyfill-php80 dependency (368737)
    Open source →
  53. 0.7.0 24 Mar 2022
    Release notes

    Changelog 0.7.0 — 24th of March 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.7.0

    Notable changes

    Warning This release introduces a major breaking change that must be considered before updating

    Constructor registration

    The automatic named constructor discovery has been disabled. It is now mandatory to explicitly register custom constructors that can be used by the mapper.

    This decision was made because of a security issue reported by @Ocramius and described in advisory advisory GHSA-xhr8-mpwq-2rr2.

    As a result, existing code must list all named constructors that were previously automatically used by the mapper, and registerer them using the method MapperBuilder::registerConstructor().

    The method MapperBuilder::bind() has been deprecated in favor of the method above that should be used instead.

    final class SomeClass
    {
        public static function namedConstructor(string $foo): self
        {
            // …
        }
    }
    
    (new \CuyZ\Valinor\MapperBuilder())
        ->registerConstructor(
            SomeClass::namedConstructor(...),
            // …or for PHP < 8.1:
            [SomeClass::class, 'namedConstructor'],
        )
        ->mapper()
        ->map(SomeClass::class, [
            // …
        ]);
    

    See documentation for more information.


    Source builder

    The Source class is a new entry point for sources that are not plain array or iterable. It allows accessing other features like camel-case keys or custom paths mapping in a convenient way.

    It should be used as follows:

    $source = \CuyZ\Valinor\Mapper\Source\Source::json($jsonString)
        ->camelCaseKeys()
        ->map([
            'towns' => 'cities',
            'towns.*.label' => 'name',
        ]);
    
    $result = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(SomeClass::class, $source);
    

    See documentation for more details about its usage.

    ⚠ BREAKING CHANGES

    • Change Attributes::ofType return type to array (1a599b)
    • Introduce method to register constructors used during mapping (ecafba)

    Features

    • Introduce a path-mapping source modifier (b7a7d2)
    • Introduce a source builder (ad5103)

    Bug Fixes

    • Handle numeric key with camel case source key modifier (b8a18f)
    • Handle parameter default object value compilation (fdef93)
    • Handle variadic arguments in callable constructors (b646cc)
    • Properly handle alias types for function reflection (e5b515)

    Other

    • Add Striker HTML report when running infection (79c7a4)
    • Handle class name in function definition (e2451d)
    • Introduce functions container to wrap definition handling (fd1117)
    Open source →
  54. 0.6.0 24 Feb 2022
    Release notes

    Changelog 0.6.0 — 24th of February 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.6.0

    ⚠ BREAKING CHANGES

    • Improve interface inferring API (1eb6e6)
    • Improve object binding API (6d4270)

    Features

    • Handle variadic parameters in constructors (b6b329)
    • Improve value altering API (422e6a)
    • Introduce a camel case source key modifier (d94652)
    • Introduce function definition repository (b49ebf)
    • Introduce method to get parameter by index (380961)

    Bug Fixes

    • Change license in composer.json (6fdd62)
    • Ensure native mixed types remain valid (18ccbe)
    • Remove string keys when unpacking variadic parameter values (cbf4e1)
    • Transform exception thrown during object binding into a message (359e32)
    • Write temporary cache file inside cache subdirectory (1b80a1)

    Other

    • Check value acceptance in separate node builder (30d447)
    • Narrow union types during node build (06e9de)
    Open source →
  55. 0.5.0 26 Jan 2022
    Release notes

    Changelog 0.5.0 — 21st of January 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.5.0

    Features

    • Introduce automatic named constructor resolution (718d3c)
    • Set up dependabot for automated weekly dependency upgrades (23b611)
    • Simplify type signature of TreeMapper#map() (e28003)

    Bug Fixes

    • Correct regex that detects @internal or @api annotations (39f0b7)
    • Improve type definitions to allow Psalm automatic inferring (f9b04c)
    • Return indexed list of attributes when filtering on type (66aa4d)
    Open source →
  56. 0.4.0 07 Jan 2022
    Release notes

    Changelog 0.4.0 — 7th of January 2022

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.4.0

    Notable changes

    Allow mapping to any type

    Previously, the method TreeMapper::map would allow mapping only to an object. It is now possible to map to any type handled by the library.

    It is for instance possible to map to an array of objects:

    $objects = (new MapperBuilder())->mapper()->map(
        'array<' . SomeClass::class . '>',
        [/* … */]
    );
    

    For simple use-cases, an array shape can be used:

    $array = (new MapperBuilder())->mapper()->map(
        'array{foo: string, bar: int}',
        [/* … */]
    );
    
    echo $array['foo'];
    echo $array['bar'] * 2;
    

    This new feature changes the possible behaviour of the mapper, meaning static analysis tools need help to understand the types correctly. An extension for PHPStan and a plugin for Psalm are now provided and can be included in a project to automatically increase the type coverage.


    Better handling of messages

    When working with messages, it can sometimes be useful to customize the content of a message — for instance to translate it.

    The helper class \CuyZ\Valinor\Mapper\Tree\Message\Formatter\MessageMapFormatter can be used to provide a list of new formats. It can be instantiated with an array where each key represents either:

    • The code of the message to be replaced
    • The content of the message to be replaced
    • The class name of the message to be replaced

    If none of those is found, the content of the message will stay unchanged unless a default one is given to the class.

    If one of these keys is found, the array entry will be used to replace the content of the message. This entry can be either a plain text or a callable that takes the message as a parameter and returns a string; it is for instance advised to use a callable in cases where a translation service is used — to avoid useless greedy operations.

    In any case, the content can contain placeholders that will automatically be replaced by, in order:

    1. The original code of the message
    2. The original content of the message
    3. A string representation of the node type
    4. The name of the node
    5. The path of the node
    try {
        (new \CuyZ\Valinor\MapperBuilder())
            ->mapper()
            ->map(SomeClass::class, [/* … */]);
    } catch (\CuyZ\Valinor\Mapper\MappingError $error) {
        $node = $error->node();
        $messages = new \CuyZ\Valinor\Mapper\Tree\Message\MessagesFlattener($node);
    
        $formatter = (new MessageMapFormatter([
            // Will match if the given message has this exact code
            'some_code' => 'new content / previous code was: %1$s',
        
            // Will match if the given message has this exact content
            'Some message content' => 'new content / previous message: %2$s',
        
            // Will match if the given message is an instance of `SomeError`
            SomeError::class => '
                - Original code of the message: %1$s
                - Original content of the message: %2$s
                - Node type: %3$s
                - Node name: %4$s
                - Node path: %5$s
            ',
        
            // A callback can be used to get access to the message instance
            OtherError::class => function (NodeMessage $message): string {
                if ((string)$message->type() === 'string|int') {
                    // …
                }
        
                return 'Some message content';
            },
        
            // For greedy operation, it is advised to use a lazy-callback
            'bar' => fn () => $this->translator->translate('foo.bar'),
        ]))
            ->defaultsTo('some default message')
            // …or…
            ->defaultsTo(fn () => $this->translator->translate('default_message'));
    
        foreach ($messages as $message) {
            echo $formatter->format($message);    
        }
    }
    

    Automatic union of objects inferring during mapping

    When the mapper needs to map a source to a union of objects, it will try to guess which object it will map to, based on the needed arguments of the objects, and the values contained in the source.

    final class UnionOfObjects
    {
        public readonly SomeFooObject|SomeBarObject $object;
    }
    
    final class SomeFooObject
    {
        public readonly string $foo;
    }
    
    final class SomeBarObject
    {
        public readonly string $bar;
    }
    
    // Will map to an instance of `SomeFooObject`
    (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(UnionOfObjects::class, ['foo' => 'foo']);
    
    // Will map to an instance of `SomeBarObject`
    (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(UnionOfObjects::class, ['bar' => 'bar']);
    

    ⚠ BREAKING CHANGES

    • Add access to root node when error occurs during mapping (54f608)
    • Allow mapping to any type (b2e810)
    • Allow object builder to yield arguments without source (8a7414)
    • Wrap node messages in proper class (a805ba)

    Features

    • Introduce automatic union of objects inferring during mapping (79d7c2)
    • Introduce helper class MessageMapFormatter (ddf69e)
    • Introduce helper class MessagesFlattener (a97b40)
    • Introduce helper NodeTraverser for recursive operations on nodes (cc1bc6)

    Bug Fixes

    • Handle nested attributes compilation (d2795b)
    • Treat forbidden mixed type as invalid type (36bd36)
    • Treat union type resolving error as message (e834cd)
    • Use locked package versions for quality assurance workflow (626f13)

    Other

    • Ignore changelog configuration file in git export (85a6a4)
    • Raise PHPStan version (0144bf)
    Open source →
  57. 0.3.0 18 Dec 2021
    Release notes

    Changelog 0.3.0 — 18th of December 2021

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.3.0

    Features

    • Handle common database datetime formats (#40) (179ba3)

    Other

    • Change Composer scripts calls (0b507c)
    • Raise version of friendsofphp/php-cs-fixer (e5ccbe)
    Open source →
  58. 0.2.0 07 Dec 2021
    Release notes

    Changelog 0.2.0 — 7th of December 2021

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.2.0

    Features

    • Handle integer range type (9f99a2)
    • Handle local type aliasing in class definition (56142d)
    • Handle type alias import in class definition (fa3ce5)

    Bug Fixes

    • Do not accept shaped array with excessive key(s) (5a578e)
    • Handle integer value match properly (9ee2cc)

    Other

    • Delete commented code (4f5612)
    • Move exceptions to more specific folder (185edf)
    • Rename GenericAssignerLexer to TypeAliasLexer (680941)
    • Use marcocesarato/php-conventional-changelog for changelog (178aa9)
    Open source →
  59. 0.1.1 01 Dec 2021
    Release notes

    Changelog 0.1.1 — 1st of December 2021

    !!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.1.1

    ⚠ BREAKING CHANGES

    • Change license from GPL 3 to MIT (a77b28)

    Features

    • Handle multiline type declaration (d99c59)

    Bug Fixes

    • Filter type symbols with strict string comparison (6cdea3)
    • Handle correctly iterable source during mapping (dd4624)
    • Handle shaped array integer key (5561d0)
    • Resolve single/double quotes when parsing doc-block type (1c628b)

    Other

    • Change PHPStan stub file extension (8fc6af)
    • Delete unwanted code (e3e169)
    • Syntax highlight stub files (#9) (9ea95f)
    • Use composer runtime API (1f754a)
    Open source →
  60. 0.1.0 28 Nov 2021

    Nothing published for this version

Every package, every release, already written down.

The archive is open and free. Watching your own project is what we are building next.

Browse the archive