Skip to content

Using provided normalizer configurators

This library provides a set of normalizer configurators out-of-the-box that can be used to apply common normalization behaviors:

Specifying date time normalization format

By default, dates will be formatted using the RFC 3339 format. The NormalizeDateTimeFormat configurator can be used to specify which format to use.

This class can be used either as a configurator for global usage or as an attribute to target a specific property.

Global usage as a configurator

use CuyZ\Valinor\Normalizer\Configurator\NormalizeDateTimeFormat;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;

$userAsArray = (new NormalizerBuilder())
    ->configureWith(new NormalizeDateTimeFormat(\DateTimeInterface::ATOM))
    ->normalizer(Format::array())
    ->normalize($user);

// [
//     'name' => 'Jane Doe',
//     'createdAt' => '2000-01-01T00:00:00+00:00',
// ]

Targeted usage as an attribute

use CuyZ\Valinor\Normalizer\Configurator\NormalizeDateTimeFormat;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;

final readonly class User
{
    public function __construct(
        public string $name,

        #[NormalizeDateTimeFormat(\DateTimeInterface::ATOM)]
        public DateTimeInterface $createdAt,
    ) {}
}

$userAsArray = (new NormalizerBuilder())
    ->normalizer(Format::array())
    ->normalize($user);

// [
//     'name' => 'Jane Doe',
//     'createdAt' => '2000-01-01T00:00:00+00:00',
// ]

Converting key case

Several configurators convert the keys of normalized objects to a different naming convention than the one used in the PHP codebase.

Configurator Result
NormalizeKeysToSnakeCase first_name
NormalizeKeysToCamelCase firstName
NormalizeKeysToPascalCase FirstName
NormalizeKeysToKebabCase first-name

Each of these classes can be used either as a configurator for global usage or as an attribute to target a specific class.

Global usage as a configurator

The keys of every normalized object are converted to the target case:

use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeysToSnakeCase;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;

$userAsArray = (new NormalizerBuilder())
    ->configureWith(new NormalizeKeysToSnakeCase())
    ->normalizer(Format::array())
    ->normalize($user);

// [
//     'first_name' => 'John',
//     'last_name' => 'Doe',
// ]

Targeted usage as an attribute

When used as an attribute, only the keys of the targeted class are converted, leaving the rest of the output untouched:

use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeysToSnakeCase;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;

#[NormalizeKeysToSnakeCase]
final readonly class User
{
    public function __construct(
        public string $firstName,
        public string $lastName,
    ) {}
}

$userAsArray = (new NormalizerBuilder())
    ->normalizer(Format::array())
    ->normalize($user);

// [
//     'first_name' => 'John',
//     'last_name' => 'Doe',
// ]

Renaming property keys

The name of a property in the data format may differ from the one used in the PHP codebase. The NormalizeKeyTo attribute renames the key of a property during normalization.

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(
            street: '221B Baker Street',
            zipCode: 'NW1 6XE',
            city: 'London', // Key will be renamed to 'town'
        )
    );

// [
//     'street' => '221B Baker Street',
//     'zipCode' => 'NW1 6XE',
//     'town' => 'London',
// ]

Flattening single property objects

When an object holds a single property, it may be useful to flatten it so that instead of ['someProperty' => 'value'] the normalized result is simply 'value'.

The NormalizeToSingleValue 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

When used as a configurator, every object with a single property is flattened:

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,
    ) {}
}

$value = (new NormalizerBuilder())
    ->configureWith(new NormalizeToSingleValue())
    ->normalizer(Format::array())
    ->normalize(new Email('john.doe@example.com'));

// 'john.doe@example.com'

Targeted usage as an attribute

When used as an attribute, only the targeted class or property is flattened, leaving the rest of the output untouched:

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,
    ) {}
}

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',
// ]

Ignoring properties

A property can be excluded from the normalized output, for instance to hide sensitive data such as a password, by marking it with the IgnoreOnNormalization attribute.

Warning

For the attribute to take effect, an IgnoreOnNormalization instance must also be registered on the builder via configureWith(). Without it, the property value is replaced by a placeholder object that raises an exception as soon as it is used (for instance when it is cast to a string or encoded to JSON), pointing to the missing registration.

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,
    ) {}
}

// Registering the configurator is required for the attribute to take effect.
$userAsArray = (new NormalizerBuilder())
    ->configureWith(new IgnoreOnNormalization())
    ->normalizer(Format::array())
    ->normalize(new User('John Doe', 's3cr3t'));

// ['name' => 'John Doe']

Custom transformer examples

Unlike the configurators above, the following behaviors are not provided out-of-the-box: they illustrate real-life use-cases whose logic is specific to each application. They are shown here as inspiration and must be implemented with a custom transformer registered on the NormalizerBuilder, see the extending the normalizer chapter.

Transforming objects

Some objects can have custom behaviors during normalization, for instance properties may need to be remapped. In the example below, a transformer will check if an object defines a normalize method and use it if it exists.

Show code example — Custom object normalization
namespace My\App;

final readonly class Address
{
    public function __construct(
        public string $road,
        public string $zipCode,
        public string $town,
    ) {}

    public function normalize(): array
    {
        return [
            'street' => $this->road,
            'postalCode' => $this->zipCode,
            'city' => $this->town,
        ];
    }
}

(new \CuyZ\Valinor\NormalizerBuilder())
    ->registerTransformer(function (object $object, callable $next) {
        return method_exists($object, 'normalize')
            ? $object->normalize()
            : $next();
    })
    ->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
    ->normalize(
        new \My\App\Address(
            road: '221B Baker Street',
            zipCode: 'NW1 6XE',
            town: 'London',
        ),
    );

// [
//     'street' => '221B Baker Street',
//     'postalCode' => 'NW1 6XE',
//     'city' => 'London',
// ]

Versioning API

API versioning can be implemented with different strategies and algorithms. The example below shows how objects can implement an interface to specify their own specific versioning behavior.

Show code example — Versioning objects
namespace My\App;

interface HasVersionedNormalization
{
    public function normalizeWithVersion(string $version): mixed;
}

final readonly class Address implements \My\App\HasVersionedNormalization
{
    public function __construct(
        public string $streetNumber,
        public string $streetName,
        public string $zipCode,
        public string $city,
    ) {}

    public function normalizeWithVersion(string $version): array
    {
        return match (true) {
            version_compare($version, '1.0.0', '<') => [
                // Street number and name are merged in a single property
                'street' => "$this->streetNumber, $this->streetName",
                'zipCode' => $this->zipCode,
                'city' => $this->city,
            ],
            default => get_object_vars($this),
        };
    }
}

function normalizeWithVersion(string $version): mixed
{
    return (new \CuyZ\Valinor\NormalizerBuilder())
        ->registerTransformer(
            fn (\My\App\HasVersionedNormalization $object) => $object->normalizeWithVersion($version)
        )
        ->normalizer(\CuyZ\Valinor\Normalizer\Format::array())
        ->normalize(
            new \My\App\Address(
                streetNumber: '221B',
                streetName: 'Baker Street',
                zipCode: 'NW1 6XE',
                city: 'London',
            )
        );
}

// Version can come for instance from HTTP request headers
$result_v0_4 = normalizeWithVersion('0.4');
$result_v1_8 = normalizeWithVersion('1.8');

// $result_v0_4 === [
//     'street' => '221B, Baker Street',
//     'zipCode' => 'NW1 6XE',
//     'city' => 'London',
// ]
// 
// $result_v1_8 === [
//     'streetNumber' => '221B',
//     'streetName' => 'Baker Street',
//     'zipCode' => 'NW1 6XE',
//     'city' => 'London',
// ]