Filter::getAliasesAndMetadata()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 17
ccs 10
cts 10
cp 1
crap 4
rs 9.9666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Atlance\HttpDoctrineOrmFilter;
6
7
use Doctrine\ORM\Mapping\ClassMetadata;
8
use Doctrine\ORM\Query\Expr;
9
use Doctrine\ORM\Query\Expr\From;
10
use Doctrine\ORM\Query\Expr\Join;
11
use Doctrine\ORM\QueryBuilder;
12
use InvalidArgumentException;
13
use Psr\SimpleCache\CacheInterface;
14
use Psr\SimpleCache\InvalidArgumentException as PsrException;
15
use Symfony\Component\Validator\Exception\ValidatorException;
16
17
final class Filter
18
{
19
    /** @var string */
20
    public const CACHE_KEY_METADATA = 'metadata';
21
22
    /** @var string */
23
    public const CACHE_KEY_FIELD = 'field';
24
25 78
    public function __construct(private readonly Query\Validator $validator, private readonly ?CacheInterface $cache = null)
26
    {
27 78
    }
28
29
    /**
30
     * @throws InvalidArgumentException if the query arguments logic exception
31
     * @throws ValidatorException       if not valid the query arguments value
32
     * @throws PsrException             if cache problem
33
     */
34 76
    public function apply(QueryBuilder $qb, Query\Configuration $configuration): void
35
    {
36 76
        $this
37 76
            ->select($qb, $configuration->filter)
38 76
            ->order($qb, $configuration->order);
39
    }
40
41
    /**
42
     * @throws InvalidArgumentException if the query arguments logic exception
43
     * @throws ValidatorException       if not valid the query arguments value
44
     * @throws PsrException             if cache problem
45
     */
46 76
    private function select(QueryBuilder $qb, array $conditions): self
47
    {
48
        /**
49
         * @var string $expr
50
         * @var array  $aliases
51
         */
52 76
        foreach ($conditions as $expr => $aliases) {
53
            /**
54
             * @var string $alias
55
             * @var array  $values
56
             */
57 75
            foreach ($aliases as $alias => $values) {
58 75
                [$cacheKey,] = Cache\Keys\Generator::generate(
59 75
                    self::CACHE_KEY_FIELD,
60 75
                    $qb->getDQL(),
61 75
                    ['query' => sprintf('[%s][%s]', $expr, $alias)]
62 75
                );
63
                /** @var Query\Field|null $field */
64 75
                $field = $this->cache?->get($cacheKey);
65 75
                if (!$field instanceof Query\Field) {
66 75
                    $field = $this->createField($qb, $alias, $expr);
67
                }
68
69 73
                $this->andWhere($qb, $field, $values, $cacheKey);
70
            }
71
        }
72
73 67
        if (!$this->validator->isValid()) {
74 3
            throw new ValidatorException((string) json_encode($this->validator->getAllViolations(), \JSON_UNESCAPED_UNICODE));
75
        }
76
77 64
        return $this;
78
    }
79
80 64
    private function order(QueryBuilder $qb, array $conditions): void
81
    {
82
        /**
83
         * @var string $alias
84
         * @var string $value
85
         */
86 64
        foreach ($conditions as $alias => $value) {
87 1
            $snakeCaseExprMethod = 'order_by';
88 1
            [$cacheKey,] = Cache\Keys\Generator::generate(
89 1
                self::CACHE_KEY_FIELD,
90 1
                $qb->getDQL(),
91 1
                ['query' => sprintf('[%s][%s]', $snakeCaseExprMethod, $alias)]
92 1
            );
93
            /** @var Query\Field|null $field */
94 1
            $field = $this->cache?->get($cacheKey);
95 1
            if (!$field instanceof Query\Field) {
96 1
                $field = $this->createField($qb, $alias, $snakeCaseExprMethod);
97
            }
98
99 1
            $this->andWhere($qb, $field, [$value], $cacheKey);
100
        }
101
    }
102
103 74
    private function andWhere(QueryBuilder $qb, Query\Field $field, array $values, string $cacheKey): void
104
    {
105 74
        if ($this->isValid($field, $values)) {
106 71
            (new Query\Builder($qb))->andWhere($field->setValues($values));
107 64
            $this->cache?->set($cacheKey, $field);
108
        }
109
    }
110
111 76
    private function createField(QueryBuilder $qb, string $tableAliasAndColumnName, string $expr): Query\Field
112
    {
113 76
        foreach ($this->getAliasesAndMetadata($qb) as $alias => $metadata) {
114 76
            if (0 === strncasecmp($tableAliasAndColumnName, $alias . '_', mb_strlen($alias . '_'))) {
115 75
                $columnName = mb_substr($tableAliasAndColumnName, mb_strlen($alias . '_'));
116
117 75
                if (\array_key_exists($columnName, $metadata->fieldNames)) {
118 74
                    return (new Query\Field($expr, $metadata->getName(), $alias))
119 74
                        ->initProperties($metadata->getFieldMapping($metadata->getFieldForColumn($columnName)));
120
                }
121
            }
122
        }
123
124 2
        throw new \InvalidArgumentException($tableAliasAndColumnName . ' not allowed');
125
    }
126
127
    /**
128
     * @psalm-suppress MixedReturnTypeCoercion
129
     * @psalm-suppress MixedAssignment
130
     *
131
     * @return array<string, ClassMetadata>
132
     * @throws PsrException
133
     */
134 76
    private function getAliasesAndMetadata(QueryBuilder $qb): array
135
    {
136 76
        [$cacheKey,] = Cache\Keys\Generator::generate(self::CACHE_KEY_METADATA, $qb->getDQL());
137 76
        if (!\is_array($aliasesAndMetadata = $this->cache?->get($cacheKey))) {
138 76
            $aliasesAndMetadata = [];
139
140 76
            foreach ($qb->getAllAliases() as $alias) {
141 76
                $metadata = $this->getMetadataByAlias($qb, $alias);
142 76
                if ($metadata instanceof ClassMetadata) {
143 76
                    $aliasesAndMetadata[$alias] = $metadata;
144
                }
145
            }
146
147 76
            $this->cache?->set($cacheKey, $aliasesAndMetadata);
148
        }
149
150 76
        return $aliasesAndMetadata;
151
    }
152
153 76
    private function getMetadataByAlias(QueryBuilder $qb, string $alias): ?ClassMetadata
154
    {
155 76
        $metadata = null;
156 76
        foreach ($this->getParts($qb) as $part) {
157 76
            if ($part->getAlias() !== $alias) {
158 76
                continue;
159
            }
160
161 76
            $metadata = $this->getMetadataByDQLPart($qb, $part);
162
        }
163
164 76
        return $metadata;
165
    }
166
167
    /**
168
     * @psalm-suppress MixedAssignment
169
     * @psalm-suppress MixedOperand
170
     * @psalm-suppress MixedInferredReturnType
171
     * @psalm-suppress MixedArrayAssignment
172
     * @psalm-suppress MixedReturnStatement
173
     *
174
     * @return Expr\From[]|Expr\Join[]
175
     */
176 76
    private function getParts(QueryBuilder $qb): array
177
    {
178 76
        $parts = [];
179
        /** @var Expr\From[]|Expr\Join[] $tmp */
180 76
        $tmp = $qb->getDQLPart('join') + $qb->getDQLPart('from');
181 76
        array_walk_recursive($tmp, static function ($part) use (&$parts): void {$parts[] = $part; });
182 76
        unset($tmp);
183
184 76
        return $parts;
185
    }
186
187
    /**
188
     * For join without class name.
189
     * Example: ->leftJoin('users.cards', 'cards', Join::WITH).
190
     */
191 76
    private function getMetadataByDQLPart(QueryBuilder $qb, Join | From $partData): ClassMetadata
192
    {
193 76
        if (!class_exists($class = $partData instanceof From ? $partData->getFrom() : $partData->getJoin())) {
194 76
            $joinAlias = explode('.', $class)[1];
195
196 76
            foreach ($qb->getRootEntities() as $rootEntity) {
197 76
                $class = $qb->getEntityManager()->getClassMetadata($rootEntity)->getAssociationTargetClass($joinAlias);
198
            }
199
        }
200
201 76
        return $qb->getEntityManager()->getClassMetadata($class);
202
    }
203
204 74
    private function isValid(Query\Field $field, array $values): bool
205
    {
206 74
        $exp = $field->getSnakeCaseExprMethod();
207
208 74
        if ($this->skipValidate($exp)) {
209 32
            return true;
210
        }
211
212 42
        $this->validator->validatePropertyValue(
213 42
            sprintf('[%s][%s]', $field->getExprMethod(), $field->generateParameter()),
214 42
            $field->getClass(),
215 42
            $field->getFieldName(),
216 42
            $values
217 42
        );
218
219 42
        return $this->validator->isValid();
220
    }
221
222 74
    private function skipValidate(string $exp): bool
223
    {
224 74
        return \in_array($exp, ['is_null', 'is_not_null', 'like', 'ilike', 'not_like', 'between', 'order_by'], true);
225
    }
226
}
227