6

I have custom action (saw in docs as recommended method) that makes some logic and returns doctrine collection of entities.

With regular api-platform action filters working perfectly. But how can i get any from default filters to work with this collection in my custom action?

When i request GET /cars?createdAt[after]=2018-08-01 or GET /drivers?createdAt[after]=2018-08-01 it works as expected.

But when i'm trying to do GET /drivers/42/cars_custom_logic?createdAt[after]=2018-08-01 it doesn't filter anything. It's expected as i didn't call filter in my custom action, but my question is – how to add this filter?


App\Entity\Car

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ORM\Entity
 * @ApiResource
 * @ApiFilter(DateFilter::class, properties={"createdAt"})
 */
class Car
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     * @Groups({"car", "driver"})
     */
    private $id;

    /**
     * @ORM\Column(type="datetime")
     * @Groups({"car", "driver"})
     */
    private $createdAt;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Driver", inversedBy="cars")
     * @Groups({"car", "driver"})
     */
    private $driver;

    public function __construct()
    {
        $this->createdAt = new \DateTime('now');
    }

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

    public function getCreatedAt(): \DateTimeInterface
    {
        return $this->createdAt;
    }

    public function getDriver(): Driver
    {
        return $this->driver;
    }
}

App\Entity\Driver

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ORM\Entity
 * @ApiResource(itemOperations={
 *     "get",
 *     "special"={
 *         "method"="GET",
 *         "path"="/drivers/{id}/cars_custom_logic",
 *         "controller"=GetDriverCarsAction::class
 *     }
 * })
 * @ApiFilter(DateFilter::class, properties={"createdAt"})
 */
class Driver
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     * @Groups({"car", "driver"})
     */
    private $id;

    /**
     * @ORM\Column(type="datetime")
     * @Groups({"car", "driver"})
     */
    private $createdAt;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Car", mappedBy="driver")
     * @Groups({"car", "driver"})
     */
    private $cars;

    public function __construct()
    {
        $this->createdAt = new \DateTime('now');
    }

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

    /**
     * @return Collection|Car[]
     */
    public function getCars(): Collection
    {
        return $this->cars;
    }
}

App\Controller\GetDriverCarsAction

<?php

namespace App\Controller;

use App\Entity\Car;
use App\Entity\Driver;
use Doctrine\Common\Collections\Collection;
use Symfony\Bridge\Doctrine\RegistryInterface;

final class GetDriverCarsAction
{
    private $doctrine;

    public function __construct(RegistryInterface $doctrine)
    {
        $this->doctrine = $doctrine;
    }

    public function __invoke(Driver $driver): Collection
    {
        $cars = $driver->getCars();

        // ..... Some domain logic .....

        // ..... Here – what should i do to make filter work here? .....

        return $cars;
    }
}

7
  • Can you show what you've done so far ? Commented Aug 21, 2018 at 14:28
  • 1
    @GregoireDucharme, sure, i updated my question with code. Commented Aug 21, 2018 at 19:16
  • @trogwar how did you solve this? Would you mind answering your own question if you came up with solution, I have exactly same situation... Commented Nov 4, 2019 at 12:20
  • @KoviNET, well, not exactly. As far as i dig into, logic is simple: if you need to implement custom operation – do all custom stuff by yourself. Commented Nov 11, 2019 at 9:37
  • 1
    @trogwar I have solved it by manually fetching parameters in controller and passing them to query builder in repository. It works but there is no auto generated hydra documentation and there is some code duplication which is not so nice. Commented Nov 11, 2019 at 17:26

2 Answers 2

1

What if you try adding via yaml like this:

# api/config/api_platform/resources.yaml
App\Entity\Book:
    attributes:
       filters: [ offer.date_filter ]
    itemOperations:
        get: ~
        special:
            method: 'GET'
            path: '/books/{id}/special'
            controller: 'App\Controller\BookSpecial'

OR

# api/config/api_platform/resources.yaml
App\Entity\Book:
    itemOperations:
        get: ~
        special:
            method: 'GET'
            path: '/books/{id}/special'
            controller: 'App\Controller\BookSpecial'
            filters: ['offer.date_filter']

For more deep look at this documentation: https://api-platform.com/docs/core/filters#doctrine-orm-filters

Hope it helps

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for answer! I saw docs and, if i understand it correctly, there is three options of configure: annotation (i used this), two ways with yaml.
With your configuration everything works like mine – no errors, but no filters. I thinks it's because somewhere inside ApiPlatform's controllers they call somehow those filters, but me not. And i didn't see in documentation how to call them from my custom action.
no cache, but cleared with “cache:clear” - same thing.
1

API Platform 3 solution

I'm studying API platform since 2 weeks but I think I've got the point digging into the CollectionProvider.

My need is a custom endpoint showing carts metadata (total number, total amount, etc.) with the ability to use filters (i.e. a date filter).

First of all, your custom operation should be a GetCollection with a state provider, or needs to be implement a custom operation class which extends the (empty) interface CollectionOperationInterface, otherwise you want pass the check of OpenApiFactory and Swagger UI would not show the filters in the UI. This feels wrong I know, in my case I'll return a DTO with stats, not a collection.

Example:

#[ApiResource]
#[GetCollection(
    uriTemplate: '/carts/meta',
    provider: CartGetMetaProvider::class,
    output: CartMeta::class,
)]
class Cart
{
}

Now, the provider. The provider should create a query builder and have the query extension injected. ORM example:

class CartGetMetaProvider implements ProviderInterface
{    
    public function __construct(
        private readonly ManagerRegistry $managerRegistry,
        #[TaggedIterator('api_platform.doctrine.orm.query_extension.collection')]
        private readonly iterable $collectionExtensions,
    ) {
    }

    public function provide(
        Operation $operation, 
        array $uriVariables = [], 
        array $context = []): object|array|null
    {
        $entityClass = $operation->getClass();
        if (($options = $operation->getStateOptions()) 
            && $options instanceof Options && $options->getEntityClass()) {
            $entityClass = $options->getEntityClass();
        }

        /** @var \Doctrine\Orm\EntityManagerInterface $manager */
        $manager = $this->managerRegistry->getManagerForClass($entityClass);

        $repository = $manager->getRepository($entityClass);
        if (!method_exists($repository, 'createStatsQueryBuilder')) {
            throw new RuntimeException(
                'The repository class must have a "createStatsQueryBuilder" method.'
            );
        }

        $queryBuilder = $repository->createStatsQueryBuilder('o');
        $queryNameGenerator = new QueryNameGenerator();

        foreach ($this->collectionExtensions as $extension) {
            $extension->applyToCollection(
                $queryBuilder, 
                $queryNameGenerator, 
                $entityClass, 
                $operation,
                $context
            );
        }

        // Here filters are applied, do whatever you want with the query
        // I.e. fill a custom output DTO with stats/metadata
       
   }
}

If you need parent/child relations like the original question, look at the CollectionProvider I linked.

enter image description here

1 Comment

Thank you for answer! I plan to migrate to v3 next month or two, so i'll definitely try your way!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.