Skip to content
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

Unable to use resource identifier #1077

Closed
gnumoksha opened this issue Mar 31, 2019 · 5 comments
Closed

Unable to use resource identifier #1077

gnumoksha opened this issue Mar 31, 2019 · 5 comments
Labels

Comments

@gnumoksha
Copy link

The issue

I want to retrieve a resource by its slug thus I'm following the instructions on this page.

I've ended up with the following code:

<?php

declare(strict_types=1);

namespace Foo\Lt\Domain\Model\Page;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Table(name="pages", indexes={@ORM\Index(name="slug_index", columns={"slug"})})
 * @ORM\Entity(repositoryClass="Foo\Infrastructure\Domain\Model\Page\DoctrinePageRepository")
 *
 * @ApiResource(
 *     shortName="Pages",
 *     description="Miscellaneous pages.",
 *     routePrefix="v0",
 *     itemOperations={
 *         "get_page"={
 *              "method"="GET",
 *              "path"="/pages/{slug}",
 *              "requirements"={"slug"="\w+"},
 *              "swagger_context"= {
 *                  "summary"= "Retrieves a page.",
 *                  "description"= "Retrieves a page (i.e. help, terms, privacy)",
 *                  "parameters"= {
 *                      {
 *                          "in"= "path",
 *                          "name"= "slug",
 *                          "type"= "string",
 *                          "example"="help"
 *                      },
 *                  },
 *              },
 *          },
 *     },
 *     collectionOperations={
 *         "get"={"method"="GET"}
 *     },
 *     iri="pages",
 * )
 */
class Page
{
    /**
     * The unique auto incremented primary key.
     *
     * @var int|null
     *
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;

    /**
     * The page slug.
     *
     * @var string
     *
     * @ORM\Column(type="string", length=255, nullable=false)
     *
     * @ApiProperty(
     *     identifier=true,
     *     attributes={
     *         "swagger_context"={
     *             "type"="string",
     *             "description"="The page slug.",
     *             "example"="help"
     *         }
     *     },
     * )
     */
    protected $slug;

    /**
     * The page text.
     *
     * @var string
     *
     * @ORM\Column(type="text", nullable=false)
     *
     * @ApiProperty(
     *     attributes={
     *         "swagger_context"={
     *             "type"="string",
     *             "description"="The page text.",
     *             "example"="Foo bar baz",
     *         },
     *     }
     * )
     */
    protected $text;

    public function getId() : int
    {
        return $this->id;
    }

    public function getSlug() : string
    {
        return $this->slug;
    }

    public function getText() : string
    {
        return $this->text;
    }
}

Unfortunately a request to api/v0/pages/help will result in:

{
  "@context": "/api/contexts/Error",
  "@type": "hydra:Error",
  "hydra:title": "An error occurred",
  "hydra:description": "Not found, because of an invalid identifier configuration",
  "trace": [
    {
      "namespace": "",
      "short_class": "",
      "class": "",
      "type": "",
      "function": "",
      "file": "/var/www/app/vendor/api-platform/core/src/EventListener/ReadListener.php",
      "line": 102,
      "args": []
    },

What did not tell anything useful to me, so after removing the try catch from ReadListener the following exception has occurred:

{
  "@context": "/api/contexts/Error",
  "@type": "hydra:Error",
  "hydra:title": "An error occurred",
  "hydra:description": "Parameter \"id\" not found",
  "trace": [
    {
      "namespace": "",
      "short_class": "",
      "class": "",
      "type": "",
      "function": "",
      "file": "/var/www/app/vendor/api-platform/core/src/DataProvider/OperationDataProviderTrait.php",
      "line": 90,
      "args": []
    },

Notes

  • I do not need a collectionOperations but I'm declaring one because of this.
  • I'm using the iri annotation like suggested here in order to avoid the errors "No collection route associated with the type" and "Unable to generate an IRI for the item of type"
  • This issue is kinda messed because at the moment I've had a lot of problems trying to configure this simple entity/resource without coupling my domain logic with API Platform.
@soyuka
Copy link
Member

soyuka commented Mar 31, 2019

It can't really work like this:

  • you're declaring 2 identifiers (id and slug)
  • the route contains slug but Api Platform can't request your data provider through this
  • it'll try to use the request given id which isn't in the route params

What you could do is use "path"="/pages/{id} and add a DataProvider that will handle id as a slug.
Or do a custom operation with a custom controller.

We don't have a way to match route params to an identifier like you want it to, I have a proposal here though: api-platform/core#2126. You could maybe add this patch in your stack by overriding some services.

@gnumoksha
Copy link
Author

@soyuka Thanks for answering.

I have some questions:

  1. Using "@ApiProperty (identifier = true)" shouldn't be sufficient to tell the API Platform to use only the slug?
  2. Should not this information be described at https://api-platform.com/docs/core/identifiers/?
  3. Could you provide an example of how to use a custom controller for this case? I'm still getting "Not Found, because of an invalid identifier configuration"

And, if it is not asking too much, can you provide an example of how to use API platform with only DTOs and serialization? At the moment I do not want to couple my application with the API platform' way to do things so I only want API Platform to populate a DTO, receive it on a controller and serialize the response.

@soyuka
Copy link
Member

soyuka commented Apr 8, 2019

Using "@ApiProperty (identifier = true)" shouldn't be sufficient to tell the API Platform to use only the slug?

If there is only this identifier yes, if not I have a proposal here to do this: api-platform/core#2126.

Should not this information be described at https://api-platform.com/docs/core/identifiers/?

It can, especially if there are no other metadata that describes a property as identifier.

I don't really have the time to offer you a full example but if you check how to declare a custom operation in the documentation you should be able to do so.

@stephanvierkant
Copy link

@gnumoksha Have you found a solution? I'm trying to achieve the same, but I'm looking for a full example too.

@gnumoksha
Copy link
Author

@stephanvierkant I had a lot of problems trying to get Api Platform to work thus I stopped trying to use its "magic" and started doing the work by myself, which means I've created some controllers, serializers and a decorator class to override some parts of generated documentation.

Below are some classes I think can be useful to you.

Entity

declare(strict_types=1);

namespace Foo\Bar\Domain\Model\Page;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Foo\Bar\Infrastructure\Symfony\Controller\Page\GetPageController;

/**
 * @ORM\Table(name="pages", indexes={@ORM\Index(name="slug_index", columns={"slug"})})
 * @ORM\Entity(repositoryClass="Foo\Bar\Infrastructure\Domain\Model\Page\DoctrinePageRepository")
 *
 * @ApiResource(
 *     shortName="Pages",
 *     description="Miscellaneous pages.",
 *     itemOperations={
 *         "get"={
 *              "method"="GET",
 *              "path"="/pages/{slug}",
 *              "requirements"={"slug"="\w+"},
 *              "controller"=GetPageController::class,
 *              "defaults"={
 *                  "_api_receive"=false
 *              },
 *              "swagger_context"= {
 *                  "summary"= "Retrieves a page.",
 *                  "description"= "Retrieves a page (i.e. help, terms, privacy, roles)",
 *                  "parameters"= {
 *                      {
 *                          "in"= "path",
 *                          "name"= "slug",
 *                          "type"= "string",
 *                          "example"="help"
 *                      },
 *                  },
 *              },
 *          },
 *     },
 *     collectionOperations={
 *     },
 * )
 */
class Page
{
    /**
     * The unique auto incremented primary key.
     *
     * @var int|null
     *
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;

    /**
     * The page slug.
     *
     * @var string
     *
     * @ORM\Column(type="string", length=255, nullable=false)
     *
     * @Assert\NotBlank
     *
     * @ApiProperty(
     *     identifier=true,
     *     attributes={
     *         "swagger_context"={
     *             "type"="string",
     *             "description"="The page slug.",
     *             "example"="help"
     *         }
     *     },
     *
     * )
     */
    protected $slug;

    /**
     * The page text.
     *
     * @var string
     *
     * @ORM\Column(type="text", nullable=false)
     *
     * @Assert\NotBlank
     *
     * @ApiProperty(
     *     attributes={
     *         "swagger_context"={
     *             "type"="string",
     *             "description"="The page text.",
     *             "example"="Foo bar baz",
     *         },
     *     }
     * )
     */
    protected $text;

    public function getId() : int
    {
        return $this->id;
    }

    public function setSlug(string $slug) : void
    {
        $this->slug = $slug;
    }

    public function getSlug() : string
    {
        return $this->slug;
    }

    public function setText(string $text) : void
    {
        $this->text = $text;
    }

    public function getText() : string
    {
        return $this->text;
    }
}

Controller

declare(strict_types=1);

namespace Foo\Bar\Infrastructure\Symfony\Controller\Page;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Foo\Bar\Domain\Model\Page\Page;
use Foo\Bar\Domain\Model\Page\PageRepository;

/**
 * @see https://api-platform.com/docs/core/operations/#creating-custom-operations-and-controllers
 */
class GetPageController
{
    /** @var \Foo\Bar\Domain\Model\Page\PageRepository */
    private $pageRepository;

    public function __construct(PageRepository $pageRepository)
    {
        $this->pageRepository = $pageRepository;
    }

    /**
     * Gets a page.
     */
    public function __invoke(Request $data) : ?Page
    {
        $requestedSlug = $data->attributes->get('slug');
        if ($requestedSlug === null) {
            throw new \InvalidArgumentException('Page not found.');
        }

        $page = $this->pageRepository->ofSlug($requestedSlug);
        if ($page !== null) {
            return $page;
        }

        throw new NotFoundHttpException('Page not found.');
    }
}

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

No branches or pull requests

4 participants