Changelog 1.8.2 — 8th of January 2024¶
+ +Bug Fixes¶
+-
+
- Allow callable type to be compiled (4a9771f) +
diff --git a/1.8/project/changelog/index.html b/1.8/project/changelog/index.html index 8b56fc7..7b15eac 100644 --- a/1.8/project/changelog/index.html +++ b/1.8/project/changelog/index.html @@ -1054,7 +1054,8 @@
1.8.1
— 8th of January 20241.8.2
— 8th of January 20241.8.1
— 8th of January 20241.8.0
— 26th of December 20231.7.0
— 23rd of October 20231.6.1
— 11th of October 2023Valinor takes care of the construction and validation of raw inputs (JSON, plain arrays, etc.) into objects, ensuring a perfectly valid state. It allows the objects to be used without having to worry about their integrity during the whole application lifecycle.
The validation system will detect any incorrect value and help the developers by providing precise and human-readable error messages.
The mapper can handle native PHP types as well as other advanced types supported by PHPStan and Psalm like shaped arrays, generics, integer ranges and more.
"},{"location":"#why","title":"Why?","text":"There are many benefits of using objects instead of plain arrays in a codebase:
This library also provides a serialization system that can help transform a given input into a data format (JSON, CSV, \u2026), while preserving the original structure.
You can find more information on this topic in the normalizer chapter.
Validating and transforming raw data into an object can be achieved easily with native PHP, but it requires a lot a boilerplate code.
Below is a simple example of doing that without a mapper:
final class Person\n{\n public readonly string $name;\n\n public readonly DateTimeInterface $birthDate;\n}\n\n$data = $client->request('GET', 'https://example.com/person/42')->toArray();\n\nif (! isset($data['name']) || ! is_string($data['name'])) {\n // Cumbersome error handling\n}\n\nif (! isset($data['birthDate']) || ! is_string($data['birthDate'])) {\n // Another cumbersome error handling\n}\n\n$birthDate = DateTimeImmutable::createFromFormat('Y-m-d', $data['birthDate']);\n\nif (! $birthDate instanceof DateTimeInterface) {\n // Yet another cumbersome error handling\n}\n\n$person = new Person($data['name'], $birthDate);\n
Using a mapper saves a lot of time and energy, especially on objects with a lot of properties:
$data = $client->request('GET', 'https://example.com/person/42')->toArray();\n\ntry {\n $person = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(Person::class, $data);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Detailed error handling\n}\n
This library provides advanced features for more complex cases, check out the next chapter to get started.
"},{"location":"getting-started/","title":"Getting started","text":""},{"location":"getting-started/#installation","title":"Installation","text":"composer require cuyz/valinor\n
"},{"location":"getting-started/#example","title":"Example","text":"An application must handle the data coming from an external API; the response has a JSON format and describes a thread and its answers. The validity of this input is unsure, besides manipulating a raw JSON string is laborious and inefficient.
{\n \"id\": 1337,\n \"content\": \"Do you like potatoes?\",\n \"date\": \"1957-07-23 13:37:42\",\n \"answers\": [\n {\n \"user\": \"Ella F.\",\n \"message\": \"I like potatoes\",\n \"date\": \"1957-07-31 15:28:12\"\n },\n {\n \"user\": \"Louis A.\",\n \"message\": \"And I like tomatoes\",\n \"date\": \"1957-08-13 09:05:24\"\n }\n ]\n}\n
The application must be certain that it can handle this data correctly; wrapping the input in a value object will help.
A schema representing the needed structure must be provided, using classes.
final class Thread\n{\n public function __construct(\n public readonly int $id,\n public readonly string $content,\n public readonly DateTimeInterface $date,\n /** @var Answer[] */\n public readonly array $answers, \n ) {}\n}\n\nfinal class Answer\n{\n public function __construct(\n public readonly string $user,\n public readonly string $message,\n public readonly DateTimeInterface $date,\n ) {}\n}\n
Then a mapper is used to hydrate a source into these objects.
public function getThread(int $id): Thread\n{\n $rawJson = $this->client->request(\"https://example.com/thread/$id\");\n\n try { \n return (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n Thread::class,\n new \\CuyZ\\Valinor\\Mapper\\Source\\JsonSource($rawJson)\n );\n } catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n }\n}\n
"},{"location":"getting-started/#mapping-advanced-types","title":"Mapping advanced types","text":"Although it is recommended to map an input to a value object, in some cases mapping to another type can be easier/more flexible.
It is for instance possible to map to an array of objects:
try {\n $objects = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n 'array<' . SomeClass::class . '>',\n [/* \u2026 */]\n );\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
For simple use-cases, an array shape can be used:
try {\n $array = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n 'array{foo: string, bar: int}',\n [/* \u2026 */]\n );\n\n echo $array['foo'];\n echo $array['bar'] * 2;\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
"},{"location":"how-to/customize-error-messages/","title":"Customizing error messages","text":"The content of a message can be changed to fit custom use cases; it can contain placeholders that will be replaced with useful information.
The placeholders below are always available; even more may be used depending on the original message.
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 {source_value}
the source value that was given to the node {original_message}
the original message before being customized Usage:
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */]);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $messages = \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Messages::flattenFromNode(\n $error->node()\n );\n\n foreach ($messages as $message) {\n if ($message->code() === 'some_code') {\n $message = $message\n ->withParameter('some_parameter', 'some custom value')\n ->withBody('new message / {message_code} / {some_parameter}');\n }\n\n // new message / some_code / some custom value\n echo $message;\n }\n}\n
The messages are formatted using the ICU library, enabling the placeholders to use advanced syntax to perform proper translations, for instance currency support.
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map('int<0, 100>', 1337);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $message = $error->node()->messages()[0];\n\n if (is_numeric($message->node()->mappedValue())) {\n $message = $message->withBody(\n 'Invalid amount {source_value, number, currency}'\n ); \n } \n\n // Invalid amount: $1,337.00\n echo $message->withLocale('en_US');\n\n // Invalid amount: \u00a31,337.00\n echo $message->withLocale('en_GB');\n\n // Invalid amount: 1 337,00 \u20ac\n echo $message->withLocale('fr_FR');\n}\n
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.
For deeper message changes, formatters can be used to customize body and parameters.
Note
Formatters can be added to messages
"},{"location":"how-to/customize-error-messages/#translation","title":"Translation","text":"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()\n // Create/override a single entry\u2026\n ->withTranslation('fr', 'some custom message', 'un message personnalis\u00e9')\n // \u2026or several entries.\n ->withTranslations([\n 'some custom message' => [\n 'en' => 'Some custom message',\n 'fr' => 'Un message personnalis\u00e9',\n 'es' => 'Un mensaje personalizado',\n ], \n 'some other message' => [\n // \u2026\n ], \n ])\n ->format($message);\n
"},{"location":"how-to/customize-error-messages/#replacement-map","title":"Replacement map","text":"The formatter MessageMapFormatter
can be used to provide a list of messages replacements. 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 custom translation service is used \u2014 to avoid useless greedy operations.
In any case, the content can contain placeholders as described above.
(new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\MessageMapFormatter([\n // Will match if the given message has this exact code\n 'some_code' => 'New content / code: {message_code}',\n\n // Will match if the given message has this exact content\n 'Some message content' => 'New content / previous: {original_message}',\n\n // Will match if the given message is an instance of `SomeError`\n SomeError::class => 'New content / value: {source_value}',\n\n // A callback can be used to get access to the message instance\n OtherError::class => function (NodeMessage $message): string {\n if ($message->path() === 'foo.bar') {\n return 'Some custom message';\n }\n\n return $message->body();\n },\n\n // For greedy operation, it is advised to use a lazy-callback\n 'foo' => fn () => $this->customTranslator->translate('foo.bar'),\n]))\n ->defaultsTo('some default message')\n // \u2026or\u2026\n ->defaultsTo(fn () => $this->customTranslator->translate('default_message'))\n ->format($message);\n
"},{"location":"how-to/customize-error-messages/#several-formatters","title":"Several formatters","text":"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(\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\LocaleMessageFormatter('fr'),\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\MessageMapFormatter([\n // \u2026\n ],\n \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\TranslationMessageFormatter::default(),\n))->format($message)\n
"},{"location":"how-to/deal-with-dates/","title":"Dealing with dates","text":"When the mapper builds a date object, it has to know which format(s) are supported. By default, any valid timestamp or RFC 3339-formatted value will be accepted.
If other formats are to be supported, they need to be registered using the following method:
(new \\CuyZ\\Valinor\\MapperBuilder())\n // Both `Cookie` and `ATOM` formats will be accepted\n ->supportDateFormats(DATE_COOKIE, DATE_ATOM)\n ->mapper()\n ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');\n
"},{"location":"how-to/deal-with-dates/#custom-date-class-implementation","title":"Custom date class implementation","text":"By default, the library will map a DateTimeInterface
to a DateTimeImmutable
instance. If other implementations are to be supported, custom constructors can be used.
Here is an implementation example for the nesbot/carbon library:
(new MapperBuilder())\n // When the mapper meets a `DateTimeInterface` it will convert it to Carbon\n ->infer(DateTimeInterface::class, fn () => \\Carbon\\Carbon::class)\n\n // We teach the mapper how to create a Carbon instance\n ->registerConstructor(function (string $time): \\Carbon\\Carbon {\n // Only `Cookie` format will be accepted\n return Carbon::createFromFormat(DATE_COOKIE, $time);\n })\n\n // Carbon uses its own exceptions, so we need to wrap it for the mapper\n ->filterExceptions(function (Throwable $exception) {\n if ($exception instanceof \\Carbon\\Exceptions\\Exception) {\n return \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessageBuilder::from($exception);\n }\n\n throw $exception;\n })\n\n ->mapper()\n ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');\n
"},{"location":"how-to/infer-interfaces/","title":"Inferring interfaces","text":"When the mapper meets an interface, it needs to understand which implementation (a class that implements this interface) will be used \u2014 this information must be provided in the mapper builder, using the method infer()
.
The callback given to this method must return the name of a class that implements the interface. Any arguments can be required by the callback; they will be mapped properly using the given source.
If the callback can return several class names, it needs to provide a return signature with the list of all class-strings that can be returned.
$mapper = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(UuidInterface::class, fn () => MyUuid::class)\n ->infer(\n SomeInterface::class, \n /** @return class-string<FirstImplementation|SecondImplementation> */\n fn (string $type) => match($type) {\n 'first' => FirstImplementation::class,\n 'second' => SecondImplementation::class,\n default => throw new DomainException(\"Unhandled type `$type`.\")\n }\n )->mapper();\n\n// Will return an instance of `FirstImplementation`\n$mapper->map(SomeInterface::class, [\n 'type' => 'first',\n 'uuid' => 'a6868d61-acba-406d-bcff-30ecd8c0ceb6',\n 'someString' => 'foo',\n]);\n\n// Will return an instance of `SecondImplementation`\n$mapper->map(SomeInterface::class, [\n 'type' => 'second',\n 'uuid' => 'a6868d61-acba-406d-bcff-30ecd8c0ceb6',\n 'someInt' => 42,\n]);\n\ninterface SomeInterface {}\n\nfinal class FirstImplementation implements SomeInterface\n{\n public readonly UuidInterface $uuid;\n\n public readonly string $someString;\n}\n\nfinal class SecondImplementation implements SomeInterface\n{\n public readonly UuidInterface $uuid;\n\n public readonly int $someInt;\n}\n
"},{"location":"how-to/infer-interfaces/#inferring-classes","title":"Inferring classes","text":"The same mechanics can be applied to infer abstract or parent classes.
Example with an abstract class:
abstract class SomeAbstractClass\n{\n public string $foo;\n\n public string $bar;\n}\n\nfinal class SomeChildClass extends SomeAbstractClass\n{\n public string $baz;\n}\n\n$result = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(\n SomeAbstractClass::class, \n fn () => SomeChildClass::class\n )\n ->mapper()\n ->map(SomeAbstractClass::class, [\n 'foo' => 'foo',\n 'bar' => 'bar',\n 'baz' => 'baz',\n ]);\n\nassert($result instanceof SomeChildClass);\nassert($result->foo === 'foo');\nassert($result->bar === 'bar');\nassert($result->baz === 'baz');\n
Example with inheritance:
class SomeParentClass\n{\n public string $foo;\n\n public string $bar;\n}\n\nfinal class SomeChildClass extends SomeParentClass\n{\n public string $baz;\n}\n\n$result = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(\n SomeParentClass::class, \n fn () => SomeChildClass::class\n )\n ->mapper()\n ->map(SomeParentClass::class, [\n 'foo' => 'foo',\n 'bar' => 'bar',\n 'baz' => 'baz',\n ]);\n\nassert($result instanceof SomeChildClass);\nassert($result->foo === 'foo');\nassert($result->bar === 'bar');\nassert($result->baz === 'baz');\n
"},{"location":"how-to/map-arguments-of-a-callable/","title":"Mapping arguments of a callable","text":"This library can map the arguments of a callable; it 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 {\n return \"$foo / $bar\";\n}\n\ntry {\n $arguments = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->argumentsMapper()\n ->mapArguments($someFunction, [\n 'foo' => 'some value',\n 'bar' => 42,\n ]);\n\n // some value / 42\n echo $someFunction(...$arguments);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
Any callable can be given to the arguments mapper:
final class SomeController\n{\n public static function someAction(string $foo, int $bar): string\n {\n return \"$foo / $bar\";\n }\n}\n\ntry {\n $arguments = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->argumentsMapper()\n ->mapArguments(SomeController::someAction(...), [\n 'foo' => 'some value',\n 'bar' => 42,\n ]);\n\n // some value / 42\n echo SomeController::someAction(...$arguments);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
"},{"location":"how-to/transform-input/","title":"Transforming input","text":"Any source can be given to the mapper, be it an array, some JSON, YAML or even a file:
$mapper = (new \\CuyZ\\Valinor\\MapperBuilder())->mapper();\n\n$mapper->map(\n SomeClass::class,\n \\CuyZ\\Valinor\\Mapper\\Source\\Source::array($someData)\n);\n\n$mapper->map(\n SomeClass::class,\n \\CuyZ\\Valinor\\Mapper\\Source\\Source::json($jsonString)\n);\n\n$mapper->map(\n SomeClass::class,\n \\CuyZ\\Valinor\\Mapper\\Source\\Source::yaml($yamlString)\n);\n\n$mapper->map(\n SomeClass::class,\n // File containing valid Json or Yaml content and with valid extension\n \\CuyZ\\Valinor\\Mapper\\Source\\Source::file(\n new SplFileObject('path/to/my/file.json')\n )\n);\n
Info
JSON or YAML given to a source may be invalid, in which case an exception can be caught and manipulated.
try {\n $source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::json('invalid JSON');\n} catch (\\CuyZ\\Valinor\\Mapper\\Source\\Exception\\InvalidSource $exception) {\n // Let the application handle the exception in the desired way.\n // It is possible to get the original source with `$exception->source()`\n}\n
"},{"location":"how-to/transform-input/#modifiers","title":"Modifiers","text":"Sometimes the source is not in the same format and/or organised in the same way as a value object. Modifiers can be used to change a source before the mapping occurs.
"},{"location":"how-to/transform-input/#camel-case-keys","title":"Camel case keys","text":"This modifier recursively forces all keys to be in camelCase format.
final class SomeClass\n{\n public readonly string $someValue;\n}\n\n$source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::array([\n 'some_value' => 'foo',\n // \u2026or\u2026\n 'some-value' => 'foo',\n // \u2026or\u2026\n 'some value' => 'foo',\n // \u2026will be replaced by `['someValue' => 'foo']`\n ])\n ->camelCaseKeys();\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, $source);\n
"},{"location":"how-to/transform-input/#path-mapping","title":"Path mapping","text":"This modifier can be used to change paths in the source data using a dot notation.
The mapping is done using an associative array of path mappings. This array must have the source path as key and the target path as value.
The source path uses the dot notation (eg A.B.C
) and can contain one *
for array paths (eg A.B.*.C
).
final class Country\n{\n /** @var non-empty-string */\n public readonly string $name;\n\n /** @var list<City> */\n public readonly array $cities;\n}\n\nfinal class City\n{\n /** @var non-empty-string */\n public readonly string $name;\n\n public readonly DateTimeZone $timeZone;\n}\n\n$source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::array([\n 'identification' => 'France',\n 'towns' => [\n [\n 'label' => 'Paris',\n 'timeZone' => 'Europe/Paris',\n ],\n [\n 'label' => 'Lyon',\n 'timeZone' => 'Europe/Paris',\n ],\n ],\n])->map([\n 'identification' => 'name',\n 'towns' => 'cities',\n 'towns.*.label' => 'name',\n]);\n\n// After modification this is what the source will look like:\n// [\n// 'name' => 'France',\n// 'cities' => [\n// [\n// 'name' => 'Paris',\n// 'timeZone' => 'Europe/Paris',\n// ],\n// [\n// 'name' => 'Lyon',\n// 'timeZone' => 'Europe/Paris',\n// ],\n// ],\n// ];\n\n(new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(Country::class, $source);\n
"},{"location":"how-to/transform-input/#custom-source","title":"Custom source","text":"The source is just an iterable, so it's easy to create a custom one. It can even be combined with the provided builder.
final class AcmeSource implements IteratorAggregate\n{\n private iterable $source;\n\n public function __construct(iterable $source)\n {\n $this->source = $this->doSomething($source);\n }\n\n private function doSomething(iterable $source): iterable\n {\n // Do something with $source\n\n return $source;\n }\n\n public function getIterator()\n {\n yield from $this->source;\n }\n}\n\n$source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::iterable(\n new AcmeSource([\n 'valueA' => 'foo',\n 'valueB' => 'bar',\n ])\n)->camelCaseKeys();\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, $source);\n
"},{"location":"how-to/use-custom-object-constructors/","title":"Using custom object constructors","text":"An object may have custom ways of being created, in such cases these constructors need to be registered to the mapper to be used. A constructor is a callable that can be either:
__invoke
methodIn any case, the return type of the callable will be resolved by the mapper to know when to use it. Any argument can be provided and will automatically be mapped using the given source. These arguments can then be used to instantiate the object in the desired way.
Registering any constructor will disable the native constructor \u2014 the __construct
method \u2014 of the targeted class. If for some reason it still needs to be handled as well, the name of the class must be given to the registration method.
If several constructors are registered, they must provide distinct signatures to prevent collision during mapping \u2014 meaning that if two constructors require several arguments with the exact same names, the mapping will fail.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n // Allow the native constructor to be used\n Color::class,\n\n // Register a named constructor (1)\n Color::fromHex(...),\n\n /**\n * An anonymous function can also be used, for instance when the desired\n * object is an external dependency that cannot be modified.\n * \n * @param 'red'|'green'|'blue' $color\n * @param 'dark'|'light' $darkness\n */\n function (string $color, string $darkness): Color {\n $main = $darkness === 'dark' ? 128 : 255;\n $other = $darkness === 'dark' ? 0 : 128;\n\n return new Color(\n $color === 'red' ? $main : $other,\n $color === 'green' ? $main : $other,\n $color === 'blue' ? $main : $other,\n );\n }\n )\n ->mapper()\n ->map(Color::class, [/* \u2026 */]);\n\nfinal class Color\n{\n /**\n * @param int<0, 255> $red\n * @param int<0, 255> $green\n * @param int<0, 255> $blue\n */\n public function __construct(\n public readonly int $red,\n public readonly int $green,\n public readonly int $blue\n ) {}\n\n /**\n * @param non-empty-string $hex\n */\n public static function fromHex(string $hex): self\n {\n if (strlen($hex) !== 6) {\n throw new DomainException('Must be 6 characters long');\n }\n\n /** @var int<0, 255> $red */\n $red = hexdec(substr($hex, 0, 2));\n /** @var int<0, 255> $green */\n $green = hexdec(substr($hex, 2, 2));\n /** @var int<0, 255> $blue */\n $blue = hexdec(substr($hex, 4, 2));\n\n return new self($red, $green, $blue);\n }\n}\n
\u2026or for PHP < 8.1:
[Color::class, 'fromHex'],\n
Registering a constructor for an enum works the same way as for a class, as described above.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n // Allow the native constructor to be used\n SomeEnum::class,\n\n // Register a named constructor\n SomeEnum::fromMatrix(...)\n )\n ->mapper()\n ->map(SomeEnum::class, [\n 'type' => 'FOO',\n 'number' => 2,\n ]);\n\nenum SomeEnum: string\n{\n case CASE_A = 'FOO_VALUE_1';\n case CASE_B = 'FOO_VALUE_2';\n case CASE_C = 'BAR_VALUE_1';\n case CASE_D = 'BAR_VALUE_2';\n\n /**\n * @param 'FOO'|'BAR' $type\n * @param int<1, 2> $number\n */\n public static function fromMatrix(string $type, int $number): self\n {\n return self::from(\"{$type}_VALUE_{$number}\");\n }\n}\n
Note
An enum constructor can be for a specific pattern:
enum SomeEnum\n{\n case FOO;\n case BAR;\n case BAZ;\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n /**\n * This constructor will be called only when pattern `SomeEnum::BA*`\n * is requested during mapping.\n * \n * @return SomeEnum::BA*\n */\n fn (string $value): SomeEnum => /* Some custom domain logic */\n )\n ->mapper()\n ->map(SomeEnum::class . '::BA*', 'some custom value');\n
"},{"location":"how-to/use-custom-object-constructors/#dynamic-constructors","title":"Dynamic constructors","text":"In some situations the type handled by a constructor is only known at runtime, in which case the constructor needs to know what class must be used to instantiate the object.
For instance, an interface may declare a static constructor that is then implemented by several child classes. One solution would be to register the constructor for each child class, which leads to a lot of boilerplate code and would require a new registration each time a new child is created. Another way is to use the attribute \\CuyZ\\Valinor\\Mapper\\Object\\DynamicConstructor
.
When a constructor uses this attribute, its first parameter must be a string and will be filled with the name of the actual class that the mapper needs to build when the constructor is called. Other arguments may be added and will be mapped normally, depending on the source given to the mapper.
interface InterfaceWithStaticConstructor\n{\n public static function from(string $value): self;\n}\n\nfinal class ClassWithInheritedStaticConstructor implements InterfaceWithStaticConstructor\n{\n private function __construct(private SomeValueObject $value) {}\n\n public static function from(string $value): self\n {\n return new self(new SomeValueObject($value));\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n #[\\CuyZ\\Valinor\\Attribute\\DynamicConstructor]\n function (string $className, string $value): InterfaceWithStaticConstructor {\n return $className::from($value);\n }\n )\n ->mapper()\n ->map(ClassWithInheritedStaticConstructor::class, 'foo');\n
"},{"location":"other/app-and-framework-integration/","title":"Application and framework integration","text":"This library is framework-agnostic, but using it in an application that relies on a framework is still possible.
For Symfony applications, check out the chapter below. For other frameworks, check out the custom integration chapter.
"},{"location":"other/app-and-framework-integration/#symfony-bundle","title":"Symfony bundle","text":"A bundle is available to automatically integrate this library into a Symfony application.
composer require cuyz/valinor-bundle\n
The documentation of this bundle can be found on the GitHub repository.
"},{"location":"other/app-and-framework-integration/#custom-integration","title":"Custom integration","text":"If the application does not have a dedicated framework integration, it is still possible to integrate this library manually.
"},{"location":"other/app-and-framework-integration/#mapper-registration","title":"Mapper registration","text":"The most important task of the integration is to correctly register the mapper(s) used in the application. Mapper instance(s) should be shared between services whenever possible; this is important because heavy operations are cached internally to improve performance during runtime.
If the framework uses a service container, it should be configured in a way where the mapper(s) are registered as shared services. In other cases, direct instantiation of the mapper(s) should be avoided.
$mapperBuilder = new \\CuyZ\\Valinor\\MapperBuilder();\n\n// \u2026customization of the mapper builder\u2026\n\n$container->addSharedService('mapper', $mapperBuilder->mapper());\n
"},{"location":"other/app-and-framework-integration/#registering-a-cache","title":"Registering a cache","text":"As mentioned above, caching is important to allow the mapper to perform well. The application really should provide a cache implementation to the mapper builder.
As stated in the performance chapter, the library provides a cache implementation out of the box which can be used in any application. Custom cache can be used as well, as long as it is PSR-16 compliant.
$cache = new \\CuyZ\\Valinor\\Cache\\FileSystemCache('path/to/cache-directory');\n\n// If the application can detect when it is in development environment, it is\n// advised to wrap the cache with a `FileWatchingCache` instance, to avoid\n// having to manually clear the cache when a file changes during development.\nif ($isApplicationInDevelopmentEnvironment) {\n $cache = new \\CuyZ\\Valinor\\Cache\\FileWatchingCache($cache);\n}\n\n$mapperBuilder = $mapperBuilder->withCache($cache);\n
"},{"location":"other/app-and-framework-integration/#warming-up-the-cache","title":"Warming up the cache","text":"The cache can be warmed up to ease the application cold start. If the framework has a way to automatically detect which classes will be used by the mapper, they should be given to the warmup
method, as stated in the cache warmup chapter.
Concerning other configurations, such as enabling flexible casting, configuring supported date formats or registering custom constructors, an integration should be provided to configure the mapper builder in a convenient way \u2014 how it is done will mostly depend on the framework features and its main philosophy.
"},{"location":"other/performance-and-caching/","title":"Performance & caching","text":"This library needs to parse a lot of information in order to handle all provided features. Therefore, it is strongly advised to activate the cache to reduce heavy workload between runtimes, especially when the application runs in a production environment.
The library provides a cache implementation out of the box, which saves cache entries into the file system.
Note
It is also possible to use any PSR-16 compliant implementation, as long as it is capable of caching the entries handled by the library.
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 \u2014 preventing the library not behaving as expected when the signature of a property or a method changes.
$cache = new \\CuyZ\\Valinor\\Cache\\FileSystemCache('path/to/cache-directory');\n\nif ($isApplicationInDevelopmentEnvironment) {\n $cache = new \\CuyZ\\Valinor\\Cache\\FileWatchingCache($cache);\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->withCache($cache)\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */]);\n
"},{"location":"other/performance-and-caching/#warming-up-cache","title":"Warming up cache","text":"The cache can be warmed up, for instance in a pipeline during the build and deployment of the application.
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-dir');\n\n$mapperBuilder = (new \\CuyZ\\Valinor\\MapperBuilder())->withCache($cache);\n\n// During the build:\n$mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class);\n\n// In the application:\n$mapper->mapper()->map(SomeClass::class, [/* \u2026 */]);\n
"},{"location":"other/static-analysis/","title":"Static analysis","text":"To help static analysis of a codebase using this library, an extension for PHPStan and a plugin for Psalm are provided. They enable these tools to better understand the behaviour of the mapper.
Note
To activate this feature, the plugin must be registered correctly:
PHPStanPsalm phpstan.neonincludes:\n - vendor/cuyz/valinor/qa/PHPStan/valinor-phpstan-configuration.php\n
composer.json\"autoload-dev\": {\n \"files\": [\n \"vendor/cuyz/valinor/qa/Psalm/ValinorPsalmPlugin.php\"\n ]\n}\n
psalm.xml<plugins>\n <pluginClass class=\"CuyZ\\Valinor\\QA\\Psalm\\ValinorPsalmPlugin\"/>\n</plugins>\n
Considering at least one of those tools are installed on a project, below are examples of the kind of errors that would be reported.
Mapping to an array of classes
final class SomeClass\n{\n public function __construct(\n public readonly string $foo,\n public readonly int $bar,\n ) {}\n}\n\n$objects = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n 'array<' . SomeClass::class . '>',\n [/* \u2026 */]\n );\n\nforeach ($objects as $object) {\n // \u2705\n echo $object->foo;\n\n // \u2705\n echo $object->bar * 2;\n\n // \u274c Cannot perform operation between `string` and `int`\n echo $object->foo * $object->bar;\n\n // \u274c Property `SomeClass::$fiz` is not defined\n echo $object->fiz;\n}\n
Mapping to a shaped array
$array = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n 'array{foo: string, bar: int}',\n [/* \u2026 */]\n );\n\n// \u2705\necho $array['foo'];\n\n// \u274c Expected `string` but got `int`\necho strtolower($array['bar']);\n\n// \u274c Cannot perform operation between `string` and `int`\necho $array['foo'] * $array['bar'];\n\n// \u274c Offset `fiz` does not exist on array\necho $array['fiz']; \n
Mapping arguments of a callable
$someFunction = function(string $foo, int $bar): string {\n return \"$foo / $bar\";\n};\n\n$arguments = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->argumentsMapper()\n ->mapArguments($someFunction, [\n 'foo' => 'some value',\n 'bar' => 42,\n ]);\n\n// \u2705 Arguments have a correct shape, no error reported\necho $someFunction(...$arguments);\n
"},{"location":"project/alternatives/","title":"Alternatives to this library","text":"Mapping and hydration have been available in the PHP world for a long time. This library aims to bring powerful features to this aspect, some of which are missing in similar packages:
list<string>
, non-empty-string
, positive-int
, int<0, 42>
, shaped arrays, generic classes and more are handled and validated properly.You may take a look at alternative projects, but some features listed above might be missing:
symfony/serializer
eventsauce/object-hydrator
crell/serde
spatie/laravel-data
jms/serializer
netresearch/jsonmapper
json-mapper/json-mapper
brick/json-mapper
Below are listed the changelogs for all released versions of the library.
"},{"location":"project/changelog/#version-1","title":"Version 1","text":"1.8.1
\u2014 8th of January 20241.8.0
\u2014 26th of December 20231.7.0
\u2014 23rd of October 20231.6.1
\u2014 11th of October 20231.6.0
\u2014 25th of August 20231.5.0
\u2014 7th of August 20231.4.0
\u2014 17th of April 20231.3.1
\u2014 13th of February 20231.3.0
\u2014 8th of February 20231.2.0
\u2014 9th of January 20231.1.0
\u2014 20th of December 20221.0.0
\u2014 28th of November 20220.17.0
\u2014 8th of November 20220.16.0
\u2014 19th of October 20220.15.0
\u2014 6th of October 20220.14.0
\u2014 1st of September 20220.13.0
\u2014 31st of July 20220.12.0
\u2014 10th of July 20220.11.0
\u2014 23rd of June 20220.10.0
\u2014 10th of June 20220.9.0
\u2014 23rd of May 20220.8.0
\u2014 9th of May 20220.7.0
\u2014 24th of March 20220.6.0
\u2014 24th of February 20220.5.0
\u2014 21st of January 20220.4.0
\u2014 7th of January 20220.3.0
\u2014 18th of December 20210.2.0
\u2014 7th of December 20210.1.1
\u2014 1st of December 2021The development of this library is mainly motivated by the kind words and the help of many people. I am grateful to everyone, especially to the contributors of this repository who directly help to push the project forward.
I have to give JetBrains credits for providing a free PhpStorm license for the development of this open-source package.
I also want to thank Blackfire for providing a license of their awesome tool, leading to notable performance gains when using this library.
"},{"location":"project/changelog/version-0.1.1/","title":"Changelog 0.1.1 \u2014 1st of December 2021","text":"See release on GitHub
"},{"location":"project/changelog/version-0.1.1/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"See release on GitHub
"},{"location":"project/changelog/version-0.10.0/#notable-changes","title":"Notable changes","text":"Documentation is now available at valinor.cuyz.io.
"},{"location":"project/changelog/version-0.10.0/#features","title":"Features","text":"@var
annotation (d8eb4d)See release on GitHub
"},{"location":"project/changelog/version-0.11.0/#notable-changes","title":"Notable changes","text":"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' => \u2026, 'bar' => \u2026, 'baz' => \u2026]
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())\n ->flexible()\n ->mapper();\n ->map('array{foo: int, bar: bool}', [\n 'foo' => '42', // Will be cast from `string` to `int`\n 'bar' => 'true', // Will be cast from `string` to `bool`\n 'baz' => '\u2026', // Will be ignored\n ]);\n
When using this library for a provider application \u2014 for instance an API endpoint that can be called with a JSON payload \u2014 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())\n // Can remain unchanged\n ->infer(SomeInterface::class, fn () => SomeImplementation::class);\n
$builder = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(\n SomeInterface::class,\n fn (string $type) => match($type) {\n 'first' => ImplementationA::class,\n 'second' => ImplementationB::class,\n default => throw new DomainException(\"Unhandled `$type`.\")\n }\n )\n // \u2026should be modified with:\n ->infer(\n SomeInterface::class,\n /** @return class-string<ImplementationA|ImplementationB> */\n fn (string $type) => match($type) {\n 'first' => ImplementationA::class,\n 'second' => ImplementationB::class,\n default => throw new DomainException(\"Unhandled `$type`.\")\n }\n );\n
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\n{\n public static function constructorA(string $foo, string $bar): self\n {\n // \u2026\n }\n\n public static function constructorB(string $foo, string $bar): self\n {\n // \u2026\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n SomeClass::constructorA(...),\n SomeClass::constructorB(...),\n )\n ->mapper();\n ->map(SomeClass::class, [\n 'foo' => 'foo',\n 'bar' => 'bar',\n ]);\n\n// Exception: A collision was detected [\u2026]\n
"},{"location":"project/changelog/version-0.11.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"See release on GitHub
"},{"location":"project/changelog/version-0.12.0/#notable-changes","title":"Notable changes","text":"SECURITY \u2014 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 \u2014 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\n{\n public function __construct(private string $value)\n {\n \\Webmozart\\Assert\\Assert::startsWith($value, 'foo_');\n }\n}\n\ntry {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->filterExceptions(function (Throwable $exception) {\n if ($exception instanceof \\Webmozart\\Assert\\InvalidArgumentException) {\n return \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\ThrowableMessage::from($exception);\n }\n\n // If the exception should not be caught by this library, it\n // must be thrown again.\n throw $exception;\n })\n ->mapper()\n ->map(SomeClass::class, 'bar_baz');\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $exception) {\n // Should print something similar to:\n // > Expected a value to start with \"foo_\". Got: \"bar_baz\"\n echo $exception->node()->messages()[0];\n}\n
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.
"},{"location":"project/changelog/version-0.12.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":".idea
folder (84ead0)See release on GitHub
"},{"location":"project/changelog/version-0.13.0/#notable-changes","title":"Notable changes","text":"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\n extends DomainException \n implements ErrorMessage, HasParameters, HasCode\n{\n private string $someParameter;\n\n public function __construct(string $someParameter)\n {\n parent::__construct();\n\n $this->someParameter = $someParameter;\n }\n\n public function body() : string\n {\n return 'Some message / {some_parameter} / {source_value}';\n }\n\n public function parameters(): array\n {\n return [\n 'some_parameter' => $this->someParameter,\n ];\n }\n\n public function code() : string\n {\n // A unique code that can help to identify the error\n return 'some_unique_code';\n }\n}\n
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'); // \u2705\n(new MapperBuilder())->mapper()->map('numeric-string', 'foo'); // \u274c\n
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)See release on GitHub
"},{"location":"project/changelog/version-0.14.0/#notable-changes","title":"Notable changes","text":"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())\n // Both `Cookie` and `ATOM` formats will be accepted\n ->supportDateFormats(DATE_COOKIE, DATE_ATOM)\n ->mapper()\n ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');\n
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())\n ->registerConstructor(\n new \\CuyZ\\Valinor\\Mapper\\Object\\BackwardCompatibilityDateTimeConstructor()\n )\n ->mapper()\n ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');\n
"},{"location":"project/changelog/version-0.14.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"DynamicConstructor
(e437d9)ClassStringType
(4bc50e)ObjectBuilderFactory::for
return signature (57849c)See release on GitHub
"},{"location":"project/changelog/version-0.15.0/#notable-changes","title":"Notable changes","text":"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\n{\n public const FOO = 1337;\n\n public const BAR = 'bar';\n\n public const BAZ = 'baz';\n}\n\n$mapper = (new MapperBuilder())->mapper();\n\n$mapper->map('SomeClassWithConstants::BA*', 1337); // error\n$mapper->map('SomeClassWithConstants::BA*', 'bar'); // ok\n$mapper->map('SomeClassWithConstants::BA*', 'baz'); // ok\n
Example for enum:
enum SomeEnum: string\n{\n case FOO = 'foo';\n case BAR = 'bar';\n case BAZ = 'baz';\n}\n\n$mapper = (new MapperBuilder())->mapper();\n\n$mapper->map('SomeEnum::BA*', 'foo'); // error\n$mapper->map('SomeEnum::BA*', 'bar'); // ok\n$mapper->map('SomeEnum::BA*', 'baz'); // ok\n
"},{"location":"project/changelog/version-0.15.0/#features","title":"Features","text":"See release on GitHub
"},{"location":"project/changelog/version-0.16.0/#features","title":"Features","text":"See release on GitHub
"},{"location":"project/changelog/version-0.17.0/#notable-changes","title":"Notable changes","text":"The main feature introduced in this release is the split of the flexible mode in three distinct modes:
Changes the behaviours explained below:
$flexibleMapper = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->enableFlexibleCasting()\n ->mapper();\n\n// ---\n// Scalar types will accept non-strict values; for instance an\n// integer type will accept any valid numeric value like the\n// *string* \"42\".\n\n$flexibleMapper->map('int', '42');\n// => 42\n\n// ---\n// List type will accept non-incremental keys.\n\n$flexibleMapper->map('list<int>', ['foo' => 42, 'bar' => 1337]);\n// => [0 => 42, 1 => 1338]\n\n// ---\n// If a value is missing in a source for a node that accepts `null`,\n// the node will be filled with `null`.\n\n$flexibleMapper->map(\n 'array{foo: string, bar: null|string}',\n ['foo' => 'foo'] // `bar` is missing\n);\n// => ['foo' => 'foo', 'bar' => null]\n\n// ---\n// Array and list types will convert `null` or missing values to an\n// empty array.\n\n$flexibleMapper->map(\n 'array{foo: string, bar: array<string>}',\n ['foo' => 'foo'] // `bar` is missing\n);\n// => ['foo' => 'foo', 'bar' => []]\n
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())\n ->allowSuperfluousKeys()\n ->mapper()\n ->map(\n 'array{foo: string, bar: int}',\n [\n 'foo' => 'foo',\n 'bar' => 42,\n 'baz' => 1337.404, // `baz` will be ignored\n ]\n );\n
Allows permissive types mixed
and object
to be used during mapping.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->allowPermissiveTypes()\n ->mapper()\n ->map(\n 'array{foo: string, bar: mixed}',\n [\n 'foo' => 'foo',\n 'bar' => 42, // Could be any value\n ]\n );\n
"},{"location":"project/changelog/version-0.17.0/#features","title":"Features","text":"strict-array
type (d456eb)uniqid()
(b81847)null
in flexible mode (92a41a)See release on GitHub
"},{"location":"project/changelog/version-0.2.0/#features","title":"Features","text":"GenericAssignerLexer
to TypeAliasLexer
(680941)marcocesarato/php-conventional-changelog
for changelog (178aa9)See release on GitHub
"},{"location":"project/changelog/version-0.3.0/#features","title":"Features","text":"friendsofphp/php-cs-fixer
(e5ccbe)See release on GitHub
"},{"location":"project/changelog/version-0.4.0/#notable-changes","title":"Notable changes","text":"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(\n 'array<' . SomeClass::class . '>',\n [/* \u2026 */]\n);\n
For simple use-cases, an array shape can be used:
$array = (new MapperBuilder())->mapper()->map(\n 'array{foo: string, bar: int}',\n [/* \u2026 */]\n);\n\necho $array['foo'];\necho $array['bar'] * 2;\n
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 \u2014 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 \u2014 to avoid useless greedy operations.
In any case, the content can contain placeholders that will automatically be replaced by, in order:
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */]);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $node = $error->node();\n $messages = new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessagesFlattener($node);\n\n $formatter = (new MessageMapFormatter([\n // Will match if the given message has this exact code\n 'some_code' => 'new content / previous code was: %1$s',\n\n // Will match if the given message has this exact content\n 'Some message content' => 'new content / previous message: %2$s',\n\n // Will match if the given message is an instance of `SomeError`\n SomeError::class => '\n - Original code of the message: %1$s\n - Original content of the message: %2$s\n - Node type: %3$s\n - Node name: %4$s\n - Node path: %5$s\n ',\n\n // A callback can be used to get access to the message instance\n OtherError::class => function (NodeMessage $message): string {\n if ((string)$message->type() === 'string|int') {\n // \u2026\n }\n\n return 'Some message content';\n },\n\n // For greedy operation, it is advised to use a lazy-callback\n 'bar' => fn () => $this->translator->translate('foo.bar'),\n ]))\n ->defaultsTo('some default message')\n // \u2026or\u2026\n ->defaultsTo(fn () => $this->translator->translate('default_message'));\n\n foreach ($messages as $message) {\n echo $formatter->format($message); \n }\n}\n
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\n{\n public readonly SomeFooObject|SomeBarObject $object;\n}\n\nfinal class SomeFooObject\n{\n public readonly string $foo;\n}\n\nfinal class SomeBarObject\n{\n public readonly string $bar;\n}\n\n// Will map to an instance of `SomeFooObject`\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(UnionOfObjects::class, ['foo' => 'foo']);\n\n// Will map to an instance of `SomeBarObject`\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(UnionOfObjects::class, ['bar' => 'bar']);\n
"},{"location":"project/changelog/version-0.4.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"MessageMapFormatter
(ddf69e)MessagesFlattener
(a97b40)NodeTraverser
for recursive operations on nodes (cc1bc6)See release on GitHub
"},{"location":"project/changelog/version-0.5.0/#features","title":"Features","text":"TreeMapper#map()
(e28003)See release on GitHub
"},{"location":"project/changelog/version-0.6.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"composer.json
(6fdd62)See release on GitHub
"},{"location":"project/changelog/version-0.7.0/#notable-changes","title":"Notable changes","text":"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\n{\n public static function namedConstructor(string $foo): self\n {\n // \u2026\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n SomeClass::namedConstructor(...),\n // \u2026or for PHP < 8.1:\n [SomeClass::class, 'namedConstructor'],\n )\n ->mapper()\n ->map(SomeClass::class, [\n // \u2026\n ]);\n
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)\n ->camelCaseKeys()\n ->map([\n 'towns' => 'cities',\n 'towns.*.label' => 'name',\n ]);\n\n$result = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, $source);\n
See documentation for more details about its usage.
"},{"location":"project/changelog/version-0.7.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"Attributes::ofType
return type to array
(1a599b)See release on GitHub
"},{"location":"project/changelog/version-0.8.0/#notable-changes","title":"Notable changes","text":"Float values handling
Allows the usage of float values, as follows:
class Foo\n{\n /** @var 404.42|1337.42 */\n public readonly float $value;\n}\n
Literal boolean true
/ false
values handling
Thanks @danog for this feature!
Allows the usage of boolean values, as follows:
class Foo\n{\n /** @var int|false */\n public readonly int|bool $value;\n}\n
Class string of union of object handling
Allows to declare several class names in a class-string
:
class Foo\n{\n /** @var class-string<SomeClass|SomeOtherClass> */\n public readonly string $className;\n}\n
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).gitattributes
(979272)Polyfill
coverage (c08fe5)symfony/polyfill-php80
dependency (368737)See release on GitHub
"},{"location":"project/changelog/version-0.9.0/#notable-changes","title":"Notable changes","text":"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 \u2014 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 \u2014 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');\n\nif ($isApplicationInDevelopmentEnvironment) {\n $cache = new \\CuyZ\\Valinor\\Cache\\FileWatchingCache($cache);\n}\n\n$mapperBuilder = (new \\CuyZ\\Valinor\\MapperBuilder())->withCache($cache);\n\n// During the build:\n$mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class);\n\n// In the application:\n$mapperBuilder->mapper()->map(SomeClass::class, [/* \u2026 */]);\n
Message formatting & translation
Major changes have been made to the messages being returned in case of a mapping error: the actual texts are now more accurate and show better information.
Warning
The method NodeMessage::format
has been removed, message formatters should be used instead. If needed, the old behaviour can be retrieved with the formatter PlaceHolderMessageFormatter
, although it is strongly advised to use the new placeholders feature (see below).
The signature of the method MessageFormatter::format
has changed as well.
It is now also easier to format the messages, for instance when they need to be translated. Placeholders can now be used in a message body, and will be replaced with useful information.
Placeholder Description{message_code}
the code of the message {node_name}
name of the node to which the message is bound {node_path}
path of the node to which the message is bound {node_type}
type of the node to which the message is bound {original_value}
the source value that was given to the node {original_message}
the original message before being customized try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */]);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $node = $error->node();\n $messages = new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessagesFlattener($node);\n\n foreach ($messages as $message) {\n if ($message->code() === 'some_code') {\n $message = $message->withBody('new message / {original_message}');\n }\n\n echo $message;\n }\n}\n
The messages are formatted using the ICU library, enabling the placeholders to use advanced syntax to perform proper translations, for instance currency support.
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map('int<0, 100>', 1337);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $message = $error->node()->messages()[0];\n\n if (is_numeric($message->value())) {\n $message = $message->withBody(\n 'Invalid amount {original_value, number, currency}'\n ); \n } \n\n // Invalid amount: $1,337.00\n echo $message->withLocale('en_US');\n\n // Invalid amount: \u00a31,337.00\n echo $message->withLocale('en_GB');\n\n // Invalid amount: 1 337,00 \u20ac\n echo $message->withLocale('fr_FR');\n}\n
See ICU documentation for more information on available syntax.
Warning If the intl
extension is not installed, a shim will be available to replace the placeholders, but it won't handle advanced syntax as described above.
The formatter TranslationMessageFormatter
can be used to translate the content of messages.
The library provides a list of all messages that can be returned; this list can be filled or modified with custom translations.
\\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\TranslationMessageFormatter::default()\n // Create/override a single entry\u2026\n ->withTranslation('fr', 'some custom message', 'un message personnalis\u00e9')\n // \u2026or several entries.\n ->withTranslations([\n 'some custom message' => [\n 'en' => 'Some custom message',\n 'fr' => 'Un message personnalis\u00e9',\n 'es' => 'Un mensaje personalizado',\n ], \n 'some other message' => [\n // \u2026\n ], \n ])\n ->format($message);\n
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(\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\LocaleMessageFormatter('fr'),\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\MessageMapFormatter([\n // \u2026\n ],\n \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\TranslationMessageFormatter::default(),\n))->format($message)\n
"},{"location":"project/changelog/version-0.9.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"ObjectBuilder
API access (11e126)InvalidParameterIndex
exception inheritance type (b75adb)See release on GitHub
First stable version! \ud83e\udd73 \ud83c\udf89
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 \u2014 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\u2122); 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! \ud83d\ude4c
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 \u2764\ufe0f sponsor my work on GitHub \u2014 if you use this library in your applications, please consider offering me a \ud83c\udf7a from time to time! \ud83e\udd17
"},{"location":"project/changelog/version-1.0.0/#notable-changes","title":"Notable changes","text":"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 {\n return \"$foo / $bar\";\n};\n\ntry {\n $arguments = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->argumentsMapper()\n ->mapArguments($someFunction, [\n 'foo' => 'some value',\n 'bar' => 42,\n ]);\n\n // some value / 42\n echo $someFunction(...$arguments);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
Support for TimeZone
objects
Native TimeZone
objects construction is now supported with a proper error handling.
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(DateTimeZone::class, 'Jupiter/Europa');\n} catch (MappingError $exception) {\n $error = $exception->node()->messages()[0];\n\n // Value 'Jupiter/Europa' is not a valid timezone.\n echo $error->toString();\n}\n
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\n{\n public readonly string $value;\n}\n\nfinal class SomeClass\n{\n public readonly Identifier $identifier;\n\n public readonly string $description;\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(SomeClass::class, [\n 'identifier' => ['value' => 'some-identifier'], // \u274c\n 'description' => 'Lorem ipsum\u2026',\n]);\n\n(new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(SomeClass::class, [\n 'identifier' => 'some-identifier', // \u2705\n 'description' => 'Lorem ipsum\u2026',\n]);\n
"},{"location":"project/changelog/version-1.0.0/#upgrading-from-0x-to-10","title":"Upgrading from 0.x to 1.0","text":"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.
Doctrine annotations support removalDoctrine annotations cannot be used anymore, PHP attributes must be used.
BackwardCompatibilityDateTimeConstructor
class removal You must use the method available in the mapper builder, see dealing with dates chapter.
Mapper builderflexible
method removal The flexible has been split in three disctint modes, see type strictness & flexibility chapter.
Mapper builderwithCacheDir
method removal You must now register a cache instance directly, see performance & caching chapter.
StaticMethodConstructor
class removal You must now register the constructors using the mapper builder, see custom object constructors chapter.
Mapper builderbind
method removal You must now register the constructors using the mapper builder, see custom object constructors chapter.
ThrowableMessage
class removal You must now use the MessageBuilder
class, see error handling chapter.
MessagesFlattener
class removal You must now use the Messages
class, see error handling chapter.
TranslatableMessage
class removal You must now use the HasParameters
class, see custom exception chapter.
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}
.
PlaceHolderMessageFormatter
class removal Other features are available to format message, see error messages customization chapter.
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.
"},{"location":"project/changelog/version-1.0.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"@pure
(0d9855)ThrowableMessage
(d36ca9)TranslatableMessage
(ceb197)strict-array
type (22c3b4)DateTimeZone
with error support (a0a4d6)null
to single node nullable type (0a98ec)psr/simple-cache
supported version (e4059a)@
from comments for future PHP versions changes (68774c)See release on GitHub
"},{"location":"project/changelog/version-1.1.0/#notable-changes","title":"Notable changes","text":"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.
/**\n * @template FirstTemplate\n * @template SecondTemplate\n */\nabstract class FirstClassWithGenerics\n{\n /** @var FirstTemplate */\n public $valueA;\n\n /** @var SecondTemplate */\n public $valueB;\n}\n\n/**\n * @template FirstTemplate\n * @extends FirstClassWithGenerics<FirstTemplate, int>\n */\nabstract class SecondClassWithGenerics extends FirstClassWithGenerics\n{\n /** @var FirstTemplate */\n public $valueC;\n}\n\n/**\n * @extends SecondClassWithGenerics<string>\n */\nfinal class ChildClass extends SecondClassWithGenerics\n{\n}\n\n$object = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(ChildClass::class, [\n 'valueA' => 'foo',\n 'valueB' => 1337,\n 'valueC' => 'bar',\n ]);\n\necho $object->valueA; // 'foo'\necho $object->valueB; // 1337\necho $object->valueC; // 'bar'\n
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\n{\n public string $foo;\n\n public string $bar;\n}\n\nfinal class SomeChildClass extends SomeAbstractClass\n{\n public string $baz;\n}\n\n$result = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(\n SomeAbstractClass::class,\n fn () => SomeChildClass::class\n )\n ->mapper()\n ->map(SomeAbstractClass::class, [\n 'foo' => 'foo',\n 'bar' => 'bar',\n 'baz' => 'baz',\n ]);\n\nassert($result instanceof SomeChildClass);\nassert($result->foo === 'foo');\nassert($result->bar === 'bar');\nassert($result->baz === 'baz');\n
"},{"location":"project/changelog/version-1.1.0/#features","title":"Features","text":"object
return type in PHPStan extension (201728)isAbstract
flag in class definition (ad0c06)isFinal
flag in class definition (25da31)TreeMapper::map()
return type signature (dc32d3)TreeMapper
(c8f362)See release on GitHub
"},{"location":"project/changelog/version-1.2.0/#notable-changes","title":"Notable changes","text":"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
"},{"location":"project/changelog/version-1.2.0/#features","title":"Features","text":"See release on GitHub
"},{"location":"project/changelog/version-1.3.0/#notable-changes","title":"Notable changes","text":"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())\n ->registerConstructor(\n // Allow the native constructor to be used\n SomeEnum::class,\n\n // Register a named constructor\n SomeEnum::fromMatrix(...)\n )\n ->mapper()\n ->map(SomeEnum::class, [\n 'type' => 'FOO',\n 'number' => 'BAR',\n ]);\n\nenum SomeEnum: string\n{\n case CASE_A = 'FOO_VALUE_1';\n case CASE_B = 'FOO_VALUE_2';\n case CASE_C = 'BAR_VALUE_1';\n case CASE_D = 'BAR_VALUE_2';\n\n /**\n * @param 'FOO'|'BAR' $type\n * @param int<1, 2> $number\n * /\n public static function fromMatrix(string $type, int $number): self\n {\n return self::from(\"{$type}_VALUE_{$number}\");\n }\n}\n
An enum constructor can be for a specific pattern:
enum SomeEnum\n{\n case FOO;\n case BAR;\n case BAZ;\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n /**\n * This constructor will be called only when pattern\n * `SomeEnum::BA*` is requested during mapping.\n *\n * @return SomeEnum::BA*\n */\n fn (string $value): SomeEnum => /* Some custom domain logic */\n )\n ->mapper()\n ->map(SomeEnum::class . '::BA*', 'some custom value');\n
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.
"},{"location":"project/changelog/version-1.3.0/#features","title":"Features","text":"See release on GitHub
Bugfix release.
"},{"location":"project/changelog/version-1.3.1/#bug-fixes","title":"Bug Fixes","text":"null
and objects (8f03a7)See release on GitHub
"},{"location":"project/changelog/version-1.4.0/#notable-changes","title":"Notable changes","text":"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 {\n $source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::json('invalid JSON');\n} catch (\\CuyZ\\Valinor\\Mapper\\Source\\Exception\\InvalidSource $error) {\n // Let the application handle the exception in the desired way.\n // It is possible to get the original source with `$error->source()`\n}\n
"},{"location":"project/changelog/version-1.4.0/#features","title":"Features","text":"InvalidSource
thrown when using invalid JSON/YAML (0739d1)array-key
type match mixed
(ccebf7)See release on GitHub
"},{"location":"project/changelog/version-1.5.0/#features","title":"Features","text":"UnresolvableType
(eaa128)UnresolvableType
(5c89c6)unserialize
when caching NULL
default values (5e9b4c)json_encode
exception to help identifying parsing errors (861c3b)See release on GitHub
"},{"location":"project/changelog/version-1.6.0/#notable-changes","title":"Notable changes","text":"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 \u2014 mostly concerning quoted strings \u2014 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\n(new MapperBuilder())\n ->mapper()\n ->map(\n \"'foo bar'|'baz fiz'\",\n 'baz fiz'\n );\n\n// Shaped array with special chars in the key\n(new MapperBuilder())\n ->mapper()\n ->map(\n \"array{'some & key': string}\",\n ['some & key' => 'value']\n );\n
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']);\n\n$mapper->map('array<42|1337, string>', [42 => 'foo']);\n\n$mapper->map('array<positive-int, string>', [42 => 'foo']);\n\n$mapper->map('array<negative-int, string>', [-42 => 'foo']);\n\n$mapper->map('array<int<-42, 1337>, string>', [42 => 'foo']);\n\n$mapper->map('array<non-empty-string, string>', ['foo' => 'foo']);\n\n$mapper->map('array<class-string, string>', ['SomeClass' => 'foo']);\n
"},{"location":"project/changelog/version-1.6.0/#features","title":"Features","text":"intl.use_exceptions=1
(29da9a)See release on GitHub
"},{"location":"project/changelog/version-1.6.1/#bug-fixes","title":"Bug Fixes","text":"See release on GitHub
"},{"location":"project/changelog/version-1.7.0/#notable-changes","title":"Notable changes","text":"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\n{\n /** @var non-positive-int */\n public int $nonPositiveInteger;\n}\n
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\n{\n /** @var non-negative-int */\n public int $nonNegativeInteger;\n}\n
"},{"location":"project/changelog/version-1.7.0/#features","title":"Features","text":"@psalm-pure
annotation to pure methods (004eb1)NativeBooleanType
a BooleanType
(d57ffa)See release on GitHub
"},{"location":"project/changelog/version-1.8.0/#notable-changes","title":"Notable changes","text":"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;\n\n$normalizer = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array());\n\n$userAsArray = $normalizer->normalize(\n new \\My\\App\\User(\n name: 'John Doe',\n age: 42,\n country: new \\My\\App\\Country(\n name: 'France',\n countryCode: 'FR',\n ),\n )\n);\n\n// `$userAsArray` is now an array and can be manipulated much more\n// easily, for instance to be serialized to the wanted data format.\n//\n// [\n// 'name' => 'John Doe',\n// 'age' => 42,\n// 'country' => [\n// 'name' => 'France',\n// 'countryCode' => 'FR',\n// ],\n// ];\n
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())\n ->registerTransformer(\n fn (\\DateTimeInterface $date) => $date->format('Y/m/d')\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
This date transformer could have been an attribute for a more granular control, as shown below.
namespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class DateTimeFormat\n{\n public function __construct(private string $format) {}\n\n public function normalize(\\DateTimeInterface $date): string\n {\n return $date->format($this->format);\n }\n}\n\nfinal readonly class Event\n{\n public function __construct(\n public string $eventName,\n #[\\My\\App\\DateTimeFormat('Y/m/d')]\n public \\DateTimeInterface $date,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\DateTimeFormat::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
More features are available, details about it can be found in the documentation.
"},{"location":"project/changelog/version-1.8.0/#features","title":"Features","text":"See release on GitHub
"},{"location":"project/changelog/version-1.8.1/#bug-fixes","title":"Bug Fixes","text":"Instead of providing transformers out-of-the-box, this library focuses on easing the creation of custom ones. This way, the normalizer is not tied up to a third-party library release-cycle and can be adapted to fit the needs of the application's business logics.
Below is a list of common features that can inspire or be implemented by third-party libraries or applications.
Info
These examples are not available out-of-the-box, they can be implemented using the library's API and should be adapted to fit the needs of the application.
By default, dates will be formatted using the RFC 3339 format, but it may be needed to use another format.
This can be done on all dates, using a global transformer, as shown in the example below:
Show code example \u2014 Global date format(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\n fn (\\DateTimeInterface $date) => $date->format('Y/m/d')\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
For a more granular control, an attribute can be used to target a specific property, as shown in the example below:
Show code example \u2014 Date format attributenamespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class DateTimeFormat\n{\n public function __construct(private string $format) {}\n\n public function normalize(\\DateTimeInterface $date): string\n {\n return $date->format($this->format);\n }\n}\n\nfinal readonly class Event\n{\n public function __construct(\n public string $eventName,\n #[\\My\\App\\DateTimeFormat('Y/m/d')]\n public \\DateTimeInterface $date,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\DateTimeFormat::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
"},{"location":"serialization/common-examples/#transforming-property-name-to-snake_case","title":"Transforming property name to \u201csnake_case\u201d","text":"Depending on the conventions of the data format, it may be necessary to transform the case of the keys, for instance from \u201ccamelCase\u201d to \u201csnake_case\u201d.
If this transformation is needed on every object, it can be done globally by using a global transformer, as shown in the example below:
Show code example \u2014 global \u201csnake_case\u201d propertiesnamespace My\\App;\n\nfinal class CamelToSnakeCaseTransformer\n{\n public function __invoke(object $object, callable $next): mixed\n {\n $result = $next();\n\n if (! is_array($result)) {\n return $result;\n }\n\n $snakeCased = [];\n\n foreach ($result as $key => $value) {\n $newKey = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($key)));\n\n $snakeCased[$newKey] = $value;\n }\n\n return $snakeCased;\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(new \\My\\App\\CamelToSnakeCaseTransformer())\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\User(\n name: 'John Doe',\n emailAddress: 'john.doe@example.com', \n age: 42,\n country: new Country(\n name: 'France',\n countryCode: 'FR',\n ),\n )\n );\n\n// [\n// 'name' => 'John Doe',\n// 'email_address' => 'john.doe@example', // snake_case\n// 'age' => 42,\n// 'country' => [\n// 'name' => 'France',\n// 'country_code' => 'FR', // snake_case\n// ],\n// ]\n
For a more granular control, an attribute can be used to target specific objects, as shown in the example below:
Show code example \u2014 \u201csnake_case\u201d attributenamespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_CLASS)]\nfinal class SnakeCaseProperties\n{\n public function normalize(object $object, callable $next): array\n {\n $result = $next();\n\n if (! is_array($result)) {\n return $result;\n }\n\n $snakeCased = [];\n\n foreach ($result as $key => $value) {\n $newKey = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($key)));\n\n $snakeCased[$newKey] = $value;\n }\n\n return $snakeCased;\n }\n}\n\n#[SnakeCaseProperties]\nfinal readonly class Country\n{\n public function __construct(\n public string $name,\n public string $countryCode,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\SnakeCaseProperties::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\User(\n name: 'John Doe',\n emailAddress: 'john.doe@example.com',\n age: 42,\n country: new Country(\n name: 'France',\n countryCode: 'FR',\n ),\n )\n );\n\n// [\n// 'name' => 'John Doe',\n// 'emailAddress' => 'john.doe@example', // camelCase\n// 'age' => 42,\n// 'country' => [\n// 'name' => 'France',\n// 'country_code' => 'FR', // snake_case\n// ],\n// ]\n
"},{"location":"serialization/common-examples/#ignoring-properties","title":"Ignoring properties","text":"Some objects might want to omit some properties during normalization, for instance, to hide sensitive data.
In the example below, an attribute is added on a property that will replace the value with a custom object that is afterward removed by a global transformer.
Show code example \u2014 Ignore property attributenamespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class Ignore\n{\n public function normalize(mixed $value): IgnoredValue\n {\n return new \\My\\App\\IgnoredValue();\n }\n}\n\nfinal class IgnoredValue\n{\n public function __construct() {}\n}\n\nfinal readonly class User\n{\n public function __construct(\n public string $name,\n #[\\My\\App\\Ignore]\n public string $password,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\Ignore::class)\n ->registerTransformer(\n fn (object $value, callable $next) => array_filter(\n $next(),\n fn (mixed $value) => ! $value instanceof \\My\\App\\IgnoredValue,\n ),\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(new \\My\\App\\User(\n name: 'John Doe',\n password: 's3cr3t-p4$$w0rd')\n );\n\n// ['name' => 'John Doe']\n
"},{"location":"serialization/common-examples/#renaming-properties","title":"Renaming properties","text":"Properties' names can differ between the object and the data format.
In the example below, an attribute is added on properties that need to be renamed during normalization
Show code example \u2014 Rename property attributenamespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class Rename\n{\n public function __construct(private string $name) {}\n\n public function normalizeKey(): string\n {\n return $this->name;\n }\n}\n\nfinal readonly class Address\n{\n public function __construct(\n public string $street,\n public string $zipCode,\n #[\\My\\App\\Rename('town')]\n public string $city,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\Rename::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new Address(\n street: '221B Baker Street', \n zipCode: 'NW1 6XE', \n city: 'London', \n )\n );\n\n// [\n// 'street' => '221B Baker Street',\n// 'zipCode' => 'NW1 6XE',\n// 'town' => 'London',\n// ]\n
"},{"location":"serialization/common-examples/#transforming-objects","title":"Transforming objects","text":"Some objects can have custom behaviors during normalization, for instance properties may need to be remapped. In the example below, a transformer will check if an object defines a normalize
method and use it if it exists.
namespace My\\App;\n\nfinal readonly class Address\n{\n public function __construct(\n public string $road,\n public string $zipCode,\n public string $town,\n ) {}\n\n public function normalize(): array\n {\n return [\n 'street' => $this->road,\n 'postalCode' => $this->zipCode,\n 'city' => $this->town,\n ];\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(function (object $object, callable $next) {\n return method_exists($object, 'normalize')\n ? $object->normalize()\n : $next();\n })\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Address(\n road: '221B Baker Street',\n zipCode: 'NW1 6XE',\n town: 'London',\n ),\n );\n\n// [\n// 'street' => '221B Baker Street',\n// 'postalCode' => 'NW1 6XE',\n// 'city' => 'London',\n// ]\n
"},{"location":"serialization/common-examples/#versioning-api","title":"Versioning API","text":"API versioning can be implemented with different strategies and algorithms. The example below shows how objects can implement an interface to specify their own specific versioning behavior.
Show code example \u2014 Versioning objectsnamespace My\\App;\n\ninterface HasVersionedNormalization\n{\n public function normalizeWithVersion(string $version): mixed;\n}\n\nfinal readonly class Address implements \\My\\App\\HasVersionedNormalization\n{\n public function __construct(\n public string $streetNumber,\n public string $streetName,\n public string $zipCode,\n public string $city,\n ) {}\n\n public function normalizeWithVersion(string $version): array\n {\n return match (true) {\n version_compare($version, '1.0.0', '<') => [\n // Street number and name are merged in a single property\n 'street' => \"$this->streetNumber, $this->streetName\",\n 'zipCode' => $this->zipCode,\n 'city' => $this->city,\n ],\n default => get_object_vars($this),\n };\n }\n}\n\nfunction normalizeWithVersion(string $version): mixed\n{\n return (new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\n fn (\\My\\App\\HasVersionedNormalization $object) => $object->normalizeWithVersion($version)\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Address(\n streetNumber: '221B',\n streetName: 'Baker Street',\n zipCode: 'NW1 6XE',\n city: 'London',\n )\n );\n}\n\n// Version can come for instance from HTTP request headers\n$result_v0_4 = normalizeWithVersion('0.4');\n$result_v1_8 = normalizeWithVersion('1.8');\n\n// $result_v0_4 === [\n// 'street' => '221B, Baker Street',\n// 'zipCode' => 'NW1 6XE',\n// 'city' => 'London',\n// ]\n// \n// $result_v1_8 === [\n// 'streetNumber' => '221B',\n// 'streetName' => 'Baker Street',\n// 'zipCode' => 'NW1 6XE',\n// 'city' => 'London',\n// ]\n
"},{"location":"serialization/normalizer/","title":"Normalizing data","text":"A normalizer is a service that transforms 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.
Info
The library only supports normalizing to arrays, but aims to support other formats like JSON or CSV in the future.
In the meantime, native functions like json_encode()
may be used as an alternative.
namespace My\\App;\n\n$normalizer = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array());\n\n$userAsArray = $normalizer->normalize(\n new \\My\\App\\User(\n name: 'John Doe',\n age: 42,\n country: new \\My\\App\\Country(\n name: 'France',\n countryCode: 'FR',\n ),\n )\n);\n\n// `$userAsArray` is now an array and can be manipulated much more easily, for\n// instance to be serialized to the wanted data format.\n//\n// [\n// 'name' => 'John Doe',\n// 'age' => 42,\n// 'country' => [\n// 'name' => 'France',\n// 'countryCode' => 'FR',\n// ],\n// ];\n
"},{"location":"serialization/normalizer/#extending-the-normalizer","title":"Extending the normalizer","text":"This library provides a normalizer out-of-the-box that can be used as-is, or extended to add custom logic. To do so, transformers must be registered within the MapperBuilder
.
A transformer can be a callable (function, closure or a class implementing the __invoke()
method), or an attribute that can target a class or a property.
Note
You can find common examples of transformers in the next chapter.
"},{"location":"serialization/normalizer/#callable-transformers","title":"Callable transformers","text":"A callable transformer must declare at least one argument, for which the type will determine when it is used during normalization. In the example below, a global transformer is used to format any date found by the normalizer.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\n fn (\\DateTimeInterface $date) => $date->format('Y/m/d')\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
Transformers can be chained. To do so, a second parameter of type callable
must be declared in a transformer. This parameter \u2014 named $next
by convention \u2014 can be used whenever needed in the transformer logic.
(new \\CuyZ\\Valinor\\MapperBuilder())\n\n // The type of the first parameter of the transformer will determine when it\n // is used during normalization.\n ->registerTransformer(\n fn (string $value, callable $next) => strtoupper($next())\n )\n\n // Transformers can be chained, the last registered one will take precedence\n // over the previous ones, which can be called using the `$next` parameter.\n ->registerTransformer(\n /**\n * Advanced type annotations like `non-empty-string` can be used to\n * target a more specific type.\n * \n * @param non-empty-string $value \n */\n fn (string $value, callable $next) => $next() . '!'\n )\n\n // A priority can be given to a transformer, to make sure it is called\n // before or after another one. The higher priority, the sooner the\n // transformer will be called. The default priority is 0.\n ->registerTransformer(\n /**\n * @param non-empty-string $value \n */\n fn (string $value, callable $next) => $next() . '?',\n priority: 100\n )\n\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize('Hello world'); // HELLO WORLD!?\n
"},{"location":"serialization/normalizer/#attribute-transformers","title":"Attribute transformers","text":"Callable transformers allow targeting any value during normalization, whereas attribute transformers allow targeting a specific class or property for a more granular control.
To be detected by the normalizer, an attribute must be registered first by giving its class name to the registerTransformer
method.
Tip
It is possible to register attributes that share a common interface by giving the interface name to the method.
namespace My\\App;\n\ninterface SomeAttributeInterface {}\n\n#[\\Attribute]\nfinal class SomeAttribute implements \\My\\App\\SomeAttributeInterface {}\n\n#[\\Attribute]\nfinal class SomeOtherAttribute implements \\My\\App\\SomeAttributeInterface {}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n // Registers both `SomeAttribute` and `SomeOtherAttribute` attributes\n ->registerTransformer(\\My\\App\\SomeAttributeInterface::class)\n \u2026\n
Attributes must declare a method named normalize
that follows the same rules as callable transformers: a mandatory first parameter and an optional second callable
parameter.
namespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class Uppercase\n{\n public function normalize(string $value, callable $next): string\n {\n return strtoupper($next());\n }\n}\n\nfinal readonly class City\n{\n public function __construct(\n public string $zipCode,\n #[Uppercase]\n public string $name,\n #[Uppercase]\n public string $country,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(Uppercase::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\City(\n zipCode: 'NW1 6XE',\n name: 'London',\n country: 'United Kingdom',\n ) \n );\n\n// [\n// 'zipCode' => 'NW1 6XE',\n// 'name' => 'LONDON',\n// 'country' => 'UNITED KINGDOM',\n// ]\n
If an attribute needs to transform the key of a property, it needs to declare a method named normalizeKey
.
namespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class PrefixedWith\n{\n public function __construct(private string $prefix) {}\n\n public function normalizeKey(string $value): string\n {\n return $this->prefix . $value;\n }\n}\n\nfinal readonly class Address\n{\n public function __construct(\n #[\\My\\App\\PrefixedWith('address_')]\n public string $road,\n #[\\My\\App\\PrefixedWith('address_')]\n public string $zipCode,\n #[\\My\\App\\PrefixedWith('address_')]\n public string $city,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(PrefixedWith::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Address(\n road: '221B Baker Street',\n zipCode: 'NW1 6XE',\n city: 'London',\n ) \n );\n\n// [\n// 'address_road' => '221B Baker Street',\n// 'address_zipCode' => 'NW1 6XE',\n// 'address_city' => 'London',\n// ]\n
"},{"location":"usage/object-construction/","title":"Object construction","text":"During the mapping, instances of objects are recursively created and hydrated with values coming from the input.
The values of an object are filled either with a constructor \u2014 which is the recommended way \u2014 or using the class properties. If a constructor exists, it will be used to create the object, otherwise the properties will be filled directly.
By default, the library will use a native constructor of a class if it is public; for advanced use cases, the library also allows the usage of custom constructors.
"},{"location":"usage/object-construction/#class-with-a-single-value","title":"Class with a single value","text":"When an object needs only one value (one constructor argument or one property), the source given to the mapper can match the type of the value \u2014 it does not need to be an array with one value with a key matching the argument/property name.
This can be useful when the application has control over the format of the source given to the mapper, in order to lessen the structure of input.
final class Identifier\n{\n public readonly string $value;\n}\n\nfinal class SomeClass\n{\n public readonly Identifier $identifier;\n\n public readonly string $description;\n}\n\n$mapper = (new \\CuyZ\\Valinor\\MapperBuilder())->mapper();\n\n$mapper->map(SomeClass::class, [\n 'identifier' => [\n // \ud83d\udc4e The `value` key feels a bit excessive\n 'value' => 'some-identifier'\n ],\n 'description' => 'Lorem ipsum\u2026',\n]); \n\n$mapper->map(SomeClass::class, [\n // \ud83d\udc4d The input has been flattened and is easier to read\n 'identifier' => 'some-identifier',\n 'description' => 'Lorem ipsum\u2026',\n]);\n
"},{"location":"usage/type-reference/","title":"Type reference","text":"To prevent conflicts or duplication of the type annotations, this library tries to handle most of the type annotations that are accepted by PHPStan and Psalm.
"},{"location":"usage/type-reference/#scalar","title":"Scalar","text":"final class SomeClass\n{\n public function __construct(\n private bool $boolean,\n\n private float $float,\n\n private int $integer,\n\n /** @var positive-int */\n private int $positiveInteger,\n\n /** @var negative-int */\n private int $negativeInteger,\n\n /** @var non-positive-int */\n private int $nonPositiveInteger,\n\n /** @var non-negative-int */\n private int $nonNegativeInteger,\n\n /** @var int<-42, 1337> */\n private int $integerRange,\n\n /** @var int<min, 0> */\n private int $integerRangeWithMinRange,\n\n /** @var int<0, max> */\n private int $integerRangeWithMaxRange,\n\n private string $string,\n\n /** @var non-empty-string */\n private string $nonEmptyString,\n\n /** @var numeric-string */\n private string $numericString,\n\n /** @var class-string */\n private string $classString,\n\n /** @var class-string<SomeInterface> */\n private string $classStringOfAnInterface,\n ) {}\n}\n
"},{"location":"usage/type-reference/#object","title":"Object","text":"final class SomeClass\n{\n public function __construct(\n private SomeClass $class,\n\n private DateTimeInterface $interface,\n\n /** @var SomeInterface&AnotherInterface */\n private object $intersection,\n\n /** @var SomeCollection<SomeClass> */\n private SomeCollection $classWithGeneric,\n ) {}\n}\n\n/**\n * @template T of object \n */\nfinal class SomeCollection\n{\n public function __construct(\n /** @var array<T> */\n private array $objects,\n ) {}\n}\n
"},{"location":"usage/type-reference/#array-lists","title":"Array & lists","text":"final class SomeClass\n{\n public function __construct(\n /** @var string[] */\n private array $simpleArray,\n\n /** @var array<string> */\n private array $arrayOfStrings,\n\n /** @var array<string, SomeClass> */\n private array $arrayOfClassWithStringKeys,\n\n /** @var array<int, SomeClass> */\n private array $arrayOfClassWithIntegerKeys,\n\n /** @var array<non-empty-string, string> */\n private array $arrayOfClassWithNonEmptyStringKeys,\n\n /** @var array<'foo'|'bar', string> */\n private array $arrayOfClassWithStringValueKeys,\n\n /** @var array<42|1337, string> */\n private array $arrayOfClassWithIntegerValueKeys,\n\n /** @var array<positive-int, string> */\n private array $arrayOfClassWithPositiveIntegerValueKeys,\n\n /** @var non-empty-array<string> */\n private array $nonEmptyArrayOfStrings,\n\n /** @var non-empty-array<string, SomeClass> */\n private array $nonEmptyArrayWithStringKeys,\n\n /** @var list<string> */\n private array $listOfStrings,\n\n /** @var non-empty-list<string> */\n private array $nonEmptyListOfStrings,\n\n /** @var array{foo: string, bar: int} */\n private array $shapedArray,\n\n /** @var array{foo: string, bar?: int} */\n private array $shapedArrayWithOptionalElement,\n\n /** @var array{string, bar: int} */\n private array $shapedArrayWithUndefinedKey,\n ) {}\n}\n
"},{"location":"usage/type-reference/#union","title":"Union","text":"final class SomeClass\n{\n public function __construct(\n private int|string $simpleUnion,\n\n /** @var class-string<SomeInterface|AnotherInterface> */\n private string $unionOfClassString,\n\n /** @var array<SomeInterface|AnotherInterface> */\n private array $unionInsideArray,\n\n /** @var int|true */\n private int|bool $unionWithLiteralTrueType;\n\n /** @var int|false */\n private int|bool $unionWithLiteralFalseType;\n\n /** @var 404.42|1337.42 */\n private float $unionOfFloatValues,\n\n /** @var 42|1337 */\n private int $unionOfIntegerValues,\n\n /** @var 'foo'|'bar' */\n private string $unionOfStringValues,\n ) {}\n}\n
"},{"location":"usage/type-reference/#class-constants","title":"Class constants","text":"final class SomeClassWithConstants\n{\n public const FOO = 1337;\n\n public const BAR = 'bar';\n\n public const BAZ = 'baz';\n}\n\nfinal class SomeClass\n{\n public function __construct(\n /** @var SomeClassWithConstants::FOO|SomeClassWithConstants::BAR */\n private int|string $oneOfTwoCasesOfConstants,\n\n /** @param SomeClassWithConstants::BA* (matches `bar` or `baz`) */\n private string $casesOfConstantsMatchingPattern,\n ) {}\n}\n
"},{"location":"usage/type-reference/#enums","title":"Enums","text":"enum SomeEnum\n{\n case FOO;\n case BAR;\n case BAZ;\n}\n\nfinal class SomeClass\n{\n public function __construct(\n private SomeEnum $enum,\n\n /** @var SomeEnum::FOO|SomeEnum::BAR */\n private SomeEnum $oneOfTwoCasesOfEnum,\n\n /** @var SomeEnum::BA* (matches BAR or BAZ) */\n private SomeEnum $casesOfEnumMatchingPattern,\n ) {}\n}\n
"},{"location":"usage/type-strictness-and-flexibility/","title":"Type strictness & flexibility","text":"The mapper is sensitive to the types of the data that is recursively populated \u2014 for instance a string \"42\"
given to a node that expects an integer will make the mapping fail because the type is not strictly respected.
Array keys that are not bound to any node are forbidden. Mapping an array ['foo' => \u2026, 'bar' => \u2026, 'baz' => \u2026]
to an object that needs only foo
and bar
will fail, because baz
is superfluous. The same rule applies for shaped arrays.
When mapping to a list, the given array must have sequential integer keys starting at 0; if any gap or invalid key is found it will fail, like for instance trying to map ['foo' => 'foo', 'bar' => 'bar']
to list<string>
.
Types that are too permissive are not permitted \u2014 if the mapper encounters a type like mixed
, object
or array
it will fail because those types are not precise enough.
If these limitations are too restrictive, the mapper can be made more flexible to disable one or several rule(s) declared above.
"},{"location":"usage/type-strictness-and-flexibility/#enabling-flexible-casting","title":"Enabling flexible casting","text":"This setting changes the behaviours explained below:
$flexibleMapper = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->enableFlexibleCasting()\n ->mapper();\n\n// ---\n// Scalar types will accept non-strict values; for instance an integer\n// type will accept any valid numeric value like the *string* \"42\".\n\n$flexibleMapper->map('int', '42');\n// => 42\n\n// ---\n// List type will accept non-incremental keys.\n\n$flexibleMapper->map('list<int>', ['foo' => 42, 'bar' => 1337]);\n// => [0 => 42, 1 => 1338]\n\n// ---\n// If a value is missing in a source for a node that accepts `null`, the\n// node will be filled with `null`.\n\n$flexibleMapper->map(\n 'array{foo: string, bar: null|string}',\n ['foo' => 'foo'] // `bar` is missing\n);\n// => ['foo' => 'foo', 'bar' => null]\n\n// ---\n// Array and list types will convert `null` or missing values to an empty\n// array.\n\n$flexibleMapper->map(\n 'array{foo: string, bar: array<string>}',\n ['foo' => 'foo'] // `bar` is missing\n);\n// => ['foo' => 'foo', 'bar' => []]\n
"},{"location":"usage/type-strictness-and-flexibility/#allowing-superfluous-keys","title":"Allowing superfluous keys","text":"With this setting enabled, 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())\n ->allowSuperfluousKeys()\n ->mapper()\n ->map(\n 'array{foo: string, bar: int}',\n [\n 'foo' => 'foo',\n 'bar' => 42,\n 'baz' => 1337.404, // `baz` will be ignored\n ]\n );\n
"},{"location":"usage/type-strictness-and-flexibility/#allowing-permissive-types","title":"Allowing permissive types","text":"This setting allows permissive types mixed
and object
to be used during mapping.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->allowPermissiveTypes()\n ->mapper()\n ->map(\n 'array{foo: string, bar: mixed}',\n [\n 'foo' => 'foo',\n 'bar' => 42, // Could be any value\n ]\n );\n
"},{"location":"usage/validation-and-error-handling/","title":"Validation and error handling","text":"The source given to a mapper can never be trusted, this is actually the very goal of this library: transforming an unstructured input to a well-defined object structure. If a value has an invalid type, or if the mapper cannot cast it properly (in flexible mode), it means that it is not able to guarantee the validity of the desired object thus it will fail.
Any issue encountered during the mapping will add an error to an upstream exception of type \\CuyZ\\Valinor\\Mapper\\MappingError
. It is therefore always recommended wrapping the mapping function call with a try/catch statement and handle the error properly.
When the mapping fails, the exception gives access to the root node. This recursive object allows retrieving all needed information through the whole mapping tree: path, values, types and messages, including the issues that caused the exception.
Node messages can be customized and iterated through with the usage of the class \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Messages
.
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */ ]);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Get flatten list of all messages through the whole nodes tree\n $messages = \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Messages::flattenFromNode(\n $error->node()\n );\n\n // Formatters can be added and will be applied on all messages\n $messages = $messages->formatWith(\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\MessageMapFormatter([\n // \u2026\n ]),\n (new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\TranslationMessageFormatter())\n ->withTranslations([\n // \u2026\n ])\n );\n\n // If only errors are wanted, they can be filtered\n $errorMessages = $messages->errors();\n\n foreach ($errorMessages as $message) {\n echo $message;\n }\n}\n
"},{"location":"usage/validation-and-error-handling/#custom-exception-messages","title":"Custom exception messages","text":"More specific validation should be done in the constructor of the object, by throwing an exception if something is wrong with the given data.
For security reasons, exceptions thrown in a constructor will not be caught by the mapper, unless one of the three options below is used.
"},{"location":"usage/validation-and-error-handling/#1-custom-exception-classes","title":"1. Custom exception classes","text":"An exception that implements \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\ErrorMessage
can be thrown. The body can contain placeholders, see message customization chapter for more information.
If more parameters can be provided, the exception can also implement the interface \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\HasParameters
that returns a list of string values, using keys as parameters names.
To help identifying an error, a unique code can be provided by implementing the interface CuyZ\\Valinor\\Mapper\\Tree\\Message\\HasCode
.
final class SomeClass\n{\n public function __construct(private string $value)\n {\n if ($this->value === 'foo') {\n throw new SomeException('some custom parameter');\n }\n }\n}\n\nuse CuyZ\\Valinor\\Mapper\\Tree\\Message\\ErrorMessage;\nuse CuyZ\\Valinor\\Mapper\\Tree\\Message\\HasCode;\nuse CuyZ\\Valinor\\Mapper\\Tree\\Message\\HasParameters;\n\nfinal class SomeException extends DomainException implements ErrorMessage, HasParameters, HasCode\n{\n private string $someParameter;\n\n public function __construct(string $someParameter)\n {\n parent::__construct();\n\n $this->someParameter = $someParameter;\n }\n\n public function body() : string\n {\n return 'Some custom message / {some_parameter} / {source_value}';\n }\n\n public function parameters(): array\n {\n return [\n 'some_parameter' => $this->someParameter,\n ];\n }\n\n public function code() : string\n {\n // A unique code that can help to identify the error\n return 'some_unique_code';\n }\n}\n\ntry {\n (new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(SomeClass::class, 'foo');\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $exception) {\n // Should print:\n // Some custom message / some custom parameter / 'foo'\n echo $exception->node()->messages()[0];\n}\n
"},{"location":"usage/validation-and-error-handling/#2-use-provided-message-builder","title":"2. Use provided message builder","text":"The utility class \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessageBuilder
can be used to build a message.
final class SomeClass\n{\n public function __construct(private string $value)\n {\n if (str_starts_with($this->value, 'foo_')) {\n throw \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessageBuilder::newError(\n 'Some custom error message: {value}.'\n )\n ->withCode('some_code')\n ->withParameter('value', $this->value)\n ->build();\n }\n }\n}\n\ntry {\n (new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(\n SomeClass::class, 'foo_bar'\n );\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $exception) {\n // Should print:\n // > Some custom error message: foo_bar.\n echo $exception->node()->messages()[0];\n}\n
"},{"location":"usage/validation-and-error-handling/#3-allow-third-party-exceptions","title":"3. Allow third party exceptions","text":"It is possible to set up a list of exceptions that can be caught by the mapper, for instance when using lightweight validation tools like Webmozart Assert.
It is advised to use this feature with caution: userland exceptions may contain sensible information \u2014 for instance an SQL exception showing a part of a query should never be allowed. Therefore, only an exhaustive list of carefully chosen exceptions should be filtered.
final class SomeClass\n{\n public function __construct(private string $value)\n {\n \\Webmozart\\Assert\\Assert::startsWith($value, 'foo_');\n }\n}\n\ntry {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->filterExceptions(function (Throwable $exception) {\n if ($exception instanceof \\Webmozart\\Assert\\InvalidArgumentException) {\n return \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessageBuilder::from($exception);\n } \n\n // If the exception should not be caught by this library, it\n // must be thrown again.\n throw $exception;\n })\n ->mapper()\n ->map(SomeClass::class, 'bar_baz');\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $exception) {\n // Should print something similar to:\n // > Expected a value to start with \"foo_\". Got: \"bar_baz\"\n echo $exception->node()->messages()[0];\n}\n
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"","text":"\u2014 From boring old arrays to shiny typed objects \u2014 Valinor takes care of the construction and validation of raw inputs (JSON, plain arrays, etc.) into objects, ensuring a perfectly valid state. It allows the objects to be used without having to worry about their integrity during the whole application lifecycle.
The validation system will detect any incorrect value and help the developers by providing precise and human-readable error messages.
The mapper can handle native PHP types as well as other advanced types supported by PHPStan and Psalm like shaped arrays, generics, integer ranges and more.
"},{"location":"#why","title":"Why?","text":"There are many benefits of using objects instead of plain arrays in a codebase:
This library also provides a serialization system that can help transform a given input into a data format (JSON, CSV, \u2026), while preserving the original structure.
You can find more information on this topic in the normalizer chapter.
Validating and transforming raw data into an object can be achieved easily with native PHP, but it requires a lot a boilerplate code.
Below is a simple example of doing that without a mapper:
final class Person\n{\n public readonly string $name;\n\n public readonly DateTimeInterface $birthDate;\n}\n\n$data = $client->request('GET', 'https://example.com/person/42')->toArray();\n\nif (! isset($data['name']) || ! is_string($data['name'])) {\n // Cumbersome error handling\n}\n\nif (! isset($data['birthDate']) || ! is_string($data['birthDate'])) {\n // Another cumbersome error handling\n}\n\n$birthDate = DateTimeImmutable::createFromFormat('Y-m-d', $data['birthDate']);\n\nif (! $birthDate instanceof DateTimeInterface) {\n // Yet another cumbersome error handling\n}\n\n$person = new Person($data['name'], $birthDate);\n
Using a mapper saves a lot of time and energy, especially on objects with a lot of properties:
$data = $client->request('GET', 'https://example.com/person/42')->toArray();\n\ntry {\n $person = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(Person::class, $data);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Detailed error handling\n}\n
This library provides advanced features for more complex cases, check out the next chapter to get started.
"},{"location":"getting-started/","title":"Getting started","text":""},{"location":"getting-started/#installation","title":"Installation","text":"composer require cuyz/valinor\n
"},{"location":"getting-started/#example","title":"Example","text":"An application must handle the data coming from an external API; the response has a JSON format and describes a thread and its answers. The validity of this input is unsure, besides manipulating a raw JSON string is laborious and inefficient.
{\n \"id\": 1337,\n \"content\": \"Do you like potatoes?\",\n \"date\": \"1957-07-23 13:37:42\",\n \"answers\": [\n {\n \"user\": \"Ella F.\",\n \"message\": \"I like potatoes\",\n \"date\": \"1957-07-31 15:28:12\"\n },\n {\n \"user\": \"Louis A.\",\n \"message\": \"And I like tomatoes\",\n \"date\": \"1957-08-13 09:05:24\"\n }\n ]\n}\n
The application must be certain that it can handle this data correctly; wrapping the input in a value object will help.
A schema representing the needed structure must be provided, using classes.
final class Thread\n{\n public function __construct(\n public readonly int $id,\n public readonly string $content,\n public readonly DateTimeInterface $date,\n /** @var Answer[] */\n public readonly array $answers, \n ) {}\n}\n\nfinal class Answer\n{\n public function __construct(\n public readonly string $user,\n public readonly string $message,\n public readonly DateTimeInterface $date,\n ) {}\n}\n
Then a mapper is used to hydrate a source into these objects.
public function getThread(int $id): Thread\n{\n $rawJson = $this->client->request(\"https://example.com/thread/$id\");\n\n try { \n return (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n Thread::class,\n new \\CuyZ\\Valinor\\Mapper\\Source\\JsonSource($rawJson)\n );\n } catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n }\n}\n
"},{"location":"getting-started/#mapping-advanced-types","title":"Mapping advanced types","text":"Although it is recommended to map an input to a value object, in some cases mapping to another type can be easier/more flexible.
It is for instance possible to map to an array of objects:
try {\n $objects = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n 'array<' . SomeClass::class . '>',\n [/* \u2026 */]\n );\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
For simple use-cases, an array shape can be used:
try {\n $array = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n 'array{foo: string, bar: int}',\n [/* \u2026 */]\n );\n\n echo $array['foo'];\n echo $array['bar'] * 2;\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
"},{"location":"how-to/customize-error-messages/","title":"Customizing error messages","text":"The content of a message can be changed to fit custom use cases; it can contain placeholders that will be replaced with useful information.
The placeholders below are always available; even more may be used depending on the original message.
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 {source_value}
the source value that was given to the node {original_message}
the original message before being customized Usage:
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */]);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $messages = \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Messages::flattenFromNode(\n $error->node()\n );\n\n foreach ($messages as $message) {\n if ($message->code() === 'some_code') {\n $message = $message\n ->withParameter('some_parameter', 'some custom value')\n ->withBody('new message / {message_code} / {some_parameter}');\n }\n\n // new message / some_code / some custom value\n echo $message;\n }\n}\n
The messages are formatted using the ICU library, enabling the placeholders to use advanced syntax to perform proper translations, for instance currency support.
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map('int<0, 100>', 1337);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $message = $error->node()->messages()[0];\n\n if (is_numeric($message->node()->mappedValue())) {\n $message = $message->withBody(\n 'Invalid amount {source_value, number, currency}'\n ); \n } \n\n // Invalid amount: $1,337.00\n echo $message->withLocale('en_US');\n\n // Invalid amount: \u00a31,337.00\n echo $message->withLocale('en_GB');\n\n // Invalid amount: 1 337,00 \u20ac\n echo $message->withLocale('fr_FR');\n}\n
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.
For deeper message changes, formatters can be used to customize body and parameters.
Note
Formatters can be added to messages
"},{"location":"how-to/customize-error-messages/#translation","title":"Translation","text":"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()\n // Create/override a single entry\u2026\n ->withTranslation('fr', 'some custom message', 'un message personnalis\u00e9')\n // \u2026or several entries.\n ->withTranslations([\n 'some custom message' => [\n 'en' => 'Some custom message',\n 'fr' => 'Un message personnalis\u00e9',\n 'es' => 'Un mensaje personalizado',\n ], \n 'some other message' => [\n // \u2026\n ], \n ])\n ->format($message);\n
"},{"location":"how-to/customize-error-messages/#replacement-map","title":"Replacement map","text":"The formatter MessageMapFormatter
can be used to provide a list of messages replacements. 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 custom translation service is used \u2014 to avoid useless greedy operations.
In any case, the content can contain placeholders as described above.
(new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\MessageMapFormatter([\n // Will match if the given message has this exact code\n 'some_code' => 'New content / code: {message_code}',\n\n // Will match if the given message has this exact content\n 'Some message content' => 'New content / previous: {original_message}',\n\n // Will match if the given message is an instance of `SomeError`\n SomeError::class => 'New content / value: {source_value}',\n\n // A callback can be used to get access to the message instance\n OtherError::class => function (NodeMessage $message): string {\n if ($message->path() === 'foo.bar') {\n return 'Some custom message';\n }\n\n return $message->body();\n },\n\n // For greedy operation, it is advised to use a lazy-callback\n 'foo' => fn () => $this->customTranslator->translate('foo.bar'),\n]))\n ->defaultsTo('some default message')\n // \u2026or\u2026\n ->defaultsTo(fn () => $this->customTranslator->translate('default_message'))\n ->format($message);\n
"},{"location":"how-to/customize-error-messages/#several-formatters","title":"Several formatters","text":"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(\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\LocaleMessageFormatter('fr'),\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\MessageMapFormatter([\n // \u2026\n ],\n \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\TranslationMessageFormatter::default(),\n))->format($message)\n
"},{"location":"how-to/deal-with-dates/","title":"Dealing with dates","text":"When the mapper builds a date object, it has to know which format(s) are supported. By default, any valid timestamp or RFC 3339-formatted value will be accepted.
If other formats are to be supported, they need to be registered using the following method:
(new \\CuyZ\\Valinor\\MapperBuilder())\n // Both `Cookie` and `ATOM` formats will be accepted\n ->supportDateFormats(DATE_COOKIE, DATE_ATOM)\n ->mapper()\n ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');\n
"},{"location":"how-to/deal-with-dates/#custom-date-class-implementation","title":"Custom date class implementation","text":"By default, the library will map a DateTimeInterface
to a DateTimeImmutable
instance. If other implementations are to be supported, custom constructors can be used.
Here is an implementation example for the nesbot/carbon library:
(new MapperBuilder())\n // When the mapper meets a `DateTimeInterface` it will convert it to Carbon\n ->infer(DateTimeInterface::class, fn () => \\Carbon\\Carbon::class)\n\n // We teach the mapper how to create a Carbon instance\n ->registerConstructor(function (string $time): \\Carbon\\Carbon {\n // Only `Cookie` format will be accepted\n return Carbon::createFromFormat(DATE_COOKIE, $time);\n })\n\n // Carbon uses its own exceptions, so we need to wrap it for the mapper\n ->filterExceptions(function (Throwable $exception) {\n if ($exception instanceof \\Carbon\\Exceptions\\Exception) {\n return \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessageBuilder::from($exception);\n }\n\n throw $exception;\n })\n\n ->mapper()\n ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');\n
"},{"location":"how-to/infer-interfaces/","title":"Inferring interfaces","text":"When the mapper meets an interface, it needs to understand which implementation (a class that implements this interface) will be used \u2014 this information must be provided in the mapper builder, using the method infer()
.
The callback given to this method must return the name of a class that implements the interface. Any arguments can be required by the callback; they will be mapped properly using the given source.
If the callback can return several class names, it needs to provide a return signature with the list of all class-strings that can be returned.
$mapper = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(UuidInterface::class, fn () => MyUuid::class)\n ->infer(\n SomeInterface::class, \n /** @return class-string<FirstImplementation|SecondImplementation> */\n fn (string $type) => match($type) {\n 'first' => FirstImplementation::class,\n 'second' => SecondImplementation::class,\n default => throw new DomainException(\"Unhandled type `$type`.\")\n }\n )->mapper();\n\n// Will return an instance of `FirstImplementation`\n$mapper->map(SomeInterface::class, [\n 'type' => 'first',\n 'uuid' => 'a6868d61-acba-406d-bcff-30ecd8c0ceb6',\n 'someString' => 'foo',\n]);\n\n// Will return an instance of `SecondImplementation`\n$mapper->map(SomeInterface::class, [\n 'type' => 'second',\n 'uuid' => 'a6868d61-acba-406d-bcff-30ecd8c0ceb6',\n 'someInt' => 42,\n]);\n\ninterface SomeInterface {}\n\nfinal class FirstImplementation implements SomeInterface\n{\n public readonly UuidInterface $uuid;\n\n public readonly string $someString;\n}\n\nfinal class SecondImplementation implements SomeInterface\n{\n public readonly UuidInterface $uuid;\n\n public readonly int $someInt;\n}\n
"},{"location":"how-to/infer-interfaces/#inferring-classes","title":"Inferring classes","text":"The same mechanics can be applied to infer abstract or parent classes.
Example with an abstract class:
abstract class SomeAbstractClass\n{\n public string $foo;\n\n public string $bar;\n}\n\nfinal class SomeChildClass extends SomeAbstractClass\n{\n public string $baz;\n}\n\n$result = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(\n SomeAbstractClass::class, \n fn () => SomeChildClass::class\n )\n ->mapper()\n ->map(SomeAbstractClass::class, [\n 'foo' => 'foo',\n 'bar' => 'bar',\n 'baz' => 'baz',\n ]);\n\nassert($result instanceof SomeChildClass);\nassert($result->foo === 'foo');\nassert($result->bar === 'bar');\nassert($result->baz === 'baz');\n
Example with inheritance:
class SomeParentClass\n{\n public string $foo;\n\n public string $bar;\n}\n\nfinal class SomeChildClass extends SomeParentClass\n{\n public string $baz;\n}\n\n$result = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(\n SomeParentClass::class, \n fn () => SomeChildClass::class\n )\n ->mapper()\n ->map(SomeParentClass::class, [\n 'foo' => 'foo',\n 'bar' => 'bar',\n 'baz' => 'baz',\n ]);\n\nassert($result instanceof SomeChildClass);\nassert($result->foo === 'foo');\nassert($result->bar === 'bar');\nassert($result->baz === 'baz');\n
"},{"location":"how-to/map-arguments-of-a-callable/","title":"Mapping arguments of a callable","text":"This library can map the arguments of a callable; it 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 {\n return \"$foo / $bar\";\n}\n\ntry {\n $arguments = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->argumentsMapper()\n ->mapArguments($someFunction, [\n 'foo' => 'some value',\n 'bar' => 42,\n ]);\n\n // some value / 42\n echo $someFunction(...$arguments);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
Any callable can be given to the arguments mapper:
final class SomeController\n{\n public static function someAction(string $foo, int $bar): string\n {\n return \"$foo / $bar\";\n }\n}\n\ntry {\n $arguments = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->argumentsMapper()\n ->mapArguments(SomeController::someAction(...), [\n 'foo' => 'some value',\n 'bar' => 42,\n ]);\n\n // some value / 42\n echo SomeController::someAction(...$arguments);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
"},{"location":"how-to/transform-input/","title":"Transforming input","text":"Any source can be given to the mapper, be it an array, some JSON, YAML or even a file:
$mapper = (new \\CuyZ\\Valinor\\MapperBuilder())->mapper();\n\n$mapper->map(\n SomeClass::class,\n \\CuyZ\\Valinor\\Mapper\\Source\\Source::array($someData)\n);\n\n$mapper->map(\n SomeClass::class,\n \\CuyZ\\Valinor\\Mapper\\Source\\Source::json($jsonString)\n);\n\n$mapper->map(\n SomeClass::class,\n \\CuyZ\\Valinor\\Mapper\\Source\\Source::yaml($yamlString)\n);\n\n$mapper->map(\n SomeClass::class,\n // File containing valid Json or Yaml content and with valid extension\n \\CuyZ\\Valinor\\Mapper\\Source\\Source::file(\n new SplFileObject('path/to/my/file.json')\n )\n);\n
Info
JSON or YAML given to a source may be invalid, in which case an exception can be caught and manipulated.
try {\n $source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::json('invalid JSON');\n} catch (\\CuyZ\\Valinor\\Mapper\\Source\\Exception\\InvalidSource $exception) {\n // Let the application handle the exception in the desired way.\n // It is possible to get the original source with `$exception->source()`\n}\n
"},{"location":"how-to/transform-input/#modifiers","title":"Modifiers","text":"Sometimes the source is not in the same format and/or organised in the same way as a value object. Modifiers can be used to change a source before the mapping occurs.
"},{"location":"how-to/transform-input/#camel-case-keys","title":"Camel case keys","text":"This modifier recursively forces all keys to be in camelCase format.
final class SomeClass\n{\n public readonly string $someValue;\n}\n\n$source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::array([\n 'some_value' => 'foo',\n // \u2026or\u2026\n 'some-value' => 'foo',\n // \u2026or\u2026\n 'some value' => 'foo',\n // \u2026will be replaced by `['someValue' => 'foo']`\n ])\n ->camelCaseKeys();\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, $source);\n
"},{"location":"how-to/transform-input/#path-mapping","title":"Path mapping","text":"This modifier can be used to change paths in the source data using a dot notation.
The mapping is done using an associative array of path mappings. This array must have the source path as key and the target path as value.
The source path uses the dot notation (eg A.B.C
) and can contain one *
for array paths (eg A.B.*.C
).
final class Country\n{\n /** @var non-empty-string */\n public readonly string $name;\n\n /** @var list<City> */\n public readonly array $cities;\n}\n\nfinal class City\n{\n /** @var non-empty-string */\n public readonly string $name;\n\n public readonly DateTimeZone $timeZone;\n}\n\n$source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::array([\n 'identification' => 'France',\n 'towns' => [\n [\n 'label' => 'Paris',\n 'timeZone' => 'Europe/Paris',\n ],\n [\n 'label' => 'Lyon',\n 'timeZone' => 'Europe/Paris',\n ],\n ],\n])->map([\n 'identification' => 'name',\n 'towns' => 'cities',\n 'towns.*.label' => 'name',\n]);\n\n// After modification this is what the source will look like:\n// [\n// 'name' => 'France',\n// 'cities' => [\n// [\n// 'name' => 'Paris',\n// 'timeZone' => 'Europe/Paris',\n// ],\n// [\n// 'name' => 'Lyon',\n// 'timeZone' => 'Europe/Paris',\n// ],\n// ],\n// ];\n\n(new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(Country::class, $source);\n
"},{"location":"how-to/transform-input/#custom-source","title":"Custom source","text":"The source is just an iterable, so it's easy to create a custom one. It can even be combined with the provided builder.
final class AcmeSource implements IteratorAggregate\n{\n private iterable $source;\n\n public function __construct(iterable $source)\n {\n $this->source = $this->doSomething($source);\n }\n\n private function doSomething(iterable $source): iterable\n {\n // Do something with $source\n\n return $source;\n }\n\n public function getIterator()\n {\n yield from $this->source;\n }\n}\n\n$source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::iterable(\n new AcmeSource([\n 'valueA' => 'foo',\n 'valueB' => 'bar',\n ])\n)->camelCaseKeys();\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, $source);\n
"},{"location":"how-to/use-custom-object-constructors/","title":"Using custom object constructors","text":"An object may have custom ways of being created, in such cases these constructors need to be registered to the mapper to be used. A constructor is a callable that can be either:
__invoke
methodIn any case, the return type of the callable will be resolved by the mapper to know when to use it. Any argument can be provided and will automatically be mapped using the given source. These arguments can then be used to instantiate the object in the desired way.
Registering any constructor will disable the native constructor \u2014 the __construct
method \u2014 of the targeted class. If for some reason it still needs to be handled as well, the name of the class must be given to the registration method.
If several constructors are registered, they must provide distinct signatures to prevent collision during mapping \u2014 meaning that if two constructors require several arguments with the exact same names, the mapping will fail.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n // Allow the native constructor to be used\n Color::class,\n\n // Register a named constructor (1)\n Color::fromHex(...),\n\n /**\n * An anonymous function can also be used, for instance when the desired\n * object is an external dependency that cannot be modified.\n * \n * @param 'red'|'green'|'blue' $color\n * @param 'dark'|'light' $darkness\n */\n function (string $color, string $darkness): Color {\n $main = $darkness === 'dark' ? 128 : 255;\n $other = $darkness === 'dark' ? 0 : 128;\n\n return new Color(\n $color === 'red' ? $main : $other,\n $color === 'green' ? $main : $other,\n $color === 'blue' ? $main : $other,\n );\n }\n )\n ->mapper()\n ->map(Color::class, [/* \u2026 */]);\n\nfinal class Color\n{\n /**\n * @param int<0, 255> $red\n * @param int<0, 255> $green\n * @param int<0, 255> $blue\n */\n public function __construct(\n public readonly int $red,\n public readonly int $green,\n public readonly int $blue\n ) {}\n\n /**\n * @param non-empty-string $hex\n */\n public static function fromHex(string $hex): self\n {\n if (strlen($hex) !== 6) {\n throw new DomainException('Must be 6 characters long');\n }\n\n /** @var int<0, 255> $red */\n $red = hexdec(substr($hex, 0, 2));\n /** @var int<0, 255> $green */\n $green = hexdec(substr($hex, 2, 2));\n /** @var int<0, 255> $blue */\n $blue = hexdec(substr($hex, 4, 2));\n\n return new self($red, $green, $blue);\n }\n}\n
\u2026or for PHP < 8.1:
[Color::class, 'fromHex'],\n
Registering a constructor for an enum works the same way as for a class, as described above.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n // Allow the native constructor to be used\n SomeEnum::class,\n\n // Register a named constructor\n SomeEnum::fromMatrix(...)\n )\n ->mapper()\n ->map(SomeEnum::class, [\n 'type' => 'FOO',\n 'number' => 2,\n ]);\n\nenum SomeEnum: string\n{\n case CASE_A = 'FOO_VALUE_1';\n case CASE_B = 'FOO_VALUE_2';\n case CASE_C = 'BAR_VALUE_1';\n case CASE_D = 'BAR_VALUE_2';\n\n /**\n * @param 'FOO'|'BAR' $type\n * @param int<1, 2> $number\n */\n public static function fromMatrix(string $type, int $number): self\n {\n return self::from(\"{$type}_VALUE_{$number}\");\n }\n}\n
Note
An enum constructor can be for a specific pattern:
enum SomeEnum\n{\n case FOO;\n case BAR;\n case BAZ;\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n /**\n * This constructor will be called only when pattern `SomeEnum::BA*`\n * is requested during mapping.\n * \n * @return SomeEnum::BA*\n */\n fn (string $value): SomeEnum => /* Some custom domain logic */\n )\n ->mapper()\n ->map(SomeEnum::class . '::BA*', 'some custom value');\n
"},{"location":"how-to/use-custom-object-constructors/#dynamic-constructors","title":"Dynamic constructors","text":"In some situations the type handled by a constructor is only known at runtime, in which case the constructor needs to know what class must be used to instantiate the object.
For instance, an interface may declare a static constructor that is then implemented by several child classes. One solution would be to register the constructor for each child class, which leads to a lot of boilerplate code and would require a new registration each time a new child is created. Another way is to use the attribute \\CuyZ\\Valinor\\Mapper\\Object\\DynamicConstructor
.
When a constructor uses this attribute, its first parameter must be a string and will be filled with the name of the actual class that the mapper needs to build when the constructor is called. Other arguments may be added and will be mapped normally, depending on the source given to the mapper.
interface InterfaceWithStaticConstructor\n{\n public static function from(string $value): self;\n}\n\nfinal class ClassWithInheritedStaticConstructor implements InterfaceWithStaticConstructor\n{\n private function __construct(private SomeValueObject $value) {}\n\n public static function from(string $value): self\n {\n return new self(new SomeValueObject($value));\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n #[\\CuyZ\\Valinor\\Attribute\\DynamicConstructor]\n function (string $className, string $value): InterfaceWithStaticConstructor {\n return $className::from($value);\n }\n )\n ->mapper()\n ->map(ClassWithInheritedStaticConstructor::class, 'foo');\n
"},{"location":"other/app-and-framework-integration/","title":"Application and framework integration","text":"This library is framework-agnostic, but using it in an application that relies on a framework is still possible.
For Symfony applications, check out the chapter below. For other frameworks, check out the custom integration chapter.
"},{"location":"other/app-and-framework-integration/#symfony-bundle","title":"Symfony bundle","text":"A bundle is available to automatically integrate this library into a Symfony application.
composer require cuyz/valinor-bundle\n
The documentation of this bundle can be found on the GitHub repository.
"},{"location":"other/app-and-framework-integration/#custom-integration","title":"Custom integration","text":"If the application does not have a dedicated framework integration, it is still possible to integrate this library manually.
"},{"location":"other/app-and-framework-integration/#mapper-registration","title":"Mapper registration","text":"The most important task of the integration is to correctly register the mapper(s) used in the application. Mapper instance(s) should be shared between services whenever possible; this is important because heavy operations are cached internally to improve performance during runtime.
If the framework uses a service container, it should be configured in a way where the mapper(s) are registered as shared services. In other cases, direct instantiation of the mapper(s) should be avoided.
$mapperBuilder = new \\CuyZ\\Valinor\\MapperBuilder();\n\n// \u2026customization of the mapper builder\u2026\n\n$container->addSharedService('mapper', $mapperBuilder->mapper());\n
"},{"location":"other/app-and-framework-integration/#registering-a-cache","title":"Registering a cache","text":"As mentioned above, caching is important to allow the mapper to perform well. The application really should provide a cache implementation to the mapper builder.
As stated in the performance chapter, the library provides a cache implementation out of the box which can be used in any application. Custom cache can be used as well, as long as it is PSR-16 compliant.
$cache = new \\CuyZ\\Valinor\\Cache\\FileSystemCache('path/to/cache-directory');\n\n// If the application can detect when it is in development environment, it is\n// advised to wrap the cache with a `FileWatchingCache` instance, to avoid\n// having to manually clear the cache when a file changes during development.\nif ($isApplicationInDevelopmentEnvironment) {\n $cache = new \\CuyZ\\Valinor\\Cache\\FileWatchingCache($cache);\n}\n\n$mapperBuilder = $mapperBuilder->withCache($cache);\n
"},{"location":"other/app-and-framework-integration/#warming-up-the-cache","title":"Warming up the cache","text":"The cache can be warmed up to ease the application cold start. If the framework has a way to automatically detect which classes will be used by the mapper, they should be given to the warmup
method, as stated in the cache warmup chapter.
Concerning other configurations, such as enabling flexible casting, configuring supported date formats or registering custom constructors, an integration should be provided to configure the mapper builder in a convenient way \u2014 how it is done will mostly depend on the framework features and its main philosophy.
"},{"location":"other/performance-and-caching/","title":"Performance & caching","text":"This library needs to parse a lot of information in order to handle all provided features. Therefore, it is strongly advised to activate the cache to reduce heavy workload between runtimes, especially when the application runs in a production environment.
The library provides a cache implementation out of the box, which saves cache entries into the file system.
Note
It is also possible to use any PSR-16 compliant implementation, as long as it is capable of caching the entries handled by the library.
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 \u2014 preventing the library not behaving as expected when the signature of a property or a method changes.
$cache = new \\CuyZ\\Valinor\\Cache\\FileSystemCache('path/to/cache-directory');\n\nif ($isApplicationInDevelopmentEnvironment) {\n $cache = new \\CuyZ\\Valinor\\Cache\\FileWatchingCache($cache);\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->withCache($cache)\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */]);\n
"},{"location":"other/performance-and-caching/#warming-up-cache","title":"Warming up cache","text":"The cache can be warmed up, for instance in a pipeline during the build and deployment of the application.
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-dir');\n\n$mapperBuilder = (new \\CuyZ\\Valinor\\MapperBuilder())->withCache($cache);\n\n// During the build:\n$mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class);\n\n// In the application:\n$mapper->mapper()->map(SomeClass::class, [/* \u2026 */]);\n
"},{"location":"other/static-analysis/","title":"Static analysis","text":"To help static analysis of a codebase using this library, an extension for PHPStan and a plugin for Psalm are provided. They enable these tools to better understand the behaviour of the mapper.
Note
To activate this feature, the plugin must be registered correctly:
PHPStanPsalm phpstan.neonincludes:\n - vendor/cuyz/valinor/qa/PHPStan/valinor-phpstan-configuration.php\n
composer.json\"autoload-dev\": {\n \"files\": [\n \"vendor/cuyz/valinor/qa/Psalm/ValinorPsalmPlugin.php\"\n ]\n}\n
psalm.xml<plugins>\n <pluginClass class=\"CuyZ\\Valinor\\QA\\Psalm\\ValinorPsalmPlugin\"/>\n</plugins>\n
Considering at least one of those tools are installed on a project, below are examples of the kind of errors that would be reported.
Mapping to an array of classes
final class SomeClass\n{\n public function __construct(\n public readonly string $foo,\n public readonly int $bar,\n ) {}\n}\n\n$objects = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n 'array<' . SomeClass::class . '>',\n [/* \u2026 */]\n );\n\nforeach ($objects as $object) {\n // \u2705\n echo $object->foo;\n\n // \u2705\n echo $object->bar * 2;\n\n // \u274c Cannot perform operation between `string` and `int`\n echo $object->foo * $object->bar;\n\n // \u274c Property `SomeClass::$fiz` is not defined\n echo $object->fiz;\n}\n
Mapping to a shaped array
$array = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(\n 'array{foo: string, bar: int}',\n [/* \u2026 */]\n );\n\n// \u2705\necho $array['foo'];\n\n// \u274c Expected `string` but got `int`\necho strtolower($array['bar']);\n\n// \u274c Cannot perform operation between `string` and `int`\necho $array['foo'] * $array['bar'];\n\n// \u274c Offset `fiz` does not exist on array\necho $array['fiz']; \n
Mapping arguments of a callable
$someFunction = function(string $foo, int $bar): string {\n return \"$foo / $bar\";\n};\n\n$arguments = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->argumentsMapper()\n ->mapArguments($someFunction, [\n 'foo' => 'some value',\n 'bar' => 42,\n ]);\n\n// \u2705 Arguments have a correct shape, no error reported\necho $someFunction(...$arguments);\n
"},{"location":"project/alternatives/","title":"Alternatives to this library","text":"Mapping and hydration have been available in the PHP world for a long time. This library aims to bring powerful features to this aspect, some of which are missing in similar packages:
list<string>
, non-empty-string
, positive-int
, int<0, 42>
, shaped arrays, generic classes and more are handled and validated properly.You may take a look at alternative projects, but some features listed above might be missing:
symfony/serializer
eventsauce/object-hydrator
crell/serde
spatie/laravel-data
jms/serializer
netresearch/jsonmapper
json-mapper/json-mapper
brick/json-mapper
Below are listed the changelogs for all released versions of the library.
"},{"location":"project/changelog/#version-1","title":"Version 1","text":"1.8.2
\u2014 8th of January 20241.8.1
\u2014 8th of January 20241.8.0
\u2014 26th of December 20231.7.0
\u2014 23rd of October 20231.6.1
\u2014 11th of October 20231.6.0
\u2014 25th of August 20231.5.0
\u2014 7th of August 20231.4.0
\u2014 17th of April 20231.3.1
\u2014 13th of February 20231.3.0
\u2014 8th of February 20231.2.0
\u2014 9th of January 20231.1.0
\u2014 20th of December 20221.0.0
\u2014 28th of November 20220.17.0
\u2014 8th of November 20220.16.0
\u2014 19th of October 20220.15.0
\u2014 6th of October 20220.14.0
\u2014 1st of September 20220.13.0
\u2014 31st of July 20220.12.0
\u2014 10th of July 20220.11.0
\u2014 23rd of June 20220.10.0
\u2014 10th of June 20220.9.0
\u2014 23rd of May 20220.8.0
\u2014 9th of May 20220.7.0
\u2014 24th of March 20220.6.0
\u2014 24th of February 20220.5.0
\u2014 21st of January 20220.4.0
\u2014 7th of January 20220.3.0
\u2014 18th of December 20210.2.0
\u2014 7th of December 20210.1.1
\u2014 1st of December 2021The development of this library is mainly motivated by the kind words and the help of many people. I am grateful to everyone, especially to the contributors of this repository who directly help to push the project forward.
I have to give JetBrains credits for providing a free PhpStorm license for the development of this open-source package.
I also want to thank Blackfire for providing a license of their awesome tool, leading to notable performance gains when using this library.
"},{"location":"project/changelog/version-0.1.1/","title":"Changelog 0.1.1 \u2014 1st of December 2021","text":"See release on GitHub
"},{"location":"project/changelog/version-0.1.1/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"See release on GitHub
"},{"location":"project/changelog/version-0.10.0/#notable-changes","title":"Notable changes","text":"Documentation is now available at valinor.cuyz.io.
"},{"location":"project/changelog/version-0.10.0/#features","title":"Features","text":"@var
annotation (d8eb4d)See release on GitHub
"},{"location":"project/changelog/version-0.11.0/#notable-changes","title":"Notable changes","text":"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' => \u2026, 'bar' => \u2026, 'baz' => \u2026]
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())\n ->flexible()\n ->mapper();\n ->map('array{foo: int, bar: bool}', [\n 'foo' => '42', // Will be cast from `string` to `int`\n 'bar' => 'true', // Will be cast from `string` to `bool`\n 'baz' => '\u2026', // Will be ignored\n ]);\n
When using this library for a provider application \u2014 for instance an API endpoint that can be called with a JSON payload \u2014 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())\n // Can remain unchanged\n ->infer(SomeInterface::class, fn () => SomeImplementation::class);\n
$builder = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(\n SomeInterface::class,\n fn (string $type) => match($type) {\n 'first' => ImplementationA::class,\n 'second' => ImplementationB::class,\n default => throw new DomainException(\"Unhandled `$type`.\")\n }\n )\n // \u2026should be modified with:\n ->infer(\n SomeInterface::class,\n /** @return class-string<ImplementationA|ImplementationB> */\n fn (string $type) => match($type) {\n 'first' => ImplementationA::class,\n 'second' => ImplementationB::class,\n default => throw new DomainException(\"Unhandled `$type`.\")\n }\n );\n
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\n{\n public static function constructorA(string $foo, string $bar): self\n {\n // \u2026\n }\n\n public static function constructorB(string $foo, string $bar): self\n {\n // \u2026\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n SomeClass::constructorA(...),\n SomeClass::constructorB(...),\n )\n ->mapper();\n ->map(SomeClass::class, [\n 'foo' => 'foo',\n 'bar' => 'bar',\n ]);\n\n// Exception: A collision was detected [\u2026]\n
"},{"location":"project/changelog/version-0.11.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"See release on GitHub
"},{"location":"project/changelog/version-0.12.0/#notable-changes","title":"Notable changes","text":"SECURITY \u2014 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 \u2014 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\n{\n public function __construct(private string $value)\n {\n \\Webmozart\\Assert\\Assert::startsWith($value, 'foo_');\n }\n}\n\ntry {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->filterExceptions(function (Throwable $exception) {\n if ($exception instanceof \\Webmozart\\Assert\\InvalidArgumentException) {\n return \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\ThrowableMessage::from($exception);\n }\n\n // If the exception should not be caught by this library, it\n // must be thrown again.\n throw $exception;\n })\n ->mapper()\n ->map(SomeClass::class, 'bar_baz');\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $exception) {\n // Should print something similar to:\n // > Expected a value to start with \"foo_\". Got: \"bar_baz\"\n echo $exception->node()->messages()[0];\n}\n
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.
"},{"location":"project/changelog/version-0.12.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":".idea
folder (84ead0)See release on GitHub
"},{"location":"project/changelog/version-0.13.0/#notable-changes","title":"Notable changes","text":"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\n extends DomainException \n implements ErrorMessage, HasParameters, HasCode\n{\n private string $someParameter;\n\n public function __construct(string $someParameter)\n {\n parent::__construct();\n\n $this->someParameter = $someParameter;\n }\n\n public function body() : string\n {\n return 'Some message / {some_parameter} / {source_value}';\n }\n\n public function parameters(): array\n {\n return [\n 'some_parameter' => $this->someParameter,\n ];\n }\n\n public function code() : string\n {\n // A unique code that can help to identify the error\n return 'some_unique_code';\n }\n}\n
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'); // \u2705\n(new MapperBuilder())->mapper()->map('numeric-string', 'foo'); // \u274c\n
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)See release on GitHub
"},{"location":"project/changelog/version-0.14.0/#notable-changes","title":"Notable changes","text":"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())\n // Both `Cookie` and `ATOM` formats will be accepted\n ->supportDateFormats(DATE_COOKIE, DATE_ATOM)\n ->mapper()\n ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');\n
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())\n ->registerConstructor(\n new \\CuyZ\\Valinor\\Mapper\\Object\\BackwardCompatibilityDateTimeConstructor()\n )\n ->mapper()\n ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');\n
"},{"location":"project/changelog/version-0.14.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"DynamicConstructor
(e437d9)ClassStringType
(4bc50e)ObjectBuilderFactory::for
return signature (57849c)See release on GitHub
"},{"location":"project/changelog/version-0.15.0/#notable-changes","title":"Notable changes","text":"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\n{\n public const FOO = 1337;\n\n public const BAR = 'bar';\n\n public const BAZ = 'baz';\n}\n\n$mapper = (new MapperBuilder())->mapper();\n\n$mapper->map('SomeClassWithConstants::BA*', 1337); // error\n$mapper->map('SomeClassWithConstants::BA*', 'bar'); // ok\n$mapper->map('SomeClassWithConstants::BA*', 'baz'); // ok\n
Example for enum:
enum SomeEnum: string\n{\n case FOO = 'foo';\n case BAR = 'bar';\n case BAZ = 'baz';\n}\n\n$mapper = (new MapperBuilder())->mapper();\n\n$mapper->map('SomeEnum::BA*', 'foo'); // error\n$mapper->map('SomeEnum::BA*', 'bar'); // ok\n$mapper->map('SomeEnum::BA*', 'baz'); // ok\n
"},{"location":"project/changelog/version-0.15.0/#features","title":"Features","text":"See release on GitHub
"},{"location":"project/changelog/version-0.16.0/#features","title":"Features","text":"See release on GitHub
"},{"location":"project/changelog/version-0.17.0/#notable-changes","title":"Notable changes","text":"The main feature introduced in this release is the split of the flexible mode in three distinct modes:
Changes the behaviours explained below:
$flexibleMapper = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->enableFlexibleCasting()\n ->mapper();\n\n// ---\n// Scalar types will accept non-strict values; for instance an\n// integer type will accept any valid numeric value like the\n// *string* \"42\".\n\n$flexibleMapper->map('int', '42');\n// => 42\n\n// ---\n// List type will accept non-incremental keys.\n\n$flexibleMapper->map('list<int>', ['foo' => 42, 'bar' => 1337]);\n// => [0 => 42, 1 => 1338]\n\n// ---\n// If a value is missing in a source for a node that accepts `null`,\n// the node will be filled with `null`.\n\n$flexibleMapper->map(\n 'array{foo: string, bar: null|string}',\n ['foo' => 'foo'] // `bar` is missing\n);\n// => ['foo' => 'foo', 'bar' => null]\n\n// ---\n// Array and list types will convert `null` or missing values to an\n// empty array.\n\n$flexibleMapper->map(\n 'array{foo: string, bar: array<string>}',\n ['foo' => 'foo'] // `bar` is missing\n);\n// => ['foo' => 'foo', 'bar' => []]\n
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())\n ->allowSuperfluousKeys()\n ->mapper()\n ->map(\n 'array{foo: string, bar: int}',\n [\n 'foo' => 'foo',\n 'bar' => 42,\n 'baz' => 1337.404, // `baz` will be ignored\n ]\n );\n
Allows permissive types mixed
and object
to be used during mapping.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->allowPermissiveTypes()\n ->mapper()\n ->map(\n 'array{foo: string, bar: mixed}',\n [\n 'foo' => 'foo',\n 'bar' => 42, // Could be any value\n ]\n );\n
"},{"location":"project/changelog/version-0.17.0/#features","title":"Features","text":"strict-array
type (d456eb)uniqid()
(b81847)null
in flexible mode (92a41a)See release on GitHub
"},{"location":"project/changelog/version-0.2.0/#features","title":"Features","text":"GenericAssignerLexer
to TypeAliasLexer
(680941)marcocesarato/php-conventional-changelog
for changelog (178aa9)See release on GitHub
"},{"location":"project/changelog/version-0.3.0/#features","title":"Features","text":"friendsofphp/php-cs-fixer
(e5ccbe)See release on GitHub
"},{"location":"project/changelog/version-0.4.0/#notable-changes","title":"Notable changes","text":"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(\n 'array<' . SomeClass::class . '>',\n [/* \u2026 */]\n);\n
For simple use-cases, an array shape can be used:
$array = (new MapperBuilder())->mapper()->map(\n 'array{foo: string, bar: int}',\n [/* \u2026 */]\n);\n\necho $array['foo'];\necho $array['bar'] * 2;\n
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 \u2014 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 \u2014 to avoid useless greedy operations.
In any case, the content can contain placeholders that will automatically be replaced by, in order:
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */]);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $node = $error->node();\n $messages = new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessagesFlattener($node);\n\n $formatter = (new MessageMapFormatter([\n // Will match if the given message has this exact code\n 'some_code' => 'new content / previous code was: %1$s',\n\n // Will match if the given message has this exact content\n 'Some message content' => 'new content / previous message: %2$s',\n\n // Will match if the given message is an instance of `SomeError`\n SomeError::class => '\n - Original code of the message: %1$s\n - Original content of the message: %2$s\n - Node type: %3$s\n - Node name: %4$s\n - Node path: %5$s\n ',\n\n // A callback can be used to get access to the message instance\n OtherError::class => function (NodeMessage $message): string {\n if ((string)$message->type() === 'string|int') {\n // \u2026\n }\n\n return 'Some message content';\n },\n\n // For greedy operation, it is advised to use a lazy-callback\n 'bar' => fn () => $this->translator->translate('foo.bar'),\n ]))\n ->defaultsTo('some default message')\n // \u2026or\u2026\n ->defaultsTo(fn () => $this->translator->translate('default_message'));\n\n foreach ($messages as $message) {\n echo $formatter->format($message); \n }\n}\n
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\n{\n public readonly SomeFooObject|SomeBarObject $object;\n}\n\nfinal class SomeFooObject\n{\n public readonly string $foo;\n}\n\nfinal class SomeBarObject\n{\n public readonly string $bar;\n}\n\n// Will map to an instance of `SomeFooObject`\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(UnionOfObjects::class, ['foo' => 'foo']);\n\n// Will map to an instance of `SomeBarObject`\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(UnionOfObjects::class, ['bar' => 'bar']);\n
"},{"location":"project/changelog/version-0.4.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"MessageMapFormatter
(ddf69e)MessagesFlattener
(a97b40)NodeTraverser
for recursive operations on nodes (cc1bc6)See release on GitHub
"},{"location":"project/changelog/version-0.5.0/#features","title":"Features","text":"TreeMapper#map()
(e28003)See release on GitHub
"},{"location":"project/changelog/version-0.6.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"composer.json
(6fdd62)See release on GitHub
"},{"location":"project/changelog/version-0.7.0/#notable-changes","title":"Notable changes","text":"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\n{\n public static function namedConstructor(string $foo): self\n {\n // \u2026\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n SomeClass::namedConstructor(...),\n // \u2026or for PHP < 8.1:\n [SomeClass::class, 'namedConstructor'],\n )\n ->mapper()\n ->map(SomeClass::class, [\n // \u2026\n ]);\n
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)\n ->camelCaseKeys()\n ->map([\n 'towns' => 'cities',\n 'towns.*.label' => 'name',\n ]);\n\n$result = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, $source);\n
See documentation for more details about its usage.
"},{"location":"project/changelog/version-0.7.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"Attributes::ofType
return type to array
(1a599b)See release on GitHub
"},{"location":"project/changelog/version-0.8.0/#notable-changes","title":"Notable changes","text":"Float values handling
Allows the usage of float values, as follows:
class Foo\n{\n /** @var 404.42|1337.42 */\n public readonly float $value;\n}\n
Literal boolean true
/ false
values handling
Thanks @danog for this feature!
Allows the usage of boolean values, as follows:
class Foo\n{\n /** @var int|false */\n public readonly int|bool $value;\n}\n
Class string of union of object handling
Allows to declare several class names in a class-string
:
class Foo\n{\n /** @var class-string<SomeClass|SomeOtherClass> */\n public readonly string $className;\n}\n
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).gitattributes
(979272)Polyfill
coverage (c08fe5)symfony/polyfill-php80
dependency (368737)See release on GitHub
"},{"location":"project/changelog/version-0.9.0/#notable-changes","title":"Notable changes","text":"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 \u2014 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 \u2014 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');\n\nif ($isApplicationInDevelopmentEnvironment) {\n $cache = new \\CuyZ\\Valinor\\Cache\\FileWatchingCache($cache);\n}\n\n$mapperBuilder = (new \\CuyZ\\Valinor\\MapperBuilder())->withCache($cache);\n\n// During the build:\n$mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class);\n\n// In the application:\n$mapperBuilder->mapper()->map(SomeClass::class, [/* \u2026 */]);\n
Message formatting & translation
Major changes have been made to the messages being returned in case of a mapping error: the actual texts are now more accurate and show better information.
Warning
The method NodeMessage::format
has been removed, message formatters should be used instead. If needed, the old behaviour can be retrieved with the formatter PlaceHolderMessageFormatter
, although it is strongly advised to use the new placeholders feature (see below).
The signature of the method MessageFormatter::format
has changed as well.
It is now also easier to format the messages, for instance when they need to be translated. Placeholders can now be used in a message body, and will be replaced with useful information.
Placeholder Description{message_code}
the code of the message {node_name}
name of the node to which the message is bound {node_path}
path of the node to which the message is bound {node_type}
type of the node to which the message is bound {original_value}
the source value that was given to the node {original_message}
the original message before being customized try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */]);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $node = $error->node();\n $messages = new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessagesFlattener($node);\n\n foreach ($messages as $message) {\n if ($message->code() === 'some_code') {\n $message = $message->withBody('new message / {original_message}');\n }\n\n echo $message;\n }\n}\n
The messages are formatted using the ICU library, enabling the placeholders to use advanced syntax to perform proper translations, for instance currency support.
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map('int<0, 100>', 1337);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n $message = $error->node()->messages()[0];\n\n if (is_numeric($message->value())) {\n $message = $message->withBody(\n 'Invalid amount {original_value, number, currency}'\n ); \n } \n\n // Invalid amount: $1,337.00\n echo $message->withLocale('en_US');\n\n // Invalid amount: \u00a31,337.00\n echo $message->withLocale('en_GB');\n\n // Invalid amount: 1 337,00 \u20ac\n echo $message->withLocale('fr_FR');\n}\n
See ICU documentation for more information on available syntax.
Warning If the intl
extension is not installed, a shim will be available to replace the placeholders, but it won't handle advanced syntax as described above.
The formatter TranslationMessageFormatter
can be used to translate the content of messages.
The library provides a list of all messages that can be returned; this list can be filled or modified with custom translations.
\\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\TranslationMessageFormatter::default()\n // Create/override a single entry\u2026\n ->withTranslation('fr', 'some custom message', 'un message personnalis\u00e9')\n // \u2026or several entries.\n ->withTranslations([\n 'some custom message' => [\n 'en' => 'Some custom message',\n 'fr' => 'Un message personnalis\u00e9',\n 'es' => 'Un mensaje personalizado',\n ], \n 'some other message' => [\n // \u2026\n ], \n ])\n ->format($message);\n
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(\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\LocaleMessageFormatter('fr'),\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\MessageMapFormatter([\n // \u2026\n ],\n \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\TranslationMessageFormatter::default(),\n))->format($message)\n
"},{"location":"project/changelog/version-0.9.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"ObjectBuilder
API access (11e126)InvalidParameterIndex
exception inheritance type (b75adb)See release on GitHub
First stable version! \ud83e\udd73 \ud83c\udf89
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 \u2014 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\u2122); 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! \ud83d\ude4c
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 \u2764\ufe0f sponsor my work on GitHub \u2014 if you use this library in your applications, please consider offering me a \ud83c\udf7a from time to time! \ud83e\udd17
"},{"location":"project/changelog/version-1.0.0/#notable-changes","title":"Notable changes","text":"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 {\n return \"$foo / $bar\";\n};\n\ntry {\n $arguments = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->argumentsMapper()\n ->mapArguments($someFunction, [\n 'foo' => 'some value',\n 'bar' => 42,\n ]);\n\n // some value / 42\n echo $someFunction(...$arguments);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Do something\u2026\n}\n
Support for TimeZone
objects
Native TimeZone
objects construction is now supported with a proper error handling.
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(DateTimeZone::class, 'Jupiter/Europa');\n} catch (MappingError $exception) {\n $error = $exception->node()->messages()[0];\n\n // Value 'Jupiter/Europa' is not a valid timezone.\n echo $error->toString();\n}\n
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\n{\n public readonly string $value;\n}\n\nfinal class SomeClass\n{\n public readonly Identifier $identifier;\n\n public readonly string $description;\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(SomeClass::class, [\n 'identifier' => ['value' => 'some-identifier'], // \u274c\n 'description' => 'Lorem ipsum\u2026',\n]);\n\n(new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(SomeClass::class, [\n 'identifier' => 'some-identifier', // \u2705\n 'description' => 'Lorem ipsum\u2026',\n]);\n
"},{"location":"project/changelog/version-1.0.0/#upgrading-from-0x-to-10","title":"Upgrading from 0.x to 1.0","text":"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.
Doctrine annotations support removalDoctrine annotations cannot be used anymore, PHP attributes must be used.
BackwardCompatibilityDateTimeConstructor
class removal You must use the method available in the mapper builder, see dealing with dates chapter.
Mapper builderflexible
method removal The flexible has been split in three disctint modes, see type strictness & flexibility chapter.
Mapper builderwithCacheDir
method removal You must now register a cache instance directly, see performance & caching chapter.
StaticMethodConstructor
class removal You must now register the constructors using the mapper builder, see custom object constructors chapter.
Mapper builderbind
method removal You must now register the constructors using the mapper builder, see custom object constructors chapter.
ThrowableMessage
class removal You must now use the MessageBuilder
class, see error handling chapter.
MessagesFlattener
class removal You must now use the Messages
class, see error handling chapter.
TranslatableMessage
class removal You must now use the HasParameters
class, see custom exception chapter.
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}
.
PlaceHolderMessageFormatter
class removal Other features are available to format message, see error messages customization chapter.
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.
"},{"location":"project/changelog/version-1.0.0/#breaking-changes","title":"\u26a0 BREAKING CHANGES","text":"@pure
(0d9855)ThrowableMessage
(d36ca9)TranslatableMessage
(ceb197)strict-array
type (22c3b4)DateTimeZone
with error support (a0a4d6)null
to single node nullable type (0a98ec)psr/simple-cache
supported version (e4059a)@
from comments for future PHP versions changes (68774c)See release on GitHub
"},{"location":"project/changelog/version-1.1.0/#notable-changes","title":"Notable changes","text":"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.
/**\n * @template FirstTemplate\n * @template SecondTemplate\n */\nabstract class FirstClassWithGenerics\n{\n /** @var FirstTemplate */\n public $valueA;\n\n /** @var SecondTemplate */\n public $valueB;\n}\n\n/**\n * @template FirstTemplate\n * @extends FirstClassWithGenerics<FirstTemplate, int>\n */\nabstract class SecondClassWithGenerics extends FirstClassWithGenerics\n{\n /** @var FirstTemplate */\n public $valueC;\n}\n\n/**\n * @extends SecondClassWithGenerics<string>\n */\nfinal class ChildClass extends SecondClassWithGenerics\n{\n}\n\n$object = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(ChildClass::class, [\n 'valueA' => 'foo',\n 'valueB' => 1337,\n 'valueC' => 'bar',\n ]);\n\necho $object->valueA; // 'foo'\necho $object->valueB; // 1337\necho $object->valueC; // 'bar'\n
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\n{\n public string $foo;\n\n public string $bar;\n}\n\nfinal class SomeChildClass extends SomeAbstractClass\n{\n public string $baz;\n}\n\n$result = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->infer(\n SomeAbstractClass::class,\n fn () => SomeChildClass::class\n )\n ->mapper()\n ->map(SomeAbstractClass::class, [\n 'foo' => 'foo',\n 'bar' => 'bar',\n 'baz' => 'baz',\n ]);\n\nassert($result instanceof SomeChildClass);\nassert($result->foo === 'foo');\nassert($result->bar === 'bar');\nassert($result->baz === 'baz');\n
"},{"location":"project/changelog/version-1.1.0/#features","title":"Features","text":"object
return type in PHPStan extension (201728)isAbstract
flag in class definition (ad0c06)isFinal
flag in class definition (25da31)TreeMapper::map()
return type signature (dc32d3)TreeMapper
(c8f362)See release on GitHub
"},{"location":"project/changelog/version-1.2.0/#notable-changes","title":"Notable changes","text":"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
"},{"location":"project/changelog/version-1.2.0/#features","title":"Features","text":"See release on GitHub
"},{"location":"project/changelog/version-1.3.0/#notable-changes","title":"Notable changes","text":"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())\n ->registerConstructor(\n // Allow the native constructor to be used\n SomeEnum::class,\n\n // Register a named constructor\n SomeEnum::fromMatrix(...)\n )\n ->mapper()\n ->map(SomeEnum::class, [\n 'type' => 'FOO',\n 'number' => 'BAR',\n ]);\n\nenum SomeEnum: string\n{\n case CASE_A = 'FOO_VALUE_1';\n case CASE_B = 'FOO_VALUE_2';\n case CASE_C = 'BAR_VALUE_1';\n case CASE_D = 'BAR_VALUE_2';\n\n /**\n * @param 'FOO'|'BAR' $type\n * @param int<1, 2> $number\n * /\n public static function fromMatrix(string $type, int $number): self\n {\n return self::from(\"{$type}_VALUE_{$number}\");\n }\n}\n
An enum constructor can be for a specific pattern:
enum SomeEnum\n{\n case FOO;\n case BAR;\n case BAZ;\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerConstructor(\n /**\n * This constructor will be called only when pattern\n * `SomeEnum::BA*` is requested during mapping.\n *\n * @return SomeEnum::BA*\n */\n fn (string $value): SomeEnum => /* Some custom domain logic */\n )\n ->mapper()\n ->map(SomeEnum::class . '::BA*', 'some custom value');\n
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.
"},{"location":"project/changelog/version-1.3.0/#features","title":"Features","text":"See release on GitHub
Bugfix release.
"},{"location":"project/changelog/version-1.3.1/#bug-fixes","title":"Bug Fixes","text":"null
and objects (8f03a7)See release on GitHub
"},{"location":"project/changelog/version-1.4.0/#notable-changes","title":"Notable changes","text":"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 {\n $source = \\CuyZ\\Valinor\\Mapper\\Source\\Source::json('invalid JSON');\n} catch (\\CuyZ\\Valinor\\Mapper\\Source\\Exception\\InvalidSource $error) {\n // Let the application handle the exception in the desired way.\n // It is possible to get the original source with `$error->source()`\n}\n
"},{"location":"project/changelog/version-1.4.0/#features","title":"Features","text":"InvalidSource
thrown when using invalid JSON/YAML (0739d1)array-key
type match mixed
(ccebf7)See release on GitHub
"},{"location":"project/changelog/version-1.5.0/#features","title":"Features","text":"UnresolvableType
(eaa128)UnresolvableType
(5c89c6)unserialize
when caching NULL
default values (5e9b4c)json_encode
exception to help identifying parsing errors (861c3b)See release on GitHub
"},{"location":"project/changelog/version-1.6.0/#notable-changes","title":"Notable changes","text":"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 \u2014 mostly concerning quoted strings \u2014 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\n(new MapperBuilder())\n ->mapper()\n ->map(\n \"'foo bar'|'baz fiz'\",\n 'baz fiz'\n );\n\n// Shaped array with special chars in the key\n(new MapperBuilder())\n ->mapper()\n ->map(\n \"array{'some & key': string}\",\n ['some & key' => 'value']\n );\n
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']);\n\n$mapper->map('array<42|1337, string>', [42 => 'foo']);\n\n$mapper->map('array<positive-int, string>', [42 => 'foo']);\n\n$mapper->map('array<negative-int, string>', [-42 => 'foo']);\n\n$mapper->map('array<int<-42, 1337>, string>', [42 => 'foo']);\n\n$mapper->map('array<non-empty-string, string>', ['foo' => 'foo']);\n\n$mapper->map('array<class-string, string>', ['SomeClass' => 'foo']);\n
"},{"location":"project/changelog/version-1.6.0/#features","title":"Features","text":"intl.use_exceptions=1
(29da9a)See release on GitHub
"},{"location":"project/changelog/version-1.6.1/#bug-fixes","title":"Bug Fixes","text":"See release on GitHub
"},{"location":"project/changelog/version-1.7.0/#notable-changes","title":"Notable changes","text":"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\n{\n /** @var non-positive-int */\n public int $nonPositiveInteger;\n}\n
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\n{\n /** @var non-negative-int */\n public int $nonNegativeInteger;\n}\n
"},{"location":"project/changelog/version-1.7.0/#features","title":"Features","text":"@psalm-pure
annotation to pure methods (004eb1)NativeBooleanType
a BooleanType
(d57ffa)See release on GitHub
"},{"location":"project/changelog/version-1.8.0/#notable-changes","title":"Notable changes","text":"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;\n\n$normalizer = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array());\n\n$userAsArray = $normalizer->normalize(\n new \\My\\App\\User(\n name: 'John Doe',\n age: 42,\n country: new \\My\\App\\Country(\n name: 'France',\n countryCode: 'FR',\n ),\n )\n);\n\n// `$userAsArray` is now an array and can be manipulated much more\n// easily, for instance to be serialized to the wanted data format.\n//\n// [\n// 'name' => 'John Doe',\n// 'age' => 42,\n// 'country' => [\n// 'name' => 'France',\n// 'countryCode' => 'FR',\n// ],\n// ];\n
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())\n ->registerTransformer(\n fn (\\DateTimeInterface $date) => $date->format('Y/m/d')\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
This date transformer could have been an attribute for a more granular control, as shown below.
namespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class DateTimeFormat\n{\n public function __construct(private string $format) {}\n\n public function normalize(\\DateTimeInterface $date): string\n {\n return $date->format($this->format);\n }\n}\n\nfinal readonly class Event\n{\n public function __construct(\n public string $eventName,\n #[\\My\\App\\DateTimeFormat('Y/m/d')]\n public \\DateTimeInterface $date,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\DateTimeFormat::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
More features are available, details about it can be found in the documentation.
"},{"location":"project/changelog/version-1.8.0/#features","title":"Features","text":"See release on GitHub
"},{"location":"project/changelog/version-1.8.1/#bug-fixes","title":"Bug Fixes","text":"See release on GitHub
"},{"location":"project/changelog/version-1.8.2/#bug-fixes","title":"Bug Fixes","text":"Instead of providing transformers out-of-the-box, this library focuses on easing the creation of custom ones. This way, the normalizer is not tied up to a third-party library release-cycle and can be adapted to fit the needs of the application's business logics.
Below is a list of common features that can inspire or be implemented by third-party libraries or applications.
Info
These examples are not available out-of-the-box, they can be implemented using the library's API and should be adapted to fit the needs of the application.
By default, dates will be formatted using the RFC 3339 format, but it may be needed to use another format.
This can be done on all dates, using a global transformer, as shown in the example below:
Show code example \u2014 Global date format(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\n fn (\\DateTimeInterface $date) => $date->format('Y/m/d')\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
For a more granular control, an attribute can be used to target a specific property, as shown in the example below:
Show code example \u2014 Date format attributenamespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class DateTimeFormat\n{\n public function __construct(private string $format) {}\n\n public function normalize(\\DateTimeInterface $date): string\n {\n return $date->format($this->format);\n }\n}\n\nfinal readonly class Event\n{\n public function __construct(\n public string $eventName,\n #[\\My\\App\\DateTimeFormat('Y/m/d')]\n public \\DateTimeInterface $date,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\DateTimeFormat::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
"},{"location":"serialization/common-examples/#transforming-property-name-to-snake_case","title":"Transforming property name to \u201csnake_case\u201d","text":"Depending on the conventions of the data format, it may be necessary to transform the case of the keys, for instance from \u201ccamelCase\u201d to \u201csnake_case\u201d.
If this transformation is needed on every object, it can be done globally by using a global transformer, as shown in the example below:
Show code example \u2014 global \u201csnake_case\u201d propertiesnamespace My\\App;\n\nfinal class CamelToSnakeCaseTransformer\n{\n public function __invoke(object $object, callable $next): mixed\n {\n $result = $next();\n\n if (! is_array($result)) {\n return $result;\n }\n\n $snakeCased = [];\n\n foreach ($result as $key => $value) {\n $newKey = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($key)));\n\n $snakeCased[$newKey] = $value;\n }\n\n return $snakeCased;\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(new \\My\\App\\CamelToSnakeCaseTransformer())\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\User(\n name: 'John Doe',\n emailAddress: 'john.doe@example.com', \n age: 42,\n country: new Country(\n name: 'France',\n countryCode: 'FR',\n ),\n )\n );\n\n// [\n// 'name' => 'John Doe',\n// 'email_address' => 'john.doe@example', // snake_case\n// 'age' => 42,\n// 'country' => [\n// 'name' => 'France',\n// 'country_code' => 'FR', // snake_case\n// ],\n// ]\n
For a more granular control, an attribute can be used to target specific objects, as shown in the example below:
Show code example \u2014 \u201csnake_case\u201d attributenamespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_CLASS)]\nfinal class SnakeCaseProperties\n{\n public function normalize(object $object, callable $next): array\n {\n $result = $next();\n\n if (! is_array($result)) {\n return $result;\n }\n\n $snakeCased = [];\n\n foreach ($result as $key => $value) {\n $newKey = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($key)));\n\n $snakeCased[$newKey] = $value;\n }\n\n return $snakeCased;\n }\n}\n\n#[SnakeCaseProperties]\nfinal readonly class Country\n{\n public function __construct(\n public string $name,\n public string $countryCode,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\SnakeCaseProperties::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\User(\n name: 'John Doe',\n emailAddress: 'john.doe@example.com',\n age: 42,\n country: new Country(\n name: 'France',\n countryCode: 'FR',\n ),\n )\n );\n\n// [\n// 'name' => 'John Doe',\n// 'emailAddress' => 'john.doe@example', // camelCase\n// 'age' => 42,\n// 'country' => [\n// 'name' => 'France',\n// 'country_code' => 'FR', // snake_case\n// ],\n// ]\n
"},{"location":"serialization/common-examples/#ignoring-properties","title":"Ignoring properties","text":"Some objects might want to omit some properties during normalization, for instance, to hide sensitive data.
In the example below, an attribute is added on a property that will replace the value with a custom object that is afterward removed by a global transformer.
Show code example \u2014 Ignore property attributenamespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class Ignore\n{\n public function normalize(mixed $value): IgnoredValue\n {\n return new \\My\\App\\IgnoredValue();\n }\n}\n\nfinal class IgnoredValue\n{\n public function __construct() {}\n}\n\nfinal readonly class User\n{\n public function __construct(\n public string $name,\n #[\\My\\App\\Ignore]\n public string $password,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\Ignore::class)\n ->registerTransformer(\n fn (object $value, callable $next) => array_filter(\n $next(),\n fn (mixed $value) => ! $value instanceof \\My\\App\\IgnoredValue,\n ),\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(new \\My\\App\\User(\n name: 'John Doe',\n password: 's3cr3t-p4$$w0rd')\n );\n\n// ['name' => 'John Doe']\n
"},{"location":"serialization/common-examples/#renaming-properties","title":"Renaming properties","text":"Properties' names can differ between the object and the data format.
In the example below, an attribute is added on properties that need to be renamed during normalization
Show code example \u2014 Rename property attributenamespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class Rename\n{\n public function __construct(private string $name) {}\n\n public function normalizeKey(): string\n {\n return $this->name;\n }\n}\n\nfinal readonly class Address\n{\n public function __construct(\n public string $street,\n public string $zipCode,\n #[\\My\\App\\Rename('town')]\n public string $city,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\\My\\App\\Rename::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new Address(\n street: '221B Baker Street', \n zipCode: 'NW1 6XE', \n city: 'London', \n )\n );\n\n// [\n// 'street' => '221B Baker Street',\n// 'zipCode' => 'NW1 6XE',\n// 'town' => 'London',\n// ]\n
"},{"location":"serialization/common-examples/#transforming-objects","title":"Transforming objects","text":"Some objects can have custom behaviors during normalization, for instance properties may need to be remapped. In the example below, a transformer will check if an object defines a normalize
method and use it if it exists.
namespace My\\App;\n\nfinal readonly class Address\n{\n public function __construct(\n public string $road,\n public string $zipCode,\n public string $town,\n ) {}\n\n public function normalize(): array\n {\n return [\n 'street' => $this->road,\n 'postalCode' => $this->zipCode,\n 'city' => $this->town,\n ];\n }\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(function (object $object, callable $next) {\n return method_exists($object, 'normalize')\n ? $object->normalize()\n : $next();\n })\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Address(\n road: '221B Baker Street',\n zipCode: 'NW1 6XE',\n town: 'London',\n ),\n );\n\n// [\n// 'street' => '221B Baker Street',\n// 'postalCode' => 'NW1 6XE',\n// 'city' => 'London',\n// ]\n
"},{"location":"serialization/common-examples/#versioning-api","title":"Versioning API","text":"API versioning can be implemented with different strategies and algorithms. The example below shows how objects can implement an interface to specify their own specific versioning behavior.
Show code example \u2014 Versioning objectsnamespace My\\App;\n\ninterface HasVersionedNormalization\n{\n public function normalizeWithVersion(string $version): mixed;\n}\n\nfinal readonly class Address implements \\My\\App\\HasVersionedNormalization\n{\n public function __construct(\n public string $streetNumber,\n public string $streetName,\n public string $zipCode,\n public string $city,\n ) {}\n\n public function normalizeWithVersion(string $version): array\n {\n return match (true) {\n version_compare($version, '1.0.0', '<') => [\n // Street number and name are merged in a single property\n 'street' => \"$this->streetNumber, $this->streetName\",\n 'zipCode' => $this->zipCode,\n 'city' => $this->city,\n ],\n default => get_object_vars($this),\n };\n }\n}\n\nfunction normalizeWithVersion(string $version): mixed\n{\n return (new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\n fn (\\My\\App\\HasVersionedNormalization $object) => $object->normalizeWithVersion($version)\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Address(\n streetNumber: '221B',\n streetName: 'Baker Street',\n zipCode: 'NW1 6XE',\n city: 'London',\n )\n );\n}\n\n// Version can come for instance from HTTP request headers\n$result_v0_4 = normalizeWithVersion('0.4');\n$result_v1_8 = normalizeWithVersion('1.8');\n\n// $result_v0_4 === [\n// 'street' => '221B, Baker Street',\n// 'zipCode' => 'NW1 6XE',\n// 'city' => 'London',\n// ]\n// \n// $result_v1_8 === [\n// 'streetNumber' => '221B',\n// 'streetName' => 'Baker Street',\n// 'zipCode' => 'NW1 6XE',\n// 'city' => 'London',\n// ]\n
"},{"location":"serialization/normalizer/","title":"Normalizing data","text":"A normalizer is a service that transforms 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.
Info
The library only supports normalizing to arrays, but aims to support other formats like JSON or CSV in the future.
In the meantime, native functions like json_encode()
may be used as an alternative.
namespace My\\App;\n\n$normalizer = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array());\n\n$userAsArray = $normalizer->normalize(\n new \\My\\App\\User(\n name: 'John Doe',\n age: 42,\n country: new \\My\\App\\Country(\n name: 'France',\n countryCode: 'FR',\n ),\n )\n);\n\n// `$userAsArray` is now an array and can be manipulated much more easily, for\n// instance to be serialized to the wanted data format.\n//\n// [\n// 'name' => 'John Doe',\n// 'age' => 42,\n// 'country' => [\n// 'name' => 'France',\n// 'countryCode' => 'FR',\n// ],\n// ];\n
"},{"location":"serialization/normalizer/#extending-the-normalizer","title":"Extending the normalizer","text":"This library provides a normalizer out-of-the-box that can be used as-is, or extended to add custom logic. To do so, transformers must be registered within the MapperBuilder
.
A transformer can be a callable (function, closure or a class implementing the __invoke()
method), or an attribute that can target a class or a property.
Note
You can find common examples of transformers in the next chapter.
"},{"location":"serialization/normalizer/#callable-transformers","title":"Callable transformers","text":"A callable transformer must declare at least one argument, for which the type will determine when it is used during normalization. In the example below, a global transformer is used to format any date found by the normalizer.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(\n fn (\\DateTimeInterface $date) => $date->format('Y/m/d')\n )\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Event(\n eventName: 'Release of legendary album',\n date: new \\DateTimeImmutable('1971-11-08'),\n )\n );\n\n// [\n// 'eventName' => 'Release of legendary album',\n// 'date' => '1971/11/08',\n// ]\n
Transformers can be chained. To do so, a second parameter of type callable
must be declared in a transformer. This parameter \u2014 named $next
by convention \u2014 can be used whenever needed in the transformer logic.
(new \\CuyZ\\Valinor\\MapperBuilder())\n\n // The type of the first parameter of the transformer will determine when it\n // is used during normalization.\n ->registerTransformer(\n fn (string $value, callable $next) => strtoupper($next())\n )\n\n // Transformers can be chained, the last registered one will take precedence\n // over the previous ones, which can be called using the `$next` parameter.\n ->registerTransformer(\n /**\n * Advanced type annotations like `non-empty-string` can be used to\n * target a more specific type.\n * \n * @param non-empty-string $value \n */\n fn (string $value, callable $next) => $next() . '!'\n )\n\n // A priority can be given to a transformer, to make sure it is called\n // before or after another one. The higher priority, the sooner the\n // transformer will be called. The default priority is 0.\n ->registerTransformer(\n /**\n * @param non-empty-string $value \n */\n fn (string $value, callable $next) => $next() . '?',\n priority: 100\n )\n\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize('Hello world'); // HELLO WORLD!?\n
"},{"location":"serialization/normalizer/#attribute-transformers","title":"Attribute transformers","text":"Callable transformers allow targeting any value during normalization, whereas attribute transformers allow targeting a specific class or property for a more granular control.
To be detected by the normalizer, an attribute must be registered first by giving its class name to the registerTransformer
method.
Tip
It is possible to register attributes that share a common interface by giving the interface name to the method.
namespace My\\App;\n\ninterface SomeAttributeInterface {}\n\n#[\\Attribute]\nfinal class SomeAttribute implements \\My\\App\\SomeAttributeInterface {}\n\n#[\\Attribute]\nfinal class SomeOtherAttribute implements \\My\\App\\SomeAttributeInterface {}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n // Registers both `SomeAttribute` and `SomeOtherAttribute` attributes\n ->registerTransformer(\\My\\App\\SomeAttributeInterface::class)\n \u2026\n
Attributes must declare a method named normalize
that follows the same rules as callable transformers: a mandatory first parameter and an optional second callable
parameter.
namespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class Uppercase\n{\n public function normalize(string $value, callable $next): string\n {\n return strtoupper($next());\n }\n}\n\nfinal readonly class City\n{\n public function __construct(\n public string $zipCode,\n #[Uppercase]\n public string $name,\n #[Uppercase]\n public string $country,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(Uppercase::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\City(\n zipCode: 'NW1 6XE',\n name: 'London',\n country: 'United Kingdom',\n ) \n );\n\n// [\n// 'zipCode' => 'NW1 6XE',\n// 'name' => 'LONDON',\n// 'country' => 'UNITED KINGDOM',\n// ]\n
If an attribute needs to transform the key of a property, it needs to declare a method named normalizeKey
.
namespace My\\App;\n\n#[\\Attribute(\\Attribute::TARGET_PROPERTY)]\nfinal class PrefixedWith\n{\n public function __construct(private string $prefix) {}\n\n public function normalizeKey(string $value): string\n {\n return $this->prefix . $value;\n }\n}\n\nfinal readonly class Address\n{\n public function __construct(\n #[\\My\\App\\PrefixedWith('address_')]\n public string $road,\n #[\\My\\App\\PrefixedWith('address_')]\n public string $zipCode,\n #[\\My\\App\\PrefixedWith('address_')]\n public string $city,\n ) {}\n}\n\n(new \\CuyZ\\Valinor\\MapperBuilder())\n ->registerTransformer(PrefixedWith::class)\n ->normalizer(\\CuyZ\\Valinor\\Normalizer\\Format::array())\n ->normalize(\n new \\My\\App\\Address(\n road: '221B Baker Street',\n zipCode: 'NW1 6XE',\n city: 'London',\n ) \n );\n\n// [\n// 'address_road' => '221B Baker Street',\n// 'address_zipCode' => 'NW1 6XE',\n// 'address_city' => 'London',\n// ]\n
"},{"location":"usage/object-construction/","title":"Object construction","text":"During the mapping, instances of objects are recursively created and hydrated with values coming from the input.
The values of an object are filled either with a constructor \u2014 which is the recommended way \u2014 or using the class properties. If a constructor exists, it will be used to create the object, otherwise the properties will be filled directly.
By default, the library will use a native constructor of a class if it is public; for advanced use cases, the library also allows the usage of custom constructors.
"},{"location":"usage/object-construction/#class-with-a-single-value","title":"Class with a single value","text":"When an object needs only one value (one constructor argument or one property), the source given to the mapper can match the type of the value \u2014 it does not need to be an array with one value with a key matching the argument/property name.
This can be useful when the application has control over the format of the source given to the mapper, in order to lessen the structure of input.
final class Identifier\n{\n public readonly string $value;\n}\n\nfinal class SomeClass\n{\n public readonly Identifier $identifier;\n\n public readonly string $description;\n}\n\n$mapper = (new \\CuyZ\\Valinor\\MapperBuilder())->mapper();\n\n$mapper->map(SomeClass::class, [\n 'identifier' => [\n // \ud83d\udc4e The `value` key feels a bit excessive\n 'value' => 'some-identifier'\n ],\n 'description' => 'Lorem ipsum\u2026',\n]); \n\n$mapper->map(SomeClass::class, [\n // \ud83d\udc4d The input has been flattened and is easier to read\n 'identifier' => 'some-identifier',\n 'description' => 'Lorem ipsum\u2026',\n]);\n
"},{"location":"usage/type-reference/","title":"Type reference","text":"To prevent conflicts or duplication of the type annotations, this library tries to handle most of the type annotations that are accepted by PHPStan and Psalm.
"},{"location":"usage/type-reference/#scalar","title":"Scalar","text":"final class SomeClass\n{\n public function __construct(\n private bool $boolean,\n\n private float $float,\n\n private int $integer,\n\n /** @var positive-int */\n private int $positiveInteger,\n\n /** @var negative-int */\n private int $negativeInteger,\n\n /** @var non-positive-int */\n private int $nonPositiveInteger,\n\n /** @var non-negative-int */\n private int $nonNegativeInteger,\n\n /** @var int<-42, 1337> */\n private int $integerRange,\n\n /** @var int<min, 0> */\n private int $integerRangeWithMinRange,\n\n /** @var int<0, max> */\n private int $integerRangeWithMaxRange,\n\n private string $string,\n\n /** @var non-empty-string */\n private string $nonEmptyString,\n\n /** @var numeric-string */\n private string $numericString,\n\n /** @var class-string */\n private string $classString,\n\n /** @var class-string<SomeInterface> */\n private string $classStringOfAnInterface,\n ) {}\n}\n
"},{"location":"usage/type-reference/#object","title":"Object","text":"final class SomeClass\n{\n public function __construct(\n private SomeClass $class,\n\n private DateTimeInterface $interface,\n\n /** @var SomeInterface&AnotherInterface */\n private object $intersection,\n\n /** @var SomeCollection<SomeClass> */\n private SomeCollection $classWithGeneric,\n ) {}\n}\n\n/**\n * @template T of object \n */\nfinal class SomeCollection\n{\n public function __construct(\n /** @var array<T> */\n private array $objects,\n ) {}\n}\n
"},{"location":"usage/type-reference/#array-lists","title":"Array & lists","text":"final class SomeClass\n{\n public function __construct(\n /** @var string[] */\n private array $simpleArray,\n\n /** @var array<string> */\n private array $arrayOfStrings,\n\n /** @var array<string, SomeClass> */\n private array $arrayOfClassWithStringKeys,\n\n /** @var array<int, SomeClass> */\n private array $arrayOfClassWithIntegerKeys,\n\n /** @var array<non-empty-string, string> */\n private array $arrayOfClassWithNonEmptyStringKeys,\n\n /** @var array<'foo'|'bar', string> */\n private array $arrayOfClassWithStringValueKeys,\n\n /** @var array<42|1337, string> */\n private array $arrayOfClassWithIntegerValueKeys,\n\n /** @var array<positive-int, string> */\n private array $arrayOfClassWithPositiveIntegerValueKeys,\n\n /** @var non-empty-array<string> */\n private array $nonEmptyArrayOfStrings,\n\n /** @var non-empty-array<string, SomeClass> */\n private array $nonEmptyArrayWithStringKeys,\n\n /** @var list<string> */\n private array $listOfStrings,\n\n /** @var non-empty-list<string> */\n private array $nonEmptyListOfStrings,\n\n /** @var array{foo: string, bar: int} */\n private array $shapedArray,\n\n /** @var array{foo: string, bar?: int} */\n private array $shapedArrayWithOptionalElement,\n\n /** @var array{string, bar: int} */\n private array $shapedArrayWithUndefinedKey,\n ) {}\n}\n
"},{"location":"usage/type-reference/#union","title":"Union","text":"final class SomeClass\n{\n public function __construct(\n private int|string $simpleUnion,\n\n /** @var class-string<SomeInterface|AnotherInterface> */\n private string $unionOfClassString,\n\n /** @var array<SomeInterface|AnotherInterface> */\n private array $unionInsideArray,\n\n /** @var int|true */\n private int|bool $unionWithLiteralTrueType;\n\n /** @var int|false */\n private int|bool $unionWithLiteralFalseType;\n\n /** @var 404.42|1337.42 */\n private float $unionOfFloatValues,\n\n /** @var 42|1337 */\n private int $unionOfIntegerValues,\n\n /** @var 'foo'|'bar' */\n private string $unionOfStringValues,\n ) {}\n}\n
"},{"location":"usage/type-reference/#class-constants","title":"Class constants","text":"final class SomeClassWithConstants\n{\n public const FOO = 1337;\n\n public const BAR = 'bar';\n\n public const BAZ = 'baz';\n}\n\nfinal class SomeClass\n{\n public function __construct(\n /** @var SomeClassWithConstants::FOO|SomeClassWithConstants::BAR */\n private int|string $oneOfTwoCasesOfConstants,\n\n /** @param SomeClassWithConstants::BA* (matches `bar` or `baz`) */\n private string $casesOfConstantsMatchingPattern,\n ) {}\n}\n
"},{"location":"usage/type-reference/#enums","title":"Enums","text":"enum SomeEnum\n{\n case FOO;\n case BAR;\n case BAZ;\n}\n\nfinal class SomeClass\n{\n public function __construct(\n private SomeEnum $enum,\n\n /** @var SomeEnum::FOO|SomeEnum::BAR */\n private SomeEnum $oneOfTwoCasesOfEnum,\n\n /** @var SomeEnum::BA* (matches BAR or BAZ) */\n private SomeEnum $casesOfEnumMatchingPattern,\n ) {}\n}\n
"},{"location":"usage/type-strictness-and-flexibility/","title":"Type strictness & flexibility","text":"The mapper is sensitive to the types of the data that is recursively populated \u2014 for instance a string \"42\"
given to a node that expects an integer will make the mapping fail because the type is not strictly respected.
Array keys that are not bound to any node are forbidden. Mapping an array ['foo' => \u2026, 'bar' => \u2026, 'baz' => \u2026]
to an object that needs only foo
and bar
will fail, because baz
is superfluous. The same rule applies for shaped arrays.
When mapping to a list, the given array must have sequential integer keys starting at 0; if any gap or invalid key is found it will fail, like for instance trying to map ['foo' => 'foo', 'bar' => 'bar']
to list<string>
.
Types that are too permissive are not permitted \u2014 if the mapper encounters a type like mixed
, object
or array
it will fail because those types are not precise enough.
If these limitations are too restrictive, the mapper can be made more flexible to disable one or several rule(s) declared above.
"},{"location":"usage/type-strictness-and-flexibility/#enabling-flexible-casting","title":"Enabling flexible casting","text":"This setting changes the behaviours explained below:
$flexibleMapper = (new \\CuyZ\\Valinor\\MapperBuilder())\n ->enableFlexibleCasting()\n ->mapper();\n\n// ---\n// Scalar types will accept non-strict values; for instance an integer\n// type will accept any valid numeric value like the *string* \"42\".\n\n$flexibleMapper->map('int', '42');\n// => 42\n\n// ---\n// List type will accept non-incremental keys.\n\n$flexibleMapper->map('list<int>', ['foo' => 42, 'bar' => 1337]);\n// => [0 => 42, 1 => 1338]\n\n// ---\n// If a value is missing in a source for a node that accepts `null`, the\n// node will be filled with `null`.\n\n$flexibleMapper->map(\n 'array{foo: string, bar: null|string}',\n ['foo' => 'foo'] // `bar` is missing\n);\n// => ['foo' => 'foo', 'bar' => null]\n\n// ---\n// Array and list types will convert `null` or missing values to an empty\n// array.\n\n$flexibleMapper->map(\n 'array{foo: string, bar: array<string>}',\n ['foo' => 'foo'] // `bar` is missing\n);\n// => ['foo' => 'foo', 'bar' => []]\n
"},{"location":"usage/type-strictness-and-flexibility/#allowing-superfluous-keys","title":"Allowing superfluous keys","text":"With this setting enabled, 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())\n ->allowSuperfluousKeys()\n ->mapper()\n ->map(\n 'array{foo: string, bar: int}',\n [\n 'foo' => 'foo',\n 'bar' => 42,\n 'baz' => 1337.404, // `baz` will be ignored\n ]\n );\n
"},{"location":"usage/type-strictness-and-flexibility/#allowing-permissive-types","title":"Allowing permissive types","text":"This setting allows permissive types mixed
and object
to be used during mapping.
(new \\CuyZ\\Valinor\\MapperBuilder())\n ->allowPermissiveTypes()\n ->mapper()\n ->map(\n 'array{foo: string, bar: mixed}',\n [\n 'foo' => 'foo',\n 'bar' => 42, // Could be any value\n ]\n );\n
"},{"location":"usage/validation-and-error-handling/","title":"Validation and error handling","text":"The source given to a mapper can never be trusted, this is actually the very goal of this library: transforming an unstructured input to a well-defined object structure. If a value has an invalid type, or if the mapper cannot cast it properly (in flexible mode), it means that it is not able to guarantee the validity of the desired object thus it will fail.
Any issue encountered during the mapping will add an error to an upstream exception of type \\CuyZ\\Valinor\\Mapper\\MappingError
. It is therefore always recommended wrapping the mapping function call with a try/catch statement and handle the error properly.
When the mapping fails, the exception gives access to the root node. This recursive object allows retrieving all needed information through the whole mapping tree: path, values, types and messages, including the issues that caused the exception.
Node messages can be customized and iterated through with the usage of the class \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Messages
.
try {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->mapper()\n ->map(SomeClass::class, [/* \u2026 */ ]);\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $error) {\n // Get flatten list of all messages through the whole nodes tree\n $messages = \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Messages::flattenFromNode(\n $error->node()\n );\n\n // Formatters can be added and will be applied on all messages\n $messages = $messages->formatWith(\n new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\MessageMapFormatter([\n // \u2026\n ]),\n (new \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\Formatter\\TranslationMessageFormatter())\n ->withTranslations([\n // \u2026\n ])\n );\n\n // If only errors are wanted, they can be filtered\n $errorMessages = $messages->errors();\n\n foreach ($errorMessages as $message) {\n echo $message;\n }\n}\n
"},{"location":"usage/validation-and-error-handling/#custom-exception-messages","title":"Custom exception messages","text":"More specific validation should be done in the constructor of the object, by throwing an exception if something is wrong with the given data.
For security reasons, exceptions thrown in a constructor will not be caught by the mapper, unless one of the three options below is used.
"},{"location":"usage/validation-and-error-handling/#1-custom-exception-classes","title":"1. Custom exception classes","text":"An exception that implements \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\ErrorMessage
can be thrown. The body can contain placeholders, see message customization chapter for more information.
If more parameters can be provided, the exception can also implement the interface \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\HasParameters
that returns a list of string values, using keys as parameters names.
To help identifying an error, a unique code can be provided by implementing the interface CuyZ\\Valinor\\Mapper\\Tree\\Message\\HasCode
.
final class SomeClass\n{\n public function __construct(private string $value)\n {\n if ($this->value === 'foo') {\n throw new SomeException('some custom parameter');\n }\n }\n}\n\nuse CuyZ\\Valinor\\Mapper\\Tree\\Message\\ErrorMessage;\nuse CuyZ\\Valinor\\Mapper\\Tree\\Message\\HasCode;\nuse CuyZ\\Valinor\\Mapper\\Tree\\Message\\HasParameters;\n\nfinal class SomeException extends DomainException implements ErrorMessage, HasParameters, HasCode\n{\n private string $someParameter;\n\n public function __construct(string $someParameter)\n {\n parent::__construct();\n\n $this->someParameter = $someParameter;\n }\n\n public function body() : string\n {\n return 'Some custom message / {some_parameter} / {source_value}';\n }\n\n public function parameters(): array\n {\n return [\n 'some_parameter' => $this->someParameter,\n ];\n }\n\n public function code() : string\n {\n // A unique code that can help to identify the error\n return 'some_unique_code';\n }\n}\n\ntry {\n (new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(SomeClass::class, 'foo');\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $exception) {\n // Should print:\n // Some custom message / some custom parameter / 'foo'\n echo $exception->node()->messages()[0];\n}\n
"},{"location":"usage/validation-and-error-handling/#2-use-provided-message-builder","title":"2. Use provided message builder","text":"The utility class \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessageBuilder
can be used to build a message.
final class SomeClass\n{\n public function __construct(private string $value)\n {\n if (str_starts_with($this->value, 'foo_')) {\n throw \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessageBuilder::newError(\n 'Some custom error message: {value}.'\n )\n ->withCode('some_code')\n ->withParameter('value', $this->value)\n ->build();\n }\n }\n}\n\ntry {\n (new \\CuyZ\\Valinor\\MapperBuilder())->mapper()->map(\n SomeClass::class, 'foo_bar'\n );\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $exception) {\n // Should print:\n // > Some custom error message: foo_bar.\n echo $exception->node()->messages()[0];\n}\n
"},{"location":"usage/validation-and-error-handling/#3-allow-third-party-exceptions","title":"3. Allow third party exceptions","text":"It is possible to set up a list of exceptions that can be caught by the mapper, for instance when using lightweight validation tools like Webmozart Assert.
It is advised to use this feature with caution: userland exceptions may contain sensible information \u2014 for instance an SQL exception showing a part of a query should never be allowed. Therefore, only an exhaustive list of carefully chosen exceptions should be filtered.
final class SomeClass\n{\n public function __construct(private string $value)\n {\n \\Webmozart\\Assert\\Assert::startsWith($value, 'foo_');\n }\n}\n\ntry {\n (new \\CuyZ\\Valinor\\MapperBuilder())\n ->filterExceptions(function (Throwable $exception) {\n if ($exception instanceof \\Webmozart\\Assert\\InvalidArgumentException) {\n return \\CuyZ\\Valinor\\Mapper\\Tree\\Message\\MessageBuilder::from($exception);\n } \n\n // If the exception should not be caught by this library, it\n // must be thrown again.\n throw $exception;\n })\n ->mapper()\n ->map(SomeClass::class, 'bar_baz');\n} catch (\\CuyZ\\Valinor\\Mapper\\MappingError $exception) {\n // Should print something similar to:\n // > Expected a value to start with \"foo_\". Got: \"bar_baz\"\n echo $exception->node()->messages()[0];\n}\n
"}]}
\ No newline at end of file
diff --git a/1.8/sitemap.xml b/1.8/sitemap.xml
index f36a466..3e8f42b 100644
--- a/1.8/sitemap.xml
+++ b/1.8/sitemap.xml
@@ -215,6 +215,11 @@