Query Extensions for Global Query Adjustments
Query Extensions for Global Query Adjustments
~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Chapter 55 filtered ONE collection via a REPLACING provider – query extensions offer a MORE ELEGANT alternative that AUGMENTS the DEFAULT provider instead of replacing it, and applies AUTOMATICALLY across MULTIPLE resources.
The QueryCollectionExtensionInterface
<?php
declare(strict_types=1);
namespace App\Doctrine;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\Project;
use App\Entity\User;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\SecurityBundle\Security;
final class OwnerFilterExtension implements QueryCollectionExtensionInterface
{
public function __construct(
private readonly Security $security,
) {
}
public function applyToCollection(
QueryBuilder $queryBuilder,
\ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = [],
): void {
if (Project::class !== $resourceClass) {
return;
}
$user = $this->security->getUser();
if (!$user instanceof User || \in_array('ROLE_ADMIN', $user->getRoles(), true)) {
return;
}
$alias = $queryBuilder->getRootAliases()[0];
$queryBuilder
->andWhere("{$alias}.owner = :current_user")
->setParameter('current_user', $user);
}
}EXACTLY the same filtering logic as OwnProjectsCollectionProvider from chapter 55 – BUT: applyToCollection() only AUGMENTS the QueryBuilder condition, instead of TAKING OVER the ENTIRE query.
The decisive advantage
BECAUSE the default QueryBuilder logic stays INTACT, pagination, SearchFilter, OrderFilter (block 4) STILL work FULLY – the REPLACING provider from chapter 55 had EXACTLY THAT as a known limitation.
Registering the extension
EXACTLY like voters (chapter 52) and filters (chapter 31), the extension gets recognized AUTOMATICALLY via the implemented interface – NO manual services.yaml registration needed, AS LONG AS Symfony's autowiring/autoconfigure is active (the DEFAULT in the api-platform distribution).
Removing the old provider
// Project.php - REMOVE the provider parameter from chapter 55
new GetCollection(), // WITHOUT provider: OwnProjectsCollectionProvider::class
Achtung: Extensions and REPLACING providers are MUTUALLY exclusive – a replacing provider IGNORES extensions COMPLETELY, since it doesn't even CALL the DEFAULT mechanism AT ALL. Chapter 55 served STEP-BY-STEP understanding; extensions are the MORE MATURE solution for THIS use case.
QueryItemExtensionInterface for single objects
ANALOGOUSLY, QueryItemExtensionInterface exists with applyToItem() for GET /api/projects/{id} – USEFUL for applying the SAME visibility rule ALSO to single-item fetches, instead of relying EXCLUSIVELY on the voter (chapter 53).
Tipp: Query extensions are the PROFESSIONAL standard path for GLOBAL, recurring database filtering logic in API Platform – RECOMMENDED OVER replacing providers WHENEVER the default Doctrine query STAYS otherwise useful.