Skip to content

Releases: CuyZ/Valinor

1.14.1

06 Nov 07:46
Compare
Choose a tag to compare

Bug Fixes

  • Properly handle partial namespace signature (9b58f2)

1.14.0

04 Nov 09:13
Compare
Choose a tag to compare

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

  • Change implicitly nullable parameter types (304db3)
  • Fix typo in property type annotation (b9c6ad)
  • Use xxh128 hash algorithm for cache keys (546c45)

1.13.0

02 Sep 12:59
Compare
Choose a tag to compare

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:

  1. Non-scalar type
  2. Integer type
  3. Float type
  4. String type
  5. 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

  • Exclude unneeded attributes in class/function definitions (1803d0)
  • Improve mapping performance for nullable union type (6fad94)
  • Move "float type accepting integer value" logic in Shell (047953)
  • Move setting values in shell (84b1ff)
  • Reorganize type resolver services (86fb7b)

1.12.0

04 Apr 16:45
Compare
Choose a tag to compare

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 optionscontributed 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

27 Mar 13:26
Compare
Choose a tag to compare

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

  • Improve union type narrowing during mapping (f73158)
  • Introduce AsTransformer attribute (13b6d0)

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

  • Reduce number of calls to class autoloader during type parsing (0f0e35)
  • Refactor generic types parsing and checking (ba6770)
  • Separate native type and docblock type for property and parameter (37993b)

1.10.0

12 Mar 07:02
Compare
Choose a tag to compare

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',
    ]); // [email protected]

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

  • Drop support for PHP 8.0 (dafcc8)
  • Improve internal definitions string types (105281)
  • Refactor file system cache to improve performance (e692f0)
  • Remove unneeded closure conversion (972e65)
  • Update dependencies (c5627f)

1.9.0

02 Feb 12:13
Compare
Choose a tag to compare

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

08 Jan 20:32
Compare
Choose a tag to compare

Bug Fixes

  • Allow callable type to be compiled (4a9771f)

1.8.1

08 Jan 18:41
Compare
Choose a tag to compare

Bug Fixes

  • Properly detect namespaced class in docblock (6f7c77)

1.8.0

26 Dec 14:54
Compare
Choose a tag to compare

Notable changes

Normalizer service (serialization)

This new service can be instantiated with the MapperBuilder. It allows transformation of a given input into scalar and array values, while preserving the original structure.

This feature can be used to share information with other systems that use a data format (JSON, CSV, XML, etc.). The normalizer will take care of recursively transforming the data into a format that can be serialized.

Below is a basic example, showing the transformation of objects into an array of scalar values.

namespace My\App;

$normalizer = (new \CuyZ\Valinor\MapperBuilder())
    ->normalizer(\CuyZ\Valinor\Normalizer\Format::array());

$userAsArray = $normalizer->normalize(
    new \My\App\User(
        name: 'John Doe',
        age: 42,
        country: new \My\App\Country(
            name: 'France',
            countryCode: 'FR',
        ),
    )
);

// `$userAsArray` is now an array and can be manipulated much more
// easily, for instance to be serialized to the wanted data format.
//
// [
//     'name' => 'John Doe',
//     'age' => 42,
//     'country' => [
//         'name' => 'France',
//         'countryCode' => 'FR',
//     ],
// ];

A normalizer can be extended by using so-called transformers, which can be either an attribute or any callable object.

In the example below, a global transformer is used to format any date found by the normalizer.

(new \CuyZ\Valinor\MapperBuilder())
    ->registerTransformer(
        fn (\DateTimeInterface $date) => $date->format('Y/m/d')
    )
    ->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
    ->normalize(
        new \My\App\Event(
            eventName: 'Release of legendary album',
            date: new \DateTimeImmutable('1971-11-08'),
        )
    );

// [
//     'eventName' => 'Release of legendary album',
//     'date' => '1971/11/08',
// ]

This date transformer could have been an attribute for a more granular control, as shown below.

namespace My\App;

#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class DateTimeFormat
{
    public function __construct(private string $format) {}

    public function normalize(\DateTimeInterface $date): string
    {
        return $date->format($this->format);
    }
}

final readonly class Event
{
    public function __construct(
        public string $eventName,
        #[\My\App\DateTimeFormat('Y/m/d')]
        public \DateTimeInterface $date,
    ) {}
}

(new \CuyZ\Valinor\MapperBuilder())
    ->registerTransformer(\My\App\DateTimeFormat::class)
    ->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
    ->normalize(
        new \My\App\Event(
            eventName: 'Release of legendary album',
            date: new \DateTimeImmutable('1971-11-08'),
        )
    );

// [
//     'eventName' => 'Release of legendary album',
//     'date' => '1971/11/08',
// ]

More features are available, details about it can be found in the documentation.

Features

  • Introduce normalizer service (1c9368)

Bug Fixes

  • Allow leading zeros in numeric string in flexible mode (f000c1)
  • Allow mapping union of scalars and classes (4f4af0)
  • Properly handle single-namespaced classes (a53ef9)
  • Properly parse class name in same single-namespace (a462fe)