Skip to content

Instantly share code, notes, and snippets.

@orangevinz
Created May 24, 2022 13:04
Show Gist options
  • Save orangevinz/99999fd28b53db9421c59e5dface76a5 to your computer and use it in GitHub Desktop.
Save orangevinz/99999fd28b53db9421c59e5dface76a5 to your computer and use it in GitHub Desktop.
Api Platform exclusion custom filter - Exclude an array of items, by their IRI or value
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Filter\ExcludeSearchFilter;
use App\Repository\EntityRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=EntityRepository::class)
* @ApiResource()
* @ApiFilter(ExcludeSearchFilter::class, properties={"status"})
*/
class Entity
{
/**
* @ORM\Column(type="smallint")
*/
private $status = 0;
}
<?php
namespace App\Filter;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
class ExcludeSearchFilter extends AbstractContextAwareFilter
{
public const QUERY_PARAMETER_KEY = 'exclude';
private IriConverterInterface $iriConverter;
public function __construct(
ManagerRegistry $managerRegistry,
?RequestStack $requestStack = null,
LoggerInterface $logger = null,
array $properties = null,
NameConverterInterface $nameConverter = null,
IriConverterInterface $iriConverter
) {
parent::__construct($managerRegistry, $requestStack, $logger, $properties, $nameConverter);
$this->iriConverter = $iriConverter;
}
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$descriptionKey = 'exclude['.$property.'][]';
$description[$descriptionKey] = [
'property' => $property,
'type' => Type::BUILTIN_TYPE_ITERABLE,
'schema' => [
'type' => Type::BUILTIN_TYPE_ARRAY,
'items' => [
'type' => Type::BUILTIN_TYPE_STRING
],
'example' => '"<iri> or value"'
],
'required' => false,
'description' => 'Filter using an array of excluded items.',
];
}
return $description;
}
protected function filterProperty(
string $property,
$value,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
string $operationName = null
) {
// otherwise filter is applied to order and page as well
if (
$property !== self::QUERY_PARAMETER_KEY
|| !is_array($value)
) {
return;
}
/**
* From:
* ?exclude[fieldName1][]=<IRI1>&exclude[fieldName1][]=<IRI2>&exclude[fieldName2][]=<value>.
*
* To:
* $mappedValues = [
* 'fieldName1' => [
* ['IRI1'],
* ['IRI2']
* ],
* 'fieldName2' => [
* ['value']
* ]
* ];.
*/
$mappedValues = [];
foreach ($value as $key => $values) {
// Restrict to enabled properties
if ($this->isPropertyEnabled($key)) {
// Allow receiving array or string
$values = (array) $values;
// Relation mapped field
if ($this->getClassMetadata($resourceClass)->hasAssociation($key)) {
try {
foreach ($values as $item) {
$mappedValues[$key][] = $this->iriConverter->getItemFromIri($item);
}
} catch (\Exception $e) {
// Invalid IRI, wrong uuid ...
$this->logger->warning($e);
}
}
// Regular mapped field
if ($this->getClassMetadata($resourceClass)->hasField($key)) {
foreach ($values as $item) {
$mappedValues[$key][] = $item;
}
}
}
}
// Override query
$rootAlias = $queryBuilder->getRootAliases()[0];
foreach ($mappedValues as $key => $items) {
$queryBuilder
->andWhere(sprintf('%s.%s NOT IN(:%s)', $rootAlias, $key, $key))
->setParameter($key, $items)
;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment