Skip to content

Releases: CuyZ/Valinor

1.1.0

20 Dec 22:09
Compare
Choose a tag to compare

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)

1.0.0

28 Nov 00:09
Compare
Choose a tag to compare

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 support removal

Doctrine annotations cannot be used anymore, PHP attributes must be used.

`BackwardCompatibilityDateTimeConstructor` class removal

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

Mapper builder `flexible` method removal

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

Mapper builder `withCacheDir` method removal

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

`StaticMethodConstructor` class removal

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

Mapper builder `bind` method removal

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

`ThrowableMessage` class removal

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

`MessagesFlattener` class removal

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

`TranslatableMessage` class removal

You must now use the HasParameters class, see custom exception chapter.

Message methods removal

The following methods have been removed:

  • \CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::name()
  • \CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::path()
  • \CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::type()
  • \CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::value()
  • \CuyZ\Valinor\Mapper\Tree\Node::value()

It is still possible to get the wanted values using the method \CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::node().

The placeholder {original_value} has also been removed, the same value can be fetched with {source_value}.

`PlaceHolderMessageFormatter` class removal

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

`Identifier` attribute removal

This feature has been part of the library since its first public release, but it was never documented because it did not fit one of the library's main philosophy which is to be almost entirely decoupled from an application's domain layer.

The feature is entirely removed and not planned to be replaced by an alternative, unless the community really feels like there is a need for something alike.

⚠ 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](a0a4d63...
Read more

0.17.0

08 Nov 12:30
Compare
Choose a tag to compare

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)

0.16.0

19 Oct 11:47
Compare
Choose a tag to compare

Features

  • Add support for PHP 8.2 (a92360)

Bug Fixes

  • Properly handle quote char in type definition (c71d6a)

Other

  • Update dependencies (c2de32)

0.15.0

06 Oct 11:50
Compare
Choose a tag to compare

Notable changes

Two similar features are introduced in this release: constants and enums wildcard notations. This is mainly useful when several cases of an enum or class constants share a common prefix.

Example for class constants:

final class SomeClassWithConstants
{
    public const FOO = 1337;

    public const BAR = 'bar';

    public const BAZ = 'baz';
}

$mapper = (new MapperBuilder())->mapper();

$mapper->map('SomeClassWithConstants::BA*', 1337); // error
$mapper->map('SomeClassWithConstants::BA*', 'bar'); // ok
$mapper->map('SomeClassWithConstants::BA*', 'baz'); // ok

Example for enum:

enum SomeEnum: string
{
    case FOO = 'foo';
    case BAR = 'bar';
    case BAZ = 'baz';
}

$mapper = (new MapperBuilder())->mapper();

$mapper->map('SomeEnum::BA*', 'foo'); // error
$mapper->map('SomeEnum::BA*', 'bar'); // ok
$mapper->map('SomeEnum::BA*', 'baz'); // ok

Full list of changes

Features

  • Add support for class constant type (1244c2)
  • Add support for wildcard in enumeration type (69ebd1)
  • Introduce utility class to build messages (cb8792)

Bug Fixes

  • Add return types for cache implementations (0e8f12)
  • Correctly handle type inferring during mapping (37f96f)
  • Fetch correct node value for children (3ee526)
  • Improve scalar values casting (212b77)
  • Properly handle static anonymous functions (c009ab)

Other

  • Import namespace token parser inside library (0b8ca9)
  • Remove unused code (b2889a, de8aa9)
  • Save type token symbols during lexing (ad0f8f)

0.14.0

01 Sep 10:52
Compare
Choose a tag to compare

Notable changes

Until this release, the behaviour of the date objects creation was very opinionated: a huge list of date formats were tested out, and if one was working it was used to create the date.

This approach resulted in two problems. First, it led to (minor) performance issues, because a lot of date formats were potentially tested for nothing. More importantly, it was not possible to define which format(s) were to be allowed (and in result deny other formats).

A new method can now be used in the MapperBuilder:

(new \CuyZ\Valinor\MapperBuilder())
    // Both `Cookie` and `ATOM` formats will be accepted
    ->supportDateFormats(DATE_COOKIE, DATE_ATOM)
    ->mapper()
    ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');

Please note that the old behaviour has been removed. From now on, only valid timestamp or ATOM-formatted value will be accepted by default.

If needed and to help with the migration, the following deprecated constructor can be registered to reactivate the previous behaviour:

(new \CuyZ\Valinor\MapperBuilder())
    ->registerConstructor(
        new \CuyZ\Valinor\Mapper\Object\BackwardCompatibilityDateTimeConstructor()
    )
    ->mapper()
    ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC');

Full list of changes

⚠ BREAKING CHANGES

  • Introduce constructor for custom date formats (f232cc)

Features

  • Handle abstract constructor registration (c37ac1)
  • Introduce attribute DynamicConstructor (e437d9)
  • Introduce helper method to describe supported date formats (11a7ea)

Bug Fixes

  • Allow trailing comma in shaped array (bf445b)
  • Correctly fetch file system cache entries (48208c)
  • Detect invalid constructor handle type (b3cb59)
  • Handle classes in a case-sensitive way in type parser (254074)
  • Handle concurrent cache file creation (fd39ae)
  • Handle inherited private constructor in class definition (73b622)
  • Handle invalid nodes recursively (a401c2)
  • Prevent illegal characters in PSR-16 cache keys (3c4d29)
  • Properly handle callable objects of the same class (ae7ddc)

Other

  • Add singleton usage of ClassStringType (4bc50e)
  • Change ObjectBuilderFactory::for return signature (57849c)
  • Extract native constructor object builder (2b46a6)
  • Fetch attributes for function definition (ec494c)
  • Refactor arguments instantiation (6414e9)

0.13.0

31 Jul 15:25
Compare
Choose a tag to compare

Notable changes

Reworking of messages body and parameters features

The \CuyZ\Valinor\Mapper\Tree\Message\Message interface is no longer a Stringable, however it defines a new method body that must return the body of the message, which can contain placeholders that will be replaced by parameters.

These parameters can now be defined by implementing the interface \CuyZ\Valinor\Mapper\Tree\Message\HasParameters.

This leads to the deprecation of the no longer needed interface \CuyZ\Valinor\Mapper\Tree\Message\TranslatableMessage which had a confusing name.

final class SomeException extends DomainException implements ErrorMessage, HasParameters, HasCode
{
    private string $someParameter;

    public function __construct(string $someParameter)
    {
        parent::__construct();

        $this->someParameter = $someParameter;
    }

    public function body() : string
    {
        return 'Some message / {some_parameter} / {source_value}';
    }

    public function parameters(): array
    {
        return [
            'some_parameter' => $this->someParameter,
        ];
    }

    public function code() : string
    {
        // A unique code that can help to identify the error
        return 'some_unique_code';
    }
}

Handle numeric-string type

Kudos to @sergkash7 for this new feature!

The new numeric-string type can be used in docblocks.

It will accept any string value that is also numeric.

(new MapperBuilder())->mapper()->map('numeric-string', '42'); // ✅
(new MapperBuilder())->mapper()->map('numeric-string', 'foo'); // ❌

Better mapping error message

Kudos to @Slamdunk for this!

The message of the exception will now contain more information, especially the total number of errors and the source that was given to the mapper. This change aims to have a better understanding of what is wrong when debugging.

Before:

Could not map type `array{foo: string, bar: int}` with the given source.

After:

Could not map type `array{foo: string, bar: int}`. An error occurred at path bar: Value 'some other string' does not match type `int`.

Full list of changes

⚠ BREAKING CHANGES

  • Rework messages body and parameters features (ad1207)

Features

  • Allow to declare parameter for message (f61eb5)
  • Display more information in mapping error message (9c1e7c)
  • Handle numeric string type (96a493)
  • Make MessagesFlattener countable (2c1c7c)

Bug Fixes

  • Handle native attribute on promoted parameter (897ca9)

Other

  • Add fixed value for root node path (0b37b4)
  • Remove types stringable behavior (b47a1b)

0.12.0

10 Jul 17:57
Compare
Choose a tag to compare

Notable changes

SECURITY — Userland exception filtering

See advisory GHSA-5pgm-3j3g-2rc7 for more information.

Userland exception thrown in a constructor will not be automatically caught by the mapper anymore. This prevents messages with sensible information from reaching the final user — for instance an SQL exception showing a part of a query.

To allow exceptions to be considered as safe, the new method MapperBuilder::filterExceptions() must be used, with caution.

final class SomeClass
{
    public function __construct(private string $value)
    {
        \Webmozart\Assert\Assert::startsWith($value, 'foo_');
    }
}

try {
    (new \CuyZ\Valinor\MapperBuilder())
        ->filterExceptions(function (Throwable $exception) {
            if ($exception instanceof \Webmozart\Assert\InvalidArgumentException) {
                return \CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage::from($exception);
            }

            // If the exception should not be caught by this library, it
            // must be thrown again.
            throw $exception;
        })
        ->mapper()
        ->map(SomeClass::class, 'bar_baz');
} catch (\CuyZ\Valinor\Mapper\MappingError $exception) {
    // Should print something similar to:
    // > Expected a value to start with "foo_". Got: "bar_baz"
    echo $exception->node()->messages()[0];
}

Tree node API rework

The class \CuyZ\Valinor\Mapper\Tree\Node has been refactored to remove access to unwanted methods that were not supposed to be part of the public API. Below are a list of all changes:

  • New methods $node->sourceFilled() and $node->sourceValue() allow accessing the source value.

  • The method $node->value() has been renamed to $node->mappedValue() and will throw an exception if the node is not valid.

  • The method $node->type() now returns a string.

  • The methods $message->name(), $message->path(), $message->type() and $message->value() have been deprecated in favor of the new method $message->node().

  • The message parameter {original_value} has been deprecated in favor of {source_value}.

Access removal of several parts of the library public API

The access to class/function definition, types and exceptions did not add value to the actual goal of the library. Keeping these features under the public API flag causes more maintenance burden whereas revoking their access allows more flexibility with the overall development of the library.

Full list of changes

⚠ BREAKING CHANGES

  • Filter userland exceptions to hide potential sensible data (6ce1a4)
  • Refactor tree node API (d3b1dc)
  • Remove API access from several parts of library (316d91)
  • Remove node visitor feature (63c87a)

Bug Fixes

  • Handle inferring methods with same names properly (dc45dd)
  • Process invalid type default value as unresolvable type (7c9ac1)
  • Properly display unresolvable type (3020db)

Other

  • Ignore .idea folder (84ead0)

0.11.0

23 Jun 09:20
Compare
Choose a tag to compare

Notable changes

Strict mode

The mapper is now more type-sensitive and will fail in the following situations:

  • When a value does not match exactly the awaited scalar type, for instance a string "42" given to a node that awaits an integer.

  • When unnecessary array keys are present, for instance mapping an array ['foo' => …, 'bar' => …, 'baz' => …] to an object that needs only foo and bar.

  • When permissive types like mixed or object are encountered.

These limitations can be bypassed by enabling the flexible mode:

(new \CuyZ\Valinor\MapperBuilder())
    ->flexible()
    ->mapper();
    ->map('array{foo: int, bar: bool}', [
        'foo' => '42', // Will be cast from `string` to `int`
        'bar' => 'true', // Will be cast from `string` to `bool`
        'baz' => '', // Will be ignored
    ]);

When using this library for a provider application — for instance an API endpoint that can be called with a JSON payload — it is recommended to use the strict mode. This ensures that the consumers of the API provide the exact awaited data structure, and prevents unknown values to be passed.

When using this library as a consumer of an external source, it can make sense to enable the flexible mode. This allows for instance to convert string numeric values to integers or to ignore data that is present in the source but not needed in the application.

Interface inferring

It is now mandatory to list all possible class-types that can be inferred by the mapper. This change is a step towards the library being able to deliver powerful new features such as compiling a mapper for better performance.

The existing calls to MapperBuilder::infer that could return several class-names must now add a signature to the callback. The callbacks that require no parameter and always return the same class-name can remain unchanged.

For instance:

$builder = (new \CuyZ\Valinor\MapperBuilder())
    // Can remain unchanged
    ->infer(SomeInterface::class, fn () => SomeImplementation::class);
$builder = (new \CuyZ\Valinor\MapperBuilder())
    ->infer(
        SomeInterface::class,
        fn (string $type) => match($type) {
            'first' => ImplementationA::class,
            'second' => ImplementationB::class,
            default => throw new DomainException("Unhandled `$type`.")
        }
    )
    // …should be modified with:
    ->infer(
        SomeInterface::class,
        /** @return class-string<ImplementationA|ImplementationB> */
        fn (string $type) => match($type) {
            'first' => ImplementationA::class,
            'second' => ImplementationB::class,
            default => throw new DomainException("Unhandled `$type`.")
        }
    );

Object constructors collision

All these changes led to a new check that runs on all registered object constructors. If a collision is found between several constructors that have the same signature (the same parameter names), an exception will be thrown.

final class SomeClass
{
    public static function constructorA(string $foo, string $bar): self
    {
        // …
    }

    public static function constructorB(string $foo, string $bar): self
    {
        // …
    }
}

(new \CuyZ\Valinor\MapperBuilder())
    ->registerConstructor(
        SomeClass::constructorA(...),
        SomeClass::constructorB(...),
    )
    ->mapper();
    ->map(SomeClass::class, [
        'foo' => 'foo',
        'bar' => 'bar',
    ]);

// Exception: A collision was detected […]

Full list of changes

⚠ BREAKING CHANGES

  • Handle exhaustive list of interface inferring (1b0ff3)
  • Make mapper more strict and allow flexible mode (90dc58)

Features

  • Improve cache warmup (44c5f1)

0.10.0

10 Jun 17:02
Compare
Choose a tag to compare

Notable changes

Documentation is now available at valinor.cuyz.io.

Full list of changes

Features

  • Support mapping to dates with no time (e0a529)

Bug Fixes

  • Allow declaring promoted parameter type with @var annotation (d8eb4d)
  • Allow mapping iterable to shaped array (628baf)