• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

j-schumann / symfony-addons / 23257254021

18 Mar 2026 05:08PM UTC coverage: 53.674% (-0.6%) from 54.23%
23257254021

push

github

web-flow
upd: RefreshDatabaseTrait: Support SQLServer, fix MySQL/MariaDB (#29)

6 of 29 new or added lines in 4 files covered. (20.69%)

1 existing line in 1 file now uncovered.

504 of 939 relevant lines covered (53.67%)

3.45 hits per line

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

76.39
/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 ApiPlatform\OpenApi\Model\Parameter;
12
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
13
use Doctrine\ORM\Query\Expr\Join;
14
use Doctrine\ORM\QueryBuilder;
15
use Doctrine\Persistence\ManagerRegistry;
16
use Psr\Log\LoggerInterface;
17
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
18

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

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

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

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

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

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

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

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

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

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

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

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

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

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

150
        return [
2✔
151
            $this->searchParameterName => [
2✔
152
                'property' => implode(', ', array_keys($props)),
2✔
153
                'type'     => 'string',
2✔
154
                'required' => false,
2✔
155
                'openapi'  => new Parameter(
2✔
156
                    $this->searchParameterName,
2✔
157
                    'query',
2✔
158
                    'Selects entities where each search term is found somewhere in at least one of the specified properties',
2✔
159
                    false,
2✔
160
                ),
2✔
161
            ],
2✔
162
        ];
2✔
163
    }
164
}
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