question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Custom GET item without to have to specify id

See original GitHub issue

I’m trying to create a custom action which could be called at GET /me.

I implemented JWT token and now I want to get the user data thanks to the security storage or something else (by Bearer or something else).

My problem is that I can’t create a custom action to get just one item but without giving an ID. It seems ApiPlatform is built to always listen ID parameter for item action, when we use _api_item_operation_name.

Action :

<?php

namespace AppBundle\Action;

use AppBundle\Repository\UserRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Routing\Annotation\Route;
use AppBundle\Entity\User;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;

class UserMe
{
    /**
     * @var UserRepository
     */
    private $repository;

    /**
     * UserMe constructor.
     *
     * @param UserRepository $repository
     */
    public function __construct(UserRepository $repository)
    {
        $this->repository= $repository;
    }

    /**
     * @Route(
     *     name="api_users_me",
     *     path="/me",
     *     defaults={"_api_resource_class"=User::class, "_api_item_operation_name"="user_me"}
     * )
     * @Method("GET")
     */
    public function __invoke($data)
    {
        // hard retrieve of an existing user in database to test
        return $this->repository->find(5);
    }
}

Result :

{
    "@context": "/app_dev.php/contexts/Error",
    "@type": "hydra:Error",
    "hydra:title": "An error occurred",
    "hydra:description": "Invalid identifier \"\", \"id\" has not been found.",
    ...
}

If you see, I don’t need to specify an “id” for my action, but API Plateform is expecting an identifier “id”.

Here my User entity :

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * User
 *
 * @ApiResource(
 *     collectionOperations={
 *         "get"={
 *             "method"="GET",
 *             "normalization_context"={"groups"={"get_collection"}}
 *         },
 *     },
 *     itemOperations={
 *         "get"={
 *             "method"="GET",
 *             "normalization_context"={"groups"={"get_item"}}
 *         },
 *         "user_me"={
 *             "route_name"="api_users_me"
 *         },
 *     }
 * )
 * @ORM\Table(name="user")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
 */
class User implements AdvancedUserInterface
{
    // ...
}

In addition, here a router debug:

------------------------------------------- -------- -------- ------ ---------------------------------------
  Name                                        Method   Scheme   Host   Path
 ------------------------------------------- -------- -------- ------ ---------------------------------------
  _wdt                                        ANY      ANY      ANY    /_wdt/{token}
  _profiler_home                              ANY      ANY      ANY    /_profiler/
  _profiler_search                            ANY      ANY      ANY    /_profiler/search
  _profiler_search_bar                        ANY      ANY      ANY    /_profiler/search_bar
  _profiler_info                              ANY      ANY      ANY    /_profiler/info/{about}
  _profiler_phpinfo                           ANY      ANY      ANY    /_profiler/phpinfo
  _profiler_search_results                    ANY      ANY      ANY    /_profiler/{token}/search/results
  _profiler_open_file                         ANY      ANY      ANY    /_profiler/open
  _profiler                                   ANY      ANY      ANY    /_profiler/{token}
  _profiler_router                            ANY      ANY      ANY    /_profiler/{token}/router
  _profiler_exception                         ANY      ANY      ANY    /_profiler/{token}/exception
  _profiler_exception_css                     ANY      ANY      ANY    /_profiler/{token}/exception.css
  _twig_error_test                            ANY      ANY      ANY    /_error/{code}.{_format}
  api_entrypoint                              ANY      ANY      ANY    /{index}.{_format}
  api_doc                                     ANY      ANY      ANY    /docs.{_format}
  api_jsonld_context                          ANY      ANY      ANY    /contexts/{shortName}.{_format}
  api_bookmakers_get_collection               GET      ANY      ANY    /bookmakers.{_format}
  api_bookmakers_get_item                     GET      ANY      ANY    /bookmakers/{id}.{_format}
  api_categories_get_collection               GET      ANY      ANY    /categories.{_format}
  api_categories_get_item                     GET      ANY      ANY    /categories/{id}.{_format}
  api_comments_get_collection                 GET      ANY      ANY    /comments.{_format}
  api_comments_post_collection                POST     ANY      ANY    /comments.{_format}
  api_comments_get_item                       GET      ANY      ANY    /comments/{id}.{_format}
  api_feed_items_get_collection               GET      ANY      ANY    /feed_items.{_format}
  api_feed_items_post_collection              POST     ANY      ANY    /feed_items.{_format}
  api_feed_items_get_item                     GET      ANY      ANY    /feed_items/{id}.{_format}
  api_feed_items_put_item                     PUT      ANY      ANY    /feed_items/{id}.{_format}
  api_follows_get_collection                  GET      ANY      ANY    /follows.{_format}
  api_follows_post_collection                 POST     ANY      ANY    /follows.{_format}
  api_follows_get_item                        GET      ANY      ANY    /follows/{id}.{_format}
  api_follows_delete_item                     DELETE   ANY      ANY    /follows/{id}.{_format}
  api_moderation_cards_get_collection         GET      ANY      ANY    /moderation_cards.{_format}
  api_moderation_cards_get_item               GET      ANY      ANY    /moderation_cards/{id}.{_format}
  api_picks_get_collection                    GET      ANY      ANY    /picks.{_format}
  api_picks_post_collection                   POST     ANY      ANY    /picks.{_format}
  api_picks_get_item                          GET      ANY      ANY    /picks/{id}.{_format}
  api_picks_put_item                          PUT      ANY      ANY    /picks/{id}.{_format}
  api_report_picks_post_collection            POST     ANY      ANY    /report_picks.{_format}
  api_subpicks_get_collection                 GET      ANY      ANY    /subpicks.{_format}
  api_subpicks_post_collection                POST     ANY      ANY    /subpicks.{_format}
  api_subpicks_get_item                       GET      ANY      ANY    /subpicks/{id}.{_format}
  api_subpicks_put_item                       PUT      ANY      ANY    /subpicks/{id}.{_format}
  api_subpick_type_events_get_item            GET      ANY      ANY    /subpick_type_events/{id}.{_format}
  api_subpick_type_frees_get_item             GET      ANY      ANY    /subpick_type_frees/{id}.{_format}
  api_subscribed_bookmakers_get_collection    GET      ANY      ANY    /subscribed_bookmakers.{_format}
  api_subscribed_bookmakers_post_collection   POST     ANY      ANY    /subscribed_bookmakers.{_format}
  api_subscribed_bookmakers_get_item          GET      ANY      ANY    /subscribed_bookmakers/{id}.{_format}
  api_subscribed_bookmakers_delete_item       DELETE   ANY      ANY    /subscribed_bookmakers/{id}.{_format}
  api_users_get_collection                    GET      ANY      ANY    /users.{_format}
  api_users_post_collection                   POST     ANY      ANY    /users.{_format}
  api_users_get_item                          GET      ANY      ANY    /users/{id}.{_format}
  api_users_put_item                          PUT      ANY      ANY    /users/{id}.{_format}
  api_user_likes_get_collection               GET      ANY      ANY    /user_likes.{_format}
  api_user_likes_post_collection              POST     ANY      ANY    /user_likes.{_format}
  api_user_likes_get_item                     GET      ANY      ANY    /user_likes/{id}.{_format}
  api_user_likes_delete_item                  DELETE   ANY      ANY    /user_likes/{id}.{_format}
  api_login_check                             ANY      ANY      ANY    /login_check
  api_jwt_refresh_token                       ANY      ANY      ANY    /token/refresh
  api_users_me                                GET      ANY      ANY    /me
 ------------------------------------------- -------- -------- ------ ---------------------------------------

The route is here.

Documentation also generate a required “id” field, while i don’t want it : Users actions

I also tried to “cheat” by using “_api_collection_operation_name”, and redapt User correctly, but i got an “Illegal offset” error even if i return an array containing the User in method __invoke($data)

Do you have a solution ? Anyway, thanks for this useful bundle !

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:23 (5 by maintainers)

github_iconTop GitHub Comments

19reactions
Sybiocommented, Jun 19, 2017

Even if it doesn’t respect the REST philosophy, I paste my solution for the ones who need to implement a similar action like /me:

Action:

<?php

namespace AppBundle\Action;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Routing\Annotation\Route;
use AppBundle\Entity\User;
use Doctrine\Common\Persistence\ManagerRegistry;

class UserMe
{
    /**
     * @Route(
     *     name="api_users_me",
     *     path="/me",
     *     defaults={"_api_resource_class"=User::class, "_api_item_operation_name"="user_me"}
     * )
     * @Method("GET")
     * @return User|null
     */
    public function __invoke($data)
    {
        // Return the User object
        return $data;
    }
}

Provider:

<?php

namespace AppBundle\DataProvider;

use AppBundle\Entity\User;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
use AppBundle\Repository\UserRepository;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Router;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;

final class UserItemDataProvider implements ItemDataProviderInterface
{
    /**
     * @var UserRepository
     */
    private $repository;

    /**
     * @var TokenStorage
     */
    private $tokenStorage;

    /**
     * UserMe constructor.
     *
     * @param UserRepository $repository
     * @param TokenStorage   $tokenStorage
     */
    public function __construct(UserRepository $repository, TokenStorage $tokenStorage)
    {
        $this->repository   = $repository;
        $this->tokenStorage = $tokenStorage;
    }

    /**
     * @param string      $resourceClass
     * @param int|string  $id
     * @param string|null $operationName
     * @param array       $context
     *
     * @return User|null
     * @throws ResourceClassNotSupportedException
     */
    public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
    {
        if (User::class !== $resourceClass) {
            throw new ResourceClassNotSupportedException();
        }

        // retrieves User from the security when hitting the route 'api_users_me' (no id needed)
        if ('users_me' === $operationName) {
            return $this->tokenStorage->getToken()->getUser();
        }

        return $this->repository->find($id); // Retrieves User normally for other actions
    }
}

Register the provider:

services:
    user.item_data_provider:
        class: AppBundle\DataProvider\UserItemDataProvider
        public: false
        arguments:
            - "@app.repository.user"
            - "@security.token_storage"
        tags:
            -  { name: 'api_platform.item_data_provider' }

And as a bonus, don’t forget to secure the /me route (if you are using LexikJWTAuthenticationBundle for example):

security:
    # ...
    access_control:
        # ...
        - { path: ^/me, methods: [GET], roles: ROLE_USER }
18reactions
Napchecommented, Mar 6, 2019

You can also just use move your action to the collection_items, since there is no required id attribute there.

resources:
    App\Entity\User:
        attributes:
            normalization_context:
                groups: ['read']
            denormalization_context:
                groups: ['write']
        itemOperations:
            get: ~
            put: ~
        collectionOperations:
            get: ~
            profile:
                path: '/profile'
                controller: App\Action\UserProfileAction
                method: 'GET'
                normalization_context:
                    groups: ['profile']
                swagger_context:
                    public: false
                    tags: ['Authentication']
                    summary: "Returns the profile of the logged in user."
                    parameters:
                        -   name: 'Bearer'
                            required: true
                            in: 'header'
                            description: 'Active JWT Token'
                    responses:
                        200:
                            description: "json array with personal profile"
Read more comments on GitHub >

github_iconTop Results From Across the Web

How to select specific element without using id or classes
Here, you could use the :nth-of-type(selector) like this. // Select the first button of your page. document.querySelector ...
Read more >
When instantiating an custom object, does it get an ID?
No, Objects do not have an Id until after insert. Insert action automatically will return Id for your record.
Read more >
Configuring the Apollo Client cache
If you define a custom cache ID that uses multiple fields, it can be challenging to calculate and provide that ID to methods...
Read more >
jQuery Selectors
jQuery selectors are used to "find" (or select) HTML elements based on their name, id, classes, types, attributes, values of attributes and much...
Read more >
<input>: The Input (Form Input) element - HTML
The HTML element is used to create interactive controls for web-based forms in order ... <input type="text" id="name" name="name" required.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found