diff --git a/src/ApiPlatform/Filter/TagFilter.php b/src/ApiPlatform/Filter/TagFilter.php new file mode 100644 index 00000000..9702cfcf --- /dev/null +++ b/src/ApiPlatform/Filter/TagFilter.php @@ -0,0 +1,102 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\PropertyInfo\Type; + +/** + * Due to their nature, tags are stored in a single string, separated by commas, which requires some more complex search logic. + * This filter allows to easily search for tags in a part entity. + */ +final class TagFilter extends AbstractFilter +{ + + protected function filterProperty( + string $property, + $value, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ?Operation $operation = null, + array $context = [] + ): void { + // Ignore filter if property is not enabled or mapped + if ( + !$this->isPropertyEnabled($property, $resourceClass) || + !$this->isPropertyMapped($property, $resourceClass) + ) { + return; + } + + //Escape any %, _ or \ in the tag + $value = addcslashes($value, '%_\\'); + + $tag_identifier_prefix = $queryNameGenerator->generateParameterName($property); + + $expr = $queryBuilder->expr(); + + $tmp = $expr->orX( + $expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_1'), + $expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_2'), + $expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_3'), + $expr->eq('o.'.$property, ':' . $tag_identifier_prefix . '_4'), + ); + + $queryBuilder->andWhere($tmp); + + //Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag) + $queryBuilder->setParameter($tag_identifier_prefix . '_1', '%,' . $value . ',%'); + $queryBuilder->setParameter($tag_identifier_prefix . '_2', '%,' . $value); + $queryBuilder->setParameter($tag_identifier_prefix . '_3', $value . ',%'); + $queryBuilder->setParameter($tag_identifier_prefix . '_4', $value); + } + + public function getDescription(string $resourceClass): array + { + if (!$this->properties) { + return []; + } + + $description = []; + foreach (array_keys($this->properties) as $property) { + $description[(string)$property] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => false, + 'description' => 'Filter for tags of a part', + 'openapi' => [ + 'example' => '', + 'allowReserved' => false,// if true, query parameters will be not percent-encoded + 'allowEmptyValue' => true, + 'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green + ], + ]; + } + return $description; + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/Part/TagsConstraint.php b/src/DataTables/Filters/Constraints/Part/TagsConstraint.php index ae9d14cd..e8bfe2a4 100644 --- a/src/DataTables/Filters/Constraints/Part/TagsConstraint.php +++ b/src/DataTables/Filters/Constraints/Part/TagsConstraint.php @@ -85,6 +85,9 @@ class TagsConstraint extends AbstractConstraint */ protected function getExpressionForTag(QueryBuilder $queryBuilder, string $tag): Orx { + //Escape any %, _ or \ in the tag + $tag = addcslashes($tag, '%_\\'); + $tag_identifier_prefix = uniqid($this->identifier . '_', false); $expr = $queryBuilder->expr(); diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index b128df76..14a7903f 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Entity\Parts; +use App\ApiPlatform\Filter\TagFilter; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; @@ -97,7 +98,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ApiFilter(PropertyFilter::class)] #[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])] #[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])] -#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "tags", "manufacturer_product_number"])] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])] +#[ApiFilter(TagFilter::class, properties: ["tags"])] #[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])] #[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]