Skip to content

[FrameworkBundle] BackedEnum Serializer priority Change Broke API Platform BackedEnum Serialization #54478

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
gnito-org opened this issue Apr 3, 2024 · 10 comments · Fixed by #54484

Comments

@gnito-org
Copy link
Contributor

gnito-org commented Apr 3, 2024

Symfony version(s) affected

7.0.6

Description

The BackedEnum serializer priority change broke API Platform's serialization of backed enums, where the backed enum is declared as an API resource. Maybe it broke Symfony's serialization as well, but I did not test that.

After I upgraded to Symfony 7.0.6, backed enums are serialized in API Platform to just a simple array with the string values of the enum, and not as a full API resource. If I hack the BackedEnum Serializer priority back to -915 in Symfony\Component\DependencyInjection\Loader\Configurator\serializer.php, the serialization works 100% like prior to 7.0.6.

The API Platform version did not change in my app.

How to reproduce

Serialize a backed enum in API Platform, as an API Platform resource.

Possible Solution

No response

Additional Context

<?php

namespace App\ApiResource;

use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\StateProvider\TeamMemberRoleCodeProvider;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(operations: [
        new Get(),
        new GetCollection(),
    ], normalizationContext: [
        'groups' => [self::RESOURCE_GET],
        'openapi_definition_name' => 'get',
    ], security: 'is_granted("'.AuthenticatedVoter::IS_AUTHENTICATED.'")', provider: TeamMemberRoleCodeProvider::class,)]
enum TeamMemberRoleCode: string
{
    case Contributor = 'contributor';
    case Reader = 'reader';
    case Manager = 'manager';
    case Owner = 'owner';
    public const string RESOURCE_GET = 'team_member_role_code:get';

    public static function createInstance(?string $id): ?self
    {
        return self::tryFrom($id);
    }

    #[Groups([self::RESOURCE_GET])]
    #[ApiProperty(description: AppEntityDescriptions::getCodeDescription, writable: false)]
    public function getCodeDescription(): string
    {
        return match ($this) {
            self::Owner => 'The person has no privilege restrictions on any resources in the user account.',
            self::Manager => 'The person can read from and write to all resources that are available to the team, and can invite, remove and change the roles of team members.',
            self::Contributor => 'The person can read from and write to all resources that are available to the team.',
            self::Reader => 'The person can read from all resources that are available to the team.',
        };
    }

    #[Groups([self::RESOURCE_GET])]
    #[ApiProperty(description: AppEntityDescriptions::id, writable: false, identifier: true)]
    public function getId(): string
    {
        return $this->value;
    }
}
<?php

namespace App\StateProvider;

use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\TeamMemberRoleCode;

class TeamMemberRoleCodeProvider implements ProviderInterface
{
    public function provide(
        Operation $operation,
        array $uriVariables = [],
        array $context = []
    ): array|object|null {
        if ($operation instanceof CollectionOperationInterface) {
            return TeamMemberRoleCode::cases();
        }

        return TeamMemberRoleCode::createInstance($uriVariables['id']);
    }
}

API Platform output prior to 7.0.6

{
  "@context": "/api/contexts/TeamMemberRoleCode",
  "@id": "/api/team-member-role-code",
  "@type": "hydra:Collection",
  "hydra:totalItems": 4,
  "hydra:member": [
    {
      "@id": "/api/team-member-role-code/contributor",
      "@type": "TeamMemberRoleCode",
      "codeDescription": "The person can read from and write to all resources that are available to the team.",
      "id": "contributor"
    },
    {
      "@id": "/api/team-member-role-code/reader",
      "@type": "TeamMemberRoleCode",
      "codeDescription": "The person can read from all resources that are available to the team.",
      "id": "reader"
    },
    {
      "@id": "/api/team-member-role-code/manager",
      "@type": "TeamMemberRoleCode",
      "codeDescription": "The person can read from and write to all resources that are available to the team, and can invite, remove and change the roles of team members.",
      "id": "manager"
    },
    {
      "@id": "/api/team-member-role-code/owner",
      "@type": "TeamMemberRoleCode",
      "codeDescription": "The person has no privilege restrictions on any resources in the user account.",
      "id": "owner"
    }
  ],
  "hydra:search": {
    "@type": "hydra:IriTemplate",
    "hydra:template": "/api/team-member-role-code{?properties[]}",
    "hydra:variableRepresentation": "BasicRepresentation",
    "hydra:mapping": [
      {
        "@type": "IriTemplateMapping",
        "variable": "properties[]",
        "property": null,
        "required": false
      }
    ]
  }
}

API Platform output with 7.0.6

{
  "@context": "/api/contexts/TeamMemberRoleCode",
  "@id": "/api/team-member-role-code",
  "@type": "hydra:Collection",
  "hydra:totalItems": 4,
  "hydra:member": [
    "contributor",
    "reader",
    "manager",
    "owner"
  ],
  "hydra:search": {
    "@type": "hydra:IriTemplate",
    "hydra:template": "/api/team-member-role-code{?properties[]}",
    "hydra:variableRepresentation": "BasicRepresentation",
    "hydra:mapping": [
      {
        "@type": "IriTemplateMapping",
        "variable": "properties[]",
        "property": null,
        "required": false
      }
    ]
  }
}

Service Config

This service configuration in 7.0.6 makes the problem go away:

    serializer.normalizer.backed_enum:
        class: Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer
        tags:
            - { name: 'serializer.normalizer', priority: -915 }
@derrabus
Copy link
Member

derrabus commented Apr 3, 2024

Does api-platform/core#6283 fix your issue?

@gnito-org
Copy link
Contributor Author

gnito-org commented Apr 3, 2024

@derrabus Unfortunately that did not fix the issue. After upgrading to API Platform 3.2.20 and disabling the services.yaml hack, the problem still exists.

@derrabus
Copy link
Member

derrabus commented Apr 3, 2024

cc @soyuka

@gnito-org
Copy link
Contributor Author

FWIW, I am not using GraphQL.

@GwendolenLynch
Copy link
Contributor

GwendolenLynch commented Apr 4, 2024

To try and better illustrate the context, these are the relative Symfony services and their priority before #54315

Service ID Priority
serializer.normalizer.translatable -890
serializer.normalizer.backed_enum -915 (changed to -880)
serializer.normalizer.json_serializable -950
serializer.denormalizer.array -990
serializer.normalizer.object -1000

With respect to API Platform, this is an incomplete list of code-documented priority needs for their serializers:

  • before serializer.normalizer.json_serializable (-950)
  • after serializer.normalizer.data_uri (-920) but before serializer.normalizer.object (-1000)
  • after serializer.denormalizer.array (-990) but before serializer.normalizer.object (-1000)
  • after api_platform.(hal|jsonld|jsonapi).normalizer.object but before serializer.normalizer.object (-1000) and serializer.denormalizer.array (-990)

Is maybe a better approach to change the priority on serializer.normalizer.translatable to something like -920?

@GwendolenLynch
Copy link
Contributor

I've submitted a set of additional tests for API Platform that hopefully shows the impact of this: api-platform/core#6288

@soyuka
Copy link
Contributor

soyuka commented Apr 4, 2024

It'd be nice to restore the priority on Symfony (as #54484 does) and I'll revert api-platform/core#6283

@gnito-org
Copy link
Contributor Author

Instead of API Platform inserting its own normalizers into the priority queue, wouldn't it be better if it instead decorated Symfony normalizers?

@derrabus
Copy link
Member

derrabus commented Apr 4, 2024

If we revert, let's please add tests that break should we ever fiddle with that specific priority again.

nicolas-grekas added a commit that referenced this issue Apr 4, 2024
… translatable (GwendolenLynch)

This PR was squashed before being merged into the 6.4 branch.

Discussion
----------

[Serializer] reset backed_enum priority, and re-prioritise translatable

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | Fix #54478 Fix api-platform/core#6285 Fix api-platform/core#6279
| License       | MIT

- `serializer.normalizer.translatable` -920 (was -890)
- `serializer.normalizer.backed_enum` -915 (originally -915, changed to -880)

Floating this as as solution to the knock-on issues from #54478

Context:
- #54478 (comment)
- api-platform/core#6288

Commits
-------

b559aa5 [Serializer] reset backed_enum priority, and re-prioritise translatable
@chalasr chalasr closed this as completed Apr 4, 2024
@soyuka
Copy link
Contributor

soyuka commented Apr 4, 2024

Instead of API Platform inserting its own normalizers into the priority queue, wouldn't it be better if it instead decorated Symfony normalizers?

Yes if only it was possible api-platform/core#2355

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

Successfully merging a pull request may close this issue.

7 participants