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);
```
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;
}
```
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']);
```
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, [
// …
]);
```
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`.
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);
}
}
```
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.
This also brings `TreeMapper#map()` closer to PHPStorm support, whilst
still benefiting from more precise `vimeo/psalm:4.18.x` types thanks to
the conditional return specification.
Ref: https://github.com/CuyZ/Valinor/pull/59#discussion_r782362428
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.
This change filters the scope of the public API that is provided by this
library.
Any class or interface that is not explicitly marked with an `@api`
annotation should never be used outside this library — any change can
and will be made without taking breaking changes rules into account.
When a breaking change happens inside the public API scope, a major
version will be released; refer to https://semver.org for more
information.
A new PHPStan extension is now part of the quality assurance process,
ensuring that all classes/interfaces must provide either `@internal` or
`@api` annotation.
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']);
```
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;
}
}
```
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);
```
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.