vudaltsov
Repos
47
Followers
217
Following
65

The Symfony PHP framework

27459
8581

Events

closed issue
[Serializer][RFC] Context object as an alternative to array builders

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.

image

Created at 2 weeks ago
closed issue
[Serializer][RFC] Context object as an alternative to array builders

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.

image

Created at 2 weeks ago
Created at 2 weeks ago
Created at 3 weeks ago
issue comment
Build dir is different on subsequent runs of composer install

Seems like issues #46176, #39254 are about the same problem.

@jderusse, may I attract your attention to this problem, since you are the author of #39360?

Created at 1 month ago
Created at 1 month ago

Logging в сборке передвинут наверх

Поправлен текст Request.title в сборке

Created at 1 month ago

Добавил лицензию MIT

Created at 1 month ago
vudaltsov create branch master
Created at 1 month ago
vudaltsov create repository
Created at 1 month ago
issue comment
[4.25.0] InvalidArgument for result of a method with a static return type

@AndrolGenhald, no, it gives the same error. Anyway, in a final class it's valid to return new self in a : static method.

Created at 1 month ago
issue comment
Update Psalm version on psalm.dev

Ok, thank you for the explanations!

Created at 1 month ago
opened issue
Update Psalm version on psalm.dev
Created at 1 month ago
issue comment
[4.25.0] InvalidArgument for result of a method with a static return type

Oh, I see, psalm.dev is at 7c4228f, not d7cd84c.

Created at 1 month ago
opened issue
[4.25.0] InvalidArgument for result of a method with a static return type
<?php

declare(strict_types=1);

namespace HappyInc;

abstract class A
{
}

final class B extends A
{
    public static function create(): static
    {
        return new self();
    }
}

final class Service
{
    public function do(): void
    {
        $this->acceptA(B::create());
    }

    private function acceptA(A $_a): void
    {
    }
}

gives

ERROR: InvalidArgument - src/A.php:23:24 - Argument 1 of HappyInc\Service::acceptA expects HappyInc\A, HappyInc\Service provided (see https://psalm.dev/004)
        $this->acceptA(B::create());

in 4.25.0.

By the way, for some reason https://psalm.dev/r/6081a71dec says "No issues!".

Created at 1 month ago
Created at 2 months ago