API Platform provides a system to extend queries on items and collections.
Extensions are specific to Doctrine and Elasticsearch-PHP, and therefore, the Doctrine ORM / MongoDB ODM support or the Elasticsearch reading support must be enabled to use this feature. If you use custom providers it’s up to you to implement your own extension system or not.
You can find a working example of a custom extension in the API Platform’s demo application.
Custom extensions must implement the ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface
and / or the ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface
interfaces, to be run when querying for a collection of items and when querying for an item respectively.
If you use custom state providers, they must support extensions and be aware of active extensions to work properly.
In the following example, we will see how to always get the offers owned by the current user. We will set up an exception, whenever the user has the ROLE_ADMIN
.
Given these two entities:
<?php
// api/src/Entity/User.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
#[ApiResource]
class User
{
// ...
}
<?php
// api/src/Entity/Offer.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
#[ApiResource]
class Offer
{
#[ORM\ManyToOne]
public User $user;
//...
}
<?php
// api/src/Doctrine/CurrentUserExtension.php
namespace App\Doctrine;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\Offer;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\SecurityBundle\Security;
final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(private readonly Security $security)
{
}
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
if (Offer::class !== $resourceClass || $this->security->isGranted('ROLE_ADMIN') || null === $user = $this->security->getUser()) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias));
$queryBuilder->setParameter('current_user', $user->getId());
}
}
Finally, if you’re not using the autoconfiguration, you have to register the custom extension with either of those tags:
# api/config/services.yaml
services:
# ...
'App\Doctrine\CurrentUserExtension':
tags:
- { name: api_platform.doctrine.orm.query_extension.collection }
- { name: api_platform.doctrine.orm.query_extension.item }
The api_platform.doctrine.orm.query_extension.collection
tag will register this service as a collection extension.
The api_platform.doctrine.orm.query_extension.item
does the same thing for items.
Note that your extensions should have a positive priority if defined. Internal extensions have negative priorities, for reference:
Service name | Priority | Class |
---|---|---|
api_platform.doctrine.orm.query_extension.eager_loading (item) | -8 | ApiPlatform\Doctrine\Orm\Extension\EagerLoadingExtension |
api_platform.doctrine.orm.query_extension.filter | -16 | ApiPlatform\Doctrine\Orm\Extension\FilterExtension |
api_platform.doctrine.orm.query_extension.filter_eager_loading | -17 | ApiPlatform\Doctrine\Orm\Extension\FilterEagerLoadingExtension |
api_platform.doctrine.orm.query_extension.eager_loading (collection) | -18 | ApiPlatform\Doctrine\Orm\Extension\EagerLoadingExtension |
api_platform.doctrine.orm.query_extension.order | -32 | ApiPlatform\Doctrine\Orm\Extension\OrderExtension |
api_platform.doctrine.orm.query_extension.pagination | -64 | ApiPlatform\Doctrine\Orm\Extension\PaginationExtension |
This example adds a WHERE
clause condition only when a fully authenticated user without ROLE_ADMIN
tries to access a resource. It means that anonymous users will be able to access all data. To prevent this potential security issue, the API must ensure that the current user is authenticated.
To secure the access to endpoints, use the following access control rule:
# app/config/package/security.yaml
security:
# ...
access_control:
# ...
- { path: ^/offers, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/users, roles: IS_AUTHENTICATED_FULLY }
Creating custom extensions is the same as with Doctrine ORM.
The interfaces are:
ApiPlatform\Doctrine\Odm\Extension\AggregationItemExtensionInterface
and ApiPlatform\Doctrine\Odm\Extension\AggregationCollectionExtensionInterface
to add stages to the aggregation builder.ApiPlatform\Doctrine\Odm\Extension\AggregationResultItemExtensionInterface
and ApiPlatform\Doctrine\Odm\Extension\AggregationResultCollectionExtensionInterface
to return a result.The tags are api_platform.doctrine_mongodb.odm.aggregation_extension.item
and api_platform.doctrine_mongodb.odm.aggregation_extension.collection
.
The custom extensions receive the aggregation builder, used to execute complex operations on data.
Currently only extensions querying for a collection of items through a search request
are supported. So your custom extensions must implement the RequestBodySearchCollectionExtensionInterface
. Register your
custom extensions as services and tag them with the api_platform.elasticsearch.request_body_search_extension.collection
tag.
Made with love by
Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.
Learn more