Valinor

PHP library that helps to map any input into a strongly-typed value object structure.

MIT License

Downloads
1.6M
Stars
1.3K
Committers
38

Bot releases are hidden (Show)

Valinor - 1.13.0 Latest Release

Published by romm about 2 months ago

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)
Valinor - 1.12.0

Published by romm 7 months ago

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)
Valinor - 1.11.0

Published by romm 7 months ago

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)
Valinor - 1.10.0

Published by romm 7 months ago

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)
Valinor - 1.9.0

Published by romm 9 months ago

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)
Valinor - 1.8.2

Published by romm 9 months ago

Bug Fixes

  • Allow callable type to be compiled (4a9771f)
Valinor - 1.8.1

Published by romm 9 months ago

Bug Fixes

  • Properly detect namespaced class in docblock (6f7c77)
Valinor - 1.8.0

Published by romm 10 months ago

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)
Valinor - 1.7.0

Published by romm 12 months ago

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)
Valinor - 1.6.1

Published by romm about 1 year ago

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)
Valinor - 1.6.0

Published by romm about 1 year ago

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)
Valinor - 1.5.0

Published by romm about 1 year ago

Features

  • Introduce method to get date formats supported during mapping (873961)

Bug Fixes

  • Allow filesystem cache to be cleared when directory does not exist (782408)
  • Allow negative timestamp to be mapped to a datetime (d358e8)
  • Allow overriding of supported datetime formats (1c70c2)
  • Correctly handle message formatting for long truncated UTF8 strings (0a8f37)
  • Make serialization of attributes possible (e8ca2f)
  • Remove exception inheritance from UnresolvableType (eaa128)
  • Remove previous exception from UnresolvableType (5c89c6)

Other

  • Avoid using unserialize when caching NULL default values (5e9b4c)
  • Catch json_encode exception to help identifying parsing errors (861c3b)
  • Update dependencies (c31e5c, 5fa107)
Valinor - 1.4.0

Published by romm over 1 year ago

Notable changes

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 {
    $source = \CuyZ\Valinor\Mapper\Source\Source::json('invalid JSON');
} catch (\CuyZ\Valinor\Mapper\Source\Exception\InvalidSource $error) {
    // Let the application handle the exception in the desired way.
    // It is possible to get the original source with `$error->source()`
}

Features

  • Introduce InvalidSource thrown when using invalid JSON/YAML (0739d1)

Bug Fixes

  • Allow integer values in float types (c6df24)
  • Make array-key type match mixed (ccebf7)
  • Prevent infinite loop when class has parent class with same name (83eb05)

Other

  • Add previous exception in various custom exceptions (b9e381)
Valinor - 1.3.1

Published by romm over 1 year ago

Bug Fixes

  • Check if temporary cache file exists before deletion (3177bf)
  • Display useful error message for invalid constructor return type (dc7f5c)
  • Keep input path when error occurs in single node (d70257)
  • Properly handle class static constructor for other class (d34974)
  • Properly handle union of null and objects (8f03a7)

Other

  • Update dependencies (f7e7f2)
Valinor - 1.3.0

Published by romm over 1 year ago

Notable changes

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())
    ->registerConstructor(
        // Allow the native constructor to be used
        SomeEnum::class,

        // Register a named constructor
        SomeEnum::fromMatrix(...)
    )
    ->mapper()
    ->map(SomeEnum::class, [
        'type' => 'FOO',
        'number' => 2,
    ]);

enum SomeEnum: string
{
    case CASE_A = 'FOO_VALUE_1';
    case CASE_B = 'FOO_VALUE_2';
    case CASE_C = 'BAR_VALUE_1';
    case CASE_D = 'BAR_VALUE_2';

    /**
     * @param 'FOO'|'BAR' $type
     * @param int<1, 2> $number
     * /
    public static function fromMatrix(string $type, int $number): self
    {
        return self::from("{$type}_VALUE_{$number}");
    }
}

An enum constructor can be for a specific pattern:

enum SomeEnum
{
    case FOO;
    case BAR;
    case BAZ;
}

(new \CuyZ\Valinor\MapperBuilder())
    ->registerConstructor(
        /**
         * This constructor will be called only when pattern
         * `SomeEnum::BA*` is requested during mapping.
         *
         * @return SomeEnum::BA*
         */
        fn (string $value): SomeEnum => /* Some custom domain logic */
    )
    ->mapper()
    ->map(SomeEnum::class . '::BA*', 'some custom value');

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.

Features

  • Handle custom enum constructors registration (217e12)

Other

  • Handle enum type as class type (5a3caf)
Valinor - 0.17.1

Published by romm almost 2 years ago

Bug Fixes

  • Use PHP 8.0 Polyfill where needed (d90a95)
Valinor - 1.2.0

Published by romm almost 2 years ago

Notable changes

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

Features

  • Allow usage of array input for single node during mapping (686186)

Bug Fixes

  • Do not re-validate single node with existing error (daaaac)

Other

  • Remove unneeded internal check (86cca5)
  • Remove unneeded internal checks and exceptions (157723)
Valinor - 1.1.0

Published by romm almost 2 years ago

Notable changes

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.

/**
 * @template FirstTemplate
 * @template SecondTemplate
 */
abstract class FirstClassWithGenerics
{
    /** @var FirstTemplate */
    public $valueA;

    /** @var SecondTemplate */
    public $valueB;
}

/**
 * @template FirstTemplate
 * @extends FirstClassWithGenerics<FirstTemplate, int>
 */
abstract class SecondClassWithGenerics extends FirstClassWithGenerics
{
    /** @var FirstTemplate */
    public $valueC;
}

/**
 * @extends SecondClassWithGenerics<string>
 */
final class ChildClass extends SecondClassWithGenerics
{
}

$object = (new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(ChildClass::class, [
        'valueA' => 'foo',
        'valueB' => 1337,
        'valueC' => 'bar',
    ]);

echo $object->valueA; // 'foo'
echo $object->valueB; // 1337
echo $object->valueC; // 'bar'

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
{
    public string $foo;

    public string $bar;
}

final class SomeChildClass extends SomeAbstractClass
{
    public string $baz;
}

$result = (new \CuyZ\Valinor\MapperBuilder())
    ->infer(
        SomeAbstractClass::class,
        fn () => SomeChildClass::class
    )
    ->mapper()
    ->map(SomeAbstractClass::class, [
        'foo' => 'foo',
        'bar' => 'bar',
        'baz' => 'baz',
    ]);

assert($result instanceof SomeChildClass);
assert($result->foo === 'foo');
assert($result->bar === 'bar');
assert($result->baz === 'baz');

Features

  • Add support for class inferring (5a90ad)
  • Handle class generic types inheritance (6506b7)

Bug Fixes

  • Handle object return type in PHPStan extension (201728)
  • Import plugin class file in PHPStan configuration (58d540)
  • Keep nested errors when superfluous keys are detected (813b3b)

Other

  • Adapt code with PHP 8.0 syntax (3fac3e)
  • Add isAbstract flag in class definition (ad0c06)
  • Add isFinal flag in class definition (25da31)
  • Enhance TreeMapper::map() return type signature (dc32d3)
  • Improve return type signature for TreeMapper (c8f362)
  • Prevent multiple cache round-trip (13b620)
Valinor - 1.0.0

Published by romm almost 2 years ago

First stable version! 🥳 🎉

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 — 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™); 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! 🙌

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 ❤️ sponsor my work on GitHub — if you use this library in your applications, please consider offering me a 🍺 from time to time! 🤗

Notable changes

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 {
    return "$foo / $bar";
};

try {
    $arguments = (new \CuyZ\Valinor\MapperBuilder())
        ->argumentsMapper()
        ->mapArguments($someFunction, [
            'foo' => 'some value',
            'bar' => 42,
        ]);

    // some value / 42
    echo $someFunction(...$arguments);
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    // Do something…
}

Support for TimeZone objects

Native TimeZone objects construction is now supported with a proper error handling.

try {
    (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(DateTimeZone::class, 'Jupiter/Europa');
} catch (MappingError $exception) {
    $error = $exception->node()->messages()[0];

    // Value 'Jupiter/Europa' is not a valid timezone.
    echo $error->toString();
}

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
{
    public readonly string $value;
}

final class SomeClass
{
    public readonly Identifier $identifier;

    public readonly string $description;
}

(new \CuyZ\Valinor\MapperBuilder())->mapper()->map(SomeClass::class, [
    'identifier' => ['value' => 'some-identifier'], // ❌
    'description' => 'Lorem ipsum…',
]);

(new \CuyZ\Valinor\MapperBuilder())->mapper()->map(SomeClass::class, [
    'identifier' => 'some-identifier', // ✅
    'description' => 'Lorem ipsum…',
]);

Upgrading from 0.x to 1.0

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 cannot be used anymore, PHP attributes must be used.

You must use the method available in the mapper builder, see dealing with dates chapter.

The flexible has been splitted in three disctint modes, see type strictness & flexibility chapter.

You must now register a cache instance directly, see performance & caching chapter.

You must now register the constructors using the mapper builder, see custom object constructors chapter.

You must now register the constructors using the mapper builder, see custom object constructors chapter.

You must now use the MessageBuilder class, see error handling chapter.

You must now use the Messages class, see error handling chapter.

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}.

Other features are available to format message, see error messages customization chapter.

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.

⚠ BREAKING CHANGES

  • Disallow array when mapping to object with one argument (72cba3)
  • Mark tree mapper and arguments mapper as @pure (0d9855)
  • Remove deprecated backward compatibility datetime constructor (a65e8d)
  • Remove deprecated class ThrowableMessage (d36ca9)
  • Remove deprecated class to flatten messages (f9ed93)
  • Remove deprecated interface TranslatableMessage (ceb197)
  • Remove deprecated message methods (e6557d)
  • Remove deprecated method constructor attribute (d76467)
  • Remove deprecated method to enable flexible mode (a2bef3)
  • Remove deprecated method to set cache directory (b0d6d2)
  • Remove deprecated method used to bind a callback (b79ed8)
  • Remove deprecated placeholder message formatter (c2723d)
  • Remove Doctrine annotations support (66c182)
  • Remove identifier attribute (8a7486)
  • Remove PHP 7.4 support (5f5a50)
  • Remove support for strict-array type (22c3b4)

Features

  • Add constructor for DateTimeZone with error support (a0a4d6)
  • Introduce mapper to map arguments of a callable (9c7e88)

Bug Fixes

  • Allow mapping null to single node nullable type (0a98ec)
  • Handle single argument mapper properly (d7bf6a)
  • Handle tree mapper call without argument in PHPStan extension (3f3a01)
  • Handle tree mapper call without argument in Psalm plugin (b425af)

Other

  • Activate value altering feature only when callbacks are registered (0f33a5)
  • Bump psr/simple-cache supported version (e4059a)
  • Remove @ from comments for future PHP versions changes (68774c)
  • Update dependencies (4afcda)
Valinor - 0.17.0

Published by romm almost 2 years ago

Notable changes

The main feature introduced in this release is the split of the flexible mode in three distinct modes:

  1. The flexible casting

    Changes the behaviours explained below:

    $flexibleMapper = (new \CuyZ\Valinor\MapperBuilder())
        ->enableFlexibleCasting()
        ->mapper();
    
    // ---
    // Scalar types will accept non-strict values; for instance an
    // integer type will accept any valid numeric value like the
    // *string* "42".
    
    $flexibleMapper->map('int', '42');
    // => 42
    
    // ---
    // List type will accept non-incremental keys.
    
    $flexibleMapper->map('list<int>', ['foo' => 42, 'bar' => 1337]);
    // => [0 => 42, 1 => 1338]
    
    // ---
    // If a value is missing in a source for a node that accepts `null`,
    // the node will be filled with `null`.
    
    $flexibleMapper->map(
        'array{foo: string, bar: null|string}',
        ['foo' => 'foo'] // `bar` is missing
    );
    // => ['foo' => 'foo', 'bar' => null]
    
    // ---
    // Array and list types will convert `null` or missing values to an
    // empty array.
    
    $flexibleMapper->map(
        'array{foo: string, bar: array<string>}',
        ['foo' => 'foo'] // `bar` is missing
    );
    // => ['foo' => 'foo', 'bar' => []]
    
  2. The superfluous keys

    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())
        ->allowSuperfluousKeys()
        ->mapper()
        ->map(
            'array{foo: string, bar: int}',
            [
                'foo' => 'foo',
                'bar' => 42,
                'baz' => 1337.404, // `baz` will be ignored
            ]
        );
    
  3. The permissive types

    Allows permissive types mixed and object to be used during mapping.

    (new \CuyZ\Valinor\MapperBuilder())
        ->allowPermissiveTypes()
        ->mapper()
        ->map(
            'array{foo: string, bar: mixed}',
            [
                'foo' => 'foo',
                'bar' => 42, // Could be any value
            ]
        );
    

Full list of changes

Features

  • Add support for strict-array type (d456eb)
  • Introduce new callback message formatter (93f898)
  • Introduce new helper class to list messages (513827)
  • Split mapper flexible mode in three distinct modes (549e5f)

Bug Fixes

  • Allow missing and null value for array node in flexible mode (034f1c)
  • Allow missing value for shaped array nullable node in flexible mode (08fb0e)
  • Handle scalar value casting in union types only in flexible mode (752ad9)

Other

  • Do not use uniqid() (b81847)
  • Transform missing source value to null in flexible mode (92a41a)