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
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
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
App\DataProvider\BookCollectionDataProvider:
bind:
$collectionExtensions: !tagged api_platform.doctrine.orm.query_extension.collection
Now create the file src/Doctrine/BookExtension.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! 🙂🚀
Top comments (0)