Development

Doctrine Extensions in API Platform

API Platform provides a way to create your own Doctrine Extensions, allowing you to modify collection and item queries for API resources.

The power here is that you can customize or create conditional query logic on the queries generated by the default controllers/operations API platform provides. There's no need to write a custom controller if you simply want to tweak the underlying query.

Use Case

Imagine an app with a Product entity which has a boolean active property. You want to restrict results to only active products for everyone except administrators.

<?php
// src/Entity/Product.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;

/**
 * @ApiResource
 */
class Product
{
    //...

   /**
     * @var boolean
     * @ORM\Column(type="boolean", nullable=false)
     */
    private $active = true;

    //...
}

The Solution

  1. Create the extension class
  2. Create a service for the class

Creating the Extension Class

  1. Create the directory: src/Doctrine
  2. Add a new file: ActiveProductExtension.php
  3. Copy the source below into the new file

The extension class implements QueryCollectionExtensionInterface and QueryItemExtensionInterface which forces us to create the applyToCollection() and applyToItem() methods.

If you were only concerned with results returned from a collection, you may only implement QueryCollectionExtensionInterface.

In addition to the 2 methods required by the interface, there is also an addWhere() method. It is called from both apply* methods and is where the real work happens.

<?php
// src/Doctrine/ActiveProductExtension.php
// Adapted from the API Platform CurrentUserExtension example.

namespace App\Doctrine;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Product;
use App\Entity\User;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class ActiveProductExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    private $tokenStorage;
    private $authorizationChecker;

    public function __construct(TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $checker)
    {
        $this->tokenStorage = $tokenStorage;
        $this->authorizationChecker = $checker;
    }

    /**
     * {@inheritdoc}
     */
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        $this->addWhere($queryBuilder, $resourceClass);
    }

    /**
     * {@inheritdoc}
     */
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
    {
        $this->addWhere($queryBuilder, $resourceClass);
    }

    /**
     *
     * @param QueryBuilder $queryBuilder
     * @param string       $resourceClass
     */
    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass)
    {
        $user = $this->tokenStorage->getToken()->getUser();

        // Add the where clause if we're operating on a Product resource, and the user is not an admin.
        if ($user instanceof User && Product::class === $resourceClass && !$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
            $rootAlias = $queryBuilder->getRootAliases()[0];
            $queryBuilder->andWhere(sprintf('%s.active = true', $rootAlias));
        }
    }
}

Creating the Service

With the extension class created, all that is left is to create the service.

Add the following to: config/services.yaml.

This assumes you have autowire enabled by default.

services:

    'App\Doctrine\CurrentProductExtension':
        tags:
            - { name: api_platform.doctrine.orm.query_extension.collection, priority: 9 }
            - { name: api_platform.doctrine.orm.query_extension.item }

Result

With the extension and service in place, /api/products and /api/products/{id} will only return records which have active set to true, unless the user has the role ROLE_ADMIN.

For more information on Doctrine Extensions with API Platform, visit the docs.

Up Next

Ready To Get Started?

Schedule a complimentary 30-minute strategy consultation with one of our Drupal experts as early as today. We promise...they don't bite!