Description
Description
I have an alternative proposal for Serializer context (replaces #43973 for #30818). It requires new contracts for serializer and normalizer, but my approach allows to combine different structured contexts easily without array conversion. Both client and library will work with objects instead of arrays, so offset constants and array processing could be dropped.
Criterion | Builders (#43973) | Context object (this) |
---|---|---|
Reuses existing contracts | ✅ | ❌ |
Always deals with structured data | ❌ | ✅ |
Code maintainability / Simplicity of adding a new normalizer | ❌ Offset constants in the normalizer class, defaults processing, builders |
✅ Only ContextItem implementation |
IDE/Autocomplete support | 🟨 Partial, since arrays are not structured |
✅ |
The idea is to have a Context
class that is a collection of individual context objects, indexed by class. Each context piece must provide a default
factory method that is called in case the context item was not explicitly passed. Individual context items cannot be mutated or replaced once they are instantiated within a serializer/normalizer call to match the current array behavior, only a new Context
instance can be created via constructor or a with
method. The rest is easier to explain with the code:
/**
* @psalm-immutable
*/
interface ContextItem
{
public static function default(): static;
}
/**
* @psalm-immutable
*/
final class Context
{
/**
* @var array<class-string<ContextItem>, ContextItem>
*/
private array $items = [];
public function __construct(
ContextItem ...$items,
) {
foreach ($items as $item) {
$this->items[$item::class] = $item;
}
}
/**
* @template T of ContextItem
* @param class-string<T> $class
* @return T
*/
public function get(string $class): ContextItem
{
/** @var T */
return $this->items[$class] ?? $class::default();
}
public function with(ContextItem $item): self
{
$context = clone $this;
$context->items[$item::class] = $item;
return $context;
}
}
interface NewNormalizerInterface
{
public function normalize(mixed $data, string $format = null, Context $context = new Context()): mixed;
public function supportsNormalization(mixed $data, string $format = null, Context $context = new Context()): bool;
}
final class DateTimeNormalizerContext implements ContextItem
{
public function __construct(
public string $format = 'Y-m-d',
) {
}
public static function default(): static
{
return new self();
}
}
final class DateTimeNormalizer implements NewNormalizerInterface
{
public function normalize(mixed $data, string $format = null, Context $context = new Context()): mixed
{
$format = $context->get(DateTimeNormalizerContext::class)->format;
// ...
}
public function supportsNormalization(mixed $data, string $format = null, Context $context = new Context()): bool
{
// ...
}
}
$serializer->serialize(/** */, new Context(new DateTimeNormalizerContext('Y-m-d H:i')));
// or
$context = new Context();
$serializer->serialize(/** */, $context->with(new DateTimeNormalizerContext('Y-m-d H:i'));
NewNormalizerInterface
is not a name proposal :) I called it so for simplicity.
BC layer ideas
We can add a new interface that supports both array
and Context
parameter types, using the union operator, and meanwhile merge ContextAwareNormalizerInterface
to reduce the number of contracts in the future.
final class Context
{
}
/**
* @deprecated use NewNormalizerInterface instead
*/
interface NormalizerInterface
{
public function normalize(mixed $object, string $format = null, array $context = []);
public function supportsNormalization(mixed $data, string $format = null);
}
/**
* @deprecated use NewNormalizerInterface instead
*/
interface ContextAwareNormalizerInterface extends NormalizerInterface
{
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool;
}
interface NewNormalizerInterface extends ContextAwareNormalizerInterface
{
public function normalize(mixed $data, string $format = null, array|Context $context = []): mixed;
public function supportsNormalization(mixed $data, string $format = null, array|Context $context = []): bool;
}
final class DateTimeNormalizer implements NewNormalizerInterface
{
public function normalize(mixed $object, string $format = null, array|Context $context = []): mixed
{
// ...
}
public function supportsNormalization(mixed $data, string $format = null, array|Context $context = []): bool
{
// ...
}
}
new DateTimeNormalizer();
It works, see https://3v4l.org/YmEbP.
Also a helper method Context::fromArray()
can be introduced to simplify deprecated array context support.
IDE support
PhpStorm supports Context::get() autocompletion.