NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Packagist · #536 most downloaded on Packagist
Dependency free PHP library that helps to map any input into a strongly-typed structure.
Last release 1 months 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
One column per quarter.
This release brings a set of new features to the library:
This release brings a set of new features to the library:
Enjoy! 🎉
A set of configurators is now available out-of-the-box for the mapper, mirroring the normalizer configurators introduced in the previous release. Each one can be used either globally through the configureWith() method or locally as an attribute targeting a specific property.
The MapToDateTimeFromFormat configurator parses the input string using the given date format, which must follow the syntax supported by DateTimeImmutable::createFromFormat():
use CuyZ\Valinor\Mapper\Configurator\MapToDateTimeFromFormat;
use CuyZ\Valinor\MapperBuilder;
use DateTimeInterface;
final readonly class Event
{
public function __construct(
public string $name,
#[MapToDateTimeFromFormat('d/m/Y')]
public DateTimeInterface $date,
) {}
}
$event = (new MapperBuilder())
->mapper()
->map(Event::class, [
'name' => 'Release of legendary album',
'date' => '08/11/1971', // mapped to a `DateTimeImmutable`
]);The MapExplodedStringToList configurator explodes a string into a list using the given separator, which is useful when the input carries a list as a single delimited string, for instance a value coming from a CSV file or a query parameter:
use CuyZ\Valinor\Mapper\Configurator\MapExplodedStringToList;
use CuyZ\Valinor\MapperBuilder;
final readonly class Product
{
public function __construct(
public string $name,
/** @var list<string> */
#[MapExplodedStringToList(separator: ',')]
public array $sizes,
) {}
}
$product = (new MapperBuilder())
->mapper()
->map(Product::class, [
'name' => 'T-Shirt',
'sizes' => 'XS,S,M,L,XL', // mapped to `['XS', 'S', 'M', 'L', 'XL']`
]);The MapArrayToList configurator discards the keys of an array and maps its values to a list, for cases where the input is an associative array, or a sparse list with missing or out-of-order indices, that should be handled as a sequential list:
use CuyZ\Valinor\Mapper\Configurator\MapArrayToList;
use CuyZ\Valinor\MapperBuilder;
final readonly class Basket
{
public function __construct(
/** @var list<string> */
#[MapArrayToList]
public array $products,
) {}
}
$basket = (new MapperBuilder())
->mapper()
->map(Basket::class, [
'a' => 'Coffee',
'b' => 'Tea',
]); // mapped to `['Coffee', 'Tea']`Finally, the MapFromJson configurator decodes a JSON string and hands the result over to the mapper, so that the usual validation and error reporting still apply to the decoded value:
use CuyZ\Valinor\Mapper\Configurator\MapFromJson;
use CuyZ\Valinor\MapperBuilder;
final readonly class User
{
public function __construct(
public string $name,
/** @var list<string> */
#[MapFromJson]
public array $roles,
) {}
}
$user = (new MapperBuilder())
->mapper()
->map(User::class, [
'name' => 'John Doe',
'roles' => '["admin", "editor"]', // mapped to `['admin', 'editor']`
]);Four configurators convert a scalar value to a specific type before mapping: MapAsBool, MapAsInt, MapAsFloat and MapAsString. They are useful when the input data carries values in a different representation than the targeted type, for instance numbers or booleans encoded as strings in a form submission, a CSV file or a JSON payload.
Used as an attribute, a single property is cast, leaving the strictness rules untouched for every other value:
use CuyZ\Valinor\Mapper\Configurator\MapAsBool;
use CuyZ\Valinor\Mapper\Configurator\MapAsInt;
use CuyZ\Valinor\MapperBuilder;
final readonly class User
{
public function __construct(
public string $name,
#[MapAsInt]
public int $age,
#[MapAsBool(true: ['on', 'yes'], false: ['off', 'no'])]
public bool $isActive,
) {}
}
$user = (new MapperBuilder())
->mapper()
->map(User::class, [
'name' => 'John Doe',
'age' => '42', // mapped to `42`
'isActive' => 'on', // mapped to `true`
]);Casting can also be enabled for every value of a given type with the new allowCastingToBoolean(), allowCastingToInteger(), allowCastingToFloat() and allowCastingToString() methods of the mapper builder. They offer a finer control than allowScalarValueCasting(), which relaxes strictness for all scalar types at once:
use CuyZ\Valinor\MapperBuilder;
$age = (new MapperBuilder())
->allowCastingToInteger()
->mapper()
->map('int', '42'); // mapped to `42`The new MapFromKey attribute feeds a class property, or a constructor/method argument, from a specific source key instead of matching it against the property name:
use CuyZ\Valinor\Mapper\Configurator\MapFromKey;
use CuyZ\Valinor\MapperBuilder;
final readonly class Person
{
public function __construct(
public string $name,
#[MapFromKey('zipCode')]
public string $postalCode,
) {}
}
$person = (new MapperBuilder())
->mapper()
->map(Person::class, [
'name' => 'John Doe',
'zipCode' => '75001', // mapped to `$postalCode`
]);This attribute is built on a lightweight protocol that is open to userland: any attribute class declaring a mapKey(string $key): string method and carrying the #[AsConverter] attribute can remap the key of the element it is placed on. This is handy to factor out a recurring transformation, such as a prefix shared by several properties:
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
#[\CuyZ\Valinor\Mapper\AsConverter]
final class MapWithPrefix
{
public function __construct(private string $prefix) {}
public function mapKey(string $key): string
{
return $this->prefix . $key;
}
}
final readonly class Configuration
{
public function __construct(
#[MapWithPrefix('app_')] // reads from `app_host`
public string $host,
#[MapWithPrefix('app_')] // reads from `app_port`
public int $port,
) {}
}Three configurators join the ones introduced in the previous release.
The NormalizeKeyTo attribute renames the key of a property during normalization, when the name used in the data format differs from the one used in the PHP codebase:
use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeyTo;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;
final readonly class Address
{
public function __construct(
public string $street,
#[NormalizeKeyTo('town')]
public string $city,
) {}
}
$addressAsArray = (new NormalizerBuilder())
->normalizer(Format::array())
->normalize(new Address('221B Baker Street', 'London'));
// [
// 'street' => '221B Baker Street',
// 'town' => 'London',
// ]The NormalizeToSingleValue class flattens an object holding a single property, so that instead of ['someProperty' => 'value'] the normalized result is simply 'value'. It can be used either as a configurator, applying to every object with a single property, or as an attribute targeting a specific class or property:
use CuyZ\Valinor\Normalizer\Configurator\NormalizeToSingleValue;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;
final readonly class Email
{
public function __construct(
public string $email,
) {}
}
final readonly class User
{
public function __construct(
public string $name,
#[NormalizeToSingleValue]
public Email $email,
) {}
}
$userAsArray = (new NormalizerBuilder())
->normalizer(Format::array())
->normalize(new User('John Doe', new Email('john.doe@example.com')));
// [
// 'name' => 'John Doe',
// 'email' => 'john.doe@example.com',
// ]The IgnoreOnNormalization attribute excludes a property from the normalized output, for instance to hide sensitive data such as a password. For the attribute to take effect, an instance of this class must also be registered on the builder via configureWith():
use CuyZ\Valinor\Normalizer\Configurator\IgnoreOnNormalization;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;
final readonly class User
{
public function __construct(
public string $name,
#[IgnoreOnNormalization]
public string $password,
) {}
}
$userAsArray = (new NormalizerBuilder())
->configureWith(new IgnoreOnNormalization())
->normalizer(Format::array())
->normalize(new User('John Doe', 's3cr3t'));
// ['name' => 'John Doe']Generics used to be limited to userland classes, because classes internal to PHP or provided by an extension cannot declare @template annotations in their own source code. The library now ships generic signatures for a wide range of them, including ArrayObject, ArrayIterator, the SPL data structures and the Ds collection classes, so they can be parameterized like any other class:
use CuyZ\Valinor\MapperBuilder;
$sizes = (new MapperBuilder())
->mapper()
->map('ArrayObject<string, int>', [
'S' => 36,
'M' => 38,
'L' => 40,
]);Every one of these templates declares a default type, so bare references like ArrayObject keep resolving as before.
A @template annotation can now declare a default type with =. A template that declares a default type may be omitted when the class is referenced, in which case the default type is used:
/**
* @template TValue
* @template TMeta of array<string, mixed> = array<string, string>
*/
final readonly class Page
{
public function __construct(
/** @var list<TValue> */
public array $items,
/** @var TMeta */
public array $meta,
) {}
}
final readonly class SomeClass
{
public function __construct(
// `TMeta` is not filled in, its default type is used
/** @var Page<string> */
public Page $pageWithDefaultMeta,
// `TMeta` is filled in, overriding its default type
/** @var Page<string, array{cursor: int}> */
public Page $pageWithCursorMeta,
) {}
}A default type is what makes it possible to add a template to a class that is already referenced elsewhere: the existing references, which do not fill the new template in, keep resolving to its default type and can be made more precise later on.
When a property, parameter or return type uses a PHPStan or Psalm syntax that the library cannot parse yet, for instance a conditional type like ($a is 1 ? int : null), the dedicated @valinor-var, @valinor-param and @valinor-return annotations can be used to give the library a type it understands. They take precedence over every other annotation, so the static analysis tools keep using their own type while the library uses the override:
final class SomeClass
{
/**
* @phpstan-param ($a is 1 ? int : null) $b
* @valinor-param int|null $b
*/
public function __construct(
public readonly int $a,
public readonly ?int $b,
) {}
}@valinor-* annotations to override an unparseable type (11938c)@template annotations (0d6efe)MapArrayToList (1f81fa)MapAsBool (65dfed)MapAsFloat (84eea9)MapAsInt (ea28a7)MapAsString (6b0528)MapExplodedStringToList (beb4db)MapFromJson (469863)MapToDateTimeFromFormat (d6e53b)IgnoreOnNormalization (9769f2)NormalizeKeyTo (947127)NormalizeToSingleValue (7c5f13)Prevent union collision caused by absent optional elements ( 81b098 )
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.5.1
Add security vulnerability reporting guidelines ( b80e2a )
This release brings a set of new features to the library:
Enjoy! 🎉
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.
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 → firstName |
new NormalizeKeysToPascalCase() |
first_name → FirstName |
new NormalizeKeysToSnakeCase() |
firstName → first_name |
new NormalizeKeysToKebabCase() |
firstName → first-name |
Used globally, the keys of every normalized object are converted:
use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeysToSnakeCase;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;
$userAsArray = (new NormalizerBuilder())
->configureWith(new NormalizeKeysToSnakeCase())
->normalizer(Format::array())
->normalize($user);
// ['first_name' => 'John']Used as an attribute, only the keys of the targeted class are converted:
use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeysToSnakeCase;
#[NormalizeKeysToSnakeCase]
final readonly class User
{
public function __construct(
public string $firstName,
) {}
}
// ['first_name' => 'John']The NormalizeDateTimeFormat configurator normalizes any DateTimeInterface instance to a string using the given format.
Used globally, every date and time encountered during normalization is formatted:
use CuyZ\Valinor\Normalizer\Configurator\NormalizeDateTimeFormat;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;
$userAsArray = (new NormalizerBuilder())
->configureWith(new NormalizeDateTimeFormat(DATE_ATOM))
->normalizer(Format::array())
->normalize($user);
// [
// 'name' => 'Jane Doe',
// 'createdAt' => '2000-01-01T00:00:00+00:00',
// ]Used as an attribute, only the targeted property is formatted:
use CuyZ\Valinor\Normalizer\Configurator\NormalizeDateTimeFormat;
final readonly class User
{
public function __construct(
public string $name,
#[NormalizeDateTimeFormat(DATE_ATOM)]
public DateTimeInterface $createdAt,
) {}
}The shaped list type list{…} is now supported. It works like a shaped array but enforces sequential integer keys starting at 0, making it the right type to describe a tuple-like list of values.
final readonly class SomeClass
{
public function __construct(
/** @var list{string, int, float} */
public array $shapedList,
/** @var list{0: string, 1: int} */
public array $shapedListWithExplicitKeys,
/** @var list{0: string, 1?: int} */
public array $shapedListWithOptionalElement,
/** @var list{string, int, ...} */
public array $unsealedShapedList,
/** @var list{string, int, ...list<float>} */
public array $unsealedShapedListWithExplicitType,
/** @var list{string, ...<float>} */
public array $unsealedShapedListWithShorthandType,
) {}
}key-of type 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,
) {}
}ConvertDateTime (bf688b)NormalizeKeysToCamelCase (b0d38f)NormalizeKeysToKebabCase (9826ca)NormalizeKeysToPascalCase (53bff2)NormalizeKeysToSnakeCase (c831e0)key-of type mapping (ff16b2)Shell (f84e78)This release brings a whole set of new features to the library:
This release brings a whole set of new features to the library:
Enjoy! 🎉
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 valuesThose attributes can be omitted entirely if the parameter is not bound to a specific source, in which case a collision error is raised if the same key is found in more than one source.
This gives controllers a clean, type-safe signature without coupling to a framework's request object, while benefiting from the library's validation and error handling.
Normal mapping rules apply there: parameters are required unless they have a default value.
Route and query parameter values coming from an HTTP request are typically strings. The mapper automatically handles scalar value casting for these parameters: a string "42" will be properly mapped to an int parameter.
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);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' => 'jane.doe@example.com',
'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.
Instead of mapping individual query parameters or body values to separate parameters, the asRoot option can be used to map all of them at once to a single parameter. This is useful when working with complex data structures or when the number of parameters is large.
use CuyZ\Valinor\Mapper\Http\FromQuery;
use CuyZ\Valinor\Mapper\Http\FromRoute;
final readonly class ArticleFilters
{
public function __construct(
/** @var non-empty-string */
public string $status,
/** @var positive-int */
public int $page = 1,
/** @var int<10, 100> */
public int $limit = 10,
) {}
}
final class ListArticles
{
/**
* GET /api/authors/{authorId}/articles?status=X&page=X&limit=X
*/
public function __invoke(
#[FromRoute] string $authorId,
#[FromQuery(asRoot: true)] ArticleFilters $filters,
): ResponseInterface { … }
}The same approach works with #[FromBody(asRoot: true)] for body values.
Tip
A shaped array can be used alongside asRoot to map all values to a single parameter:
use CuyZ\Valinor\Mapper\Http\FromQuery;
use CuyZ\Valinor\Mapper\Http\FromRoute;
final class ListArticles
{
/**
* GET /api/authors/{authorId}/articles?status=X&&page=X&limit=X
*
* @param array{
* status: non-empty-string,
* page?: positive-int,
* limit?: int<10, 100>,
* } $filters
*/
public function __invoke(
#[FromRoute] string $authorId,
#[FromQuery(asRoot: true)] array $filters,
): ResponseInterface { … }
}Instead of mapping to a callable's arguments, an HttpRequest can be mapped directly to an object. The attributes work the same way on constructor parameters or promoted properties.
use CuyZ\Valinor\Mapper\Http\FromBody;
use CuyZ\Valinor\Mapper\Http\FromRoute;
use CuyZ\Valinor\Mapper\Http\HttpRequest;
use CuyZ\Valinor\MapperBuilder;
final readonly class PostComment
{
public function __construct(
#[FromRoute] public int $postId,
/** @var non-empty-string */
#[FromBody] public string $author,
/** @var non-empty-string */
#[FromBody] public string $content,
) {}
}
$request = new HttpRequest(
routeParameters: ['postId' => 1337],
bodyValues: [
'author' => 'jane.doe@example.com',
'content' => 'Great article, thanks for sharing!',
],
);
$comment = (new MapperBuilder())
->mapper()
->map(PostComment::class, $request);
// $comment->postId === 1337
// $comment->author === 'jane.doe@example.com'
// $comment->content === 'Great article, thanks for sharing!'An HttpRequest instance can be built directly from a PSR-7 ServerRequestInterface. This is the recommended approach when integrating with frameworks that use PSR-7.
use CuyZ\Valinor\Mapper\Http\HttpRequest;
use CuyZ\Valinor\MapperBuilder;
// `$psrRequest` is a PSR-7 `ServerRequestInterface` instance
// `$routeParameters` are the parameters extracted by the router
$request = HttpRequest::fromPsr($psrRequest, $routeParameters);
$arguments = (new MapperBuilder())
->argumentsMapper()
->mapArguments($controller, $request);The factory method extracts query parameters from getQueryParams() and body values from getParsedBody(). It also passes the original PSR-7 request object through, so it can be injected into controller parameters if needed (see below).
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 instanceWhen the mapping fails — for instance because a required query parameter is missing or a body value has the wrong type — a MappingError is thrown, just like with regular mapping.
Read the validation and error handling chapter for more information.
Introduce MapperBuilderConfigurator and NormalizerBuilderConfigurator interfaces along with a configureWith() method on both builders.
A configurator is a reusable piece of configuration logic that can be applied to a MapperBuilder or a NormalizerBuilder instance. This is useful when the same configuration needs to be applied in multiple places across an application, or when configuration logic needs to be distributed as a package.
In the example below, we apply two configuration settings to a MapperBuilder inside a single class, but this could contain any number of customizations, depending on the needs of the application.
namespace My\App;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Mapper\Configurator\MapperBuilderConfigurator;
final class ApplicationMappingConfigurator implements MapperBuilderConfigurator
{
public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
{
return $builder
->allowSuperfluousKeys()
->registerConstructor(
\My\App\CustomerId::fromString(...),
);
}
}This configurator can be registered within the MapperBuilder instance:
$result = (new \CuyZ\Valinor\MapperBuilder())
->configureWith(new \My\App\ApplicationMappingConfigurator())
->mapper()
->map(\My\App\User::class, [
'id' => '604e4b36-5b76-4b1a-9e6c-02d5acb53a4d',
'name' => 'John Doe',
'extraField' => 'ignored because superfluous keys are allowed',
]);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.
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);Two configurators are available to convert the keys of input data before mapping them to object properties or shaped array keys. This allows accepting data with a different naming convention than the one used in the PHP codebase.
ConvertKeysToCamelCase| Conversion |
|---|
first_name → firstName |
FirstName → firstName |
first-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`
]);ConvertKeysToSnakeCase| Conversion |
|---|
firstName → first_name |
FirstName → first_name |
first-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',
]);Four configurators restrict which key case is accepted when mapping input data to objects or shaped arrays. If a key does not match the expected case, a mapping error will be raised.
This is useful, for instance, to enforce a consistent naming convention across an API's input to ensure that a JSON payload only contains camelCase, snake_case, PascalCase or kebab-case keys.
Available configurators:
| Configurator | Example |
|---|---|
new RestrictKeysToCamelCase() |
firstName |
new RestrictKeysToPascalCase() |
FirstName |
new RestrictKeysToSnakeCase() |
first_name |
new RestrictKeysToKebabCase() |
first-name |
$user = (new \CuyZ\Valinor\MapperBuilder())
->configureWith(
new \CuyZ\Valinor\Mapper\Configurator\RestrictKeysToCamelCase()
)
->mapper()
->map(\My\App\User::class, [
'firstName' => 'John', // Ok
'last_name' => 'Doe', // Error
]);FileWatchingCache (d445e4)PHP 8.1 security support has ended on the 31st of December 2025.
End of PHP 8.1 support
PHP 8.1 security support has ended on the 31st of December 2025.
See: https://www.php.net/supported-versions.php
Removal of composer-runtime-api package dependency
Using the composer-runtime-api library leads to unnecessary IO everytime the library is used; therefore, we prefer to use a basic constant that contains the package version.
This change slightly increases performance and makes the package completely dependency free. 🎉
composer-runtime-api requirement by PHP constant usage (8152be)Handle default value retrieval for properties ( 45b9de )
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.3.1
Enjoy the upcoming PHP 8.5 version before it is even officially released!
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!
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.3.0
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!
Handle object arguments default value ( c2cee2 )
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.2.2
This release contains a lot of internal refactorings that were needed to fix an important bug regarding converters. Although we made our best to provi
This release contains a lot of internal refactorings that were needed to fix an important bug regarding converters. Although we made our best to provide a stable release, bugs can have slipped through the cracks. If that's the case, please open an issue describing the issue and we will try to fix it as soon as possible.
⚠️ This fix is not backward-compatible in some cases, which are explained below. If you use mapper converters in your application, you should definitely read the following changes carefully.
The commit d9e3cf0 is the result of a long journey whose goal was to fix a very upsetting bug that would make mapper converters being called when they shouldn't be. This could result in unexpected behaviors and could even lead to invalid data being mapped.
Take the following example below:
We register a converter that will return null if the string length is lower than 5. For this converter to be called, the target type should match the string|null type, because that is what the converter can return.
In this example, we want to map a value to string, which is not matched by the converter return type because it does not contain null. This means that the converter should never be called, because it could return an invalid value (null will never be a valid string).
(new \CuyZ\Valinor\MapperBuilder())
->registerConverter(
// If the string length is lower than 5, we return `null`
fn (string $val): ?string => strlen($val) < 5 ? null : $val
)
->mapper()
->map('string', 'foo');Before this commit, the converter would be called and return null, which would raise an unexpected error:
An error occurred at path root: value null is not a valid string.
This error was caused by the following line:
if (! $shell->type->matches($converter->returnType)) {
continue;
}It should have been:
if (! $converter->returnType->matches($shell->type)) {
continue;
}Easy fix, isn't it?
Well… actually no. Because changing this completely modifies the behavior of the converters, and the library is now missing a lot of information to properly infer the return type of the converter.
In some cases this change was enough, but in some more complex cases we now would need more information.
For instance, let's take the CamelCaseKeys example as it was written in the documentation before this commit:
final class CamelCaseKeys
{
/**
* @param array<mixed> $value
* @param callable(array<mixed>): object $next
*/
public function map(array $value, callable $next): object { … }
}There is a big issue in the types signature of this converter: the object return type means that the converter can return anything, as long as this is an object. This breaks the type matching contract and the converter should never be called. But it was.
This is the new way of writing this converter:
final class CamelCaseKeys
{
/**
* @template T of object
* @param array<mixed> $value
* @param callable(array<mixed>): T $next
* @return T
*/
public function map(array $value, callable $next): object { … }Now, the type matching contract is respected because of the @template annotation, and the converter is called when mapping to any object.
To be able to properly infer the return type of the converter, we needed to:
@template annotations inside functionsThis 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);
}
}INF constant to detect default converter value (72079b)callable type parsing (2563a3)Mapping error messages improvements
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}`.non-empty-list (9739cd)iterable type the same way it is done with array (6291a7)unknown (c8ef49)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.2.0
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}`.
non-empty-list (9739cd)iterable type the same way it is done with array (6291a7)unknown (c8ef49)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.1.2
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.1.2
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.1.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.1.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.1.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.1.0
Attribute converters
!!! info
Fetch common examples of mapping converters [in the documentation].
[in the documentation]: ../../how-to/convert-input.md
Callable converters allow targeting any value during mapping, whereas attribute converters allow targeting a specific class or property for a more granular control.
To be detected by the mapper, an attribute class must be registered first by
adding the AsConverter attribute to it.
Attributes must declare a method named map that follows the same rules as
callable converters: a mandatory first parameter and an optional second
callable parameter.
Below is an example of an attribute converter that converts string inputs to boolean values based on specific string inputs:
namespace My\App;
#[\CuyZ\Valinor\Mapper\AsConverter]
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class CastToBool
{
/**
* @param callable(mixed): bool $next
*/
public function map(string $value, callable $next): bool
{
$value = match ($value) {
'yes', 'on' => true,
'no', 'off' => false,
default => $value,
};
return $next($value);
}
}
final class User
{
public string $name;
#[\My\App\CastToBool]
public bool $isActive;
}
$user = (new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(User::class, [
'name' => 'John Doe',
'isActive' => 'yes',
]);
$user->name === 'John Doe';
$user->isActive === true;
Attribute converters can also be used on function parameters when mapping arguments:
function someFunction(string $name, #[\My\App\CastToBool] bool $isActive) {
// …
};
$arguments = (new \CuyZ\Valinor\MapperBuilder())
->argumentsMapper()
->mapArguments(someFunction(...), [
'name' => 'John Doe',
'isActive' => 'yes',
]);
$arguments['name'] === 'John Doe';
$arguments['isActive'] === true;
When there is no control over the converter attribute class, it is possible to
register it using the registerConverter method.
(new \CuyZ\Valinor\MapperBuilder())
->registerConverter(\Some\External\ConverterAttribute::class)
->mapper()
->map(…);
It is also possible to register attributes that share a common interface by giving the interface name to the registration method.
namespace My\App;
interface SomeAttributeInterface {}
#[\Attribute]
final class SomeAttribute implements \My\App\SomeAttributeInterface {}
#[\Attribute]
final class SomeOtherAttribute implements \My\App\SomeAttributeInterface {}
(new \CuyZ\Valinor\MapperBuilder())
// Registers both `SomeAttribute` and `SomeOtherAttribute` attributes
->registerConverter(\My\App\SomeAttributeInterface::class)
->mapper()
->map(…);
= token when reading types (9a511d)array_find (540741)@internal (f3eace)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/2.0.0
!!! 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.
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:
These two types are enough for the library to know when to call the converter and can contain advanced type annotations for more specific use cases.
Below is a basic example of a converter that converts string inputs to uppercase:
(new \CuyZ\Valinor\MapperBuilder())
->registerConverter(
fn (string $value): string => strtoupper($value)
)
->mapper()
->map('string', 'hello world'); // 'HELLO WORLD'
Converters can be chained, allowing multiple transformations to be applied to a
value. A second callable parameter can be declared, allowing the current
converter to call the next one in the chain.
A priority can be given to a converter to control the order in which converters are applied. The higher the priority, the earlier the converter will be executed. The default priority is 0.
(new \CuyZ\Valinor\MapperBuilder())
->registerConverter(
function(string $value, callable $next): string {
return $next(strtoupper($value));
}
)
->registerConverter(
function(string $value, callable $next): string {
return $next($value . '!');
},
priority: -10,
)
->registerConverter(
function(string $value, callable $next): string {
return $next($value . '?');
},
priority: 10,
)
->mapper()
->map('string', 'hello world'); // 'HELLO WORLD?!'
More information can be found in the mapper converter chapter.
NormalizerBuilder introduction
The NormalizerBuilder class has been introduced and will now be the main entry
to instantiate normalizers. Therefore, the methods inMapperBuilder that used
to configure and return normalizers have been removed.
This decision aims to make a clear distinction between the mapper and the normalizer configuration API, where confusion could arise when using both.
The NormalizerBuilder can be used like this:
$normalizer = (new \CuyZ\Valinor\NormalizerBuilder())
->registerTransformer(
fn (\DateTimeInterface $date) => $date->format('Y/m/d')
)
->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
->normalize($someData);
Changes to messages/errors handling
Some changes have been made to the way messages and errors are handled.
It is now easier to fetch messages when error(s) occur during mapping:
try {
(new \CuyZ\Valinor\MapperBuilder())->mapper()->map(/* … */);
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
// Before (1.x):
$messages = \CuyZ\Valinor\Mapper\Tree\Message\Messages::flattenFromNode(
$error->node()
);
// After (2.x):
$messages = $error->messages();
}
MapperBuilder and NormalizerBuilder (123058)MappingError (378141)NormalizerBuilder as the main entry for normalizers (f79ce2)@internal (7fe5fe)MapperBuilder::alter() in favor of mapper converters (bee098)MapperBuilder::enableFlexibleCasting() (f8f16d)PrioritizedList (0b8c89)IdentifiableSource (aefb20)MapperBuilder::warmup() method to warmupCacheFor() (963156)MapperBuilder and NormalizerBuilder to clear cache (fe318c)The mapper setting enableFlexibleCasting is (softly) deprecated in favor of three distinct modes, which guarantee the same functionalities as before.
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.17.0
Flexible casting setting split
The mapper setting enableFlexibleCasting is (softly) deprecated in favor of
three distinct modes, which guarantee the same functionalities as before.
Allowing scalar value casting:
With this setting enabled, scalar types will accept castable values:
Integer types will accept any valid numeric value, for instance the
string value "42".
Float types will accept any valid numeric value, for instance the
string value "1337.42".
String types will accept any integer, float or object implementing the
Stringable interface.
Boolean types will accept any truthy or falsy value:
(string) "true", (string) "1" and (int) 1 will be cast to true(string) "false", (string) "0" and (int) 0 will be cast to false(new \CuyZ\Valinor\MapperBuilder())
->allowScalarValueCasting()
->mapper()
->map('array{id: string, price: float, active: bool}', [
'id' => 549465210, // Will be cast to string
'price' => '42.39', // Will be cast to float
'active' => 1, // Will be cast to bool
]);
Allowing non-sequential lists:
By default, list types will only accept sequential keys starting from 0.
This setting allows the mapper to convert associative arrays to a list with sequential keys.
(new \CuyZ\Valinor\MapperBuilder())
->allowNonSequentialList()
->mapper()
->map('list<int>', [
'foo' => 42,
'bar' => 1337,
]);
// => [0 => 42, 1 => 1337]
Allowing undefined values:
Allows the mapper to accept undefined values (missing from the input), by
converting them to null (if the current type is nullable) or an empty array (if the current type is an object or an iterable).
(new \CuyZ\Valinor\MapperBuilder())
->allowUndefinedValues()
->mapper()
->map('array{name: string, age: int|null}', [
'name' => 'John Doe',
// 'age' is not defined
]);
// => ['name' => 'John Doe', 'age' => null]
ValueNode implementation (7e6ccf)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.16.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.16.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.16.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.15.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.15.0
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…
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.4
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.4
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.3
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.3
Fix PHP 8.4 deprecations (b698de)
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.2
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.14.0
PHP 8.4 support 🐘
Enjoy the upcoming PHP 8.4 version before it is even officially released!
Pretty JSON output
The JSON_PRETTY_PRINT option is now supported by the JSON normalizer and will
format the ouput with whitespaces and line breaks:
$input = [
'value' => 'foo',
'list' => [
'foo',
42,
['sub']
],
'associative' => [
'value' => 'foo',
'sub' => [
'string' => 'foo',
'integer' => 42,
],
],
];
(new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
->withOptions(\JSON_PRETTY_PRINT)
->normalize($input);
// Result:
// {
// "value": "foo",
// "list": [
// "foo",
// 42,
// [
// "sub"
// ]
// ],
// "associative": {
// "value": "foo",
// "sub": {
// "string": "foo",
// "integer": 42
// }
// }
// }
Force array as object in JSON output
The JSON_FORCE_OBJECT option is now supported by the JSON normalizer and will
force the output of an array to be an object:
(new \CuyZ\Valinor\MapperBuilder())
->normalizer(Format::json())
->withOptions(JSON_FORCE_OBJECT)
->normalize(['foo', 'bar']);
// {"0":"foo","1":"bar"}
JSON_FORCE_OBJECT option in JSON normalizer (f3e8c1)JSON_PRETTY_PRINT option with the JSON normalizer (950395)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.13.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.13.0
Microseconds support for timestamp format
Prior to this patch, this would require a custom constructor in the form of:
static fn(float | int $timestamp): DateTimeImmutable => new
DateTimeImmutable(sprintf("@%d", $timestamp)),
This bypasses the datetime format support of Valinor entirely. This is required
because the library does not support floats as valid DateTimeInterface input
values.
This commit adds support for floats and registers timestamp.microseconds
(U.u) as a valid default format.
Support for value-of<BackedEnum> type
This type can be used as follows:
enum Suit: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
$suit = (new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map('value-of<Suit>', 'D');
// $suit === 'D'
Object constructors parameters types inferring improvements
The collision system that checks object constructors parameters types is now way more clever, as it no longer checks for parameters' names only. Types are now also checked, and only true collision will be detected, for instance when two constructors share a parameter with the same name and type.
Note that when two parameters share the same name, the following type priority operates:
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€'); // ✅
value-of<BackedEnum> type (b1017c)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.12.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.12.0
Introduce unsealed shaped array syntax
This syntax enables an extension of the shaped array type by allowing additional values that must respect a certain type.
$mapper = (new \CuyZ\Valinor\MapperBuilder())->mapper();
// Default syntax can be used like this:
$mapper->map(
'array{foo: string, ...array<string>}',
[
'foo' => 'foo',
'bar' => 'bar', // ✅ valid additional value
]
);
$mapper->map(
'array{foo: string, ...array<string>}',
[
'foo' => 'foo',
'bar' => 1337, // ❌ invalid value 1337
]
);
// Key type can be added as well:
$mapper->map(
'array{foo: string, ...array<int, string>}',
[
'foo' => 'foo',
42 => 'bar', // ✅ valid additional key
]
);
$mapper->map(
'array{foo: string, ...array<int, string>}',
[
'foo' => 'foo',
'bar' => 'bar' // ❌ invalid key
]
);
// Advanced types can be used:
$mapper->map(
"array{
'en_US': non-empty-string,
...array<non-empty-string, non-empty-string>
}",
[
'en_US' => 'Hello',
'fr_FR' => 'Salut', // ✅ valid additional value
]
);
$mapper->map(
"array{
'en_US': non-empty-string,
...array<non-empty-string, non-empty-string>
}",
[
'en_US' => 'Hello',
'fr_FR' => '', // ❌ invalid value
]
);
// If the permissive type is enabled, the following will work:
(new \CuyZ\Valinor\MapperBuilder())
->allowPermissiveTypes()
->mapper()
->map(
'array{foo: string, ...}',
['foo' => 'foo', 'bar' => 'bar', 42 => 1337]
); // ✅
Interface constructor registration
By default, the mapper cannot instantiate an interface, as it does not know
which implementation to use. To do so, the MapperBuilder::infer() method can
be used, but it is cumbersome in most cases.
It is now also possible to register a constructor for an interface, in the same way as for a class.
Because the mapper cannot automatically guess which implementation can be used
for an interface, it is not possible to use the Constructor attribute, the
MapperBuilder::registerConstructor() method must be used instead.
In the example below, the mapper is taught how to instantiate an implementation
of UuidInterface from package ramsey/uuid:
(new \CuyZ\Valinor\MapperBuilder())
->registerConstructor(
// The static method below has return type `UuidInterface`;
// therefore, the mapper will build an instance of `Uuid` when
// it needs to instantiate an implementation of `UuidInterface`.
Ramsey\Uuid\Uuid::fromString(...)
)
->mapper()
->map(
Ramsey\Uuid\UuidInterface::class,
'663bafbf-c3b5-4336-b27f-1796be8554e0'
);
JSON normalizer formatting options
By default, the JSON normalizer will only use JSON_THROW_ON_ERROR to encode
non-boolean scalar values. There might be use-cases where projects will need
flags like JSON_JSON_PRESERVE_ZERO_FRACTION.
This can be achieved by passing these flags to the new
JsonNormalizer::withOptions() method:
namespace My\App;
$normalizer = (new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
->withOptions(\JSON_PRESERVE_ZERO_FRACTION);
$lowerManhattanAsJson = $normalizer->normalize(
new \My\App\Coordinates(
longitude: 40.7128,
latitude: -74.0000
)
);
// `$lowerManhattanAsJson` is a valid JSON string representing the data:
// {"longitude":40.7128,"latitude":-74.0000}
The method accepts an int-mask of the following JSON_* constant
representations:
JSON_HEX_QUOTJSON_HEX_TAGJSON_HEX_AMPJSON_HEX_APOSJSON_INVALID_UTF8_IGNOREJSON_INVALID_UTF8_SUBSTITUTEJSON_NUMERIC_CHECKJSON_PRESERVE_ZERO_FRACTIONJSON_UNESCAPED_LINE_TERMINATORSJSON_UNESCAPED_SLASHESJSON_UNESCAPED_UNICODEJSON_THROW_ON_ERROR is always enforced and thus is not accepted.
See official doc for more information: https://www.php.net/manual/en/json.constants.php
array-key type (5020d6)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.11.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.11.0
Improvement of union types narrowing
The algorithm used by the mapper to narrow a union type has been greatly improved, and should cover more edge-cases that would previously prevent the mapper from performing well.
If an interface, a class or a shaped array is matched by the input, it will take precedence over arrays or scalars.
(new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(
signature: 'array<int>|' . Color::class,
source: [
'red' => 255,
'green' => 128,
'blue' => 64,
],
); // Returns an instance of `Color`
When superfluous keys are allowed, if the input matches several interfaces, classes or shaped array, the one with the most children node will be prioritized, as it is considered the most specific type:
(new \CuyZ\Valinor\MapperBuilder())
->allowSuperfluousKeys()
->mapper()
->map(
// Even if the first shaped array matches the input, the second one is
// used because it's more specific.
signature: 'array{foo: int}|array{foo: int, bar: int}',
source: [
'foo' => 42,
'bar' => 1337,
],
);
If the input matches several types within the union, a collision will occur and cause the mapper to fail:
(new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(
// Even if the first shaped array matches the input, the second one is
// used because it's more specific.
signature: 'array{red: int, green: int, blue: int}|' . Color::class,
source: [
'red' => 255,
'green' => 128,
'blue' => 64,
],
);
// ⚠️ Invalid value array{red: 255, green: 128, blue: 64}, it matches at
// least two types from union.
Introducing AsTransformer attribute
After the introduction of the Constructor attribute
used for the mapper, the new AsTransformer attribute is now available for the
normalizer to ease the registration of a transformer.
namespace My\App;
#[\CuyZ\Valinor\Normalizer\AsTransformer]
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class DateTimeFormat
{
public function __construct(private string $format) {}
public function normalize(\DateTimeInterface $date): string
{
return $date->format($this->format);
}
}
final readonly class Event
{
public function __construct(
public string $eventName,
#[\My\App\DateTimeFormat('Y/m/d')]
public \DateTimeInterface $date,
) {}
}
(new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
->normalize(new \My\App\Event(
eventName: 'Release of legendary album',
date: new \DateTimeImmutable('1971-11-08'),
));
// [
// 'eventName' => 'Release of legendary album',
// 'date' => '1971/11/08',
// ]
ArrayObject normalization (4f555d)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.10.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.10.0
Dropping support for PHP 8.0
PHP 8.0 security support has ended on the 26th of November 2023. Therefore, we are dropping support for PHP 8.0 in this version.
If any security issue was to be found, we might consider backporting the fix to the 1.9.x version if people need it, but we strongly recommend upgrading your application to a supported PHP version.
Introducing Constructor attribute
A long awaited feature has landed in the library!
The Constructor attribute can be assigned to any method inside an object, to
automatically mark the method as a constructor for the class. This is a more
convenient way of registering constructors than using the
MapperBuilder::registerConstructor method, although it does not replace it.
The method targeted by a Constructor attribute must be public, static and
return an instance of the class it is part of.
final readonly class Email
{
// When another constructor is registered for the class, the native
// constructor is disabled. To enable it again, it is mandatory to
// explicitly register it again.
#[\CuyZ\Valinor\Mapper\Object\Constructor]
public function __construct(public string $value) {}
#[\CuyZ\Valinor\Mapper\Object\Constructor]
public static function createFrom(
string $userName, string $domainName
): self {
return new self($userName . '@' . $domainName);
}
}
(new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(Email::class, [
'userName' => 'john.doe',
'domainName' => 'example.com',
]); // john.doe@example.com
Constructor attribute (d86295)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.9.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.9.0
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:
Basic usage:
namespace My\App;
$normalizer = (new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json());
$userAsJson = $normalizer->normalize(
new \My\App\User(
name: 'John Doe',
age: 42,
country: new \My\App\Country(
name: 'France',
code: 'FR',
),
)
);
// `$userAsJson` is a valid JSON string representing the data:
// {"name":"John Doe","age":42,"country":{"name":"France","code":"FR"}}
By default, the JSON normalizer will return a JSON string representing the data it was given. Instead of getting a string, it is possible to stream the JSON data to a PHP resource:
$file = fopen('path/to/some_file.json', 'w');
$normalizer = (new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
->streamTo($file);
$normalizer->normalize(/* … */);
// The file now contains the JSON data
Another benefit of streaming the data to a PHP resource is that it may be more memory-efficient when using generators — for instance when querying a database:
// In this example, we assume that the result of the query below is a
// generator, every entry will be yielded one by one, instead of
// everything being loaded in memory at once.
$users = $database->execute('SELECT * FROM users');
$file = fopen('path/to/some_file.json', 'w');
$normalizer = (new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
->streamTo($file);
// Even if there are thousands of users, memory usage will be kept low
// when writing JSON into the file.
$normalizer->normalize($users);
DateTimeZone (acf097)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.8.2
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.8.2
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.8.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.8.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.8.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.8.0
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.
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.7.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.7.0
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;
}
@psalm-pure annotation to pure methods (004eb1)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.6.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.6.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.6.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.6.0
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']);
intl.use_exceptions=1 (29da9a)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.5.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.5.0
UnresolvableType (eaa128)UnresolvableType (5c89c6)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.4.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.4.0
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()`
}
InvalidSource thrown when using invalid JSON/YAML (0739d1)array-key type match mixed (ccebf7)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.3.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.3.1
Bugfix release.
null and objects (8f03a7)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.3.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.3.0
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.
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.2.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.2.0
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
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.1.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/1.1.0
Handle class generic types inheritance
It is now possible to use the @extends tag (already handled by PHPStan and
Psalm) to declare the type of a parent class generic. This logic is recursively
applied to all parents.
/**
* @template FirstTemplate
* @template SecondTemplate
*/
abstract class FirstClassWithGenerics
{
/** @var FirstTemplate */
public $valueA;
/** @var SecondTemplate */
public $valueB;
}
/**
* @template FirstTemplate
* @extends FirstClassWithGenerics<FirstTemplate, int>
*/
abstract class SecondClassWithGenerics extends FirstClassWithGenerics
{
/** @var FirstTemplate */
public $valueC;
}
/**
* @extends SecondClassWithGenerics<string>
*/
final class ChildClass extends SecondClassWithGenerics
{
}
$object = (new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(ChildClass::class, [
'valueA' => 'foo',
'valueB' => 1337,
'valueC' => 'bar',
]);
echo $object->valueA; // 'foo'
echo $object->valueB; // 1337
echo $object->valueC; // 'bar'
Added support for class inferring
It is now possible to infer abstract or parent classes the same way it can be done for interfaces.
Example with an abstract class:
abstract class SomeAbstractClass
{
public string $foo;
public string $bar;
}
final class SomeChildClass extends SomeAbstractClass
{
public string $baz;
}
$result = (new \CuyZ\Valinor\MapperBuilder())
->infer(
SomeAbstractClass::class,
fn () => SomeChildClass::class
)
->mapper()
->map(SomeAbstractClass::class, [
'foo' => 'foo',
'bar' => 'bar',
'baz' => 'baz',
]);
assert($result instanceof SomeChildClass);
assert($result->foo === 'foo');
assert($result->bar === 'bar');
assert($result->baz === 'baz');
object return type in PHPStan extension (201728)…means that any backward incompatible change (aka breaking change) will lead to a bump of the major version.
!!! 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! 🤗
End of PHP 7.4 support
PHP 7.4 security support has ended on the 28th of November 2022; the minimum version supported by this library is now PHP 8.0.
New mapper to map arguments of a callable
This new mapper can be used to ensure a source has the right shape before calling a function/method.
The mapper builder can be configured the same way it would be with a tree mapper, for instance to customize the type strictness.
$someFunction = function(string $foo, int $bar): string {
return "$foo / $bar";
};
try {
$arguments = (new \CuyZ\Valinor\MapperBuilder())
->argumentsMapper()
->mapArguments($someFunction, [
'foo' => 'some value',
'bar' => 42,
]);
// some value / 42
echo $someFunction(...$arguments);
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
// Do something…
}
Support for TimeZone objects
Native TimeZone objects construction is now supported with a proper error
handling.
try {
(new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(DateTimeZone::class, 'Jupiter/Europa');
} catch (MappingError $exception) {
$error = $exception->node()->messages()[0];
// Value 'Jupiter/Europa' is not a valid timezone.
echo $error->toString();
}
Mapping object with one property
When a class needs only one value, the source given to the mapper must match the type of the single property/parameter.
This change aims to bring consistency on how the mapper behaves when mapping an object that needs one argument. Before this change, the source could either match the needed type, or be an array with a single entry and a key named after the argument.
See example below:
final class Identifier
{
public readonly string $value;
}
final class SomeClass
{
public readonly Identifier $identifier;
public readonly string $description;
}
(new \CuyZ\Valinor\MapperBuilder())->mapper()->map(SomeClass::class, [
'identifier' => ['value' => 'some-identifier'], // ❌
'description' => 'Lorem ipsum…',
]);
(new \CuyZ\Valinor\MapperBuilder())->mapper()->map(SomeClass::class, [
'identifier' => 'some-identifier', // ✅
'description' => 'Lorem ipsum…',
]);
As this is a major release, all deprecated features have been removed, leading to an important number of breaking changes.
You can click on the entries below to get advice on available replacements.
??? tip "Doctrine annotations support removal"
Doctrine annotations cannot be used anymore, [PHP attributes] must be used.
??? tip "BackwardCompatibilityDateTimeConstructor class removal"
You must use the method available in the mapper builder, see [dealing
with dates chapter].
??? tip "Mapper builder flexible method removal"
The flexible has been split in three disctint modes, see [type strictness
& flexibility chapter].
??? tip "Mapper builder withCacheDir method removal"
You must now register a cache instance directly, see [performance &
caching chapter].
??? tip "StaticMethodConstructor class removal"
You must now register the constructors using the mapper builder, see [custom
object constructors chapter].
??? tip "Mapper builder bind method removal"
You must now register the constructors using the mapper builder, see [custom
object constructors chapter].
??? tip "ThrowableMessage class removal"
You must now use the `MessageBuilder` class, see [error handling chapter].
??? tip "MessagesFlattener class removal"
You must now use the `Messages` class, see [error handling chapter].
??? tip "TranslatableMessage class removal"
You must now use the `HasParameters` class, see [custom exception chapter].
??? tip "Message methods removal"
The following methods have been removed:
- `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::name()`
- `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::path()`
- `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::type()`
- `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::value()`
- `\CuyZ\Valinor\Mapper\Tree\Node::value()`
It is still possible to get the wanted values using the method
`\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::node()`.
The placeholder `{original_value}` has also been removed, the same value can
be fetched with `{source_value}`.
??? tip "PlaceHolderMessageFormatter class removal"
Other features are available to format message, see [error messages
customization chapter].
??? tip "Identifier attribute removal"
This feature has been part of the library since its first public release,
but it was never documented because it did not fit one of the library's main
philosophy which is to be almost entirely decoupled from an application's
domain layer.
The feature is entirely removed and not planned to be replaced by an
alternative, unless the community really feels like there is a need for
something alike.
@pure (0d9855)ThrowableMessage (d36ca9)TranslatableMessage (ceb197)strict-array type (22c3b4)DateTimeZone with error support (a0a4d6)null to single node nullable type (0a98ec)Nothing published for this version
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.17.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.17.0
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 mixed and object to be used during
mapping.
(new \CuyZ\Valinor\MapperBuilder())
->allowPermissiveTypes()
->mapper()
->map(
'array{foo: string, bar: mixed}',
[
'foo' => 'foo',
'bar' => 42, // Could be any value
]
);
strict-array type (d456eb)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.16.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.16.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.15.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.15.0
Two similar features are introduced in this release: constants and enums wildcard notations. This is mainly useful when several cases of an enum or class constants share a common prefix.
Example for class constants:
final class SomeClassWithConstants
{
public const FOO = 1337;
public const BAR = 'bar';
public const BAZ = 'baz';
}
$mapper = (new MapperBuilder())->mapper();
$mapper->map('SomeClassWithConstants::BA*', 1337); // error
$mapper->map('SomeClassWithConstants::BA*', 'bar'); // ok
$mapper->map('SomeClassWithConstants::BA*', 'baz'); // ok
Example for enum:
enum SomeEnum: string
{
case FOO = 'foo';
case BAR = 'bar';
case BAZ = 'baz';
}
$mapper = (new MapperBuilder())->mapper();
$mapper->map('SomeEnum::BA*', 'foo'); // error
$mapper->map('SomeEnum::BA*', 'bar'); // ok
$mapper->map('SomeEnum::BA*', 'baz'); // ok
If needed and to help with the migration, the following deprecated constructor can be registered to reactivate the previous behaviour:
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.14.0
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');
DynamicConstructor (e437d9)This leads to the deprecation of the no longer needed interface \CuyZ\Valinor\Mapper\Tree\Message\TranslatableMessage which had a confusing name.
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.13.0
Reworking of messages body and parameters features
The \CuyZ\Valinor\Mapper\Tree\Message\Message interface is no longer a
Stringable, however it defines a new method body that must return the body
of the message, which can contain placeholders that will be replaced by
parameters.
These parameters can now be defined by implementing the interface
\CuyZ\Valinor\Mapper\Tree\Message\HasParameters.
This leads to the deprecation of the no longer needed interface
\CuyZ\Valinor\Mapper\Tree\Message\TranslatableMessage which had a confusing
name.
final class SomeException
extends DomainException
implements ErrorMessage, HasParameters, HasCode
{
private string $someParameter;
public function __construct(string $someParameter)
{
parent::__construct();
$this->someParameter = $someParameter;
}
public function body() : string
{
return 'Some message / {some_parameter} / {source_value}';
}
public function parameters(): array
{
return [
'some_parameter' => $this->someParameter,
];
}
public function code() : string
{
// A unique code that can help to identify the error
return 'some_unique_code';
}
}
Handle numeric-string type
The new numeric-string type can be used in docblocks.
It will accept any string value that is also numeric.
(new MapperBuilder())->mapper()->map('numeric-string', '42'); // ✅
(new MapperBuilder())->mapper()->map('numeric-string', 'foo'); // ❌
Better mapping error message
The message of the exception will now contain more information, especially the total number of errors and the source that was given to the mapper. This change aims to have a better understanding of what is wrong when debugging.
Before:
Could not map type `array{foo: string, bar: int}` with the given source.
After:
Could not map type `array{foo: string, bar: int}`. An error occurred at path bar: Value 'some other string' does not match type `int`.
MessagesFlattener countable (2c1c7c)The methods $message->name(), $message->path(), $message->type() and $message->value() have been deprecated in favor of the new method $message->node(…
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.12.0
SECURITY — Userland exception filtering
See advisory GHSA-5pgm-3j3g-2rc7 for more information.
Userland exception thrown in a constructor will not be automatically caught by the mapper anymore. This prevents messages with sensible information from reaching the final user — for instance an SQL exception showing a part of a query.
To allow exceptions to be considered as safe, the new method
MapperBuilder::filterExceptions() must be used, with caution.
final class SomeClass
{
public function __construct(private string $value)
{
\Webmozart\Assert\Assert::startsWith($value, 'foo_');
}
}
try {
(new \CuyZ\Valinor\MapperBuilder())
->filterExceptions(function (Throwable $exception) {
if ($exception instanceof \Webmozart\Assert\InvalidArgumentException) {
return \CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage::from($exception);
}
// If the exception should not be caught by this library, it
// must be thrown again.
throw $exception;
})
->mapper()
->map(SomeClass::class, 'bar_baz');
} catch (\CuyZ\Valinor\Mapper\MappingError $exception) {
// Should print something similar to:
// > Expected a value to start with "foo_". Got: "bar_baz"
echo $exception->node()->messages()[0];
}
Tree node API rework
The class \CuyZ\Valinor\Mapper\Tree\Node has been refactored to remove access
to unwanted methods that were not supposed to be part of the public API. Below
are a list of all changes:
New methods $node->sourceFilled() and $node->sourceValue() allow accessing
the source value.
The method $node->value() has been renamed to $node->mappedValue() and
will throw an exception if the node is not valid.
The method $node->type() now returns a string.
The methods $message->name(), $message->path(), $message->type() and
$message->value() have been deprecated in favor of the new method
$message->node().
The message parameter {original_value} has been deprecated in favor of
{source_value}.
Access removal of several parts of the library public API
The access to class/function definition, types and exceptions did not add value to the actual goal of the library. Keeping these features under the public API flag causes more maintenance burden whereas revoking their access allows more flexibility with the overall development of the library.
.idea folder (84ead0)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.11.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.11.0
Strict mode
The mapper is now more type-sensitive and will fail in the following situations:
When a value does not match exactly the awaited scalar type, for instance a
string "42" given to a node that awaits an integer.
When unnecessary array keys are present, for instance mapping an array
['foo' => …, 'bar' => …, 'baz' => …] to an object that needs only foo and
bar.
When permissive types like mixed or object are encountered.
These limitations can be bypassed by enabling the flexible mode:
(new \CuyZ\Valinor\MapperBuilder())
->flexible()
->mapper();
->map('array{foo: int, bar: bool}', [
'foo' => '42', // Will be cast from `string` to `int`
'bar' => 'true', // Will be cast from `string` to `bool`
'baz' => '…', // Will be ignored
]);
When using this library for a provider application — for instance an API endpoint that can be called with a JSON payload — it is recommended to use the strict mode. This ensures that the consumers of the API provide the exact awaited data structure, and prevents unknown values to be passed.
When using this library as a consumer of an external source, it can make sense to enable the flexible mode. This allows for instance to convert string numeric values to integers or to ignore data that is present in the source but not needed in the application.
Interface inferring
It is now mandatory to list all possible class-types that can be inferred by the mapper. This change is a step towards the library being able to deliver powerful new features such as compiling a mapper for better performance.
The existing calls to MapperBuilder::infer that could return several
class-names must now add a signature to the callback. The callbacks that require
no parameter and always return the same class-name can remain unchanged.
For instance:
$builder = (new \CuyZ\Valinor\MapperBuilder())
// Can remain unchanged
->infer(SomeInterface::class, fn () => SomeImplementation::class);
$builder = (new \CuyZ\Valinor\MapperBuilder())
->infer(
SomeInterface::class,
fn (string $type) => match($type) {
'first' => ImplementationA::class,
'second' => ImplementationB::class,
default => throw new DomainException("Unhandled `$type`.")
}
)
// …should be modified with:
->infer(
SomeInterface::class,
/** @return class-string<ImplementationA|ImplementationB> */
fn (string $type) => match($type) {
'first' => ImplementationA::class,
'second' => ImplementationB::class,
default => throw new DomainException("Unhandled `$type`.")
}
);
Object constructors collision
All these changes led to a new check that runs on all registered object constructors. If a collision is found between several constructors that have the same signature (the same parameter names), an exception will be thrown.
final class SomeClass
{
public static function constructorA(string $foo, string $bar): self
{
// …
}
public static function constructorB(string $foo, string $bar): self
{
// …
}
}
(new \CuyZ\Valinor\MapperBuilder())
->registerConstructor(
SomeClass::constructorA(...),
SomeClass::constructorB(...),
)
->mapper();
->map(SomeClass::class, [
'foo' => 'foo',
'bar' => 'bar',
]);
// Exception: A collision was detected […]
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.10.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.10.0
Documentation is now available at valinor.cuyz.io.
The method MapperBuilder::withCacheDir() has been deprecated in favor of a new method MapperBuilder::withCache() which accepts any PSR-16 compliant im…
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.9.0
Cache injection and warmup
The cache feature has been revisited, to give more control to the user on how and when to use it.
The method MapperBuilder::withCacheDir() has been deprecated in favor of a new
method MapperBuilder::withCache() which accepts any PSR-16 compliant
implementation.
Warning
These changes lead up to the default cache not being automatically registered anymore. If you still want to enable the cache (which you should), you will have to explicitly inject it (see below).
A default implementation is provided out of the box, which saves cache entries into the file system.
When the application runs in a development environment, the cache implementation
should be decorated with FileWatchingCache, which will watch the files of the
application and invalidate cache entries when a PHP file is modified by a
developer — preventing the library not behaving as expected when the signature
of a property or a method changes.
The cache can be warmed up, for instance in a pipeline during the build and deployment of the application — kudos to @boesing for the feature!
Note The cache has to be registered first, otherwise the warmup will end up being useless.
$cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-directory');
if ($isApplicationInDevelopmentEnvironment) {
$cache = new \CuyZ\Valinor\Cache\FileWatchingCache($cache);
}
$mapperBuilder = (new \CuyZ\Valinor\MapperBuilder())->withCache($cache);
// During the build:
$mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class);
// In the application:
$mapperBuilder->mapper()->map(SomeClass::class, [/* … */]);
Message formatting & translation
Major changes have been made to the messages being returned in case of a mapping error: the actual texts are now more accurate and show better information.
Warning
The method
NodeMessage::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 TranslationMessageFormatter can be used to translate the content
of messages.
The library provides a list of all messages that can be returned; this list can be filled or modified with custom translations.
\CuyZ\Valinor\Mapper\Tree\Message\Formatter\TranslationMessageFormatter::default()
// Create/override a single entry…
->withTranslation('fr', 'some custom message', 'un message personnalisé')
// …or several entries.
->withTranslations([
'some custom message' => [
'en' => 'Some custom message',
'fr' => 'Un message personnalisé',
'es' => 'Un mensaje personalizado',
],
'some other message' => [
// …
],
])
->format($message);
It is possible to join several formatters into one formatter by using the
AggregateMessageFormatter. This instance can then easily be injected in a
service that will handle messages.
The formatters will be called in the same order they are given to the aggregate.
(new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\AggregateMessageFormatter(
new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\LocaleMessageFormatter('fr'),
new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\MessageMapFormatter([
// …
],
\CuyZ\Valinor\Mapper\Tree\Message\Formatter\TranslationMessageFormatter::default(),
))->format($message)
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.8.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.8.0
Float values handling
Allows the usage of float values, as follows:
class Foo
{
/** @var 404.42|1337.42 */
public readonly float $value;
}
Literal boolean true / false values handling
Thanks @danog for this feature!
Allows the usage of boolean values, as follows:
class Foo
{
/** @var int|false */
public readonly int|bool $value;
}
Class string of union of object handling
Allows to declare several class names in a class-string:
class Foo
{
/** @var class-string<SomeClass|SomeOtherClass> */
public readonly string $className;
}
Allow psalm and phpstan prefix in docblocks
Thanks @boesing for this feature!
The following annotations are now properly handled: @psalm-param,
@phpstan-param, @psalm-return and @phpstan-return.
If one of those is found along with a basic @param or @return annotation, it
will take precedence over the basic value.
psalm and phpstan prefix in docblocks (64e0a2)true / false types (afcedf)> Warning This release introduces a major breaking change that must be considered before updating
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.7.0
Warning This release introduces a major breaking change that must be considered before updating
Constructor registration
The automatic named constructor discovery has been disabled. It is now mandatory to explicitly register custom constructors that can be used by the mapper.
This decision was made because of a security issue reported by @Ocramius and described in advisory advisory GHSA-xhr8-mpwq-2rr2.
As a result, existing code must list all named constructors that were previously
automatically used by the mapper, and registerer them using the
method MapperBuilder::registerConstructor().
The method MapperBuilder::bind() has been deprecated in favor of the method
above that should be used instead.
final class SomeClass
{
public static function namedConstructor(string $foo): self
{
// …
}
}
(new \CuyZ\Valinor\MapperBuilder())
->registerConstructor(
SomeClass::namedConstructor(...),
// …or for PHP < 8.1:
[SomeClass::class, 'namedConstructor'],
)
->mapper()
->map(SomeClass::class, [
// …
]);
See documentation for more information.
Source builder
The Source class is a new entry point for sources that are not plain array or
iterable. It allows accessing other features like camel-case keys or custom
paths mapping in a convenient way.
It should be used as follows:
$source = \CuyZ\Valinor\Mapper\Source\Source::json($jsonString)
->camelCaseKeys()
->map([
'towns' => 'cities',
'towns.*.label' => 'name',
]);
$result = (new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(SomeClass::class, $source);
See documentation for more details about its usage.
Attributes::ofType return type to array (1a599b)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.6.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.6.0
composer.json (6fdd62)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.5.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.5.0
TreeMapper#map() (e28003)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.4.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.4.0
Allow mapping to any type
Previously, the method TreeMapper::map would allow mapping only to an object.
It is now possible to map to any type handled by the library.
It is for instance possible to map to an array of objects:
$objects = (new MapperBuilder())->mapper()->map(
'array<' . SomeClass::class . '>',
[/* … */]
);
For simple use-cases, an array shape can be used:
$array = (new MapperBuilder())->mapper()->map(
'array{foo: string, bar: int}',
[/* … */]
);
echo $array['foo'];
echo $array['bar'] * 2;
This new feature changes the possible behaviour of the mapper, meaning static analysis tools need help to understand the types correctly. An extension for PHPStan and a plugin for Psalm are now provided and can be included in a project to automatically increase the type coverage.
Better handling of messages
When working with messages, it can sometimes be useful to customize the content of a message — for instance to translate it.
The helper
class \CuyZ\Valinor\Mapper\Tree\Message\Formatter\MessageMapFormatter can be
used to provide a list of new formats. It can be instantiated with an array
where each key represents either:
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:
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']);
MessageMapFormatter (ddf69e)MessagesFlattener (a97b40)NodeTraverser for recursive operations on nodes (cc1bc6)!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.3.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.3.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.2.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.2.0
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.1.1
!!! info inline end "[See release on GitHub]" [See release on GitHub]: https://github.com/CuyZ/Valinor/releases/tag/0.1.1
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →