Skip to content

Changelog 0.9.0 — 23rd of May 2022

Notable changes

Cache injection and warmup

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

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


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.


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

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

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

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

    foreach ($messages as $message) {
        if ($message->code() === 'some_code') {
            $message = $message->withBody('new message / {original_message}');

        echo $message;

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

try {
    (new \CuyZ\Valinor\MapperBuilder())->mapper()->map('int<0, 100>', 1337);
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    $message = $error->node()->messages()[0];

    if (is_numeric($message->value())) {
        $message = $message->withBody(
            'Invalid amount {original_value, number, currency}'

    // Invalid amount: $1,337.00
    echo $message->withLocale('en_US');

    // Invalid amount: £1,337.00
    echo $message->withLocale('en_GB');

    // Invalid amount: 1 337,00 €
    echo $message->withLocale('fr_FR');

See ICU documentation for more information on available syntax.

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

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

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

    // Create/override a single entry…
    ->withTranslation('fr', 'some custom message', 'un message personnalisé')
    // …or several entries.
        'some custom message' => [
            'en' => 'Some custom message',
            'fr' => 'Un message personnalisé',
            'es' => 'Un mensaje personalizado',
        'some other 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([
        // …


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


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

Bug Fixes

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


  • Change InvalidParameterIndex exception inheritance type (b75adb)
  • Introduce layer for object builder arguments (48f936)
Back to top