Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Serializer][RFC] Context object as an alternative to array builders #45330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
vudaltsov opened this issue Feb 6, 2022 · 9 comments
Closed

Comments

@vudaltsov
Copy link
Contributor

vudaltsov commented Feb 6, 2022

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

@vudaltsov vudaltsov changed the title [Serializer][WIP] Alternative context refactoring [Serializer][WIP] Context object as an alternative to array builders Feb 6, 2022
@vudaltsov
Copy link
Contributor Author

vudaltsov commented Feb 9, 2022

@dunglas, may I attract your attention to this RFC?

@vudaltsov vudaltsov changed the title [Serializer][WIP] Context object as an alternative to array builders [Serializer][RFC] Context object as an alternative to array builders Feb 11, 2022
@nicolas-grekas
Copy link
Member

I would very much prefer to not change the interfaces again. Using array here makes sense from a component pov also.

But what about allowing builders directly into the $context variable, indexed by numbers?

$context[] = (new DateTimeNormalizerContext())->etc.? This could replace context builders maybe?

@mtarld WDYT? Anyone up to digging into this idea?

@mtarld
Copy link
Contributor

mtarld commented Feb 23, 2022

Yes I already have worked on a PR like that.
There was some issues which BC that I couldn't figure out (such as updating context value by reference), that's why I paused it.

But it simplify a lot normalizers and encoders because they is no need to validate the context in them

Maybe I can retry with your help if you want?

@vudaltsov
Copy link
Contributor Author

I would very much prefer to not change the interfaces again.

Why?

what about allowing builders directly into the $context variable, indexed by numbers?

We can instead index them by class name, so that it's easy to get a context instance or check that it's not set.

such as updating context value by reference

You don't need that if the config objects are immutable (then they are not builders, of course). Simply replace certain config with a new one if you want to change it.

@nicolas-grekas
Copy link
Member

I would very much prefer to not change the interfaces again.

Because the gain doesn't look worth the cost to me. An object there looks barely better than an array to me. That's just my current understanding of the topic of course.

@mtarld
Copy link
Contributor

mtarld commented Feb 27, 2022

You don't need that if the config objects are immutable (then they are not builders, of course). Simply replace a certain config with a new one if you want to change it.

Replacing context could not work everywhere IMHO. As an example, the protected AbstractNormalizer::instantiateObject method, without changing its signature (which breaks the BC promise), won't be able to update the context anymore (https://github.com/symfony/symfony/blob/6.1/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php#L316)

For the record, here was the implementation I tried weeks ago: mtarld@e165bbe.
This allows handling BC promise without introducing new interfaces

@vudaltsov
Copy link
Contributor Author

You don't need that if the config objects are immutable (then they are not builders, of course). Simply replace certain config with a new one if you want to change it.

@mtarld, I wrote that about your alternative proposal, not about mine. So we keep context as an array, add immutable context items and add them with class name index.

@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@carsonbot
Copy link

Hello? This issue is about to be closed if nobody replies.

@vudaltsov vudaltsov closed this as not planned Won't fix, can't repro, duplicate, stale Sep 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants