• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

j-schumann / symfony-addons / 15680683335

16 Jun 2025 12:22PM UTC coverage: 53.664%. First build
15680683335

push

github

j-schumann
Merge branch 'develop'

# Conflicts:
#	.php-cs-fixer.dist.php
#	composer.json
#	src/PHPUnit/ApiPlatformTestCase.php
#	src/PHPUnit/AuthenticatedClientTrait.php

103 of 382 new or added lines in 10 files covered. (26.96%)

476 of 887 relevant lines covered (53.66%)

3.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.36
/src/Filter/SimpleSearchFilter.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Vrok\SymfonyAddons\Filter;
6

7
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
8
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
9
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
10
use ApiPlatform\Metadata\Operation;
11
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
12
use Doctrine\ORM\Query\Expr\Join;
13
use Doctrine\ORM\QueryBuilder;
14
use Doctrine\Persistence\ManagerRegistry;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
17

18
/**
19
 * Selects entities where the search term is found (case insensitive) in at least
20
 * one of the specified properties.
21
 * All specified properties type must be string.
22
 *
23
 * @todo UnitTests w/ Mariadb + Postgres
24
 */
25
class SimpleSearchFilter extends AbstractFilter
26
{
27
    /**
28
     * Add configuration parameter
29
     * {@inheritdoc}
30
     *
31
     * @param string $searchParameterName The parameter whose value this filter searches for
32
     */
33
    public function __construct(
34
        ManagerRegistry $managerRegistry,
35
        ?LoggerInterface $logger = null,
36
        ?array $properties = null,
37
        ?NameConverterInterface $nameConverter = null,
38
        private readonly string $searchParameterName = 'pattern',
39
    ) {
40
        parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
8✔
41
    }
42

43
    protected function filterProperty(
44
        string $property,
45
        mixed $value,
46
        QueryBuilder $queryBuilder,
47
        QueryNameGeneratorInterface $queryNameGenerator,
48
        string $resourceClass,
49
        ?Operation $operation = null,
50
        array $context = [],
51
    ): void {
52
        if (null === $value || $property !== $this->searchParameterName) {
6✔
53
            return;
×
54
        }
55

56
        $this->addWhere(
6✔
57
            $queryBuilder,
6✔
58
            $queryNameGenerator,
6✔
59
            $value,
6✔
60
            $queryNameGenerator->generateParameterName($property),
6✔
61
            $resourceClass
6✔
62
        );
6✔
63
    }
64

65
    private function addWhere(
66
        QueryBuilder $queryBuilder,
67
        QueryNameGeneratorInterface $queryNameGenerator,
68
        mixed $value,
69
        string $parameterName,
70
        string $resourceClass,
71
    ): void {
72
        $alias = $queryBuilder->getRootAliases()[0];
6✔
73

74
        $em =  $queryBuilder->getEntityManager();
6✔
75
        $platform = $em->getConnection()->getDatabasePlatform();
6✔
76
        $from = $queryBuilder->getRootEntities()[0];
6✔
77
        $classMetadata = $em->getClassMetadata($from);
6✔
78

79
        // Build OR expression
80
        $orExp = $queryBuilder->expr()->orX();
6✔
81
        foreach ($this->getProperties() as $prop => $_) {
6✔
82
            if (
83
                null === $value
6✔
84
                || !$this->isPropertyEnabled($prop, $resourceClass)
6✔
85
                || !$this->isPropertyMapped($prop, $resourceClass, true)
6✔
86
            ) {
87
                return;
×
88
            }
89

90
            // @todo refactor to deduplicate code
91
            if ($this->isPropertyNested($prop, $resourceClass)) {
6✔
92
                [$joinAlias, $field, $associations] = $this->addJoinsForNestedProperty(
1✔
93
                    $prop,
1✔
94
                    $alias,
1✔
95
                    $queryBuilder,
1✔
96
                    $queryNameGenerator,
1✔
97
                    $resourceClass,
1✔
98
                    Join::LEFT_JOIN
1✔
99
                );
1✔
100

101
                $metadata = $this->getNestedMetadata($resourceClass, $associations);
1✔
102

103
                // special handling for JSON fields on Postgres
104
                if ($platform instanceof PostgreSQLPlatform) {
1✔
105
                    $fieldMeta = $metadata->getFieldMapping($field);
×
106
                    if ('json' === $fieldMeta['type']) {
×
NEW
107
                        $orExp->add($queryBuilder->expr()->like(
×
NEW
108
                            "LOWER(CAST($joinAlias.$field, 'text'))",
×
NEW
109
                            ":$parameterName"
×
NEW
110
                        ));
×
111
                        continue;
×
112
                    }
113
                }
114

115
                $orExp->add($queryBuilder->expr()->like(
1✔
116
                    "LOWER($joinAlias.$field)",
1✔
117
                    ":$parameterName"
1✔
118
                ));
1✔
119
                continue;
1✔
120
            }
121

122
            // special handling for JSON fields on Postgres
123
            if ($platform instanceof PostgreSQLPlatform) {
5✔
124
                $fieldMeta = $classMetadata->getFieldMapping($prop);
×
125
                if ('json' === $fieldMeta['type']) {
×
NEW
126
                    $orExp->add($queryBuilder->expr()->like(
×
NEW
127
                        "LOWER(CAST($alias.$prop, 'text'))",
×
NEW
128
                        ":$parameterName"
×
NEW
129
                    ));
×
130
                    continue;
×
131
                }
132
            }
133

134
            $orExp->add($queryBuilder->expr()->like("LOWER($alias.$prop)", ":$parameterName"));
5✔
135
        }
136

137
        $queryBuilder
6✔
138
            ->andWhere("($orExp)")
6✔
139
            ->setParameter($parameterName, '%'.strtolower((string) $value).'%');
6✔
140
    }
141

142
    public function getDescription(string $resourceClass): array
143
    {
144
        $props = $this->getProperties();
2✔
145
        if (null === $props) {
2✔
146
            throw new InvalidArgumentException('Properties must be specified');
×
147
        }
148

149
        return [
2✔
150
            $this->searchParameterName => [
2✔
151
                'property' => implode(', ', array_keys($props)),
2✔
152
                'type'     => 'string',
2✔
153
                'required' => false,
2✔
154
                'openapi'  => [
2✔
155
                    'description' => 'Selects entities where each search term is found somewhere in at least one of the specified properties',
2✔
156
                ],
2✔
157
            ],
2✔
158
        ];
2✔
159
    }
160
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc