Releases: CuyZ/Valinor
1.14.1
1.14.0
Notable changes
PHP 8.4 support 🐘
Enjoy the upcoming PHP 8.4 version before it is even officially released!
Pretty JSON output
The JSON_PRETTY_PRINT
option is now supported by the JSON normalizer and will format the ouput with whitespaces and line breaks:
$input = [
'value' => 'foo',
'list' => [
'foo',
42,
['sub']
],
'associative' => [
'value' => 'foo',
'sub' => [
'string' => 'foo',
'integer' => 42,
],
],
];
(new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
->withOptions(\JSON_PRETTY_PRINT)
->normalize($input);
// Result:
// {
// "value": "foo",
// "list": [
// "foo",
// 42,
// [
// "sub"
// ]
// ],
// "associative": {
// "value": "foo",
// "sub": {
// "string": "foo",
// "integer": 42
// }
// }
// }
Force array as object in JSON output
The JSON_FORCE_OBJECT
option is now supported by the JSON normalizer and will force the output of an array to be an object:
(new \CuyZ\Valinor\MapperBuilder())
->normalizer(Format::json())
->withOptions(JSON_FORCE_OBJECT)
->normalize(['foo', 'bar']);
// {"0":"foo","1":"bar"}
Features
- Add support for
JSON_FORCE_OBJECT
option in JSON normalizer (f3e8c1) - Add support for PHP 8.4 (07a06a)
- Handle
JSON_PRETTY_PRINT
option with the JSON normalizer (950395)
Bug Fixes
- Handle float type casting properly (8742b2)
- Handle namespace for Closure without class scope (7a0fc2)
- Prevent cache corruption when normalizing and mapping to enum (e695b2)
- Properly handle class sharing class name and namespace group name (6e68d6)
Other
1.13.0
Notable changes
Microseconds support for timestamp format
Prior to this patch, this would require a custom constructor in the form of:
static fn(float | int $timestamp): DateTimeImmutable => new
DateTimeImmutable(sprintf("@%d", $timestamp)),
This bypasses the datetime format support of Valinor entirely. This is required because the library does not support floats as valid DateTimeInterface
input values.
This commit adds support for floats and registers timestamp.microseconds
(U.u
) as a valid default format.
Support for value-of<BackedEnum>
type
This type can be used as follows:
enum Suit: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
$suit = (new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map('value-of<Suit>', 'D');
// $suit === 'D'
Object constructors parameters types inferring improvements
The collision system that checks object constructors parameters types is now way more clever, as it no longer checks for parameters' names only. Types are now also checked, and only true collision will be detected, for instance when two constructors share a parameter with the same name and type.
Note that when two parameters share the same name, the following type priority operates:
- Non-scalar type
- Integer type
- Float type
- String type
- Boolean type
With this change, the code below is now valid:
final readonly class Money
{
private function __construct(
public int $value,
) {}
#[\CuyZ\Valinor\Mapper\Object\Constructor]
public static function fromInt(int $value): self
{
return new self($value);
}
#[\CuyZ\Valinor\Mapper\Object\Constructor]
public static function fromString(string $value): self
{
if (! preg_match('/^\d+€$/', $value)) {
throw new \InvalidArgumentException('Invalid money format');
}
return new self((int)rtrim($value, '€'));
}
}
$mapper = (new \CuyZ\Valinor\MapperBuilder())->mapper();
$mapper->map(Money::class, 42); // ✅
$mapper->map(Money::class, '42€'); // ✅
Features
- Add microseconds support to timestamp format (02bd2e)
- Add support for
value-of<BackedEnum>
type (b1017c) - Improve object constructors parameters types inferring (2150dc)
Bug Fixes
- Allow any constant in class constant type (694275)
- Allow docblock for transformer callable type (69e0e3)
- Do not override invalid variadic parameter type (c5860f)
- Handle interface generics (40e6fa)
- Handle iterable objects as iterable during normalization (436e3c)
- Properly format empty object with JSON normalizer (ba22b5)
- Properly handle nested local type aliases (127839)
Other
1.12.0
Notable changes
Introduce unsealed shaped array syntax
This syntax enables an extension of the shaped array type by allowing additional values that must respect a certain type.
$mapper = (new \CuyZ\Valinor\MapperBuilder())->mapper();
// Default syntax can be used like this:
$mapper->map(
'array{foo: string, ...array<string>}',
[
'foo' => 'foo',
'bar' => 'bar', // ✅ valid additional value
]
);
$mapper->map(
'array{foo: string, ...array<string>}',
[
'foo' => 'foo',
'bar' => 1337, // ❌ invalid value 1337
]
);
// Key type can be added as well:
$mapper->map(
'array{foo: string, ...array<int, string>}',
[
'foo' => 'foo',
42 => 'bar', // ✅ valid additional key
]
);
$mapper->map(
'array{foo: string, ...array<int, string>}',
[
'foo' => 'foo',
'bar' => 'bar' // ❌ invalid key
]
);
// Advanced types can be used:
$mapper->map(
"array{
'en_US': non-empty-string,
...array<non-empty-string, non-empty-string>
}",
[
'en_US' => 'Hello',
'fr_FR' => 'Salut', // ✅ valid additional value
]
);
$mapper->map(
"array{
'en_US': non-empty-string,
...array<non-empty-string, non-empty-string>
}",
[
'en_US' => 'Hello',
'fr_FR' => '', // ❌ invalid value
]
);
// If the permissive type is enabled, the following will work:
(new \CuyZ\Valinor\MapperBuilder())
->allowPermissiveTypes()
->mapper()
->map(
'array{foo: string, ...}',
['foo' => 'foo', 'bar' => 'bar', 42 => 1337]
); // ✅
Interface constructor registration
By default, the mapper cannot instantiate an interface, as it does not know which implementation to use. To do so, the MapperBuilder::infer()
method can be used, but it is cumbersome in most cases.
It is now also possible to register a constructor for an interface, in the same way as for a class.
Because the mapper cannot automatically guess which implementation can be used for an interface, it is not possible to use the Constructor
attribute, the MapperBuilder::registerConstructor()
method must be used instead.
In the example below, the mapper is taught how to instantiate an implementation of UuidInterface
from package ramsey/uuid
:
(new \CuyZ\Valinor\MapperBuilder())
->registerConstructor(
// The static method below has return type `UuidInterface`;
// therefore, the mapper will build an instance of `Uuid` when
// it needs to instantiate an implementation of `UuidInterface`.
Ramsey\Uuid\Uuid::fromString(...)
)
->mapper()
->map(
Ramsey\Uuid\UuidInterface::class,
'663bafbf-c3b5-4336-b27f-1796be8554e0'
);
JSON normalizer formatting options — contributed by @boesing
By default, the JSON normalizer will only use JSON_THROW_ON_ERROR
to encode non-boolean scalar values. There might be use-cases where projects will need flags like JSON_JSON_PRESERVE_ZERO_FRACTION
.
This can be achieved by passing these flags to the new JsonNormalizer::withOptions()
method:
namespace My\App;
$normalizer = (new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
->withOptions(\JSON_PRESERVE_ZERO_FRACTION);
$lowerManhattanAsJson = $normalizer->normalize(
new \My\App\Coordinates(
longitude: 40.7128,
latitude: -74.0000
)
);
// `$lowerManhattanAsJson` is a valid JSON string representing the data:
// {"longitude":40.7128,"latitude":-74.0000}
The method accepts an int-mask of the following JSON_*
constant representations:
JSON_HEX_QUOT
JSON_HEX_TAG
JSON_HEX_AMP
JSON_HEX_APOS
JSON_INVALID_UTF8_IGNORE
JSON_INVALID_UTF8_SUBSTITUTE
JSON_NUMERIC_CHECK
JSON_PRESERVE_ZERO_FRACTION
JSON_UNESCAPED_LINE_TERMINATORS
JSON_UNESCAPED_SLASHES
JSON_UNESCAPED_UNICODE
JSON_THROW_ON_ERROR
is always enforced and thus is not accepted.
See official doc for more information:
https://www.php.net/manual/en/json.constants.php
Features
- Allow JSON normalizer to set JSON formatting options (cd5df9)
- Allow mapping to
array-key
type (5020d6) - Handle interface constructor registration (13f69a)
- Handle type importation from interface (3af22d)
- Introduce unsealed shaped array syntax (fa8bb0)
Bug Fixes
- Handle class tokens only when needed during lexing (c4be75)
- Load needed information only during interface inferring (c8e204)
Other
- Rename internal class (4c62d8)
1.11.0
Notable changes
Improvement of union types narrowing
The algorithm used by the mapper to narrow a union type has been greatly improved, and should cover more edge-cases that would previously prevent the mapper from performing well.
If an interface, a class or a shaped array is matched by the input, it will take precedence over arrays or scalars.
(new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(
signature: 'array<int>|' . Color::class,
source: [
'red' => 255,
'green' => 128,
'blue' => 64,
],
); // Returns an instance of `Color`
When superfluous keys are allowed, if the input matches several interfaces, classes or shaped array, the one with the most children node will be prioritized, as it is considered the most specific type:
(new \CuyZ\Valinor\MapperBuilder())
->allowSuperfluousKeys()
->mapper()
->map(
// Even if the first shaped array matches the input, the second one is
// used because it's more specific.
signature: 'array{foo: int}|array{foo: int, bar: int}',
source: [
'foo' => 42,
'bar' => 1337,
],
);
If the input matches several types within the union, a collision will occur and cause the mapper to fail:
(new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(
// Even if the first shaped array matches the input, the second one is
// used because it's more specific.
signature: 'array{red: int, green: int, blue: int}|' . Color::class,
source: [
'red' => 255,
'green' => 128,
'blue' => 64,
],
);
// ⚠️ Invalid value array{red: 255, green: 128, blue: 64}, it matches at
// least two types from union.
Introducing AsTransformer
attribute
After the introduction of the Constructor
attribute used for the mapper, the new AsTransformer
attribute is now available for the normalizer to ease the registration of a transformer.
namespace My\App;
#[\CuyZ\Valinor\Normalizer\AsTransformer]
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class DateTimeFormat
{
public function __construct(private string $format) {}
public function normalize(\DateTimeInterface $date): string
{
return $date->format($this->format);
}
}
final readonly class Event
{
public function __construct(
public string $eventName,
#[\My\App\DateTimeFormat('Y/m/d')]
public \DateTimeInterface $date,
) {}
}
(new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
->normalize(new \My\App\Event(
eventName: 'Release of legendary album',
date: new \DateTimeImmutable('1971-11-08'),
));
// [
// 'eventName' => 'Release of legendary album',
// 'date' => '1971/11/08',
// ]
Features
Bug Fixes
- Handle single array mapping when a superfluous value is present (86d021)
- Properly handle
ArrayObject
normalization (4f555d) - Properly handle class type with matching name and namespace (0f5e96)
- Properly handle nested unresolvable type during mapping (194706)
- Strengthen type tokens extraction (c9dc97)
Other
1.10.0
Notable changes
Dropping support for PHP 8.0
PHP 8.0 security support has ended on the 26th of November 2023. Therefore, we are dropping support for PHP 8.0 in this version.
If any security issue was to be found, we might consider backporting the fix to the 1.9.x version if people need it, but we strongly recommend upgrading your application to a supported PHP version.
Introducing Constructor
attribute
A long awaited feature has landed in the library!
The Constructor
attribute can be assigned to any method inside an object, to automatically mark the method as a constructor for the class. This is a more convenient way of registering constructors than using the MapperBuilder::registerConstructor
method, although it does not replace it.
The method targeted by a Constructor
attribute must be public, static and return an instance of the class it is part of.
final readonly class Email
{
// When another constructor is registered for the class, the native
// constructor is disabled. To enable it again, it is mandatory to
// explicitly register it again.
#[\CuyZ\Valinor\Mapper\Object\Constructor]
public function __construct(public string $value) {}
#[\CuyZ\Valinor\Mapper\Object\Constructor]
public static function createFrom(
string $userName, string $domainName
): self {
return new self($userName . '@' . $domainName);
}
}
(new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(Email::class, [
'userName' => 'john.doe',
'domainName' => 'example.com',
]); // john.doe@example.com
Features
- Introduce
Constructor
attribute (d86295)
Bug Fixes
- Properly encode scalar value in JSON normalization (2107ea)
- Properly handle list type when input contains superfluous keys (1b8efa)
Other
1.9.0
Notable changes
JSON normalizer
The normalizer is able to normalize a data structure to JSON without using the native json_encode()
function.
Using the normalizer instead of the native json_encode()
function offers some benefits:
- Values will be recursively normalized using the default transformations
- All registered transformers will be applied to the data before it is formatted
- The JSON can be streamed to a PHP resource in a memory-efficient way
Basic usage:
namespace My\App;
$normalizer = (new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json());
$userAsJson = $normalizer->normalize(
new \My\App\User(
name: 'John Doe',
age: 42,
country: new \My\App\Country(
name: 'France',
code: 'FR',
),
)
);
// `$userAsJson` is a valid JSON string representing the data:
// {"name":"John Doe","age":42,"country":{"name":"France","code":"FR"}}
By default, the JSON normalizer will return a JSON string representing the data it was given. Instead of getting a string, it is possible to stream the JSON data to a PHP resource:
$file = fopen('path/to/some_file.json', 'w');
$normalizer = (new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
->streamTo($file);
$normalizer->normalize(/* … */);
// The file now contains the JSON data
Another benefit of streaming the data to a PHP resource is that it may be more memory-efficient when using generators — for instance when querying a database:
// In this example, we assume that the result of the query below is a
// generator, every entry will be yielded one by one, instead of
// everything being loaded in memory at once.
$users = $database->execute('SELECT * FROM users');
$file = fopen('path/to/some_file.json', 'w');
$normalizer = (new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::json())
->streamTo($file);
// Even if there are thousands of users, memory usage will be kept low
// when writing JSON into the file.
$normalizer->normalize($users);
Features
- Introduce JSON normalizer (959740)
Bug Fixes
- Add default transformer for
DateTimeZone
(acf097) - Detect circular references linearly through objects (36aead)
Other
- Refactor attribute definition to include class definition (4b8cf6)
1.8.2
1.8.1
1.8.0
Notable changes
Normalizer service (serialization)
This new service can be instantiated with the MapperBuilder
. It allows transformation of a given input into scalar and array values, while preserving the original structure.
This feature can be used to share information with other systems that use a data format (JSON, CSV, XML, etc.). The normalizer will take care of recursively transforming the data into a format that can be serialized.
Below is a basic example, showing the transformation of objects into an array of scalar values.
namespace My\App;
$normalizer = (new \CuyZ\Valinor\MapperBuilder())
->normalizer(\CuyZ\Valinor\Normalizer\Format::array());
$userAsArray = $normalizer->normalize(
new \My\App\User(
name: 'John Doe',
age: 42,
country: new \My\App\Country(
name: 'France',
countryCode: 'FR',
),
)
);
// `$userAsArray` is now an array and can be manipulated much more
// easily, for instance to be serialized to the wanted data format.
//
// [
// 'name' => 'John Doe',
// 'age' => 42,
// 'country' => [
// 'name' => 'France',
// 'countryCode' => 'FR',
// ],
// ];
A normalizer can be extended by using so-called transformers, which can be either an attribute or any callable object.
In the example below, a global transformer is used to format any date found by the normalizer.
(new \CuyZ\Valinor\MapperBuilder())
->registerTransformer(
fn (\DateTimeInterface $date) => $date->format('Y/m/d')
)
->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
->normalize(
new \My\App\Event(
eventName: 'Release of legendary album',
date: new \DateTimeImmutable('1971-11-08'),
)
);
// [
// 'eventName' => 'Release of legendary album',
// 'date' => '1971/11/08',
// ]
This date transformer could have been an attribute for a more granular control, as shown below.
namespace My\App;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class DateTimeFormat
{
public function __construct(private string $format) {}
public function normalize(\DateTimeInterface $date): string
{
return $date->format($this->format);
}
}
final readonly class Event
{
public function __construct(
public string $eventName,
#[\My\App\DateTimeFormat('Y/m/d')]
public \DateTimeInterface $date,
) {}
}
(new \CuyZ\Valinor\MapperBuilder())
->registerTransformer(\My\App\DateTimeFormat::class)
->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
->normalize(
new \My\App\Event(
eventName: 'Release of legendary album',
date: new \DateTimeImmutable('1971-11-08'),
)
);
// [
// 'eventName' => 'Release of legendary album',
// 'date' => '1971/11/08',
// ]
More features are available, details about it can be found in the documentation.
Features
- Introduce normalizer service (1c9368)