Commit Graph

69 Commits

Author SHA1 Message Date
Romain Canon
5f9d41cf35 test: require and make use of vfsStream library 2022-05-08 16:48:15 +02:00
Romain Canon
17d646ef51 test: rename test 2022-05-08 16:48:15 +02:00
Romain Canon
8443847cb8 misc: bump dev-dependencies 2022-05-06 14:00:43 +02:00
Romain Canon
3ef4276649 test: add tests to check parsing of types followed by description 2022-05-05 19:49:45 +02:00
Maximilian Bösing
64e0a2d5ac
feat: allow psalm and phpstan prefix in docblocks
The following annotations are now properly handled: `@psalm-param`, 
`@phpstan-param`, `@psalm-return` and `@phpstan-return`.

If one of those found along with a basic `@param` or `@return` 
annotation, it will override the basic value.
2022-05-05 19:40:11 +02:00
Romain Canon
a681c78281 test: fix compiled cache file test 2022-04-09 17:44:32 +02:00
Romain Canon
3687379217 misc: remove symfony/polyfill-php80 dependency 2022-04-09 17:44:32 +02:00
Romain Canon
095e257884 test: add test for native constructor registration 2022-04-06 18:25:40 +02:00
Romain Canon
511a0dfee8 fix: handle function definition cache invalidation when file is modified 2022-04-06 18:24:16 +02:00
Romain Canon
0b042bc495 feat: handle filename in function definition 2022-04-06 18:24:16 +02:00
Romain Canon
b7923bc383 feat: handle class string of union of object 2022-04-06 18:18:17 +02:00
Romain Canon
aab7ae9adb test: add test to cover several value altering functions registration 2022-04-05 19:41:58 +02:00
Nathan Boiron
2f08e1a9b3 fix: call value altering function only if value is accepted 2022-04-05 19:41:58 +02:00
Nathan Boiron
ad51039cc3
feat: introduce a source builder
The `Source` class is a new entry point for sources that are not plain 
array or iterable. It allows accessing other features like camel-case 
keys or custom paths mapping in a convenient way.

It should be used as follows:

```php
$source = \CuyZ\Valinor\Mapper\Source\Source::json($jsonString)
    ->camelCaseKeys()
    ->map([
        'towns' => 'cities',
        'towns.*.label' => 'name',
    ]);

$result = (new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(SomeClass::class, $source);
```
2022-03-24 14:23:03 +01:00
Romain Canon
ecafba3b21 feat!: introduce method to register constructors used during mapping
It is now mandatory to explicitly register custom constructors —
including named constructors — that can be used by the mapper. The
former automatic registration of named constructor feature doesn't
work anymore.

BREAKING CHANGE: existing code must list all named constructors that
were previously automatically used by the mapper, and registerer them
using the method `MapperBuilder::registerConstructor()`.

The method `MapperBuilder::bind()` has been deprecated, the method above
should be used instead.

```php
final class SomeClass
{
    public static function namedConstructor(string $foo): self
    {
        // …
    }
}

(new \CuyZ\Valinor\MapperBuilder())
    ->registerConstructor(
        SomeClass::namedConstructor(...),
        // …or for PHP < 8.1:
        [SomeClass::class, 'namedConstructor'],
    )
    ->mapper()
    ->map(SomeClass::class, [
        // …
    ]);
```
2022-03-24 13:03:55 +01:00
Romain Canon
b646ccecf2 fix: handle variadic arguments in callable constructors 2022-03-17 21:58:38 +01:00
Romain Canon
e2451df2c1 misc: handle class name in function definition 2022-03-17 21:41:08 +01:00
Romain Canon
1a599b0bdf misc!: change Attributes::ofType return type to array
There was no benefits having the return type as `iterable`, but it would
make it harder to use the result of the method.
2022-03-17 21:15:11 +01:00
Romain Canon
fd11177b06 misc: introduce functions container to wrap definition handling 2022-03-17 21:12:16 +01:00
Romain Canon
fdef93074c fix: handle parameter default object value compilation 2022-03-09 10:33:40 +01:00
Lucian Olariu
e3592e18c5
test: strengthen datetime mapping tests 2022-03-03 13:02:01 +01:00
Baptiste Lafontaine
b8a18feadc
fix: handle numeric key with camel case source key modifier
Co-authored-by: Nathan Boiron <nathan.boiron@gmail.com>
2022-03-01 18:37:58 +01:00
Nathan Boiron
b7a7d22993
feat: introduce a path-mapping source modifier
This modifier can be used to change paths in the source data using a dot
notation.

The mapping is done using an associative array of path mappings. This
array must have the source path as key and the target path as value.

The source path uses the dot notation (eg `A.B.C`) and can contain one
`*` for array paths (eg `A.B.*.C`).

```php
final class Country
{
    /** @var City[] */
    public readonly array $cities;
}

final class City
{
    public readonly string $name;
}

$source = new \CuyZ\Valinor\Mapper\Source\Modifier\PathMapping([
    'towns' => [
        ['label' => 'Ankh Morpork'],
        ['label' => 'Minas Tirith'],
    ],
], [
    'towns' => 'cities',
    'towns.*.label' => 'name',
]);

// After modification this is what the source will look like:
[
    'cities' => [
        ['name' => 'Ankh Morpork'],
        ['name' => 'Minas Tirith'],
    ],
];

(new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(Country::class, $source);
```
2022-02-26 11:33:50 +01:00
Romain Canon
359e32d03d
fix: transform exception thrown during object binding into a message 2022-02-24 11:27:27 +01:00
Romain Canon
cbf4e11154 fix: remove string keys when unpacking variadic parameter values 2022-02-24 11:23:26 +01:00
Romain Canon
1eb6e61913 feat!: improve interface inferring API
The method `MapperBuilder::infer()` can be used to infer an
implementation for a given interface.

The callback given to this method must return the name of a class that
implements the interface. Any arguments can be required by the callback;
they will be mapped properly using the given source.

```php
$mapper = (new \CuyZ\Valinor\MapperBuilder())
    ->infer(UuidInterface::class, fn () => MyUuid::class)
    ->infer(SomeInterface::class, fn (string $type) => match($type) {
        'first' => FirstImplementation::class,
        'second' => SecondImplementation::class,
        default => throw new DomainException("Unhandled type `$type`.")
    })->mapper();

// Will return an instance of `FirstImplementation`
$mapper->map(SomeInterface::class, [
    'type' => 'first',
    'uuid' => 'a6868d61-acba-406d-bcff-30ecd8c0ceb6',
    'someString' => 'foo',
]);

// Will return an instance of `SecondImplementation`
$mapper->map(SomeInterface::class, [
    'type' => 'second',
    'uuid' => 'a6868d61-acba-406d-bcff-30ecd8c0ceb6',
    'someInt' => 42,
]);

interface SomeInterface {}

final class FirstImplementation implements SomeInterface
{
    public readonly UuidInterface $uuid;

    public readonly string $someString;
}

final class SecondImplementation implements SomeInterface
{
    public readonly UuidInterface $uuid;

    public readonly int $someInt;
}
```
2022-02-24 10:48:49 +01:00
Romain Canon
b6b3296638 feat: handle variadic parameters in constructors
Using variadic parameters is now handled properly by the library,
meaning the following example will run:

```php
final class SomeClass
{
    /** @var string[] */
    private array $values;

    public function __construct(string ...$values)
    {
        $this->values = $values;
    }
}

(new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(SomeClass::class, ['foo', 'bar', 'baz']);
```
2022-02-19 20:17:03 +01:00
Romain Canon
c1a884fadd qa: require and include phpstan/phpstan-phpunit rules 2022-02-19 19:58:28 +01:00
Romain Canon
6d427088f7 feat!: improve object binding API
The method `MapperBuilder::bind()` can be used to define a custom way to
build an object during the mapping.

The return type of the callback will be resolved by the mapping to know
when to use it.

The callback can take any arguments, that will automatically be mapped
using the given source. These arguments can then be used to instantiate
the object in the desired way.

Example:

```php
(new \CuyZ\Valinor\MapperBuilder())
    ->bind(function(string $string, OtherClass $otherClass): SomeClass {
        $someClass = new SomeClass($string);
        $someClass->addOtherClass($otherClass);

        return $someClass;
    })
    ->mapper()
    ->map(SomeClass::class, [
        // …
    ]);
```
2022-02-19 19:58:28 +01:00
Romain Canon
422e6a8b27 feat: improve value altering API 2022-02-19 19:58:28 +01:00
Romain Canon
380961247e feat: introduce method to get parameter by index 2022-02-19 19:58:28 +01:00
Romain Canon
d6e778aff7 refactor: regroup object inferring strategies
Inferring object unions and named constructor are now done using the
same algorithm — in class `ObjectBuilderFilterer` — which is called from
a unique entry point in `ClassNodeBuilder`.
2022-02-19 19:58:28 +01:00
Romain Canon
b49ebf37be feat: introduce function definition repository 2022-02-19 19:58:28 +01:00
Nathan Boiron
d9465222f4
feat: introduce a camel case source key modifier 2022-02-19 19:47:04 +01:00
mtouellette
18ccbebb9a
fix: ensure native mixed types remain valid 2022-02-16 15:50:14 +01:00
Marek Mikula
1b80a1df9d
fix: write temporary cache file inside cache subdirectory
Prevents potential issues with permissions on the system default 
temporary directory returned by `sys_get_temp_dir()`.
2022-02-15 23:24:15 +01:00
Fred-Jan van der Eijken
66aa4d688a
fix: return indexed list of attributes when filtering on type 2022-01-26 18:59:57 +01:00
Romain Canon
718d3c1bc2 feat: introduce automatic named constructor resolution
An object may have several ways of being created — in such cases it is
common to use so-called named constructors, also known as static factory
methods. If one or more are found, they can be called during the mapping
to create an instance of the object.

What defines a named constructor is a method that:

1. is public
2. is static
3. returns an instance of the object
4. has one or more arguments

```php
final class Color
{
    /**
     * @param int<0, 255> $red
     * @param int<0, 255> $green
     * @param int<0, 255> $blue
     */
    private function __construct(
        public readonly int $red,
        public readonly int $green,
        public readonly int $blue
    ) {}

    /**
     * @param int<0, 255> $red
     * @param int<0, 255> $green
     * @param int<0, 255> $blue
     */
    public static function fromRgb(
        int $red,
        int $green,
        int $blue,
    ): self {
        return new self($red, $green, $blue);
    }

    /**
     * @param non-empty-string $hex
     */
    public static function fromHex(string $hex): self
    {
        if (strlen($hex) !== 6) {
            throw new DomainException('Must be 6 characters long');
        }

        /** @var int<0, 255> $red */
        $red = hexdec(substr($hex, 0, 2));
        /** @var int<0, 255> $green */
        $green = hexdec(substr($hex, 2, 2));
        /** @var int<0, 255> $blue */
        $blue = hexdec(substr($hex, 4, 2));

        return new self($red, $green, $blue);
    }
}
```
2022-01-25 18:32:28 +01:00
Romain Canon
7869cbd09c refactor: remove unnecessary ClassSignature
This abstraction layer was not useful, so it is removed to simplify the
API around `ClassDefinition`.

A new method `ClassDefinition::type()` is also added, giving access to
the `ClassType` instance when working with a class definition.
2022-01-25 18:32:28 +01:00
Marco Pivetta
7a49f45175 test: configure vimeo/psalm to verify type inference
This is mostly to include type inference in regression tests: out-of-the
box type inference for 99% of usages is based on
`TreeMapper#map(class-string<T>, mixed): T`, and it is worth preserving
that behavior without the need of plugins.

This test covers that.
2022-01-13 19:55:20 +01:00
Romain Canon
79d7c266ec feat: introduce automatic union of objects inferring during mapping
When the mapper needs to map a source to a union of objects, it will try
to guess which object it will map to, based on the needed arguments of
the objects, and the values contained in the source.

```php
final class UnionOfObjects
{
    public readonly SomeFooObject|SomeBarObject $object;
}

final class SomeFooObject
{
    public readonly string $foo;
}

final class SomeBarObject
{
    public readonly string $bar;
}

// Will map to an instance of `SomeFooObject`
(new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(UnionOfObjects::class, ['foo' => 'foo']);

// Will map to an instance of `SomeBarObject`
(new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(UnionOfObjects::class, ['bar' => 'bar']);
```
2022-01-07 13:41:15 +01:00
Romain Canon
8a74147d4c misc!: allow object builder to yield arguments without source
The `Argument` class must now be instantiated with one of the `required`
or `optional` static constructors.
2022-01-07 13:41:15 +01:00
Romain Canon
47ecccee2b test: fix cache directory creation 2022-01-07 13:41:15 +01:00
Romain Canon
a97b406154 feat: introduce helper class MessagesFlattener
Will recursively flatten messages of a node and all its children.

This helper can for instance be used when errors occurred during a
mapping to flatten all caught errors into a basic array of string that
can then easily be used to inform the user of what is wrong.

```
try {
    // …
} catch(MappingError $error) {
    $messages = (new MessagesFlattener($error->node()))->errors();

    foreach ($messages as $message) {
        echo $message;
    }
}
```
2022-01-06 14:11:42 +01:00
Romain Canon
ddf69efaaa feat: introduce helper class MessageMapFormatter
Can be used to customize the content of messages added during a mapping.

An implementation is provided by the library — `MessageMapFormatter`:

The constructor parameter is an array where each key represents either:
- The code of the message to be replaced
- The content of the message to be replaced
- The class name of the message to be replaced

If none of those is found, the content of the message will stay
unchanged unless a default one is given to this class.

If one of these keys is found, the array entry will be used to replace
the content of the message. This entry can be either a plain text or a
callable that takes the message as a parameter and returns a string; it
is for instance advised to use a callable in cases where a translation
service is used — to avoid useless greedy operations.

In any case, the content can contain placeholders that can be used the
same way as `\CuyZ\Valinor\Mapper\Tree\Message\NodeMessage::format()`.

See usage examples below:

```
$formatter = (new MessageMapFormatter([
    // Will match if the given message has this exact code
    'some_code' => 'new content / previous code was: %1$s',

    // Will match if the given message has this exact content
    'Some message content' => 'new content / previous message: %2$s',

    // Will match if the given message is an instance of this class
    SomeError::class => '
        - Original code of the message: %1$s
        - Original content of the message: %2$s
        - Node type: %3$s
        - Node name: %4$s
        - Node path: %5$s
    ',

    // A callback can be used to get access to the message instance
    OtherError::class => function (NodeMessage $message): string {
        if ((string)$message->type() === 'string|int') {
            // …
        }

        return 'Some message content';
    },

    // For greedy operation, it is advised to use a lazy-callback
    'bar' => fn () => $this->translator->translate('foo.bar'),
]))
    ->defaultsTo('some default message')
    // …or…
    ->defaultsTo(fn () => $this->translator->translate('default'));

$content = $formatter->format($message);
```
2022-01-06 14:11:42 +01:00
Romain Canon
cc1bc66bbe feat: introduce helper NodeTraverser for recursive operations on nodes 2022-01-06 14:11:42 +01:00
Romain Canon
2fd0bc10d0 test: fix wrong namespace for some classes 2022-01-06 14:11:42 +01:00
Romain Canon
a805ba0442 feat!: wrap node messages in proper class
A new class `NodeMessage` is used to wrap messages added to a node
during the mapping. This class will allow further features by giving
access to useful data related to the bound node.

BREAKING CHANGE: as of now every message is wrapped into a `NodeMessage`
it is therefore not possible to check whether the message is an instance
of `Throwable` — a new method `$message->isError()` is now to be used
for such cases.
2022-01-06 14:11:42 +01:00
Romain Canon
2c7e1156db test: improve FakeType and introduce FakeNode 2022-01-06 14:11:42 +01:00
Romain Canon
b2e810e3ce feat!: allow mapping to any type
Previously, the method `TreeMapper::map` would allow mapping only to an
object. It is now possible to map to any type handled by the library.

It is for instance possible to map to an array of objects:

```php
$objects = (new \CuyZ\Valinor\MapperBuilder())->mapper()->map(
    'array<' . SomeClass::class . '>',
    [/* … */]
);
```

For simple use-cases, an array shape can be used:

```php
$array = (new \CuyZ\Valinor\MapperBuilder())->mapper()->map(
    'array{foo: string, bar: int}',
    [/* … */]
);

echo strtolower($array['foo']);
echo $array['bar'] * 2;
```

This new feature changes the possible behaviour of the mapper, meaning
static analysis tools need help to understand the types correctly. An
extension for PHPStan and a plugin for Psalm are now provided and can be
included in a project to automatically increase the type coverage.
2022-01-02 00:48:01 +01:00