From 697439e358979b1093abbf32594c59a8578fce1e Mon Sep 17 00:00:00 2001 From: Teoh Han Hui Date: Thu, 7 Apr 2016 15:32:45 +0800 Subject: [PATCH] Allow multiple values for a property in search filter when using "exact" strategy --- features/crud.feature | 14 ++- .../Doctrine/Orm/Filter/SearchFilter.php | 84 ++++++++++----- .../Doctrine/Orm/Filter/SearchFilterTest.php | 102 ++++++++++++++++++ 3 files changed, 174 insertions(+), 26 deletions(-) diff --git a/features/crud.feature b/features/crud.feature index 378021c2973..bea9456a137 100644 --- a/features/crud.feature +++ b/features/crud.feature @@ -109,7 +109,7 @@ Feature: Create-Retrieve-Update-Delete "hydra:totalItems": 1, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "/dummies{?id,name,alias,description,relatedDummy.name,relatedDummies,relatedDummies[],order[id],order[name],order[relatedDummy.symfony],dummyDate[before],dummyDate[after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[after],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte]}", + "hydra:template": "/dummies{?id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],order[id],order[name],order[relatedDummy.symfony],dummyDate[before],dummyDate[after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[after],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte]}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { @@ -118,6 +118,12 @@ Feature: Create-Retrieve-Update-Delete "property": "id", "required": false }, + { + "@type": "IriTemplateMapping", + "variable": "id[]", + "property": "id", + "required": false + }, { "@type": "IriTemplateMapping", "variable": "name", @@ -142,6 +148,12 @@ Feature: Create-Retrieve-Update-Delete "property": "relatedDummy.name", "required": false }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummy.name[]", + "property": "relatedDummy.name", + "required": false + }, { "@type": "IriTemplateMapping", "variable": "relatedDummies", diff --git a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php index 8324da797ce..3d4d1c362a3 100644 --- a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php @@ -113,15 +113,37 @@ public function apply(QueryBuilder $queryBuilder, string $resourceClass, string } if ($metadata->hasField($field)) { - if (!is_string($value)) { + $values = (array) $value; + foreach ($values as $k => $v) { + if (!is_int($k) || !is_string($v)) { + unset($values[$k]); + } + } + $values = array_values($values); + + if (empty($values)) { continue; } if ('id' === $field) { - $value = $this->getFilterValueFromUrl($value); + $values = array_map([$this, 'getFilterValueFromUrl'], $values); } - $this->addWhereByStrategy($this->properties[$property] ?? self::STRATEGY_EXACT, $queryBuilder, $alias, $field, $value); + $strategy = $this->properties[$property] ?? self::STRATEGY_EXACT; + + if (1 === count($values)) { + $this->addWhereByStrategy($strategy, $queryBuilder, $alias, $field, $values[0]); + } else { + if (self::STRATEGY_EXACT !== $strategy) { + continue; + } + + $valueParameter = QueryNameGenerator::generateParameterName($field); + + $queryBuilder + ->andWhere(sprintf('%s.%s IN (:%s)', $alias, $field, $valueParameter)) + ->setParameter($valueParameter, $values); + } } elseif ($metadata->hasAssociation($field)) { $values = (array) $value; foreach ($values as $k => $v) { @@ -167,43 +189,47 @@ public function apply(QueryBuilder $queryBuilder, string $resourceClass, string * @param string $value * * @throws InvalidArgumentException If strategy does not exist - * - * @return string */ - private function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, string $alias, string $field, string $value) : string + private function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, string $alias, string $field, string $value) { $valueParameter = QueryNameGenerator::generateParameterName($field); switch ($strategy) { case null: case self::STRATEGY_EXACT: - return $queryBuilder + $queryBuilder ->andWhere(sprintf('%s.%s = :%s', $alias, $field, $valueParameter)) ->setParameter($valueParameter, $value); + break; case self::STRATEGY_PARTIAL: - return $queryBuilder + $queryBuilder ->andWhere(sprintf('%s.%s LIKE :%s', $alias, $field, $valueParameter)) ->setParameter($valueParameter, sprintf('%%%s%%', $value)); + break; case self::STRATEGY_START: - return $queryBuilder + $queryBuilder ->andWhere(sprintf('%s.%s LIKE :%s', $alias, $field, $valueParameter)) ->setParameter($valueParameter, sprintf('%s%%', $value)); + break; case self::STRATEGY_END: - return $queryBuilder + $queryBuilder ->andWhere(sprintf('%s.%s LIKE :%s', $alias, $field, $valueParameter)) ->setParameter($valueParameter, sprintf('%%%s', $value)); + break; case self::STRATEGY_WORD_START: - return $queryBuilder + $queryBuilder ->andWhere(sprintf('%1$s.%2$s LIKE :%3$s_1 OR %1$s.%2$s LIKE :%3$s_2', $alias, $field, $valueParameter)) ->setParameter(sprintf('%s_1', $valueParameter), sprintf('%s%%', $value)) ->setParameter(sprintf('%s_2', $valueParameter), sprintf('%% %s%%', $value)); - } + break; - throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy)); + default: + throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy)); + } } /** @@ -233,22 +259,30 @@ public function getDescription(string $resourceClass) : array } if ($metadata->hasField($field)) { - $description[$property] = [ - 'property' => $property, - 'type' => $metadata->getTypeOfField($field), - 'required' => false, - 'strategy' => isset($this->properties[$property]) ? $this->properties[$property] : self::STRATEGY_EXACT, + $typeOfField = $metadata->getTypeOfField($field); + $strategy = $this->properties[$property] ?? self::STRATEGY_EXACT; + $filterParameterNames = [ + $property, ]; + if (self::STRATEGY_EXACT === $strategy) { + $filterParameterNames[] = $property.'[]'; + } + foreach ($filterParameterNames as $filterParameterName) { + $description[$filterParameterName] = [ + 'property' => $property, + 'type' => $typeOfField, + 'required' => false, + 'strategy' => $strategy, + ]; + } } elseif ($metadata->hasAssociation($field)) { $association = $field; - $description[$property] = [ - 'property' => $property, - 'type' => 'iri', - 'required' => false, - 'strategy' => self::STRATEGY_EXACT, + $filterParameterNames = [ + $property, + $property.'[]', ]; - if ($metadata->hasAssociation($association)) { - $description[$property.'[]'] = [ + foreach ($filterParameterNames as $filterParameterName) { + $description[$filterParameterName] = [ 'property' => $property, 'type' => 'iri', 'required' => false, diff --git a/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php index f937b172830..f1f5abcf280 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php @@ -130,54 +130,108 @@ public function testGetDescription() 'required' => false, 'strategy' => 'exact', ], + 'id[]' => [ + 'property' => 'id', + 'type' => 'integer', + 'required' => false, + 'strategy' => 'exact', + ], 'name' => [ 'property' => 'name', 'type' => 'string', 'required' => false, 'strategy' => 'exact', ], + 'name[]' => [ + 'property' => 'name', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + ], 'alias' => [ 'property' => 'alias', 'type' => 'string', 'required' => false, 'strategy' => 'exact', ], + 'alias[]' => [ + 'property' => 'alias', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + ], 'description' => [ 'property' => 'description', 'type' => 'string', 'required' => false, 'strategy' => 'exact', ], + 'description[]' => [ + 'property' => 'description', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + ], 'dummy' => [ 'property' => 'dummy', 'type' => 'string', 'required' => false, 'strategy' => 'exact', ], + 'dummy[]' => [ + 'property' => 'dummy', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + ], 'dummyDate' => [ 'property' => 'dummyDate', 'type' => 'datetime', 'required' => false, 'strategy' => 'exact', ], + 'dummyDate[]' => [ + 'property' => 'dummyDate', + 'type' => 'datetime', + 'required' => false, + 'strategy' => 'exact', + ], 'dummyPrice' => [ 'property' => 'dummyPrice', 'type' => 'decimal', 'required' => false, 'strategy' => 'exact', ], + 'dummyPrice[]' => [ + 'property' => 'dummyPrice', + 'type' => 'decimal', + 'required' => false, + 'strategy' => 'exact', + ], 'jsonData' => [ 'property' => 'jsonData', 'type' => 'json_array', 'required' => false, 'strategy' => 'exact', ], + 'jsonData[]' => [ + 'property' => 'jsonData', + 'type' => 'json_array', + 'required' => false, + 'strategy' => 'exact', + ], 'nameConverted' => [ 'property' => 'nameConverted', 'type' => 'string', 'required' => false, 'strategy' => 'exact', ], + 'nameConverted[]' => [ + 'property' => 'nameConverted', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + ], ], $filter->getDescription($this->resourceClass)); $filter = new SearchFilter( @@ -205,48 +259,96 @@ public function testGetDescription() 'required' => false, 'strategy' => 'exact', ], + 'id[]' => [ + 'property' => 'id', + 'type' => 'integer', + 'required' => false, + 'strategy' => 'exact', + ], 'name' => [ 'property' => 'name', 'type' => 'string', 'required' => false, 'strategy' => 'exact', ], + 'name[]' => [ + 'property' => 'name', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + ], 'alias' => [ 'property' => 'alias', 'type' => 'string', 'required' => false, 'strategy' => 'exact', ], + 'alias[]' => [ + 'property' => 'alias', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + ], 'dummy' => [ 'property' => 'dummy', 'type' => 'string', 'required' => false, 'strategy' => 'exact', ], + 'dummy[]' => [ + 'property' => 'dummy', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + ], 'dummyDate' => [ 'property' => 'dummyDate', 'type' => 'datetime', 'required' => false, 'strategy' => 'exact', ], + 'dummyDate[]' => [ + 'property' => 'dummyDate', + 'type' => 'datetime', + 'required' => false, + 'strategy' => 'exact', + ], 'jsonData' => [ 'property' => 'jsonData', 'type' => 'json_array', 'required' => false, 'strategy' => 'exact', ], + 'jsonData[]' => [ + 'property' => 'jsonData', + 'type' => 'json_array', + 'required' => false, + 'strategy' => 'exact', + ], 'nameConverted' => [ 'property' => 'nameConverted', 'type' => 'string', 'required' => false, 'strategy' => 'exact', ], + 'nameConverted[]' => [ + 'property' => 'nameConverted', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + ], 'relatedDummies.dummyDate' => [ 'property' => 'relatedDummies.dummyDate', 'type' => 'datetime', 'required' => false, 'strategy' => 'exact', ], + 'relatedDummies.dummyDate[]' => [ + 'property' => 'relatedDummies.dummyDate', + 'type' => 'datetime', + 'required' => false, + 'strategy' => 'exact', + ], 'relatedDummy' => [ 'property' => 'relatedDummy', 'type' => 'iri',