Filtering collections in api-platform with DataProvider maintaining pagination

Viewed 213

We implemented a RolePermissionChecker, which should control the security/accessibility of a specific resource (Offer) comparing attributes of the accessor and creator of the resource. For itemOperations we are using a Voter (https://api-platform.com/docs/core/security/#hooking-custom-permission-checks-using-voters) which is working perfectly.

According to https://api-platform.com/docs/core/security/#filtering-collection-according-to-the-current-user-permissions for collectionOperations DataProvider should be used - so we implemented one.

The issue we got now is that the OfferCollectionDataProvider firstly calls its parent ApiPlatform\Core\Bridge\Doctrine\Orm\CollectionDataProvider::getCollection function in order to apply all extensions and retrieve the collections. Afterwards we filter the entities the user has no access to read. But as we are using pagination and filtering on a ready page, the pagination is broken afterswards as for example half of the page entries will be filtered and for the user it looks like there is no more data.

Does anybody have a good idea how to filter collections without breaking pagination and without using extensions as our permissions checks are a bit complex and very hard to convert into queries?

<?php

namespace App\DataProvider;

use ApiPlatform\Core\Bridge\Doctrine\Orm\CollectionDataProvider;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator;
use App\Entity\Offer;
use App\Entity\User;
use App\Service\RolePermissionChecker;
use ArrayIterator;
use Doctrine\Persistence\ManagerRegistry;
use ReflectionProperty;
use Symfony\Component\Security\Core\Security;

/**
 * Class OfferCollectionDataProvider
 *
 * @package                  App\DataProvider
 */
class OfferCollectionDataProvider extends CollectionDataProvider
{
    /**
     * @var RolePermissionChecker
     */
    protected RolePermissionChecker $rolePermissionChecker;

    /**
     * @var Security $security
     */
    protected Security $security;

    /**
     * @param RolePermissionChecker $rolePermissionChecker
     * @param Security              $security
     * @param ManagerRegistry       $managerRegistry
     * @param iterable              $collectionExtensions
     */
    public function __construct(
        RolePermissionChecker $rolePermissionChecker,
        Security              $security,
        ManagerRegistry       $managerRegistry,
        iterable              $collectionExtensions = []
    )
    {
        parent::__construct(
            $managerRegistry,
            $collectionExtensions
        );

        $this->rolePermissionChecker = $rolePermissionChecker;
        $this->security              = $security;
    }

    /**
     * @param string      $resourceClass
     * @param string|null $operationName
     * @param array       $context
     *
     * @return bool
     */
    public function supports(
        string $resourceClass,
        string $operationName = null,
        array  $context = []
    ): bool
    {
        return Offer::class === $resourceClass;
    }

    /**
     * @inheritdoc
     */
    public function getCollection(
        string $resourceClass,
        string $operationName = null,
        array  $context = []
    ): iterable
    {
        $collection = parent::getCollection(
            $resourceClass,
            $operationName,
            $context
        );
        $user       = $this->security->getUser();

        if ($user instanceof User) {
            $hasReadPermissionsOnOffer = fn($offer) => $this->rolePermissionChecker->hasReadPermission(
                $offer,
                $user
            );

            if ($collection instanceof Paginator) {
                $newIteratorArray = [];

                foreach ($collection->getIterator() as $offer) {
                    if ($hasReadPermissionsOnOffer($offer)) {
                        $newIteratorArray[] = $offer;
                    }
                }

                $paginatorReflectionProperty = new ReflectionProperty(
                    Paginator::class,
                    'paginator'
                );

                $paginatorReflectionProperty->setAccessible(true);

                $paginator = $paginatorReflectionProperty->getValue($collection);

                return new FilteredPaginator(
                    $paginator,
                    new ArrayIterator(
                        $newIteratorArray
                    ),
                    $collection->getCurrentPage(),
                    $collection->getItemsPerPage(),
                    $collection->getLastPage(),
                    $collection->getTotalItems(),
                );
            } else if (
                is_array($collection)
            ) {
                $collection = array_filter(
                    $collection,
                    $hasReadPermissionsOnOffer,
                );
            }
        }

        return $collection;
    }
}
0 Answers
Related