api-platform: Custom operation always return w/ error 404

API Platform version(s) affected: 3.0.0

Description
I would create a list of my Regions and a nested attribute, the Provider’s name. The easiest way to create it w/ serialization groups. I created a custom operation for that, based on the docs, but the request is responded w/ error 404.

Regions --> ManyToOne --> Provider

How to reproduce
So, I created a new operation, and I put the Groups on every property I need

#[ApiResource]
#[GetCollection(
    uriTemplate: '/regions/get-with-provider',
    normalizationContext: ['groups' => ['Region:withProvider']],
)]
class Region ...

I tried this also:

#[ApiResource(
    operations: [
        new Get(),
        new Post(),
        new Put(),
        new Delete(),
        new Patch(),
        new GetCollection(),
        new GetCollection(
            uriTemplate: '/regions/get-with-provider',
            normalizationContext: ['groups' => ['Region:withProvider']],
        ),
    ],
)]
class Region ...

I tried to delete (composer.lock, symfony.lock, vendor dir etc.) and reinstall the whole vendor files.

… and I tried everything. 🥲

Possible Solution
I read the whole, related documentation and I tried to dig in the source code, but I did not success. I tried w/ the \ApiPlatform\Metadata\Get and w/ the \ApiPlatform\Metadata\GetCollection. I found this in docs: “Note: The #[GetCollection] attribute is an alias for #[Get(collection: true)]” Btw, in the Get class the collection is not working at all, because the class does not contain this property.

After I tried to investigate some information through xdebug, I figured out that, somehow the route was recognized as an item operation instead of collection.

Screenshot from 2022-09-26 21-05-38

If I want to debug the router:

$ sf debug:router | grep get-with-provider                                                                                                                                                                                                                                                 
  _api_/regions/get-with-provider_get_collection                         GET      ANY      ANY    /api/regions/get-with-provider                             

Two cases are possible:

  1. There are a couple of bug in the code
  2. I am very badly messing up something

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 15 (2 by maintainers)

Most upvoted comments

stop using controllers, use providers everything is possible and working now. If your case is not covered by our doctrine provider create yours. I bet my money everything is working. Please note that I can’t help everyone but you can still pm me (sf slack) or ask for paid support for your use cases!

I don’t know if the problem is still there, but I solved mine by changing the order of operations.

It works in this order:

#[ApiResource(
    operations: [
        new GetCollection(normalizationContext: ['groups' => 'getPosts']),
        new GetCollection(
            name: 'count',
            uriTemplate: '/posts/count',
            controller: PostCountController::class,
        ),
        new Get(normalizationContext: ['groups' => 'getPost']),
        new Post(),
        new Post(
            name: 'publish',
            uriTemplate: '/posts/{id}/publish',
            controller: PostPublishController::class,
            openapiContext: ['summary' => 'Permet de publier un article']
        ),
        new Put()
    ]
)]

Same issue with api platform 2.7, and changing the order of the operations in the annotation solved it.

@soyuka Well, I will gladly accept your money. Such a bold statement.

#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Get(
            uriTemplate: '/users/me',
            provider: MeProvider::class
        ),
        new Put(processor: UserPasswordHasher::class),
        new Patch(processor: UserPasswordHasher::class),
        new Delete(),
    ],
    normalizationContext: ['groups' => ['user:read']],
    denormalizationContext: ['groups' => ['user:create', 'user:update']],
)]

MeProvider:

readonly class MeProvider implements ProviderInterface
{
    public function __construct(
        private Security $security
    )
    {
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): UserInterface
    {
        return $this->security->getUser();
    }
}

Results in 404 error.

However specifying the requirements in generic Get operation works. Thanks @kevin-h3y for the tip.

With API Platfrom 3 my use case was to have an endpoint that return the user profile depending on the JWT token, the URI was like /user/me.

The command bin/console router:match <your-route> can help to find which route match first. You can change the order of the operations. Or what I did is adding a requirement on Get like :

#[ApiResource(
    operations: [
         new Get(requirements: ['id' => '\d+']), // /user/me won't match but user/123 will
         new GetCollection(),
         new GetCollection(uriTemplate: '/me', controller: UserProfileAction::class, paginationEnabled: false) 
    ],
    routePrefix: 'api',
// ...

Same issue in API Platform 2.7 as well whenever you are using different DTOs as Inputs and using a combination of uriTemplates + routePrefixes.

It seems that the generated API names are based only on uriTemplate, meaning that endpoints with the same uriTemplate (like login) would work only in one of the prefixed.

E.g.: ✔️ /admin/login would work - as the name would be _api_/login_post/frontend/login would not - as the name would be _api_/login_post

The first would be overwritten. This can be solved by manually specifying names, however it would be better if we could at least have some kind of hook to implement this ourselves (not sure if it does exist, Normalizers don’t seem to help in this case).

Obviously changing the order of the operations in this case would not help as they are in separate files.