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

systemsdk / docker-symfony-api / #74

pending completion
#74

push

DKravtsov
Php 8.2, symfony 6.2, updated RabbitMQ, updated composer dependencies, refactoring.

51 of 51 new or added lines in 44 files covered. (100.0%)

1479 of 2668 relevant lines covered (55.43%)

23.59 hits per line

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

12.35
/src/General/Infrastructure/Rest/RepositoryHelper.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\General\Infrastructure\Rest;
6

7
use App\General\Domain\Rest\UuidHelper;
8
use Closure;
9
use Doctrine\ORM\Query\Expr\Composite;
10
use Doctrine\ORM\Query\Expr\Literal;
11
use Doctrine\ORM\QueryBuilder;
12
use InvalidArgumentException;
13
use Ramsey\Uuid\Exception\InvalidUuidStringException;
14
use stdClass;
15

16
use function array_combine;
17
use function array_key_exists;
18
use function array_map;
19
use function array_walk;
20
use function call_user_func_array;
21
use function is_array;
22
use function is_numeric;
23
use function str_contains;
24
use function strcmp;
25
use function strtolower;
26
use function syslog;
27

28
/**
29
 * Class RepositoryHelper
30
 *
31
 * @package App\General
32
 */
33
class RepositoryHelper
34
{
35
    /**
36
     * Parameter count in current query, this is used to track parameters which are bind to current query.
37
     */
38
    public static int $parameterCount = 0;
39

40
    /**
41
     * Method to reset current parameter count value
42
     */
43
    public static function resetParameterCount(): void
44
    {
45
        self::$parameterCount = 0;
21✔
46
    }
47

48
    /**
49
     * Process given criteria which is given by ?where parameter. This is given as JSON string, which is converted
50
     * to assoc array for this process.
51
     *
52
     * Note that this supports by default (without any extra work) just 'eq' and 'in' expressions. See example array
53
     * below:
54
     *
55
     *  [
56
     *      'u.id' => 3,
57
     *      'u.uid' => 'uid',
58
     *      'u.foo' => [1, 2, 3],
59
     *      'u.bar' => ['foo', 'bar'],
60
     *  ]
61
     *
62
     * And these you can make easily happen within REST controller and simple 'where' parameter. See example below:
63
     *
64
     *  ?where={"u.id":3,"u.uid":"uid","u.foo":[1,2,3],"u.bar":["foo","bar"]}
65
     *
66
     * Also note that you can make more complex use case fairly easy, just follow instructions below.
67
     *
68
     * If you're trying to make controller specified special criteria with projects generic Rest controller, just
69
     * add 'processCriteria(array &$criteria)' method to your own controller and pre-process that criteria in there
70
     * the way you want it to be handled. In other words just modify that basic key-value array just as you like it,
71
     * main goal is to create array that is compatible with 'getExpression' method in this class. For greater detail
72
     * just see that method comments.
73
     *
74
     * tl;dr Modify your $criteria parameter in your controller with 'processCriteria(array &$criteria)' method.
75
     *
76
     * @see \App\General\Infrastructure\Repository\Traits\RepositoryMethodsTrait::getQueryBuilder()
77
     *
78
     * @param array<int|string, mixed>|null $criteria
79
     *
80
     * @throws InvalidArgumentException
81
     */
82
    public static function processCriteria(QueryBuilder $queryBuilder, ?array $criteria = null): void
83
    {
84
        $criteria ??= [];
64✔
85

86
        if ($criteria === []) {
64✔
87
            return;
64✔
88
        }
89

90
        // Initialize condition array
91
        $condition = [];
×
92
        // Create used condition array
93
        array_walk($criteria, self::getIterator($condition));
×
94
        // And attach search term condition to main query
95
        $queryBuilder->andWhere(self::getExpression($queryBuilder, $queryBuilder->expr()->andX(), $condition));
×
96
    }
97

98
    /**
99
     * Helper method to process given search terms and create criteria about those. Note that each repository
100
     * has 'searchColumns' property which contains the fields where search term will be affected.
101
     *
102
     * @see \App\General\Infrastructure\Repository\Traits\RepositoryMethodsTrait::getQueryBuilder()
103
     *
104
     * @param array<int, string> $columns
105
     * @phpstan-param array<mixed>|null $terms
106
     *
107
     * @throws InvalidArgumentException
108
     */
109
    public static function processSearchTerms(QueryBuilder $queryBuilder, array $columns, ?array $terms = null): void
110
    {
111
        $terms ??= [];
64✔
112

113
        if ($columns === []) {
64✔
114
            return;
23✔
115
        }
116

117
        // Iterate search term sets
118
        foreach ($terms as $operand => $search) {
43✔
119
            $criteria = SearchTerm::getCriteria($columns, $search, $operand);
×
120

121
            if ($criteria !== null) {
×
122
                $queryBuilder->andWhere(self::getExpression($queryBuilder, $queryBuilder->expr()->andX(), $criteria));
×
123
            }
124
        }
125
    }
126

127
    /**
128
     * Simple process method for order by part of for current query builder.
129
     *
130
     * @param array<string, string>|null $orderBy
131
     */
132
    public static function processOrderBy(QueryBuilder $queryBuilder, ?array $orderBy = null): void
133
    {
134
        $orderBy ??= [];
64✔
135

136
        foreach ($orderBy as $column => $order) {
64✔
137
            if (!str_contains($column, '.')) {
×
138
                $column = 'entity.' . $column;
×
139
            }
140

141
            $queryBuilder->addOrderBy($column, $order);
×
142
        }
143
    }
144

145
    /**
146
     * Recursively takes the specified criteria and adds too the expression.
147
     *
148
     * The criteria is defined in an array notation where each item in the list
149
     * represents a comparison <fieldName, operator, value>. The operator maps to
150
     * comparison methods located in ExpressionBuilder. The key in the array can
151
     * be used to identify grouping of comparisons.
152
     *
153
     * Currently supported Doctrine\ORM\Query\Expr methods:
154
     *
155
     * OPERATOR EXAMPLE INPUT ARRAY GENERATED QUERY RESULT NOTES
156
     *  eq ['u.id', 'eq', 123] u.id = ?1 -
157
     *  neq ['u.id', 'neq', 123] u.id <> ?1 -
158
     *  lt ['u.id', 'lt', 123] u.id < ?1 -
159
     *  lte ['u.id', 'lte', 123] u.id <= ?1 -
160
     *  gt ['u.id', 'gt', 123] u.id > ?1 -
161
     *  gte ['u.id', 'gte', 123] u.id >= ?1 -
162
     *  in ['u.id', 'in', [1,2]] u.id IN (1,2) third value may contain n values
163
     *  notIn ['u.id', 'notIn', [1,2]] u.id NOT IN (1,2) third value may contain n values
164
     *  isNull ['u.id', 'isNull', null] u.id IS NULL third value must be set, but not used
165
     *  isNotNull ['u.id', 'isNotNull', null] u.id IS NOT NULL third value must be set, but not used
166
     *  like ['u.id', 'like', 'abc'] u.id LIKE ?1 -
167
     *  notLike ['u.id', 'notLike', 'abc'] u.id NOT LIKE ?1 -
168
     *  between ['u.id', 'between', [1,6]] u.id BETWEEN ?1 AND ?2 third value must contain two values
169
     *
170
     * Also note that you can easily combine 'and' and 'or' queries like following examples:
171
     *
172
     * EXAMPLE INPUT ARRAY GENERATED QUERY RESULT
173
     *  [
174
     *      'and' => [
175
     *          ['u.firstName', 'eq', 'foo bar']
176
     *          ['u.lastName', 'neq', 'not this one']
177
     *      ]
178
     *  ] (u.firstName = ?1 AND u.lastName <> ?2)
179
     *  [
180
     *      'or' => [
181
     *          ['u.firstName', 'eq', 'foo bar']
182
     *          ['u.lastName', 'neq', 'not this one']
183
     *      ]
184
     *  ] (u.firstName = ?1 OR u.lastName <> ?2)
185
     *
186
     * Also note that you can nest these criteria arrays as many levels as you need - only the sky is the limit...
187
     *
188
     * @example
189
     *  $criteria = [
190
     *      'or' => [
191
     *          ['entity.field1', 'like', '%field1Value%'],
192
     *          ['entity.field2', 'like', '%field2Value%'],
193
     *      ],
194
     *      'and' => [
195
     *          ['entity.field3', 'eq', 3],
196
     *          ['entity.field4', 'eq', 'four'],
197
     *      ],
198
     *      ['entity.field5', 'neq', 5],
199
     *  ];
200
     *
201
     * $qb = $this->createQueryBuilder('entity');
202
     * $qb->where($this->getExpression($qb, $qb->expr()->andX(), $criteria));
203
     * $query = $qb->getQuery();
204
     * echo $query->getSQL();
205
     *
206
     * // Result:
207
     * // SELECT *
208
     * // FROM tableName
209
     * // WHERE ((field1 LIKE '%field1Value%') OR (field2 LIKE '%field2Value%'))
210
     * // AND ((field3 = '3') AND (field4 = 'four'))
211
     * // AND (field5 <> '5')
212
     *
213
     * Also note that you can nest these queries as many times as you wish...
214
     *
215
     * @see https://gist.github.com/jgornick/8671644
216
     *
217
     * @param array<int|string, mixed> $criteria
218
     *
219
     * @throws InvalidArgumentException
220
     */
221
    public static function getExpression(
222
        QueryBuilder $queryBuilder,
223
        Composite $expression,
224
        array $criteria,
225
    ): Composite {
226
        self::processExpression($queryBuilder, $expression, $criteria);
×
227

228
        return $expression;
×
229
    }
230

231
    /**
232
     * @param array<int|string, mixed> $criteria
233
     *
234
     * @throws InvalidArgumentException
235
     */
236
    private static function processExpression(QueryBuilder $queryBuilder, Composite $expression, array $criteria): void
237
    {
238
        $iterator = static function (array $comparison, string | int $key) use ($queryBuilder, $expression): void {
×
239
            $expressionAnd = ($key === 'and' || array_key_exists('and', $comparison));
×
240
            $expressionOr = ($key === 'or' || array_key_exists('or', $comparison));
×
241

242
            self::buildExpression($queryBuilder, $expression, $expressionAnd, $expressionOr, $comparison);
×
243
        };
×
244

245
        array_walk($criteria, $iterator);
×
246
    }
247

248
    /**
249
     * @param array<int|string, mixed> $comparison
250
     *
251
     * @throws InvalidArgumentException
252
     */
253
    private static function buildExpression(
254
        QueryBuilder $queryBuilder,
255
        Composite $expression,
256
        bool $expressionAnd,
257
        bool $expressionOr,
258
        array $comparison
259
    ): void {
260
        if ($expressionAnd) {
×
261
            $expression->add(self::getExpression($queryBuilder, $queryBuilder->expr()->andX(), $comparison));
×
262
        } elseif ($expressionOr) {
×
263
            $expression->add(self::getExpression($queryBuilder, $queryBuilder->expr()->orX(), $comparison));
×
264
        } else {
265
            [$comparison, $parameters] = self::determineComparisonAndParameters($queryBuilder, $comparison);
×
266
            /** @var callable $callable */
267
            $callable = [$queryBuilder->expr(), $comparison->operator];
×
268
            // And finally add new expression to main one with specified parameters
269
            $expression->add(call_user_func_array($callable, $parameters));
×
270
        }
271
    }
272

273
    /**
274
     * Lambda function to create condition array for 'getExpression' method.
275
     *
276
     * @param string|array<int, string> $value
277
     *
278
     * @return array{0: string, 1: string, 2: string|array<int, string>}
279
     */
280
    private static function createCriteria(string $column, string | array $value): array
281
    {
282
        if (!str_contains($column, '.')) {
×
283
            $column = 'entity.' . $column;
×
284
        }
285

286
        $operator = is_array($value) ? 'in' : 'eq';
×
287

288
        return [$column, $operator, $value];
×
289
    }
290

291
    /**
292
     * @param array<int|string, string|array<mixed>> $comparison
293
     *
294
     * @return array<int, mixed>
295
     */
296
    private static function determineComparisonAndParameters(QueryBuilder $queryBuilder, array $comparison): array
297
    {
298
        /** @var stdClass $comparisonObject */
299
        $comparisonObject = (object)array_combine(['field', 'operator', 'value'], $comparison);
×
300
        // Increase parameter count
301
        self::$parameterCount++;
×
302
        // Initialize used callback parameters
303
        $parameters = [$comparisonObject->field];
×
304
        $lowercaseOperator = strtolower($comparisonObject->operator);
×
305

306
        if ($lowercaseOperator !== 'isnull' && $lowercaseOperator !== 'isnotnull') {
×
307
            $parameters = self::getComparisonParameters(
×
308
                $queryBuilder,
×
309
                $comparisonObject,
×
310
                $lowercaseOperator,
×
311
                $parameters
×
312
            );
×
313
        }
314

315
        return [$comparisonObject, $parameters];
×
316
    }
317

318
    /**
319
     * @param array<int, string> $parameters
320
     * @param array<int, mixed> $value
321
     *
322
     * @return array<int, array<int, Literal>|string>
323
     */
324
    private static function getParameters(
325
        QueryBuilder $queryBuilder,
326
        string $lowercaseOperator,
327
        array $parameters,
328
        array $value,
329
    ): array {
330
        // Operator is between, so we need to add third parameter for Expr method
331
        if ($lowercaseOperator === 'between') {
×
332
            $parameters[] = '?' . self::$parameterCount;
×
333
            $queryBuilder->setParameter(self::$parameterCount, $value[0], UuidHelper::getType((string)$value[0]));
×
334
            self::$parameterCount++;
×
335
            $parameters[] = '?' . self::$parameterCount;
×
336
            $queryBuilder->setParameter(self::$parameterCount, $value[1], UuidHelper::getType((string)$value[1]));
×
337
        } else {
338
            // Otherwise this must be IN or NOT IN expression
339
            try {
340
                $value = array_map(static fn (string $value): string => UuidHelper::getBytes($value), $value);
×
341
            } catch (InvalidUuidStringException $exception) {
×
342
                // Ok so value isn't list of UUIDs
343
                syslog(LOG_INFO, $exception->getMessage());
×
344
            }
345

346
            $parameters[] = array_map(
×
347
                static fn (string $value): Literal => $queryBuilder->expr()->literal(
×
348
                    is_numeric($value) ? (int)$value : $value
×
349
                ),
×
350
                $value
×
351
            );
×
352
        }
353

354
        return $parameters;
×
355
    }
356

357
    /**
358
     * @param array<int|string, string|array<int|string, string>> $condition
359
     */
360
    private static function getIterator(array &$condition): Closure
361
    {
362
        return static function (string | array $value, string $column) use (&$condition): void {
×
363
            // If criteria contains 'and' OR 'or' key(s) assume that array in only in the right format
364
            if (strcmp($column, 'and') === 0 || strcmp($column, 'or') === 0) {
×
365
                $condition[$column] = $value;
×
366
            } else {
367
                // Add condition
368
                $condition[] = self::createCriteria($column, $value);
×
369
            }
370
        };
×
371
    }
372

373
    /**
374
     * @param array<int, string> $parameters
375
     *
376
     * @return array<int, array<int, Literal>|string>
377
     */
378
    private static function getComparisonParameters(
379
        QueryBuilder $queryBuilder,
380
        stdClass $comparison,
381
        string $lowercaseOperator,
382
        array $parameters,
383
    ): array {
384
        if (is_array($comparison->value)) {
×
385
            $value = $comparison->value;
×
386
            $parameters = self::getParameters($queryBuilder, $lowercaseOperator, $parameters, $value);
×
387
        } else {
388
            $parameters[] = '?' . self::$parameterCount;
×
389
            $queryBuilder->setParameter(
×
390
                self::$parameterCount,
×
391
                $comparison->value,
×
392
                UuidHelper::getType((string)$comparison->value)
×
393
            );
×
394
        }
395

396
        return $parameters;
×
397
    }
398
}
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

© 2025 Coveralls, Inc