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 2026Releases
latest 60-
2.6.011 Aug 2026Release notes
Open source →Notable changes
This release brings a set of new features to the library:
- Provided mapper configurators
- Scalar value casting
- Mapping a property from a specific key
- New normalizer configurators
- Generics of PHP internal classes
- Default types for templates
- Overriding an unparseable type
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
MapToDateTimeFromFormatconfigurator parses the input string using the given date format, which must follow the syntax supported byDateTimeImmutable::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
MapExplodedStringToListconfigurator 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
MapArrayToListconfigurator 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
MapFromJsonconfigurator 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,MapAsFloatandMapAsString. 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()andallowCastingToString()methods of the mapper builder. They offer a finer control thanallowScalarValueCasting(), 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
MapFromKeyattribute 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): stringmethod 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
NormalizeKeyToattribute 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
NormalizeToSingleValueclass 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
IgnoreOnNormalizationattribute 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 viaconfigureWith():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
@templateannotations in their own source code. The library now ships generic signatures for a wide range of them, includingArrayObject,ArrayIterator, the SPL data structures and theDscollection 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
ArrayObjectkeep resolving as before.
Default types for templates
A
@templateannotation 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-paramand@valinor-returnannotations 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
@templateannotations (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
Release notes
Open source →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:
- Provided mapper configurators
- Scalar value casting
- Mapping a property from a specific key
- New normalizer configurators
- Generics of PHP internal classes
- Default types for templates
- Overriding an unparseable type
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
MapToDateTimeFromFormatconfigurator parses the input string using the given date format, which must follow the syntax supported byDateTimeImmutable::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
MapExplodedStringToListconfigurator 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
MapArrayToListconfigurator 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
MapFromJsonconfigurator 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,MapAsFloatandMapAsString. 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()andallowCastingToString()methods of the mapper builder. They offer a finer control thanallowScalarValueCasting(), 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
MapFromKeyattribute 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): stringmethod 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
NormalizeKeyToattribute 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
NormalizeToSingleValueclass 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
IgnoreOnNormalizationattribute 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 viaconfigureWith():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
@templateannotations in their own source code. The library now ships generic signatures for a wide range of them, includingArrayObject,ArrayIterator, the SPL data structures and theDscollection 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
ArrayObjectkeep resolving as before.
Default types for templates
A
@templateannotation 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-paramand@valinor-returnannotations 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
@templateannotations (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
-
2.5.128 Jul 2026Release notes
Open source →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)
Release notes
Open source →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)
-
2.5.028 Jun 2026Release notes
Open source →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_name→firstNamenew NormalizeKeysToPascalCase()first_name→FirstNamenew NormalizeKeysToSnakeCase()firstName→first_namenew NormalizeKeysToKebabCase()firstName→first-nameUsed 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
NormalizeDateTimeFormatconfigurator normalizes anyDateTimeInterfaceinstance 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 at0, 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-oftype supportThe
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-oftype 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
Release notes
Open source →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_name→firstNamenew NormalizeKeysToPascalCase()first_name→FirstNamenew NormalizeKeysToSnakeCase()firstName→first_namenew NormalizeKeysToKebabCase()firstName→first-nameUsed 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
NormalizeDateTimeFormatconfigurator normalizes anyDateTimeInterfaceinstance 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 at0, 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-oftype supportThe
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-oftype 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
- Add normalizer configurator
-
2.4.023 Mar 2026Release notes
Open source →Notable changes
This release brings a whole set of new features to the library:
- HTTP request mapping support
- Mapper/Normalizer configurators support
- CamelCase/snake_case keys conversion support
- Keys case restriction support
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 anintparameter.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
asRootoption 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
asRootto 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
HttpRequestcan 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
HttpRequestinstance can be built directly from a PSR-7ServerRequestInterface. 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 fromgetParsedBody(). 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
MappingErroris thrown, just like with regular mapping.Read the validation and error handling chapter for more information.
Mapper/Normalizer configurators support
Introduce
MapperBuilderConfiguratorandNormalizerBuilderConfiguratorinterfaces along with aconfigureWith()method on both builders.A configurator is a reusable piece of configuration logic that can be applied to a
MapperBuilderor aNormalizerBuilderinstance. 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
MapperBuilderinside 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
MapperBuilderinstance:$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
NormalizerBuilderConfiguratorThe 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.
ConvertKeysToCamelCaseConversion first_name→firstNameFirstName→firstNamefirst-name→firstName$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` ]);
ConvertKeysToSnakeCaseConversion firstName→first_nameFirstName→first_namefirst-name→first_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,PascalCaseorkebab-casekeys.Available configurators:
Configurator Example new RestrictKeysToCamelCase()firstNamenew RestrictKeysToPascalCase()FirstNamenew RestrictKeysToSnakeCase()first_namenew 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
Release notes
Open source →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:
- HTTP request mapping support
- Mapper/Normalizer configurators support
- CamelCase/snake_case keys conversion support
- Keys case restriction support
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 anintparameter.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
asRootoption 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
HttpRequestcan 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
HttpRequestinstance can be built directly from a PSR-7ServerRequestInterface. 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 fromgetParsedBody(). 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 instanceError handling
When the mapping fails — for instance because a required query parameter is missing or a body value has the wrong type — a
MappingErroris thrown, just like with regular mapping.Read the validation and error handling chapter for more information.
Mapper/Normalizer configurators support
Introduce
MapperBuilderConfiguratorandNormalizerBuilderConfiguratorinterfaces along with aconfigureWith()method on both builders.A configurator is a reusable piece of configuration logic that can be applied to a
MapperBuilderor aNormalizerBuilderinstance. 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
MapperBuilderinside 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
MapperBuilderinstance:$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
NormalizerBuilderConfiguratorThe 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.
ConvertKeysToCamelCaseConversion first_name→firstNameFirstName→firstNamefirst-name→firstName$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` ]);ConvertKeysToSnakeCaseConversion firstName→first_nameFirstName→first_namefirst-name→first_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,PascalCaseorkebab-casekeys.Available configurators:
Configurator Example new RestrictKeysToCamelCase()firstNamenew RestrictKeysToPascalCase()FirstNamenew RestrictKeysToSnakeCase()first_namenew 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
-
2.3.223 Jan 2026Release notes
Open source →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-apipackage dependencyUsing the
composer-runtime-apilibrary 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-apirequirement by PHP constant usage (8152be) - Standardize documentation comments (274207)
- Use internal interface for mapping logical exception (8e00d3)
Other
Release notes
Open source →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-apipackage dependencyUsing the
composer-runtime-apilibrary 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-apirequirement by PHP constant usage (8152be) - Standardize documentation comments (274207)
- Use internal interface for mapping logical exception (8e00d3)
Other
-
2.3.121 Oct 2025Release notes
Open source →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)
-
2.3.021 Oct 2025Release notes
Open source →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
Other
- Support empty shaped array (a3eec8)
Internal
Release notes
Open source →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
Other
- Support empty shaped array (a3eec8)
Internal
-
2.2.213 Oct 2025Release notes
Open source →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)
-
2.2.112 Oct 2025Release notes
Open source →⚠️ 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|nulltype, 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 containnull. This means that the converter should never be called, because it could return an invalid value (nullwill never be a validstring).(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
CamelCaseKeysexample 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
objectreturn 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
@templateannotation, 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:
- Be able to understand
@templateannotations inside functions - Be able to statically infer the generics using these annotations
- Assign the inferred generics to the whole converter
- 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
INFconstant to detect default converter value (72079b)
Other
- Enhance
callabletype parsing (2563a3)
Release notes
Open source →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|nulltype, 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 containnull. This means that the converter should never be called, because it could return an invalid value (nullwill never be a validstring).(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
CamelCaseKeysexample 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
objectreturn 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
@templateannotation, 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:
- Be able to understand
@templateannotations inside functions - Be able to statically infer the generics using these annotations
- Assign the inferred generics to the whole converter
- 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
INFconstant to detect default converter value (72079b)
Other
- Enhance
callabletype parsing (2563a3)
- Be able to understand
-
2.2.029 Sep 2025Release notes
Open source →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
iterabletype the same way it is done witharray(6291a7) - Rework how type traversing is used (20f17f)
- Set default exception error code to
unknown(c8ef49)
Release notes
Open source →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
iterabletype the same way it is done witharray(6291a7) - Rework how type traversing is used (20f17f)
- Set default exception error code to
unknown(c8ef49)
-
2.1.228 Aug 2025Release notes
Open source →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
-
2.1.123 Jul 2025Release notes
Open source →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
-
2.1.023 Jul 2025Release notes
Open source →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.mdCallable 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
AsConverterattribute to it.Attributes must declare a method named
mapthat follows the same rules as callable converters: a mandatory first parameter and an optional secondcallableparameter.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
registerConvertermethod.(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)
-
2.0.027 Jun 2025Release notes
Open source →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
callableparameter 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.
NormalizerBuilderintroductionThe
NormalizerBuilderclass has been introduced and will now be the main entry to instantiate normalizers. Therefore, the methods inMapperBuilderthat 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
NormalizerBuildercan 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
⚠ BREAKING CHANGES
- Add purity markers in
MapperBuilderandNormalizerBuilder(123058) - Add type and source accessors to
MappingError(378141) - Change exposed error messages codes (15bb11)
- Introduce
NormalizerBuilderas 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 towarmupCacheFor()(963156) - Rework mapper node and messages handling (14d5ca)
Features
- Allow
MapperBuilderandNormalizerBuilderto clear cache (fe318c) - Introduce mapper converters to apply custom logic during mapping (46c823)
Bug Fixes
- Update file system cache entries permissions (6ffb0f)
Other
-
1.17.020 Jun 2025Release notes
Open source →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
enableFlexibleCastingis (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
Stringableinterface. -
Boolean types will accept any truthy or falsy value:
(string) "true",(string) "1"and(int) 1will be cast totrue(string) "false",(string) "0"and(int) 0will be cast tofalse
(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
ValueNodeimplementation (7e6ccf)
-
-
1.16.119 May 2025Release notes
Open source →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)
-
1.16.019 May 2025Release notes
Open source →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
-
1.15.030 Mar 2025Release notes
Open source →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
-
1.14.423 Feb 2025Release notes
Open source →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)
-
1.14.317 Feb 2025Release notes
Open source →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
-
1.14.209 Jan 2025Release notes
Open source →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
-
1.14.106 Nov 2024Release notes
Open source →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)
-
1.14.004 Nov 2024Release notes
Open source →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_PRINToption 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_OBJECToption 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_OBJECToption in JSON normalizer (f3e8c1) - Add support for PHP 8.4 (07a06a)
- Handle
JSON_PRETTY_PRINToption 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
- Add support for
-
1.13.002 Sep 2024Release notes
Open source →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
DateTimeInterfaceinput values.This commit adds support for floats and registers
timestamp.microseconds(U.u) as a valid default format.Support for
value-of<BackedEnum>typeThis 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:
- Non-scalar type
- Integer type
- Float type
- String type
- 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
-
1.12.004 Apr 2024Release notes
Open source →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
Constructorattribute, theMapperBuilder::registerConstructor()method must be used instead.In the example below, the mapper is taught how to instantiate an implementation of
UuidInterfacefrom packageramsey/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_ERRORto encode non-boolean scalar values. There might be use-cases where projects will need flags likeJSON_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_QUOTJSON_HEX_TAGJSON_HEX_AMPJSON_HEX_APOSJSON_INVALID_UTF8_IGNOREJSON_INVALID_UTF8_SUBSTITUTEJSON_NUMERIC_CHECKJSON_PRESERVE_ZERO_FRACTIONJSON_UNESCAPED_LINE_TERMINATORSJSON_UNESCAPED_SLASHESJSON_UNESCAPED_UNICODE
JSON_THROW_ON_ERRORis 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-keytype (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)
-
1.11.027 Mar 2024Release notes
Open source →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
AsTransformerattributeAfter the introduction of the
Constructorattribute used for the mapper, the newAsTransformerattribute 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
Bug Fixes
- Handle single array mapping when a superfluous value is present (86d021)
- Properly handle
ArrayObjectnormalization (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
-
1.10.012 Mar 2024Release notes
Open source →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
ConstructorattributeA long awaited feature has landed in the library!
The
Constructorattribute 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 theMapperBuilder::registerConstructormethod, although it does not replace it.The method targeted by a
Constructorattribute 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
Constructorattribute (d86295)
Bug Fixes
- Properly encode scalar value in JSON normalization (2107ea)
- Properly handle list type when input contains superfluous keys (1b8efa)
Other
- Introduce
-
1.9.002 Feb 2024Release notes
Open source →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 dataAnother 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)
-
1.8.208 Jan 2024Release notes
Open source →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)
-
1.8.108 Jan 2024Release notes
Open source →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)
-
1.8.026 Dec 2023Release notes
Open source →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
-
1.7.023 Oct 2023Release notes
Open source →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
Bug Fixes
- Add missing
@psalm-pureannotation to pure methods (004eb1) - Handle comments in classes when parsing types imports (3b663a)
Other
- Add missing
-
1.6.111 Oct 2023Release notes
Open source →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
-
1.6.025 Aug 2023Release notes
Open source →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
-
1.5.007 Aug 2023Release notes
Open source →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
-
1.4.017 Apr 2023Release notes
Open source →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
InvalidSourcethrown when using invalid JSON/YAML (0739d1)
Bug Fixes
- Allow integer values in float types (c6df24)
- Make
array-keytype matchmixed(ccebf7) - Prevent infinite loop when class has parent class with same name (83eb05)
Other
- Add previous exception in various custom exceptions (b9e381)
- Introduce
-
1.3.113 Feb 2023Release notes
Open source →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
nulland objects (8f03a7)
Other
- Update dependencies (f7e7f2)
-
1.3.008 Feb 2023Release notes
Open source →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)
-
1.2.009 Jan 2023Release notes
Open source →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
-
1.1.020 Dec 2022Release notes
Open source →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
@extendstag (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
Bug Fixes
- Handle
objectreturn type in PHPStan extension (201728) - Import plugin class file in PHPStan configuration (58d540)
- Keep nested errors when superfluous keys are detected (813b3b)
Other
- Handle
-
1.0.027 Nov 2022Release notes
Open source →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
TimeZoneobjectsNative
TimeZoneobjects 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 "
BackwardCompatibilityDateTimeConstructorclass removal"You must use the method available in the mapper builder, see [dealing with dates chapter].??? tip "Mapper builder
flexiblemethod removal"The flexible has been split in three disctint modes, see [type strictness & flexibility chapter].??? tip "Mapper builder
withCacheDirmethod removal"You must now register a cache instance directly, see [performance & caching chapter].??? tip "
StaticMethodConstructorclass removal"You must now register the constructors using the mapper builder, see [custom object constructors chapter].??? tip "Mapper builder
bindmethod removal"You must now register the constructors using the mapper builder, see [custom object constructors chapter].??? tip "
ThrowableMessageclass removal"You must now use the `MessageBuilder` class, see [error handling chapter].??? tip "
MessagesFlattenerclass removal"You must now use the `Messages` class, see [error handling chapter].??? tip "
TranslatableMessageclass 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 "
PlaceHolderMessageFormatterclass removal"Other features are available to format message, see [error messages customization chapter].??? tip "
Identifierattribute 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-arraytype (22c3b4)
Features
- Add constructor for
DateTimeZonewith error support (a0a4d6) - Introduce mapper to map arguments of a callable (9c7e88)
Bug Fixes
- Allow mapping
nullto 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
-
0.17.118 Jan 2023Nothing published for this version
-
0.17.008 Nov 2022Release notes
Open source →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:
-
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' => []] -
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 ] ); -
The permissive types
Allows permissive types
mixedandobjectto 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-arraytype (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
-
-
0.16.019 Oct 2022Release notes
Open source →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)
-
0.15.006 Oct 2022Release notes
Open source →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'); // okExample 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'); // okFeatures
- 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
-
0.14.001 Sep 2022Release notes
Open source →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
-
0.13.031 Jul 2022Release notes
Open source →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\Messageinterface is no longer aStringable, however it defines a new methodbodythat 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\TranslatableMessagewhich 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-stringtypeThe new
numeric-stringtype 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
MessagesFlattenercountable (2c1c7c)
Bug Fixes
- Handle native attribute on promoted parameter (897ca9)
Other
-
0.12.010 Jul 2022Release notes
Open source →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\Nodehas 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
.ideafolder (84ead0)
-
-
0.11.023 Jun 2022Release notes
Open source →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 onlyfooandbar. -
When permissive types like
mixedorobjectare 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::inferthat 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)
-
-
0.10.010 Jun 2022Release notes
Open source →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
-
0.9.023 May 2022Release notes
Open source →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 methodMapperBuilder::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::formathas been removed, message formatters should be used instead. If needed, the old behaviour can be retrieved with the formatterPlaceHolderMessageFormatter, although it is strongly advised to use the new placeholders feature (see below).The signature of the method
MessageFormatter::formathas 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
intlextension is not installed, a shim will be available to replace the placeholders, but it won't handle advanced syntax as described above.The formatter
TranslationMessageFormattercan 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
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
-
0.8.009 May 2022Release notes
Open source →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/falsevalues handlingThanks @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
psalmandphpstanprefix in docblocksThanks @boesing for this feature!
The following annotations are now properly handled:
@psalm-param,@phpstan-param,@psalm-returnand@phpstan-return.If one of those is found along with a basic
@paramor@returnannotation, it will take precedence over the basic value.Features
- Allow
psalmandphpstanprefix 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/falsetypes (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
- Allow
-
0.7.024 Mar 2022Release notes
Open source →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
Sourceclass 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::ofTypereturn type toarray(1a599b) - Introduce method to register constructors used during mapping (ecafba)
Features
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
- Change
-
0.6.024 Feb 2022Release notes
Open source →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
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
-
0.5.026 Jan 2022Release notes
Open source →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
-
0.4.007 Jan 2022Release notes
Open source →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::mapwould 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\MessageMapFormattercan 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:
- The original code of the message
- The original content of the message
- A string representation of the node type
- The name of the node
- 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
NodeTraverserfor 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
-
0.3.018 Dec 2021Release notes
Open source →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
-
0.2.007 Dec 2021Release notes
Open source →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
-
0.1.101 Dec 2021Release notes
Open source →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
-
0.1.028 Nov 2021Nothing published for this version