Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Serializer] Add template parameter to avoid necessity to assert data in normalize #59200

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

Open
wants to merge 1 commit into
base: 7.3
Choose a base branch
Loading
from

Conversation

danrot
Copy link
Contributor

@danrot danrot commented Dec 12, 2024

Q A
Branch? TBH I am not sure about this one 😅
Bug fix? Also not sure...
New feature? no
Deprecations? maybe
Issues Didn't create one, since the code change is minimal anyway
License MIT

In a project I am working on we are using quite some value objects, and I also want to normalize some of them. Instead of repeating the serialization part for every object, I like to create a separate ValueObjectNormalizer (whereby I use ValueObject as a placeholder here and in the below code).

We are also using PHPStan with its highest level, and then there are some "issues" with that. The problem is that I always have to add a call to assert in the normalize function, since the $data variable is typed as mixed, and whatever I am doing with the passed data PHPStan won't like. This looks something like this:

<?php

namespace App\Serializer;

use App\Domain\ValueObject;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

final class ValueObjectNormalizer implements NormalizerInterface
{
	public function normalize(mixed $object, null|string $format = null, array $context = []): string
	{
		\assert($object instanceof ValueObject);

		return $object->someValueObjectProperty;
	}

	public function supportsNormalization(mixed $data, null|string $format = null, array $context = []): bool
	{
		return $data instanceof ValueObject;
	}

	public function getSupportedTypes(null|string $format): array
	{
		return [
			ValueObject::class => true,
		];
	}
}

With the change being introduced in this PR, it would be possible to make use of generics in order to avoid this assert call, which I think is a bit nicer.

<?php

declare(strict_types=1);

namespace App\Serializer;

use App\Domain\ValueObject;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
 * @implements NormalizerInterface<ValueObject>
 */
final class ValueObjectNormalizer implements NormalizerInterface
{
	public function normalize(mixed $object, null|string $format = null, array $context = []): string
	{
		return $object->someValueObjectProperty;
	}

	public function supportsNormalization(mixed $data, null|string $format = null, array $context = []): bool
	{
		return $data instanceof ValueObject;
	}

	public function getSupportedTypes(null|string $format): array
	{
		return [
			ValueObject::class => true,
		];
	}
}

What do you think about this? I am also not sure in which branch such a change would have to go, and I am also not sure if you consider this a breaking change.

@danrot danrot requested a review from dunglas as a code owner December 12, 2024 15:22
@carsonbot carsonbot added this to the 7.3 milestone Dec 12, 2024
@carsonbot
Copy link

Hey!

Thanks for your PR. You are targeting branch "7.3" but it seems your PR description refers to branch "TBH I am not sure about this one 😅".
Could you update the PR description or change target branch? This helps core maintainers a lot.

Cheers!

Carsonbot

@danrot
Copy link
Contributor Author

danrot commented Dec 12, 2024

I already saw that this causes some issues... But instead of fixing all of that now and occupying github action pipelines I'll wait for your feedback to see if you would accept this PR.

@@ -41,8 +43,8 @@ public function normalize(mixed $data, ?string $format = null, array $context =
/**
* Checks whether the given class is supported for normalization by this normalizer.
*
* @param mixed $data Data to normalize
* @param string|null $format The format being (de-)serialized from or into
* @param TData|mixed $data Data to normalize
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why keep mixed?

Copy link
Contributor

@uuf6429 uuf6429 Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For supportsNormalization(), it might presumably receive all sorts of data (since it's testing for serialization support).

The idea is that (for example) NormalizerInterface<string> will only normalize() strings, but still might receive other types to test for serialization (otherwise (NormalizerInterface<string>)->supportsNormalization(123, ...) will cause a static analysis tool warning, which is undesirable and unintended).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you could argue that mixed already contains TData as well, so we might as well just keep mixed as a type.

@carsonbot carsonbot changed the title Add template parameter to avoid necessity to assert data in normalize [Serializer] Add template parameter to avoid necessity to assert data in normalize Jan 7, 2025
@wouterj
Copy link
Member

wouterj commented Jan 7, 2025

Looking at the normalizers shipped in Symfony, they all verify the passed data to normalize()/denormalize() is indeed what is expected (and throw a serializer exception otherwise)

So to me, it sounds like it is a good practice to verify the type in the method itself rather than relying on the return value of supportsNormalization()/getSupportedTypes()?

@uuf6429
Copy link
Contributor

uuf6429 commented Jan 7, 2025

Looking at the normalizers shipped in Symfony, they all verify the passed data to normalize()/denormalize() is indeed what is expected (and throw a serializer exception otherwise)

@wouterj I just checked two normalizers randomly and that doesn't seem to be the case (unless I misunderstood your point):

I don't know how/if psalm/phpstan is set up, but in either case I would have expected a warning that the usage of $object was not expected (first case not iterable, second case undefined symbol getMessage() etc).

With this PR (and some small adjustments), static analysis tools should figure out $object's type, and modern IDEs should be able to provide autocompletion with normalize() (and possibly denormalize() - but that's not covered in this PR - yet) - (without having to redeclare the PHPDoc for (de)normalise()).

People should still be able to opt-out by using @implements NormalizerInterface<mixed>.

@wouterj
Copy link
Member

wouterj commented Jan 7, 2025

Interesting, so we are inconsistent about this in Symfony. See e.g.

public function normalize(mixed $object, ?string $format = null, array $context = []): string
{
if (!$object instanceof \DateInterval) {
throw new InvalidArgumentException('The object must be an instance of "\DateInterval".');
}
and
public function normalize(mixed $object, ?string $format = null, array $context = []): string
{
if (!$object instanceof \SplFileInfo) {
throw new InvalidArgumentException('The object must be an instance of "\SplFileInfo".');
}

@danrot
Copy link
Contributor Author

danrot commented Jan 8, 2025

I have to admit that this is kind of an edge case. It is not absolutely safe to just assume the type passed to normalize is the one we check for in supportsNormalization, since somebody might as well instantiate a ValueObjectNormalizer on their own and call normalize without calling supportsNormalization before.

I.e. this is probably how Symfony is calling this under the hood:

$valueObjectNormalizer = new ValueObjectNormalizer();

if ($valueObjectNormalizer->supportsNormalization($value)) {
    $valueObjectNormalizer->normalize($value);
}

But nothing stops a developer from instantiating that manually and call it in the "wrong" way:

$valueObjectNormalizer = new ValueObjectNormalizer();

$valueObjectNormalizer->normalize($value);

Nevertheless, I would say this is an edge case, and at least for me the current PR would still be a nice addition, since it is closer to how this is actually working under the hood.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.