Skip to content

Instantly share code, notes, and snippets.

@Xorder664
Forked from byhoratiss/StrategyFilter.php
Created May 25, 2023 08:45
Show Gist options
  • Select an option

  • Save Xorder664/fde9bfde7d2529734d96d252e7bec5ba to your computer and use it in GitHub Desktop.

Select an option

Save Xorder664/fde9bfde7d2529734d96d252e7bec5ba to your computer and use it in GitHub Desktop.

Revisions

  1. @byhoratiss byhoratiss revised this gist Sep 12, 2019. 2 changed files with 8 additions and 1 deletion.
    7 changes: 7 additions & 0 deletions annotations.yml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    use App\Filter\StrategyFilter;

    /**
    * @ApiFilter(StrategyFilter::class, properties={
    * "name"="exact|start",
    * })
    */
    2 changes: 1 addition & 1 deletion services.yml
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    services:
    product.search_filter:
    class: App\StrategyFilter
    class: App\Filter\StrategyFilter
    parent: 'api_platform.doctrine.orm.search_filter'
    arguments: [ { 'name': 'start|exact' } ]
    tags: [ 'api_platform.filter' ]
  2. @byhoratiss byhoratiss created this gist Sep 5, 2019.
    282 changes: 282 additions & 0 deletions StrategyFilter.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,282 @@
    <?php

    namespace App\Filter;

    use ApiPlatform\Core\Api\IriConverterInterface;
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
    use ApiPlatform\Core\Exception\InvalidArgumentException;
    use Doctrine\Common\Persistence\ManagerRegistry;
    use Doctrine\DBAL\Types\Type;
    use Doctrine\ORM\QueryBuilder;
    use Psr\Log\LoggerInterface;
    use Symfony\Component\HttpFoundation\RequestStack;
    use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

    class StrategyFilter extends SearchFilter
    {
    protected $propertiesWithMultipleStrategies = [];

    /**
    * {@inheritdoc}
    */
    public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack = null, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null)
    {
    $props = [];

    // Remap properties when multiple strategies were applied
    foreach ($properties as $property => $strategies) {

    // Property with only one strategy
    if (strpos($strategies, '|') === false) {
    $props[$property] = $strategies;
    continue;
    }

    // Process multiple strategies
    foreach (explode('|', $strategies) as $strategy) {

    $propertyName = sprintf('%s[%s]', $property, $strategy);
    $props[$propertyName] = $strategy;

    // Store first defined strategy for BC
    if (!isset($props[$property])) {
    $props[$property] = $strategy;

    // Avoid creating another filter with the first one
    continue;
    }

    // Store this properties with multiple strategies for further usage
    $this->propertiesWithMultipleStrategies[$propertyName] = [
    'property' => $property,
    'strategy' => $strategy,
    ];
    }
    }

    parent::__construct($managerRegistry, $requestStack, $iriConverter, $propertyAccessor, $logger, $props);
    }

    /**
    * {@inheritdoc}
    */
    public function getDescription(string $resourceClass): array
    {
    $description = parent::getDescription($resourceClass);

    foreach ($this->propertiesWithMultipleStrategies as $parameterName => $mapping) {

    $property = $mapping['property'];
    $strategy = $mapping['strategy'];

    if (!$this->isPropertyMapped($property, $resourceClass, true)) {
    continue;
    }

    if ($this->isPropertyNested($property, $resourceClass)) {
    $propertyParts = $this->splitPropertyParts($property, $resourceClass);
    $field = $propertyParts['field'];
    $metadata = $this->getNestedMetadata($resourceClass, $propertyParts['associations']);
    } else {
    $field = $property;
    $metadata = $this->getClassMetadata($resourceClass);
    }

    if ($metadata->hasField($field)) {
    $typeOfField = $this->getType($metadata->getTypeOfField($field));
    $filterParameterNames = [$parameterName];

    foreach ($filterParameterNames as $filterParameterName) {
    $description[$filterParameterName] = [
    'property' => $property,
    'type' => $typeOfField,
    'required' => false,
    'strategy' => $strategy,
    ];
    }
    } elseif ($metadata->hasAssociation($field)) {
    $filterParameterNames = [
    $parameterName,
    ];

    foreach ($filterParameterNames as $filterParameterName) {
    $description[$filterParameterName] = [
    'property' => $property,
    'type' => 'string',
    'required' => false,
    'strategy' => self::STRATEGY_EXACT,
    ];
    }
    }

    }

    return $description;
    }

    /**
    * {@inheritdoc}
    */
    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
    if (
    null === $value ||
    !$this->isPropertyEnabled($property, $resourceClass) ||
    !$this->isPropertyMapped($property, $resourceClass, true)
    ) {
    return;
    }

    $alias = $queryBuilder->getRootAliases()[0];
    $field = $property;
    $strategy = NULL;

    if ($this->isPropertyNested($property, $resourceClass)) {
    list($alias, $field, $associations) = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
    $metadata = $this->getNestedMetadata($resourceClass, $associations);
    } else {
    $metadata = $this->getClassMetadata($resourceClass);
    }

    if (is_array($value) && sizeof($value) == 1) {
    $strategy = array_keys($value)[0];
    $value = array_values($value)[0];
    }

    $values = $this->normalizeValues((array) $value);

    if (empty($values)) {
    $this->logger->notice('Invalid filter ignored', [
    'exception' => new InvalidArgumentException(sprintf('At least one value is required, multiple values should be in "%1$s[]=firstvalue&%1$s[]=secondvalue" format', $property)),
    ]);

    return;
    }

    $caseSensitive = true;

    if ($metadata->hasField($field)) {
    if ('id' === $field) {
    $values = array_map([$this, 'getIdFromValue'], $values);
    }

    if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) {
    $this->logger->notice('Invalid filter ignored', [
    'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
    ]);

    return;
    }

    if ( ! $strategy) {
    $strategy = $this->properties[$property] ?? self::STRATEGY_EXACT;
    }

    // prefixing the strategy with i makes it case insensitive
    if (0 === strpos($strategy, 'i')) {
    $strategy = substr($strategy, 1);
    $caseSensitive = false;
    }

    if (1 === \count($values)) {
    $this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $values[0], $caseSensitive);

    return;
    }

    if (self::STRATEGY_EXACT !== $strategy) {
    $this->logger->notice('Invalid filter ignored', [
    'exception' => new InvalidArgumentException(sprintf('"%s" strategy selected for "%s" property, but only "%s" strategy supports multiple values', $strategy, $property, self::STRATEGY_EXACT)),
    ]);

    return;
    }

    $wrapCase = $this->createWrapCase($caseSensitive);
    $valueParameter = $queryNameGenerator->generateParameterName($field);

    $queryBuilder
    ->andWhere(sprintf($wrapCase('%s.%s').' IN (:%s)', $alias, $field, $valueParameter))
    ->setParameter($valueParameter, $caseSensitive ? $values : array_map('strtolower', $values));
    }

    // metadata doesn't have the field, nor an association on the field
    if (!$metadata->hasAssociation($field)) {
    return;
    }

    $values = array_map([$this, 'getIdFromValue'], $values);

    if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) {
    $this->logger->notice('Invalid filter ignored', [
    'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
    ]);

    return;
    }

    $association = $field;
    $valueParameter = $queryNameGenerator->generateParameterName($association);

    if ($metadata->isCollectionValuedAssociation($association)) {
    $associationAlias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $association);
    $associationField = 'id';
    } else {
    $associationAlias = $alias;
    $associationField = $field;
    }

    if (1 === \count($values)) {
    $queryBuilder
    ->andWhere(sprintf('%s.%s = :%s', $associationAlias, $associationField, $valueParameter))
    ->setParameter($valueParameter, $values[0]);
    } else {
    $queryBuilder
    ->andWhere(sprintf('%s.%s IN (:%s)', $associationAlias, $associationField, $valueParameter))
    ->setParameter($valueParameter, $values);
    }
    }


    /**
    * Converts a Doctrine type in PHP type.
    *
    * @param string $doctrineType
    *
    * @return string
    */
    private function getType(string $doctrineType): string
    {
    switch ($doctrineType) {
    case Type::TARRAY:
    return 'array';
    case Type::BIGINT:
    case Type::INTEGER:
    case Type::SMALLINT:
    return 'int';
    case Type::BOOLEAN:
    return 'bool';
    case Type::DATE:
    case Type::TIME:
    case Type::DATETIME:
    case Type::DATETIMETZ:
    return \DateTimeInterface::class;
    case Type::FLOAT:
    return 'float';
    }

    if (\defined(Type::class.'::DATE_IMMUTABLE')) {
    switch ($doctrineType) {
    case Type::DATE_IMMUTABLE:
    case Type::TIME_IMMUTABLE:
    case Type::DATETIME_IMMUTABLE:
    case Type::DATETIMETZ_IMMUTABLE:
    return \DateTimeInterface::class;
    }
    }

    return 'string';
    }
    }
    6 changes: 6 additions & 0 deletions services.yml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    services:
    product.search_filter:
    class: App\StrategyFilter
    parent: 'api_platform.doctrine.orm.search_filter'
    arguments: [ { 'name': 'start|exact' } ]
    tags: [ 'api_platform.filter' ]