Commit Graph

16 Commits

Author SHA1 Message Date
afcedf9e56
feat: handle literal boolean true / false types
Allows the usage of boolean values, as follows:

```php
class Foo
{
    /** @var int|false */
    public readonly int|bool $value;
}
```
2022-05-09 21:14:46 +02:00
Romain Canon
790df8a3b8 feat: handle float value type
Allows the usage of float values, as follows:

```php
class Foo
{
    /** @var 404.42|1337.42 */
    public readonly float $value;
}
```
2022-05-09 19:17:22 +02:00
Romain Canon
6e6b9746da doc: add documentation for integer/string literal values 2022-05-09 19:17:22 +02:00
Romain Canon
b7923bc383 feat: handle class string of union of object 2022-04-06 18:18:17 +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
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
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
Nathan Boiron
d9465222f4
feat: introduce a camel case source key modifier 2022-02-19 19:47:04 +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
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
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
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
Romain Canon
54f608e5b1 feat!: add access to root node when error occurs during mapping
When an error occurs during mapping, the root instance of `Node` can now
be accessed from the exception. This recursive object allows retrieving
all needed information through the whole mapping tree: path, values,
types and messages, including the issues that caused the exception.

It can be used like the following:

```php
try {
   (new \CuyZ\Valinor\MapperBuilder())
       ->mapper()
       ->map(SomeClass::class, [/* ... */]);
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    // Do something with `$error->node()`
    // See README for more information
}
```

This change removes the method `MappingError::describe()` which provided
a flattened view of messages of all the errors that were encountered
during the mapping. The same behaviour can still be retrieved, see the
example below:

```php
use CuyZ\Valinor\Mapper\Tree\Message\Message;
use CuyZ\Valinor\Mapper\Tree\Node;

/**
 * @implements \IteratorAggregate<string, array<\Throwable&Message>>
 */
final class MappingErrorList implements \IteratorAggregate
{
    private Node $node;

    public function __construct(Node $node)
    {
        $this->node = $node;
    }

    /**
     * @return \Traversable<string, array<\Throwable&Message>>
     */
    public function getIterator(): \Traversable
    {
        yield from $this->errors($this->node);
    }

    /**
     * @return \Traversable<string, array<\Throwable&Message>>
     */
    private function errors(Node $node): \Traversable
    {
        $errors = array_filter(
            $node->messages(),
            static fn (Message $m) => $m instanceof \Throwable
        );

        if (! empty($errors)) {
            yield $node->path() => array_values($errors);
        }

        foreach ($node->children() as $child) {
            yield from $this->errors($child);
        }
    }
}

try {
   (new \CuyZ\Valinor\MapperBuilder())
       ->mapper()
       ->map(SomeClass::class, [/* ... */]);
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    $errors = iterator_to_array(new MappingErrorList($error->node()));
}
```

The class `CannotMapObject` is deleted, as it does not provide any
value; this means that `MappingError` which was previously an interface
becomes a class.
2021-12-27 13:52:36 +01:00
Romain Canon
9f99a2a1ef feat: handle integer range type
Integer range can be used as follows:

```php
final class SomeClass
{
    /** @var int<42, 1337> */
    public int $intRange; // accepts any int between 42 and 1337

    /** @var int<-1337, 1337> */
    public int $negativeIntRange; // also works with negative values

    /** @var int<min, 1337> */
    public int $minIntRange; // `min` can be used…

    /** @var int<0, max> */
    public int $maxIntRange; // …as well as `max`
}
```

Note that `min` and `max` will check the range with PHP's internal
constants `PHP_INT_MIN` and `PHP_INT_MAX`.
2021-12-07 18:20:25 +01:00
Romain Canon
396f64a524 feat: initial release
🎉
2021-11-28 18:21:56 +01:00