feat: add a bunch of mapper/normalizer configurators - #829
Merged
Conversation
Renames the key of a property during normalization. This is useful when
the name of a property in the data format differs from the one used in
the PHP codebase.
```php
use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeyTo;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;
final readonly class Address
{
public function __construct(
public string $street,
public string $zipCode,
#[NormalizeKeyTo('town')]
public string $city,
) {}
}
$addressAsArray = (new NormalizerBuilder())
->normalizer(Format::array())
->normalize(new Address('221B Baker Street', 'NW1 6XE', 'London'));
// [
// 'street' => '221B Baker Street',
// 'zipCode' => 'NW1 6XE',
// 'town' => 'London',
// ]
```
The manual "Renaming properties" normalizer documentation example now
points to this configurator chapter.
Flattens objects that hold a single property, so that instead of
`['someProperty' => 'value']` the normalized result is simply `'value'`.
This class can be used either as a configurator for global usage or as
an attribute to target a specific class or property.
Global usage as a configurator
------------------------------
```php
use CuyZ\Valinor\Normalizer\Configurator\NormalizeToSingleValue;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;
final readonly class Email
{
public function __construct(
public string $email,
) {}
}
// Every object with a single property is flattened
$value = (new NormalizerBuilder())
->configureWith(new NormalizeToSingleValue())
->normalizer(Format::array())
->normalize(new Email('john.doe@example.com'));
// 'john.doe@example.com'
```
Local usage as an attribute
---------------------------
```php
use CuyZ\Valinor\Normalizer\Configurator\NormalizeToSingleValue;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;
final readonly class Email
{
public function __construct(
public string $email,
) {}
}
// Only the targeted property is flattened
final readonly class User
{
public function __construct(
public string $name,
#[NormalizeToSingleValue]
public Email $email,
) {}
}
$userAsArray = (new NormalizerBuilder())
->normalizer(Format::array())
->normalize(new User('John Doe', new Email('john.doe@example.com')));
// [
// 'name' => 'John Doe',
// 'email' => 'john.doe@example.com',
// ]
```
The manual "Flattening single property objects" normalizer documentation
example now points to this configurator chapter.
Excludes a property from the normalized output, for instance to hide
sensitive data such as a password.
For the attribute to take effect, an instance of this class must also be
registered on the normalizer builder via `configureWith()`. Without this
registration the property is not ignored.
```php
use CuyZ\Valinor\Normalizer\Configurator\IgnoreOnNormalization;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;
final readonly class User
{
public function __construct(
public string $name,
#[IgnoreOnNormalization]
public string $password,
) {}
}
$userAsArray = (new NormalizerBuilder())
->configureWith(new IgnoreOnNormalization())
->normalizer(Format::array())
->normalize(new User('John Doe', 's3cr3t'));
// ['name' => 'John Doe']
```
Shaped arrays and HTTP requests now resolve through a single KeyConverterNodeBuilder -> ShapedArrayNodeBuilder -> HttpRequestNodeBuilder chain instead of branching on the value type in TypeNodeBuilder, with key conversion always wired in.
Source keys can now be remapped to a specific property or argument using
attributes: either the provided `MapFromKey` attribute or any custom
attribute implementing the key-mapping protocol.
`MapFromKey` feeds a class property, or a constructor/method argument,
from a specific source key instead of matching it against the property
name. This is useful when the source data uses a key that differs from
the name of the property it should be mapped to:
```php
use CuyZ\Valinor\Mapper\Configurator\MapFromKey;
use CuyZ\Valinor\MapperBuilder;
final readonly class Person
{
public function __construct(
public string $name,
#[MapFromKey('zipCode')]
public string $postalCode,
) {}
}
$person = (new MapperBuilder())
->mapper()
->map(Person::class, [
'name' => 'John Doe',
'zipCode' => '75001',
]);
// $person->postalCode === '75001';
```
The given key is read as-is: it is not affected by the key converters
registered with `registerKeyConverter()`, and the property name is no
longer accepted, the source is read only from the given key. It also
works when mapping an HTTP request, alongside the `#[FromRoute]`,
`#[FromQuery]` and `#[FromBody]` attributes.
More generally, any attribute class declaring a `mapKey(string): string`
method, and flagged with `#[AsConverter]`, can remap the key of the
element it is placed on, for instance to factor out a recurring
transformation such as a shared prefix:
```php
final class MapWithPrefix
{
public function __construct(private string $prefix) {}
public function mapKey(string $key): string
{
return $this->prefix . $key;
}
}
```
Converts the given string and integer representations to a real `bool`
before mapping. By default `1`, `'1'` and `'true'` are converted to
`true`, and `0`, `'0'` and `'false'` to `false`.
```php
use CuyZ\Valinor\Mapper\Configurator\MapAsBool;
use CuyZ\Valinor\MapperBuilder;
final readonly class User
{
public function __construct(
public string $name,
#[MapAsBool]
public bool $isActive,
) {}
}
$user = (new MapperBuilder())
->mapper()
->map(User::class, [
'name' => 'John Doe',
'isActive' => 'true', // mapped to `true`
]);
```
The accepted representations can be customized, for instance to also
recognize `'on'` and `'off'`:
```php
#[MapAsBool(true: ['on', 'yes'], false: ['off', 'no'])]
public bool $isActive;
```
The manual "Casting to boolean" converter example now points to this
configurator chapter.
Converts a string representation of an integer to a real `int` before
mapping. Any value that is not a valid integer representation is left
untouched and handed over to the mapper, which will raise an error if it
cannot be mapped to an integer.
```php
use CuyZ\Valinor\Mapper\Configurator\MapAsInt;
use CuyZ\Valinor\MapperBuilder;
final readonly class User
{
public function __construct(
public string $name,
#[MapAsInt]
public int $age,
) {}
}
$user = (new MapperBuilder())
->mapper()
->map(User::class, [
'name' => 'John Doe',
'age' => '42', // mapped to `42`
]);
```
The manual "Casting to integer" converter example now points to this
configurator chapter.
Converts a string representation of a number to a real `float` before
mapping. Any value that is not a valid number representation is left
untouched and handed over to the mapper, which will raise an error if it
cannot be mapped to a float.
```php
use CuyZ\Valinor\Mapper\Configurator\MapAsFloat;
use CuyZ\Valinor\MapperBuilder;
final readonly class Product
{
public function __construct(
public string $name,
#[MapAsFloat]
public float $price,
) {}
}
$product = (new MapperBuilder())
->mapper()
->map(Product::class, [
'name' => 'Coffee',
'price' => '4.50', // mapped to `4.5`
]);
```
The manual "Casting to float" converter example now points to this
configurator chapter.
Converts an integer or a float to a `string` before mapping. This is
useful when the input data carries numbers that must be handled as
strings, for instance an identifier or a postal code.
```php
use CuyZ\Valinor\Mapper\Configurator\MapAsString;
use CuyZ\Valinor\MapperBuilder;
final readonly class User
{
public function __construct(
public string $name,
#[MapAsString]
public string $id,
) {}
}
$user = (new MapperBuilder())
->mapper()
->map(User::class, [
'name' => 'John Doe',
'id' => 42, // mapped to `'42'`
]);
```
The manual "Casting to string" converter example now points to this
configurator chapter.
Parses the input string using the given date format before mapping. This
is useful when the input data carries a date in a specific format that
the mapper would not otherwise recognize.
The format must follow the syntax supported by
`DateTimeImmutable::createFromFormat()`. A value that does not match the
given format raises a mapping error.
```php
use CuyZ\Valinor\Mapper\Configurator\MapToDateTimeFromFormat;
use CuyZ\Valinor\MapperBuilder;
use DateTimeInterface;
final readonly class Event
{
public function __construct(
public string $name,
#[MapToDateTimeFromFormat('d/m/Y')]
public DateTimeInterface $date,
) {}
}
$event = (new MapperBuilder())
->mapper()
->map(Event::class, [
'name' => 'Release of legendary album',
'date' => '08/11/1971', // mapped to a `DateTimeImmutable`
]);
```
The error message raised on failure is registered in the default
translations, and the manual "Custom datetime format" converter example
now points to this configurator chapter.
Discards the keys of an array and maps its values to a list before
mapping. This is useful when the input data is an associative array, or a
sparse list with missing or out-of-order indices, that should be handled
as a sequential list.
```php
use CuyZ\Valinor\Mapper\Configurator\MapArrayToList;
use CuyZ\Valinor\MapperBuilder;
final readonly class Basket
{
public function __construct(
/** @var list<string> */
#[MapArrayToList]
public array $products,
) {}
}
$basket = (new MapperBuilder())
->mapper()
->map(Basket::class, [
'a' => 'Coffee',
'b' => 'Tea',
]); // mapped to `['Coffee', 'Tea']`
```
The manual "Array to list" converter example now points to this
configurator chapter.
Decodes a JSON string and hands the result over to the mapper. This is
useful when the input data carries a nested structure as an encoded JSON
string, for instance a column stored in a database or a field in a form
submission.
The decoded value is then mapped against the targeted type, so the usual
validation and error reporting still apply. An invalid JSON string raises
a mapping error.
```php
use CuyZ\Valinor\Mapper\Configurator\MapFromJson;
use CuyZ\Valinor\MapperBuilder;
final readonly class User
{
public function __construct(
public string $name,
/** @var list<string> */
#[MapFromJson]
public array $roles,
) {}
}
$user = (new MapperBuilder())
->mapper()
->map(User::class, [
'name' => 'John Doe',
'roles' => '["admin", "editor"]', // mapped to `['admin', 'editor']`
]);
```
The error message raised on invalid JSON is registered in the default
translations, and the manual "Json decode" converter example now points
to this configurator chapter.
Explodes a string into a list using the given separator before mapping.
This is useful when the input data carries a list as a single delimited
string, for instance a comma-separated value coming from a CSV file or a
query parameter.
The resulting list is then mapped against the targeted type, so the items
can be cast further, for instance to a `list<int>`.
```php
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Mapper\Configurator\MapExplodedStringToList;
final readonly class Product
{
public function __construct(
public string $name,
#[MapExplodedStringToList(separator: ',')]
/** @var list<string> */
public array $sizes,
) {}
}
$product = (new MapperBuilder())
->mapper()
->map(Product::class, [
'name' => 'T-Shirt',
'sizes' => 'XS,S,M,L,XL', // mapped to `['XS', 'S', 'M', 'L', 'XL']`
]);
```
� Conflicts: � src/Type/Types/BooleanValueType.php � src/Type/Types/ClassStringType.php � src/Type/Types/FloatValueType.php � src/Type/Types/IntegerRangeType.php � src/Type/Types/IntegerValueType.php � src/Type/Types/NativeBooleanType.php � src/Type/Types/NativeFloatType.php � src/Type/Types/NativeIntegerType.php � src/Type/Types/NativeStringType.php � src/Type/Types/NegativeIntegerType.php � src/Type/Types/NonEmptyStringType.php � src/Type/Types/NonNegativeIntegerType.php � src/Type/Types/NonPositiveIntegerType.php � src/Type/Types/NumericStringType.php � src/Type/Types/PositiveIntegerType.php � src/Type/Types/ScalarConcreteType.php � src/Type/Types/StringValueType.php
The converter examples were only pointing to the provided mapper configurators and are dropped entirely. Most transformer examples likewise pointed to provided normalizer configurators. The two remaining ones that cannot be provided out-of-the-box, custom object normalization and API versioning, are moved into the provided normalizer configurators page and clearly marked as custom, non-provided examples.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Provided mapper configurators
A set of configurators is now available out-of-the-box for the mapper, mirroring the normalizer configurators introduced in the previous release. Each one can be used either globally through the
configureWith()method or locally as an attribute targeting a specific property.The
MapToDateTimeFromFormatconfigurator parses the input string using the given date format, which must follow the syntax supported byDateTimeImmutable::createFromFormat():The
MapExplodedStringToListconfigurator explodes a string into a list using the given separator, which is useful when the input carries a list as a single delimited string, for instance a value coming from a CSV file or a query parameter:The
MapArrayToListconfigurator discards the keys of an array and maps its values to a list, for cases where the input is an associative array, or a sparse list with missing or out-of-order indices, that should be handled as a sequential list:Finally, the
MapFromJsonconfigurator decodes a JSON string and hands the result over to the mapper, so that the usual validation and error reporting still apply to the decoded value:Scalar value casting
Four configurators convert a scalar value to a specific type before mapping:
MapAsBool,MapAsInt,MapAsFloatandMapAsString. They are useful when the input data carries values in a different representation than the targeted type, for instance numbers or booleans encoded as strings in a form submission, a CSV file or a JSON payload.Used as an attribute, a single property is cast, leaving the strictness rules untouched for every other value:
Casting can also be enabled for every value of a given type with the new
allowCastingToBoolean(),allowCastingToInteger(),allowCastingToFloat()andallowCastingToString()methods of the mapper builder. They offer a finer control thanallowScalarValueCasting(), which relaxes strictness for all scalar types at once:Mapping a property from a specific key
The new
MapFromKeyattribute feeds a class property, or a constructor/method argument, from a specific source key instead of matching it against the property name:This attribute is built on a lightweight protocol that is open to userland: any attribute class declaring a
mapKey(string $key): stringmethod and carrying the#[AsConverter]attribute can remap the key of the element it is placed on. This is handy to factor out a recurring transformation, such as a prefix shared by several properties:New normalizer configurators
Three configurators join the ones introduced in the previous release.
The
NormalizeKeyToattribute renames the key of a property during normalization, when the name used in the data format differs from the one used in the PHP codebase:The
NormalizeToSingleValueclass flattens an object holding a single property, so that instead of['someProperty' => 'value']the normalized result is simply'value'. It can be used either as a configurator, applying to every object with a single property, or as an attribute targeting a specific class or property:The
IgnoreOnNormalizationattribute excludes a property from the normalized output, for instance to hide sensitive data such as a password. For the attribute to take effect, an instance of this class must also be registered on the builder viaconfigureWith():