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);
}
}
```
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']);
```
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.