How to create a Collection Data Provider and keep Doctrine Extension, Filters and Pagination on it [Api-Platform]
Tags: php, api, symfony, apiplatform
Published: 2022-05-10 18:02:53

I would like to share with you how override the query of a collection in api platform and keep the pagination, filters and doctrine extensions on it.

We're not going to create a full project but you can go to my repository to see the provider in a project with fixtures. 

This is the homepage of the repository https://github.com/aratinau/api-platform-pagination (who show multiples others examples to create a pagination with custom datas with api platform) 
You can see the Merge Request here: https://github.com/aratinau/api-platform-pagination/commit/1d27f16adecda1c0956fcc5d0da81f017d915c1b

We're going to create a Provider who will return only books not archived when the param `includeArchived` is missing. And all books (archived or not) when the param is `includeArchived=true`

Then we will create a Doctrine Extension to return only books by the locale defined.

This is our book entity `src/Entity/Book.php`
```php
<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\BookRepository;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;

/**
 * @ApiResource()
 * @ApiFilter(OrderFilter::class)
 * @ORM\Entity(repositoryClass=BookRepository::class)
 */
class Book
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $title;

    /**
     * @ORM\Column(type="boolean")
     */
    private $isArchived;

    /**
     * @ORM\Column(type="string", length=4)
     */
    private $locale;

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

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): self
    {
        $this->title = $title;

        return $this;
    }

    public function getIsArchived(): ?bool
    {
        return $this->isArchived;
    }

    public function setIsArchived(bool $isArchived): self
    {
        $this->isArchived = $isArchived;

        return $this;
    }

    public function getLocale(): ?string
    {
        return $this->locale;
    }

    public function setLocale(string $locale): self
    {
        $this->locale = $locale;

        return $this;
    }
}
```

Create the `src/DataProvider/BookCollectionDataProvider.php`

```php
<?php

namespace App\DataProvider;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
use App\DTO\SessionParameter;
use App\Entity\Book;
use App\Entity\Session;
use Doctrine\Persistence\ManagerRegistry;

class BookCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
    public function __construct(
        private ManagerRegistry $managerRegistry,
        private $collectionExtensions,
    ) {
    }

    public function getCollection(
        string $resourceClass,
        string $operationName = null,
        array $context = []
    ): iterable {
        // here we define to false when the param 'includeArchived' is missing (by default)
        $includeArchived = $context['filters']['includeArchived'] ?? 'false';

        $manager = $this->managerRegistry->getManagerForClass($resourceClass);
        $repository = $manager->getRepository($resourceClass);
        $queryBuilder = $repository->createQueryBuilder('o');
        $alias = $queryBuilder->getRootAliases()[0];

        // by default we want only books not archived
        if ($includeArchived === 'false') {
            $queryBuilder->andWhere("$alias.isArchived = false");
        }

        /*
            Then we will add to all extensions our queryBuilder updated.
            We could inject only the extension we needs but I wanted to show them all to you.
            The first extension (BookExtension) will be created after
        */
        $queryNameGenerator = new QueryNameGenerator();
        foreach ($this->collectionExtensions as $extension) {
            /*
             * Extensions are (in this order)
             * - "App\Doctrine\BookExtension"
             * - "ApiPlatform\Doctrine\Orm\Extension\FilterExtension"
             * - "ApiPlatform\Doctrine\Orm\Extension\FilterEagerLoadingExtension"
             * - "ApiPlatform\Doctrine\Orm\Extension\EagerLoadingExtension"
             * - "ApiPlatform\Doctrine\Orm\Extension\OrderExtension"
             * - "ApiPlatform\Doctrine\Orm\Extension\PaginationExtension"
             */
            $extension->applyToCollection(
                $queryBuilder,
                $queryNameGenerator,
                $resourceClass,
                $operationName,
                $context
            );

            /*
                This next condition check if we have the pagination activated (by default is yes) and the result is returned
            */
            if (
                $extension instanceof QueryResultCollectionExtensionInterface
                &&
                $extension->supportsResult($resourceClass, $operationName, $context)
            ) {
                return $extension->getResult($queryBuilder, $resourceClass, $operationName, $context);
            }
        }

        // we are here only if we have deactivate the pagination
        return $queryBuilder->getQuery()->getResult();
    }

    public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
    {
        return Book::class === $resourceClass;
    }
}
```

To inject the `$collectionExtensions` we have to add in `config/services.yaml`

```yaml
App\DataProvider\BookCollectionDataProvider:
    bind:
        $collectionExtensions: !tagged api_platform.doctrine.orm.query_extension.collection
```

Now create the file `src/Doctrine/BookExtension.php`

```php
<?php

namespace App\Doctrine;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use App\Entity\Book;
use Doctrine\ORM\QueryBuilder;

class BookExtension implements QueryCollectionExtensionInterface
{
    private const LOCALE = 'fr';

    public function applyToCollection(
        QueryBuilder $queryBuilder,
        $queryNameGenerator,
        string $resourceClass,
        string $operationName = null
    ) {
        /*
            We ask to return only the book with the locale 'fr'
        */
        if ($resourceClass === Book::class) {
            $rootAlias = $queryBuilder->getRootAliases()[0];
            $queryBuilder
                ->andWhere("$rootAlias.locale = :locale")
                ->setParameter('locale', self::LOCALE)
            ;
        }
    }
}
```

Now you can add on your `Book` entity filters, for example `OrderFilter` and keep the collection filtred through your collection provider.

GET `/books` return books not archived.
GET `/books?includeArchived=false` return books not archived.
GET `/books?includeArchived=true` return all books.

Hope you like! 🙂🚀