Wide banner showcasing playful tech illustrations, including a quirky spider-like character representing API Platform, over a gentle blue and white background.

API Platform: How to Implement Custom Collection Data Providers with Filters?

03/03/2023

In most cases, when you are trying to filter data from your endpoint, default ApiPlatform filters are more than enough to cover all your needs. Sometimes though, additional actions are needed before your collection can be returned to the world.

How to Implement Custom Collection Data Providers with Filters?

This article explains how to implement a custom collection data provider with filters in API Platform version 2.7.4 and is a part of our series of articles on API Platform which you can find on our blog.

Imagine the following scenario: Let's say we have a collection named transactions. We already created endpoints to fetch data GET /api/transactions:

collectionOperations={
    "get"={
        "normalization_context"={"groups"={"transaction:read"}}
    },
}

We also have some filters; for example, employee exact.

>@ApiFilter(SearchFilter::class, properties={"employee"="exact"})

By default when you will call your endpoint with an additional param in the query string project=1, your list will be filtered by only these transactions that are related to a specific project - in this example id 1. The problem starts when you want to do something extra in the DataProvider in this collection. For example, let's filter collection in a data provider based on logged users. If a user has a specific role, then they see all transactions, if they’re a "normal" user then they can only see transactions related to his user.

public function getCollection(string $resourceClass, string $operationName = null, array $context = []): iterable
{
    $queryBuilder = $this->managerRegistry->getRepository(Transaction::class)->createQueryBuilder('t');

    if (!$this->security->isGranted('ROLE_MANAGER')) {
        $queryBuilder
            ->andWhere('ewh.employee = :employee')
            ->setParameter('employee', $this->security->getUser())
        ;
    }

    return $queryBuilder->getQuery()->getResult();
}

However, what’s important to know is that this will remove default filters. Hence, you will not be able to filter by project anymore. What’s the solution?

To force DataProvider to use default filters. In order to do that, we need to inject extensions collection from the api platform.

public function __construct(
    private object $collectionExtensions,
    private ManagerRegistry $managerRegistry,
    private Security $security,
)
{}

Next, we need to inject a specific collection into our service.

App\DataProvider\TransactionsDataProvider:
    tags:
        - { name: 'api_platform.collection_data_provider', priority: 3 }
    autoconfigure: false
    arguments:
        $collectionExtensions: !tagged api_platform.doctrine.orm.query_extension.collection

From this moment forward, we will have all extensions available in our service. Some of them are filters, other ones could be paginations. Now, we can use them to filter our Query by additional criteria - by project in our example. To do that let's use the following code where we are applying each extension.

$queryNameGenerator = new QueryNameGenerator();

/** @var QueryCollectionExtensionInterface $extension */
foreach ($this->collectionExtensions as $extension) {
    $extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);

    if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) {
        return $extension->getResult($queryBuilder);
    }
}

Now, we can finally call our endpoint to check if everything works well. All class including all previous code snippets is available here:

<?php

declare(strict_types=1);

namespace App\DataProvider;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Entity\Transaction;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Security;

final class TransactionsDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{
    private ManagerRegistry $managerRegistry;

    private Security $security;

    private object $collectionExtensions;

    public function __construct(
        private object $collectionExtensions,
        private ManagerRegistry $managerRegistry,
        private Security $security,
    )
    {}

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

    public function getCollection(string $resourceClass, string $operationName = null, array $context = []): iterable
    {
        $queryBuilder = $this->managerRegistry->getRepository(Transaction::class)->createQueryBuilder('t');

        if (!$this->security->isGranted('ROLE_MANAGER')) {
            $queryBuilder
                ->andWhere('t.employee = :employee')
                ->setParameter('employee', $this->security->getUser())
            ;
        }

        $queryNameGenerator = new QueryNameGenerator();

        /** @var QueryCollectionExtensionInterface $extension */
        foreach ($this->collectionExtensions as $extension) {
            $extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);

            if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) {
                return $extension->getResult($queryBuilder);
            }
        }

        return $queryBuilder->getQuery()->getResult();
    }
}

Hope this helps!

Ma
Image of Marek Krokwa, confidently posing with arms crossed. He's CTO at Primotly and contributor to our insightful technology articles. Marek is wearing a dark collared shirt and glasses, presenting a smart and approachable look, set against a clean white background
Marek Krokwa
CEO

Latest articles

Tech-themed illustration with icons representing various technology and business areas in which Fractional CTO can help, arranged in a circular pattern on a light background.

Business | 26/07/2024

Fractional CTO Services: Expert Support Tailored to Your Company's Needs

Łukasz Kopaczewski

Can you benefit from the experience of an IT expert without hiring one full-time? Fractional CTOs offer a solution that provides you with a Chief Technology Officer to the extent you need them most. This is an excellent option, particularly for startups and medium-sized companies. In this article, we will explore the essence of the Fractional CTO role and discuss the benefits it can bring to your company. We will also learn about situations where the use of a CTO can be a key element in developing and accelerating technological progress in a company.

Read more
Illustration to the article about AI transparency highlighting main pillars of transparent AI system.

Business | 17/07/2024

The Importance of AI Transparency

Bernhard Huber

From automating routine tasks to making complex decisions, AI systems are becoming integral to many organizations' daily operations. According to statistics general trust in AI is decreasing. As the adoption of AI continues to grow, so does the need for transparency which can improve how AI is seen.

Read more
Preview image for the article about custom software solutions

Business | 11/07/2024

How Custom Software Supports Your Business?

Łukasz Kopaczewski

In many cases, off-the-shelf software cannot be adapted to the specific nature of a given industry or company's business profile. If none of the available solutions meet your expectations, it is worth considering dedicated software. What is it and what benefits can it offer for companies?

Read more