Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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

api/src/Doctrine/OwnerFilterExtension.php
<?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.