Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

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

Sign up
Appearance settings

[Serializer] DocumentSerializerInterface exceptions#59306

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged

Conversation

VincentLanglet
Copy link
Contributor

@VincentLangletVincentLanglet commentedDec 27, 2024
edited by nicolas-grekas
Loading

QA
Branch?7.3
Bug fix?no
New feature?no
Deprecations?no
Issues-
LicenseMIT

This

  • warns the developper that calls to SerializerInterface::serialize and deserialize might throws exception
  • avoid PHPStan to report useless try/catch when writing
try {     $serializer->serialize(...);} catch (NotEncodableValueException) {} // for instance

SerializerInterface::serialize can throws

  • NotEncodableValueException
  • all exceptions thrown by NormalizerInterface::normalize
  • all exceptions thrown by EncoderInterface::encode

Since it might be a lot of exception and there is no real need to list them all, I simplified with@throws ExceptionInterface.

valtzu and hxv reacted with thumbs up emoji
@wouterj
Copy link
Member

Any method everywhere might throw some type of\Exception. I'm personally not convinced this phpdoc adds any value (and if it does, we should add it everywhere?).

@VincentLangletVincentLanglet changed the title[Serializer] Document SerializerInterface exceptions @VincentLanglet[Serializer] Document SerializerInterface exceptionsDec 27, 2024
@VincentLanglet
Copy link
ContributorAuthor

VincentLanglet commentedDec 27, 2024
edited
Loading

Any method everywhere might throw.

This is not true. Some methods (a lot) doesn't throws Exception at all, even in the Symfony codebase.

And with well-documented methods, PHPStan helps a lot catching unwanted exceptions
https://phpstan.org/blog/bring-your-exceptions-under-control

Currently, with code like

try {     $serializer->serialize(...);} catch (NotEncodableValueException) {}

I'm getting

Dead catch - NotEncodableValueException is never thrown in the try block.

because the PHPdoc is not describing the exceptions thrown.

some type of\Exception.

Notice I didn't added\Exception but used the scoped interfaceSymfony\Component\Serializer\Exception\ExceptionInterface which already provides some value to

I'm personally not convinced this phpdoc adds any value

It adds value for static analysis tools (like PHPStan) users.
It also provide such value for IDE (like PHPStorm) users.

I'm personally not convinced this phpdoc adds any value (and if it does, we should add it everywhere?).

@throws ExceptionInterface exists on the NormalizerInterface

* @throws ExceptionInterface Occurs for all the other cases of errors

And a@throws tag exists on the EncoderInterface

I don't see a reason to add it to SerializerInterface which is a similar interface.

For reference, I already added missing@throws tag in some other class/interfaces likehttps://github.com/symfony/symfony/pull/57519/files orhttps://github.com/symfony/symfony/pull/52915/files

@fabpot
Copy link
Member

I agree with@wouterj. If we think it's useful, we should do it consistently across the board, not just on one interface. Maybe it's mostly useful for interfaces? But then, do we want to reference the interface or the concrete exception implementations that can be thrown?

@VincentLanglet
Copy link
ContributorAuthor

VincentLanglet commentedDec 29, 2024
edited
Loading

I agree with@wouterj. If we think it's useful, we should do it consistently across the board, not just on one interface.

There is already a looot of method/class/interface already with@throws annotation.
And every times I find one missing I open (and still will open) a PR to fix it.

Maybe it's mostly useful for interfaces?

Since people works mainly with interface, it is.

This also explains which exceptions we are supposed to throw when implementing the interface.
Here customSerializerInterface::serialize implementation should only throwSymfony\Component\Serializer\Exception\ExceptionInterface.
And the Symfony Serializer implementation respects this contract.

It throws:

  • Symfony\Component\Serializer\Exception\UnsupportedFormatException
  • Symfony\Component\Serializer\Exception\RuntimeException
  • Symfony\Component\Serializer\Exception\LogicException
  • Symfony\Component\Serializer\Exception\NotNormalizableValueException
  • All the exceptions thrown byNormalizerInterface::normalize (which includes ExceptionInterface)
    * @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer
    * @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
    * reference handler can fix it
    * @throws LogicException Occurs when the normalizer is not called in an expected context
    * @throws ExceptionInterface Occurs for all the other cases of errors

AndSerializerInterface::deserialize throws

  • Symfony\Component\Serializer\Exception\UnsupportedFormatException
  • Symfony\Component\Serializer\Exception\RuntimeException
  • All the exceptions thrown byDenormalizerInterface::denormalize (which includes ExceptionInterface)
    * @throws BadMethodCallException Occurs when the normalizer is not called in an expected context
    * @throws InvalidArgumentException Occurs when the arguments are not coherent or not supported
    * @throws UnexpectedValueException Occurs when the item cannot be hydrated with the given data
    * @throws ExtraAttributesException Occurs when the item doesn't have attribute to receive given data
    * @throws LogicException Occurs when the normalizer is not supposed to denormalize
    * @throws RuntimeException Occurs if the class cannot be instantiated
    * @throws ExceptionInterface Occurs for all the other cases of errors

But then, do we want to reference the interface or the concrete exception implementations that can be thrown?

So far, I would say both of the strategy are used. For instance

* @throws BadMethodCallException Occurs when the normalizer is not called in an expected context
* @throws InvalidArgumentException Occurs when the arguments are not coherent or not supported
* @throws UnexpectedValueException Occurs when the item cannot be hydrated with the given data
* @throws ExtraAttributesException Occurs when the item doesn't have attribute to receive given data
* @throws LogicException Occurs when the normalizer is not supposed to denormalize
* @throws RuntimeException Occurs if the class cannot be instantiated
* @throws ExceptionInterface Occurs for all the other cases of errors

We can list all the exceptions thrown with the explanation of the reason but we still have to add a
@throws ExceptionInterface "fallback" (like

* @throws ExceptionInterface Occurs for all the other cases of errors
) since

  • The SerializerInterface use the Normalizer/DenormalizerInterface which can throw any of these exceptions
  • The@throws on interfaces should be considered as "contract" about which exceptions are allowed to be thrown by custom implementation. And this interface should IMHO allows every scoped ExceptionInterface.

I considered it was already a good step to add@throws ExceptionInterface but do you prefer to be more explicit@fabpot ?

@wouterj
Copy link
Member

wouterj commentedDec 29, 2024
edited
Loading

Thinking about this more, I'm actually thinking the@throw annotations can be "harmful" (and so is the PHPstan rule imho, if not all vendors use it).

They are documenting a contract ("this method will only throw things that are instance of X") while there is no way to enforce that. Even more, retrospectively adding it to methods creates a new contract for any implementation that they were never aware of.
In this case, ourSerializer::serialize() will call normalizers. Maybe Symfony doesn't include any normalizer that throws something other thanExceptionInterface, but there is no way in knowing if any third party library and application normalizer conforms to this as well.
The same also applies to other implementations of theSerializerInterface out there. We never told them to only throwExceptionInterface exceptions, so we can't make this contract to our users. That will wrongly inform them that they can completely ignore serializer exceptions by catchingExceptionInterface, which will lead to broken applications when an unexpected exception occurs.

I would say this means that adding@throw on interfaces might be much more dangerous than adding them on actual implementations.

@wouterj
Copy link
Member

wouterj commentedDec 29, 2024
edited
Loading

And this is not a theoretical thing. It took me only a quick look to find Symfony itself does not conform this new contract proposed in the PR:

useSymfony\Component\Serializer\Encoder\YamlEncoder;useSymfony\Component\Serializer\Normalizer\ObjectNormalizer;useSymfony\Component\Serializer\Serializer;$encoders = [newYamlEncoder()];$normalizers = [newObjectNormalizer()];$serializer =newSerializer($normalizers,$encoders);$serializer->deserialize('[1','stdClass','yaml');// throws Symfony\Component\Yaml\Exception\ParseException

@VincentLanglet
Copy link
ContributorAuthor

Thinking about this more, I'm actually thinking the@throw annotations can be harmful (and so is the PHPstan rule imho, if not all vendors use it).

PHPStan has two mode about the exceptions (https://phpstan.org/blog/bring-your-exceptions-under-control#what-does-absent-%40throws-above-a-function-mean%3F):

By default PHPStan consider that no@throws means it can throws anything, but it provides a stricter option to say that no@throws means never throw. It has to consider that itthrows anything because of the big number of poor documented method/interface, but when using well documented API, it's powerful to report dead-catch which are often code smell or possible bugs. I already solve multiple errors because of it (like the fact of not catching the right exception).

Even more, retrospectively adding it to methods creates a new contract for any implementation that they were never aware of. [...] We never told them to only throwExceptionInterface exceptions, so we can't make this contract to our users.

Technically, there is already a contract which say "This method is not supposed to throw any exception". And any implementation is breaking this contract. I'm trying to fix/improve the contract.

Symfony already rely on the possible exception thrown since some are caught in the SerializerErrorEncoder, even ifnone is documented

A better documentation would lead to a better try/catch.

That will wrongly inform them that they can completely ignore serializer exceptions by catchingExceptionInterface, which will lead to broken applications when an unexpected exception occurs.

Currently the interface is wrongly informing that no exception are thrown.
Adding a@throws Throwable would be the minimum needed changes but so far

@VincentLanglet
Copy link
ContributorAuthor

And this is not a theoretical thing. It took me only a quick look to find Symfony itself does not conform this new contract proposed in the PR:

use Symfony\Component\Serializer\Encoder\YamlEncoder;use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;use Symfony\Component\Serializer\Serializer;$encoders = [new YamlEncoder()];$normalizers = [new ObjectNormalizer()];$serializer = new Serializer($normalizers, $encoders);$serializer->deserialize('[1', 'stdClass', 'yaml'); // throws Symfony\Component\Yaml\Exception\ParseException

The issue is not about the SerializerInterface contract but about the YamlEncoder whichalready does not respect the EncoderInterface contract.

* @throws UnexpectedValueException
*/
publicfunctiondecode(string$data,string$format,array$context = []):mixed;

This YamlEncoder was introduced in Symfony 3 while the@throws annotation exists since Symfony 2.
When looking at other Encoder, a lot of them are throwingNotEncodableValueException extends UnexpectedValueException in such situation.

So I would think the YamlEncoder need to be fixed.
I opened the PR#59323, and will be happy to add more fix if needed.

@OskarStarkOskarStark changed the title[Serializer] Document SerializerInterface exceptions[Serializer] DocumentSerializerInterface exceptionsDec 29, 2024
@wouterjwouterj modified the milestones:6.4,7.3Dec 29, 2024
fabpot added a commit that referenced this pull requestJan 6, 2025
…tLanglet)This PR was merged into the 6.4 branch.Discussion----------[Serializer] Fix exception thrown by `YamlEncoder`| Q             | A| ------------- | ---| Branch?       | 6.4| Bug fix?      | yes| New feature?  | no| Deprecations? | no| Issues        | Fix the example given#59306 (comment)| License       | MITDecoderInterface::decode is supposed to throw scoped exception cfhttps://github.com/symfony/symfony/blob/4078cb1642332cf5bbd45f5118edf766580cfb1e/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php#L33-L35but the YamlEncoder::decode could throw ParseException.Others similar Encoder, like XmlEncoder or JsonEncoder are throwing NotEncodableValue so I did the same cf examplehttps://github.com/symfony/symfony/blob/da155534524f0d9c501023d10bb423fce4cd4482/src/Symfony/Component/Serializer/Encoder/JsonEncode.php#L41-L45Commits-------ab9de2d Fix exception thrown by YamlEncoder
@nicolas-grekas
Copy link
Member

They are documenting a contract ("this method will only throw things that are instance of X") while there is no way to enforce that

That is certainly not what this annotation is about. Exceptions are for cases where something unexpected happens. In some cases, they're for signaling rare situations that shouldn't happen but can happen (the unhappy path).@throws is for those cases: it's augmenting the API with erroneous situations which, from a domain pov, can be caught and deal-with. Any other kind of exceptions can be thrown, but should be unhandled, unless specific reasons. (It should also be fine to leave API-defined exceptions unhandled, unlike what some IDEs think).

So to me:@throws should be crystal clear about when catching can make sense, from a domain pov.
A generic annotation with the interface doesn't fit this requirement to me and feels useless.
We should do better.

@VincentLanglet
Copy link
ContributorAuthor

So to me:@throws should be crystal clear about when catching can make sense, from a domain pov. A generic annotation with the interface doesn't fit this requirement to me and feels useless. We should do better.

I tried doing better by adding

  • NotEncodableValueException
  • NotNormalizableValueException

Would it be ok like this or should I be more explicit@nicolas-grekas ?
Having0 throws annotation is not ok to me, but I don't think it will be better to list dozen of throw annotation either so I'm trying to find the right number.

Copy link
Member

@nicolas-grekasnicolas-grekas left a comment

Choose a reason for hiding this comment

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

LGTM, here are some more comments and that would be good on my side.

@nicolas-grekasnicolas-grekas changed the base branch from6.4 to7.3January 6, 2025 14:14
@nicolas-grekas
Copy link
Member

Thank you@VincentLanglet.

@nicolas-grekasnicolas-grekas merged commitcdc1101 intosymfony:7.3Jan 6, 2025
10 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@nicolas-grekasnicolas-grekasnicolas-grekas approved these changes

@OskarStarkOskarStarkOskarStark approved these changes

@dunglasdunglasAwaiting requested review from dunglasdunglas is a code owner

Assignees
No one assigned
Projects
None yet
Milestone
7.3
Development

Successfully merging this pull request may close these issues.

6 participants
@VincentLanglet@wouterj@fabpot@nicolas-grekas@OskarStark@carsonbot

[8]ページ先頭

©2009-2025 Movatter.jp