8000 [DX][JsonEncoder] Can we find a better name for normalizer/encoder · Issue #59288 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[DX][JsonEncoder] Can we find a better name for normalizer/encoder #59288

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
wouterj opened this issue Dec 23, 2024 · 14 comments · Fixed by #59290
Closed

[DX][JsonEncoder] Can we find a better name for normalizer/encoder #59288

wouterj opened this issue Dec 23, 2024 · 14 comments · Fixed by #59290
Labels
DX DX = Developer eXperience (anything that improves the experience of using Symfony) JsonStreamer

Comments

@wouterj
Copy link
Member
wouterj commented Dec 23, 2024

Description

A frequent thing I'm hearing regarding the Serializer is that the diagram in the docs is the most used piece of doc by many developers, as they always forget which direction is normalization, and which is denormalization.

In the new JsonEncoder component, I see we are reusing this name. I think it would be good if we can maybe try finding a different name for this.

On top of the general confusion about the name, it's also a slightly different concept in both components. I think using the same name in 2 components in the same scope can reduce DX as well. For instance, it will probably not be immediately clear what normalizer we talk about in doc articles like How to Create your Custom Normalizer (given we're probably going to document JsonEncoder somewhere in the "Serializer" guides as well). It can also lead to IDE autocompletion suggesting the wrong interface (like what we're discussing for a different class conflict in #58991).

Can we think of a different name for the classes? (e.g. ValueTransformer) And maybe clearly document the direction in the method name (something like encodedValueToPhpValue()?).

cc @mtarld @chalasr

@carsonbot carsonbot added DX DX = Developer eXperience (anything that improves the experience of using Symfony) JsonStreamer labels Dec 23, 2024
@mtarld
Copy link
Contributor
mtarld commented Dec 23, 2024

Indeed, the naming confusing. And that's unfortunate because IMO this is an excellent name if the Serializer component did not exist. But I agree that we must change it for the sake of a better understanding.

I think that we can merge the two NormalizerInterface and DenormalizerInterface into a single one, creating something like:

interface ValueTransformerInterface
{
    public function toJsonValue(mixed $phpValue, array $options = []): mixed;

    public function toPhpValue(mixed $jsonValue, array $options = []): mixed;

    public static function getJsonValueType(): Type;
}

WDYT?

On a side note, this will make the code easier, because we'll end up with only one service locator (instead of 2)

@chalasr
Copy link
Member
chalasr commented Dec 23, 2024

Agreed 👍 I like ValueTransformer. Once that done I'm not sure we need to be as explicit as what you propose for the method name but that's a detail, some level of "redundancy" doesn't hurt if it makes the thing self-explanatory.

@wouterj
Copy link
Member Author
wouterj commented Dec 23, 2024

I like the shorter method names proposed by @mtarld 👍 . And combining it in one interface sounds like a good idea as well, although I'm not the best person to determine if there is a common use-case for only implementing one direction?

@mtarld
Copy link
Contributor
mtarld commented Dec 23, 2024

I think that for plenty of use cases, both direction will be required.

Maybe we can add a little example in the documentation showing how to create a value transformer that works in one direction only:

public function toJsonValue(mixed $phpValue, array $options = []): mixed
{
    throw new \BadMethodCallException(sprintf('"%s" cannot convert value to JSON.', self::class));
}

Plus, I think I'm going to create the following attribute to replace the Normalizer/Denormalizer attributes:

class ValueTransformer
{
    private \Closure|null $toPhpValue;
    private \Closure|null $toJsonValue;

    /**
     * @param (callable(mixed, array<string, mixed>?): mixed)|(callable(mixed): mixed) $toPhpValue
     * @param (callable(mixed, array<string, mixed>?): mixed)|(callable(mixed): mixed) $toJsonValue
     */
    public function __construct(
        private string|null $serviceId = null,
        mixed $toPhpValue = null,
        mixed $toJsonValue = null,
    ) {
        if (!$valueTransformer && !($toPhpValue && $toJsonValue)) {
            throw new LogicException('#[ValueTransformer] attribute must declare either a $serviceId or $toPhpValue/$toJsonValue callables.');
        }

        if ($valueTransformer && ($toPhpValue || $toJsonValue)) {
            throw new LogicException('#[ValueTransformer] attribute must declare a $serviceId or $toPhpValue/$toJsonValue callables, but not both.');
        }

        // ...
    }

    // ...
}

Does it sound good to you?

@chalasr
Copy link
Member
chalasr commented Dec 23, 2024

👍 for the proposed 2-way API. Although it's a whole topic, the attribute idea also looks interesting too.

@Aerendir
Copy link
Contributor

I personally agree on the entire discussion but the merge of the two interfaces: implementing both directions is not always what is needed.

Think at the situation in which we only consume APIs, without subitting ant data: why should have I be forced to implement both directions when I only need toPhp?

Aldo about the “transformation” I have some perolexities.

I imagine a transformation like something that changes the shape of the array resulting from the decodification of the json string.

But hydrating an object from a json, to me, seems more like a conversion.

My 2 cents.

@chalasr
Copy link
Member
chalasr commented Dec 31, 2024

I imagine a transformation like something that changes the shape of the array resulting from the decodification of the json string.

Which can be the case. Changing a value at a given position to make it fit the target object’s property and vice versa, which may also imply to change its type, is transformation. That’s also consistent with other places doing the same kind of processing like Form or jolicode/automapper. Alternative suggestions are welcome but going with a more abstract term would be a mistake I think.

@Aerendir
Copy link
Contributor
Aerendir commented Jan 2, 2025

I imagine a transformation like something that changes the shape of the array resulting from the decodification of the json string.

Which can be the case. Changing a value at a given position to make it fit the target object’s property and vice versa, which may also imply to change its type, is transformation. That’s also consistent with other places doing the same kind of processing like Form or jolicode/automapper. Alternative suggestions are welcome but going with a more abstract term would be a mistake I think.

@chalasr , ok... Thank you for your kindly reply.

... implementing both directions is not always what is needed.

Think at the situation in which we only consume APIs, without subitting ant data: why should have I be forced to implement both directions when I only need toPhp?

Remains the forcing of implementing both the two interfaces that is, to me, a useless forcing: as told, implementing both directions is not always required. 8000

@mtarld
Copy link
Contributor
mtarld commented Jan 6, 2025

To me, it's ok to have only on interface, as the common use case is to transform a native value to JSON and the other way around.
Whenever only one direction is supported, the user could IMO throw a BadMethodCallException, and this could be explicitly said in the Symfony documentation.

@Aerendir
Copy link
Contributor
Aerendir commented Jan 6, 2025

... as the common use case ...

To me, Symfony is the best framework because it doesn't assume nothing and permits to do everything in the best way possible.

Whenever only one direction is supported, the user could IMO throw a BadMethodCallException

Yes, it is possible, but is one more method in a class and one more test for the coverage: the class gets "dirty" and I personally don't like this approach (but, as told, is just my opinion): I prefer two different interfaces.

Having two different interfaces also permits to use the IDE to know which directions are implemented where, while having only one interface forces to open each file to see if it implementes both directions or only one and which. In very big projects, it will cause a lot of consumed time (think at an appa that, for example, deals with many apis - as one of my apps that connects with social media, ecommerce platforms, etc... many many apis).

More, sometimes methods are complex, also if the common enterprise patterns are followed and sometimes is more useful to split the methods in two different classes than having them in one: having two interfaces permits to do this; having only one doesn't.

Finally, I also think having only one interface violates the "Single responsibility principle": also if they affer the same flow, serialization and deserialization are two different responsibilities.

If you really like the possibility of having only one interface, it could be created an interface that implements both, so we would all be happy anyway.

and this could be explicitly said in the Symfony documentation.

Yes, but, again, the less is required to be told in the documentation, the better, IMHO.

Anyway, my intent was to focus our attention on little details that, ever in my opinion, made Symfony the best framework out there: no magic, no assumptions, clear code.

Maybe you don't agree with me and this is perfectly ok: the only thing I can do anyway, is to thank you for this great component :)

@mtarld
Copy link
Contributor
mtarld commented Jan 7, 2025

@Aerendir, I understand your point, thank you!

Actually, I'm not against having two interfaces (I even created two interfaces in the first place).

Maybe we can try finding proper and really clear names for these interfaces? Something like NativeValueTransformer / JsonValueTransformer for example. Any ideas?

@Aerendir
8000 Copy link
Contributor
Aerendir commented Jan 8, 2025

@mtarld , thank you for your understanding 🙏

Maybe a simple toJsonTransformer and toPhpTransformer are sufficient? The context is already set by the component itself that is named JsonEncoder, so we know that toPhpTransformer takes a json (array) value (?).

@mtarld
Copy link
Contributor
mtarld commented Jan 8, 2025

I think I have a good idea. We can have the following interface:

interface ValueTransformerInterface
{
    public function transform(mixed $value, array $options = []): mixed;

    public static function getJsonValueType(): Type;
}

And then bind it like we want:

#[ValueTransformer(
  toNative: FooTransformer::class,
  toJson: BarTransformer::class,
])
// or
#[ValueTransformer(
  toNative: BazTransformer::class,
])

In that way, you can only implement the transformer in the direction you want, and we still have only one kind of service!

WDYT?

@Aerendir
Copy link
Contributor
Aerendir commented Jan 9, 2025

Yes, this seems a good and clever compromise 💪

Thank you for your patience and for having spent time thinking at a solution 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DX DX = Developer eXperience (anything that improves the experience of using Symfony) JsonStreamer
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants
0