Skip to content

Releases: CuyZ/Valinor

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

  • 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)

1.7.0

23 Oct 11:05
Compare
Choose a tag to compare

Notable changes

Non-positive integer

Non-positive integer can be used as below. It will accept any value equal to or lower than zero.

final class SomeClass
{
    /** @var non-positive-int */
    public int $nonPositiveInteger;
}

Non-negative integer

Non-negative integer can be used as below. It will accept any value equal to or greater than zero.

final class SomeClass
{
    /** @var non-negative-int */
    public int $nonNegativeInteger;
}

Features

  • Handle non-negative integer type (f444ea)
  • Handle non-positive integer type (53e404)

Bug Fixes

  • Add missing @psalm-pure annotation to pure methods (004eb1)
  • Handle comments in classes when parsing types imports (3b663a)

Other

  • Add comment for future PHP version change (461898)
  • Fix some typos (5cf8ae)
  • Make NativeBooleanType a BooleanType (d57ffa)

1.6.1

11 Oct 08:41
Compare
Choose a tag to compare

Bug Fixes

  • Correctly handle multiline type alias in classes (c23102)
  • Handle integer key in path mapping modifier (9419f6)
  • Handle variadic parameters declared in docblock (f4884c)

1.6.0

25 Aug 10:27
Compare
Choose a tag to compare

Notable changes

Symfony Bundle

A bundle is now available for Symfony applications, it will ease the integration and usage of the Valinor library in the framework. The documentation can be found in the CuyZ/Valinor-Bundle repository.

Note that the documentation has been updated to add information about the bundle as well as tips on how to integrate the library in other frameworks.

PHP 8.3 support

Thanks to @TimWolla, the library now supports PHP 8.3, which entered its beta phase. Do not hesitate to test the library with this new version, and report any encountered issue on the repository.

Better type parsing

The first layer of the type parser has been completely rewritten. The previous one would use regex to split a raw type in tokens, but that led to limitations — mostly concerning quoted strings — that are now fixed.

Although this change should not impact the end user, it is a major change in the library, and it is possible that some edge cases were not covered by tests. If that happens, please report any encountered issue on the repository.

Example of previous limitations, now solved:

// Union of strings containing space chars
(new MapperBuilder())
    ->mapper()
    ->map(
        "'foo bar'|'baz fiz'",
        'baz fiz'
    );

// Shaped array with special chars in the key
(new MapperBuilder())
    ->mapper()
    ->map(
        "array{'some & key': string}",
        ['some & key' => 'value']
    );

More advanced array-key handling

It is now possible to use any string or integer as an array key. The following types are now accepted and will work properly with the mapper:

$mapper->map("array<'foo'|'bar', string>", ['foo' => 'foo']);

$mapper->map('array<42|1337, string>', [42 => 'foo']);

$mapper->map('array<positive-int, string>', [42 => 'foo']);

$mapper->map('array<negative-int, string>', [-42 => 'foo']);

$mapper->map('array<int<-42, 1337>, string>', [42 => 'foo']);

$mapper->map('array<non-empty-string, string>', ['foo' => 'foo']);

$mapper->map('array<class-string, string>', ['SomeClass' => 'foo']);

Features

  • Add support for PHP 8.3 (5c44f8)
  • Allow any string or integer in array key (12af3e)
  • Support microseconds in the Atom / RFC 3339 / ISO 8601 format (c25721)

Bug Fixes

  • Correctly handle type inferring for method coming from interface (2657f8)
  • Detect missing closing bracket after comma in shaped array type (2aa4b6)
  • Handle class name collision while parsing types inside a class (044072)
  • Handle invalid Intl formats with intl.use_exceptions=1 (29da9a)
  • Improve cache warmup by creating required directories (a3341a)
  • Load attributes lazily during runtime and cache access (3e7c63)
  • Properly handle class/enum name in shaped array key (1964d4)

Other

  • Improve attributes arguments compilation (c4acb1)
  • Replace regex-based type parser with character-based one (ae8303)
  • Simplify symbol parsing algorithm (f260cf)
  • Update Rector dependency (669ff9)