Issues (2551)

src/Impl/AbstractQuery.php (16 issues)

1
<?php
2
3
namespace Jabe\Impl;
4
5
use Jabe\ProcessEngineException;
6
use Jabe\Impl\Context\Context;
7
use Jabe\Impl\Db\ListQueryParameterObject;
8
use Jabe\Impl\Interceptor\{
9
    CommandInterface,
10
    CommandContext,
11
    CommandExecutorInterface
12
};
13
use Jabe\Impl\Util\{
14
    EnsureUtil,
15
    QueryMaxResultsLimitUtil
16
};
17
use Jabe\Query\{
18
    QueryInterface,
19
    QueryPropertyInterface
20
};
21
22
abstract class AbstractQuery extends ListQueryParameterObject implements CommandInterface, QueryInterface
23
{
24
    public const SORTORDER_ASC = "asc";
25
    public const SORTORDER_DESC = "desc";
26
27
    protected const RESULT_TYPES = [
28
        'LIST' => 'LIST', 'LIST_PAGE' => 'LIST_PAGE', 'LIST_IDS' => 'LIST_IDS', 'LIST_DEPLOYMENT_ID_MAPPINGS' => 'LIST_DEPLOYMENT_ID_MAPPINGS', 'SINGLE_RESULT' => 'SINGLE_RESULT', 'COUNT' => 'COUNT'
29
    ];
30
31
    protected $commandExecutor;
32
33
    protected $resultType;
34
35
    protected $expressions = [];
36
37
    protected $validators = [];
38
39
    protected $maxResultsLimitEnabled;
40
41
    public function __construct(CommandExecutorInterface $commandExecutor = null)
42
    {
43
        if ($commandExecutor !== null) {
44
            $this->commandExecutor = $commandExecutor;
45
46
            // all queries that are created with a dedicated command executor
47
            // are treated as adhoc queries (i.e. queries not created in the context
48
            // of a command)
49
            $this->addValidator(AdhocQueryValidator::instance());
50
        }
51
    }
52
53
    public function setCommandExecutor(CommandExecutorInterface $commandExecutor): AbstractQuery
54
    {
55
        $this->commandExecutor = $commandExecutor;
56
        return $this;
57
    }
58
59
    public function orderBy($property): QueryInterface
60
    {
61
        if ($property instanceof QueryPropertyInterface) {
62
            return $this->orderBy(new QueryOrderingProperty(null, $property));
63
        } else /*if ($property instanceof QueryOrderingProperty)*/{
64
            $this->orderingProperties[] = $property;
65
            return $this;
66
        }
67
    }
68
69
    public function asc(): QueryInterface
70
    {
71
        return $this->direction(Direction::ascending());
72
    }
73
74
    public function desc(): QueryInterface
75
    {
76
        return $this->direction(Direction::descending());
0 ignored issues
show
Bug Best Practice introduced by
The method Jabe\Impl\Direction::descending() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

76
        return $this->direction(Direction::/** @scrutinizer ignore-call */ descending());
Loading history...
77
    }
78
79
    public function direction(Direction $direction): QueryInterface
80
    {
81
        $currentOrderingProperty = null;
82
83
        if (!empty($this->orderingProperties)) {
84
            $currentOrderingProperty = $this->orderingProperties[count($this->orderingProperties) - 1];
85
        }
86
87
        EnsureUtil::ensureNotNull("You should call any of the orderBy methods first before specifying a direction", "currentOrderingProperty", $currentOrderingProperty);
88
89
        if ($currentOrderingProperty->getDirection() !== null) {
90
            EnsureUtil::ensureNull("Invalid query: can specify only one direction desc() or asc() for an ordering constraint", "direction", $direction);
0 ignored issues
show
The method ensureNull() does not exist on Jabe\Impl\Util\EnsureUtil. Did you maybe mean ensureNotNull()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

90
            EnsureUtil::/** @scrutinizer ignore-call */ 
91
                        ensureNull("Invalid query: can specify only one direction desc() or asc() for an ordering constraint", "direction", $direction);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
91
        }
92
93
        $currentOrderingProperty->setDirection($direction);
94
        return $this;
95
    }
96
97
    protected function checkQueryOk(): void
98
    {
99
        foreach ($this->orderingProperties as $orderingProperty) {
100
            EnsureUtil::ensureNotNull("Invalid query: call asc() or desc() after using orderByXX()", "direction", $orderingProperty->getDirection());
101
        }
102
    }
103
104
    public function singleResult()
105
    {
106
        $this->resultType = self::RESULT_TYPES['SINGLE_RESULT'];
107
        return $this->executeResult($this->resultType);
108
    }
109
110
    public function list(): array
111
    {
112
        $this->resultType = self::RESULT_TYPES['LIST'];
113
        return $this->executeResult($this->resultType);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->executeResult($this->resultType) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
114
    }
115
116
    public function listPage(int $firstResult, int $maxResults): array
117
    {
118
        $this->firstResult = $firstResult;
119
        $this->maxResults = $maxResults;
120
        $this->resultType = self::RESULT_TYPES['LIST_PAGE'];
121
        return $this->executeResult($this->resultType);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->executeResult($this->resultType) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
122
    }
123
124
    public function executeResult(string $resultType)
125
    {
126
        if ($this->commandExecutor !== null) {
127
            if (!$this->maxResultsLimitEnabled) {
128
                $this->maxResultsLimitEnabled = Context::getCommandContext() === null;
129
            }
130
            return $this->commandExecutor->execute($this);
131
        }
132
133
        switch ($resultType) {
134
            case self::RESULT_TYPES['SINGLE_RESULT']:
135
                return $this->executeSingleResult(Context::getCommandContext());
0 ignored issues
show
It seems like Jabe\Impl\Context\Context::getCommandContext() can also be of type null; however, parameter $commandContext of Jabe\Impl\AbstractQuery::executeSingleResult() does only seem to accept Jabe\Impl\Interceptor\CommandContext, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

135
                return $this->executeSingleResult(/** @scrutinizer ignore-type */ Context::getCommandContext());
Loading history...
136
            case self::RESULT_TYPES['LIST_PAGE']:
137
            case self::RESULT_TYPES['LIST']:
138
                return $this->evaluateExpressionsAndExecuteList(Context::getCommandContext(), null);
0 ignored issues
show
null of type null is incompatible with the type Jabe\Impl\Page expected by parameter $page of Jabe\Impl\AbstractQuery:...essionsAndExecuteList(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

138
                return $this->evaluateExpressionsAndExecuteList(Context::getCommandContext(), /** @scrutinizer ignore-type */ null);
Loading history...
It seems like Jabe\Impl\Context\Context::getCommandContext() can also be of type null; however, parameter $commandContext of Jabe\Impl\AbstractQuery:...essionsAndExecuteList() does only seem to accept Jabe\Impl\Interceptor\CommandContext, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

138
                return $this->evaluateExpressionsAndExecuteList(/** @scrutinizer ignore-type */ Context::getCommandContext(), null);
Loading history...
139
            default:
140
                throw new ProcessEngineException("Unknown result type!");
141
        }
142
    }
143
144
    public function count(): int
145
    {
146
        $this->resultType = self::RESULT_TYPES['COUNT'];
147
        if ($this->commandExecutor !== null) {
148
            return $this->commandExecutor->execute($this);
149
        }
150
        return $this->evaluateExpressionsAndExecuteCount(Context::getCommandContext());
0 ignored issues
show
It seems like Jabe\Impl\Context\Context::getCommandContext() can also be of type null; however, parameter $commandContext of Jabe\Impl\AbstractQuery:...ssionsAndExecuteCount() does only seem to accept Jabe\Impl\Interceptor\CommandContext, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

150
        return $this->evaluateExpressionsAndExecuteCount(/** @scrutinizer ignore-type */ Context::getCommandContext());
Loading history...
151
    }
152
153
    public function unlimitedList(): array
154
    {
155
        $this->resultType = self::RESULT_TYPES['LIST'];
156
        if ($this->commandExecutor !== null) {
157
            return $this->commandExecutor->execute($this);
158
        }
159
        return $this->evaluateExpressionsAndExecuteList(Context::getCommandContext(), null);
0 ignored issues
show
It seems like Jabe\Impl\Context\Context::getCommandContext() can also be of type null; however, parameter $commandContext of Jabe\Impl\AbstractQuery:...essionsAndExecuteList() does only seem to accept Jabe\Impl\Interceptor\CommandContext, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

159
        return $this->evaluateExpressionsAndExecuteList(/** @scrutinizer ignore-type */ Context::getCommandContext(), null);
Loading history...
null of type null is incompatible with the type Jabe\Impl\Page expected by parameter $page of Jabe\Impl\AbstractQuery:...essionsAndExecuteList(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

159
        return $this->evaluateExpressionsAndExecuteList(Context::getCommandContext(), /** @scrutinizer ignore-type */ null);
Loading history...
160
    }
161
162
    public function execute(CommandContext $commandContext)
163
    {
164
        if ($this->resultType == self::RESULT_TYPES['LIST']) {
165
            return $this->evaluateExpressionsAndExecuteList($commandContext, null);
0 ignored issues
show
null of type null is incompatible with the type Jabe\Impl\Page expected by parameter $page of Jabe\Impl\AbstractQuery:...essionsAndExecuteList(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

165
            return $this->evaluateExpressionsAndExecuteList($commandContext, /** @scrutinizer ignore-type */ null);
Loading history...
166
        } elseif ($this->resultType == self::RESULT_TYPES['SINGLE_RESULT']) {
167
            return $this->executeSingleResult($commandContext);
168
        } elseif ($this->resultType == self::RESULT_TYPES['LIST_PAGE']) {
169
            return $this->evaluateExpressionsAndExecuteList($commandContext, null);
170
        } elseif ($this->resultType == self::RESULT_TYPES['LIST_IDS']) {
171
            return $this->evaluateExpressionsAndExecuteIdsList($commandContext);
172
        } elseif ($this->resultType == self::RESULT_TYPES['LIST_DEPLOYMENT_ID_MAPPINGS']) {
173
            return $this->evaluateExpressionsAndExecuteDeploymentIdMappingsList($commandContext);
174
        } else {
175
            return $this->evaluateExpressionsAndExecuteCount($commandContext);
176
        }
177
    }
178
179
    public function evaluateExpressionsAndExecuteCount(CommandContext $commandContext): int
180
    {
181
        $this->validate();
182
        $this->evaluateExpressions();
183
        return !$this->hasExcludingConditions() ? $this->executeCount($commandContext) : 0;
184
    }
185
186
    abstract public function executeCount(CommandContext $commandContext): int;
187
188
    public function evaluateExpressionsAndExecuteList(CommandContext $commandContext, Page $page): array
189
    {
190
        $this->checkMaxResultsLimit();
191
        $this->validate();
192
        $this->evaluateExpressions();
193
        return !$this->hasExcludingConditions() ? $this->executeList($commandContext, $page) : [];
194
    }
195
196
    /**
197
     * Whether or not the query has excluding conditions. If the query has excluding conditions,
198
     * (e.g. task due date before and after are excluding), the SQL query is avoided and a default result is
199
     * returned. The returned result is the same as if the SQL was executed and there were no entries.
200
     *
201
     * @return {@code true} if the query does have excluding conditions, {@code false} otherwise
0 ignored issues
show
Documentation Bug introduced by
The doc comment {@code at position 0 could not be parsed: Unknown type name '{' at position 0 in {@code.
Loading history...
202
     */
203
    protected function hasExcludingConditions(): bool
204
    {
205
        return false;
206
    }
207
208
    /**
209
     * Executes the actual query to retrieve the list of results.
210
     * @param page used if the results must be paged. If null, no paging will be applied.
211
     */
212
    abstract public function executeList(CommandContext $commandContext, Page $page): array;
213
214
    public function executeSingleResult(CommandContext $commandContext)
215
    {
216
        $this->disableMaxResultsLimit();
217
        $results = $this->evaluateExpressionsAndExecuteList($commandContext, new Page(0, 2));
218
        if (count($results) == 1) {
219
            return $results[0];
220
        } elseif (count($results) > 1) {
221
            throw new ProcessEngineException("Query return " . count($results) . " results instead of max 1");
222
        }
223
        return null;
224
    }
225
226
    public function getExpressions(): array
227
    {
228
        return $this->expressions;
229
    }
230
231
    public function setExpressions(array $expressions): void
232
    {
233
        $this->expressions = $expressions;
234
    }
235
236
    public function addExpression(string $key, string $expression): void
237
    {
238
        $this->expressions[$key] = $expression;
239
    }
240
241
    protected function evaluateExpressions(): void
242
    {
243
        // we cannot iterate directly on the entry set cause the expressions
244
        // are removed by the setter methods during the iteration
245
        $entries = $this->expressions;
246
247
        foreach ($entries as $methodName => $expression) {
248
            $value = null;
0 ignored issues
show
The assignment to $value is dead and can be removed.
Loading history...
249
250
            try {
251
                $value = Context::getProcessEngineConfiguration()
252
                    ->getExpressionManager()
0 ignored issues
show
The method getExpressionManager() does not exist on Jabe\Impl\Cfg\ProcessEngineConfigurationImpl. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

252
                    ->/** @scrutinizer ignore-call */ getExpressionManager()

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
253
                    ->createExpression($expression)
254
                    ->getValue(null);
255
            } catch (ProcessEngineException $e) {
256
                throw new ProcessEngineException("Unable to resolve expression '" . $expression . "' for method '" . $methodName . "' on class '" . get_class($this) . "'", $e);
257
            }
258
259
            // automatically convert DateTime to date
260
            if ($value instanceof \DateTime) {
261
                $value = $value->format('c');
262
            }
263
264
            try {
265
                $method = $this->getMethod($methodName);
266
                $method->invoke($this, $value);
267
            } catch (\Exception $e) {
268
                throw new ProcessEngineException("Unable to access method '" . $methodName . "' on class '" . get_class($this) . "'", $e);
269
            }
270
        }
271
    }
272
273
    protected function getMethod(string $methodName): \ReflectionMethod
274
    {
275
        $ref = new \ReflectionClass($this);
276
        foreach ($ref->getMethods() as $method) {
277
            if ($method->name == $methodName) {
278
                return $method;
279
            }
280
        }
281
        throw new ProcessEngineException("Unable to find method '" . $methodName . "' on class '"  . get_class($this) .  "'");
282
    }
283
284
    public function extend(QueryInterface $extendingQuery)
285
    {
286
        throw new ProcessEngineException("Extending of query type '" . get_class($extendingQuery) . "' currently not supported");
287
    }
288
289
    protected function mergeOrdering(AbstractQuery $extendedQuery, AbstractQuery $extendingQuery): void
290
    {
291
        $extendedQuery->orderingProperties = $this->orderingProperties;
292
        if (!empty($extendingQuery->orderingProperties)) {
293
            if (empty($extendedQuery->orderingProperties)) {
294
                $extendedQuery->orderingProperties = $extendingQuery->orderingProperties;
295
            } else {
296
                $extendedQuery->orderingProperties = array_merge($extendedQuery->orderingProperties, $extendingQuery->orderingProperties);
297
            }
298
        }
299
    }
300
301
    protected function mergeExpressions(AbstractQuery $extendedQuery, AbstractQuery $extendingQuery): void
302
    {
303
        $mergedExpressions = $extendingQuery->getExpressions();
304
        foreach ($this->getExpressions() as $key => $value) {
305
            if (!array_key_exists($key, $mergedExpressions)) {
306
                $mergedExpressions[$key] = $value;
307
            }
308
        }
309
        $extendedQuery->setExpressions($mergedExpressions);
310
    }
311
312
    public function validate(ValidatorInterface $validator = null): void
313
    {
314
        if ($validator !== null) {
315
            $validator->validate($this);
316
        } else {
317
            foreach ($this->validators as $validator) {
318
                $this->validate($validator);
319
            }
320
        }
321
    }
322
323
    public function addValidator(ValidatorInterface $validator): void
324
    {
325
        $this->validators[] = $validator;
326
    }
327
328
    public function removeValidator(ValidatorInterface $validator): void
329
    {
330
        foreach ($this->validators as $key => $curValidator) {
331
            if ($curValidator == $validator) {
332
                unset($this->validators[$key]);
333
            }
334
        }
335
    }
336
337
    public function listIds(): array
338
    {
339
        $this->resultType = self::RESULT_TYPES['LIST_IDS'];
340
        $ids = [];
341
        if ($this->commandExecutor !== null) {
342
            $ids = $this->commandExecutor->execute($this);
343
        } else {
344
            $ids = $this->evaluateExpressionsAndExecuteIdsList(Context::getCommandContext());
0 ignored issues
show
It seems like Jabe\Impl\Context\Context::getCommandContext() can also be of type null; however, parameter $commandContext of Jabe\Impl\AbstractQuery:...ionsAndExecuteIdsList() does only seem to accept Jabe\Impl\Interceptor\CommandContext, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

344
            $ids = $this->evaluateExpressionsAndExecuteIdsList(/** @scrutinizer ignore-type */ Context::getCommandContext());
Loading history...
345
        }
346
347
        if (!empty($ids)) {
348
            QueryMaxResultsLimitUtil::checkMaxResultsLimit(count($ids));
349
        }
350
351
        return $ids;
352
    }
353
354
    public function listDeploymentIdMappings(): array
355
    {
356
        $this->resultType = self::RESULT_TYPES['LIST_DEPLOYMENT_ID_MAPPINGS'];
357
        $ids = [];
358
        if ($this->commandExecutor !== null) {
359
            $ids = $this->commandExecutor->execute($this);
360
        } else {
361
            $ids = $this->evaluateExpressionsAndExecuteDeploymentIdMappingsList(Context::getCommandContext());
0 ignored issues
show
It seems like Jabe\Impl\Context\Context::getCommandContext() can also be of type null; however, parameter $commandContext of Jabe\Impl\AbstractQuery:...loymentIdMappingsList() does only seem to accept Jabe\Impl\Interceptor\CommandContext, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

361
            $ids = $this->evaluateExpressionsAndExecuteDeploymentIdMappingsList(/** @scrutinizer ignore-type */ Context::getCommandContext());
Loading history...
362
        }
363
364
        if (!empty($ids)) {
365
            QueryMaxResultsLimitUtil::checkMaxResultsLimit(count($ids));
366
        }
367
368
        return $ids;
369
    }
370
371
    public function evaluateExpressionsAndExecuteIdsList(CommandContext $commandContext): array
372
    {
373
        $this->validate();
374
        $this->evaluateExpressions();
375
        return !$this->hasExcludingConditions() ? $this->executeIdsList($commandContext) : [];
376
    }
377
378
    public function executeIdsList(CommandContext $commandContext): array
379
    {
380
        throw new UnsupportedOperationException("executeIdsList not supported by " . get_class($this));
381
    }
382
383
    public function evaluateExpressionsAndExecuteDeploymentIdMappingsList(CommandContext $commandContext): array
384
    {
385
        $this->validate();
386
        $this->evaluateExpressions();
387
        return !$this->hasExcludingConditions() ? $this->executeDeploymentIdMappingsList($commandContext) : [];
388
    }
389
390
    public function executeDeploymentIdMappingsList(CommandContext $commandContext): array
391
    {
392
        throw new UnsupportedOperationException("executeDeploymentIdMappingsList not supported by " . get_class($this));
393
    }
394
395
    protected function checkMaxResultsLimit(): void
396
    {
397
        if ($this->maxResultsLimitEnabled) {
398
            QueryMaxResultsLimitUtil::checkMaxResultsLimit($this->maxResults);
399
        }
400
    }
401
402
    public function enableMaxResultsLimit(): void
403
    {
404
        $this->maxResultsLimitEnabled = true;
405
    }
406
407
    public function disableMaxResultsLimit(): void
408
    {
409
        $this->maxResultsLimitEnabled = false;
410
    }
411
}
412