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.
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.
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.
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`.
Type aliases can now be imported from another class definition.
Both PHPStan and Psalm syntax are handled.
```php
/**
* @phpstan-type SomeTypeAlias = array{foo: string}
*/
final class SomeClass
{
/** @var SomeTypeAlias */
public array $someTypeAlias;
}
/**
* @phpstan-import-type SomeTypeAlias from SomeClass
*/
final class SomeOtherClass
{
/** @var SomeTypeAlias */
public array $someTypeAlias;
}
```
Type aliases can now be added to a class definition.
Both PHPStan and Psalm syntax are handled.
```php
/**
* @phpstan-type SomeTypeAlias = array{foo: string}
* @psalm-type SomeOtherTypeAlias = array{bar: int}
*/
final class SomeClass
{
/** @var SomeTypeAlias */
public array $someTypeAlias;
/** @var SomeOtherTypeAlias */
public array $someOtherTypeAlias;
}
```