Skip to content

Commit 6225d2c

Browse files
committed
Implemented an easy to use APIFilter for tags
This makes the process of filters more easily and intuitive. This fixes issue #750
1 parent 01fc652 commit 6225d2c

File tree

3 files changed

+108
-1
lines changed

3 files changed

+108
-1
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
24+
namespace App\ApiPlatform\Filter;
25+
26+
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
27+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
28+
use ApiPlatform\Metadata\Operation;
29+
use Doctrine\ORM\QueryBuilder;
30+
use Symfony\Component\PropertyInfo\Type;
31+
32+
/**
33+
* Due to their nature, tags are stored in a single string, separated by commas, which requires some more complex search logic.
34+
* This filter allows to easily search for tags in a part entity.
35+
*/
36+
final class TagFilter extends AbstractFilter
37+
{
38+
39+
protected function filterProperty(
40+
string $property,
41+
$value,
42+
QueryBuilder $queryBuilder,
43+
QueryNameGeneratorInterface $queryNameGenerator,
44+
string $resourceClass,
45+
?Operation $operation = null,
46+
array $context = []
47+
): void {
48+
// Ignore filter if property is not enabled or mapped
49+
if (
50+
!$this->isPropertyEnabled($property, $resourceClass) ||
51+
!$this->isPropertyMapped($property, $resourceClass)
52+
) {
53+
return;
54+
}
55+
56+
//Escape any %, _ or \ in the tag
57+
$value = addcslashes($value, '%_\\');
58+
59+
$tag_identifier_prefix = $queryNameGenerator->generateParameterName($property);
60+
61+
$expr = $queryBuilder->expr();
62+
63+
$tmp = $expr->orX(
64+
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_1'),
65+
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_2'),
66+
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_3'),
67+
$expr->eq('o.'.$property, ':' . $tag_identifier_prefix . '_4'),
68+
);
69+
70+
$queryBuilder->andWhere($tmp);
71+
72+
//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)
73+
$queryBuilder->setParameter($tag_identifier_prefix . '_1', '%,' . $value . ',%');
74+
$queryBuilder->setParameter($tag_identifier_prefix . '_2', '%,' . $value);
75+
$queryBuilder->setParameter($tag_identifier_prefix . '_3', $value . ',%');
76+
$queryBuilder->setParameter($tag_identifier_prefix . '_4', $value);
77+
}
78+
79+
public function getDescription(string $resourceClass): array
80+
{
81+
if (!$this->properties) {
82+
return [];
83+
}
84+
85+
$description = [];
86+
foreach (array_keys($this->properties) as $property) {
87+
$description[(string)$property] = [
88+
'property' => $property,
89+
'type' => Type::BUILTIN_TYPE_STRING,
90+
'required' => false,
91+
'description' => 'Filter for tags of a part',
92+
'openapi' => [
93+
'example' => '',
94+
'allowReserved' => false,// if true, query parameters will be not percent-encoded
95+
'allowEmptyValue' => true,
96+
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
97+
],
98+
];
99+
}
100+
return $description;
101+
}
102+
}

src/DataTables/Filters/Constraints/Part/TagsConstraint.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ public function getTags(): array
8585
*/
8686
protected function getExpressionForTag(QueryBuilder $queryBuilder, string $tag): Orx
8787
{
88+
//Escape any %, _ or \ in the tag
89+
$tag = addcslashes($tag, '%_\\');
90+
8891
$tag_identifier_prefix = uniqid($this->identifier . '_', false);
8992

9093
$expr = $queryBuilder->expr();

src/Entity/Parts/Part.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
namespace App\Entity\Parts;
2424

25+
use App\ApiPlatform\Filter\TagFilter;
2526
use Doctrine\Common\Collections\Criteria;
2627
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
2728
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
@@ -97,7 +98,8 @@
9798
#[ApiFilter(PropertyFilter::class)]
9899
#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])]
99100
#[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])]
100-
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "tags", "manufacturer_product_number"])]
101+
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])]
102+
#[ApiFilter(TagFilter::class, properties: ["tags"])]
101103
#[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])]
102104
#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])]
103105
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

0 commit comments

Comments
 (0)