Issues (56)

src/Query/EntityQuery.php (6 issues)

1
<?php
2
3
/**
4
 * Platine ORM
5
 *
6
 * Platine ORM provides a flexible and powerful ORM implementing a data-mapper pattern.
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine ORM
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining a copy
13
 * of this software and associated documentation files (the "Software"), to deal
14
 * in the Software without restriction, including without limitation the rights
15
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
 * copies of the Software, and to permit persons to whom the Software is
17
 * furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included in all
20
 * copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
 * SOFTWARE.
29
 */
30
31
/**
32
 *  @file EntityQuery.php
33
 *
34
 *  The EntityQuery class
35
 *
36
 *  @package    Platine\Orm\Query
37
 *  @author Platine Developers Team
38
 *  @copyright  Copyright (c) 2020
39
 *  @license    http://opensource.org/licenses/MIT  MIT License
40
 *  @link   https://www.platine-php.com
41
 *  @version 1.0.0
42
 *  @filesource
43
 */
44
45
declare(strict_types=1);
46
47
namespace Platine\Orm\Query;
48
49
use Closure;
50
use Platine\Database\Connection;
51
use Platine\Database\Query\ColumnExpression;
52
use Platine\Database\Query\Delete;
53
use Platine\Database\Query\Expression;
54
use Platine\Database\Query\HavingStatement;
55
use Platine\Database\Query\QueryStatement;
56
use Platine\Database\Query\Update;
57
use Platine\Database\ResultSet;
58
use Platine\Orm\Entity;
59
use Platine\Orm\EntityManager;
60
use Platine\Orm\Mapper\EntityMapper;
61
use Platine\Orm\Relation\RelationLoader;
62
63
/**
64
 * @class EntityQuery
65
 * @package Platine\Orm\Query
66
 * @template TEntity as Entity
67
 */
68
class EntityQuery extends Query
69
{
70
    /**
71
     *
72
     * @var EntityManager<TEntity>
73
     */
74
    protected EntityManager $manager;
75
76
    /**
77
     *
78
     * @var EntityMapper<TEntity>
79
     */
80
    protected EntityMapper $mapper;
81
82
    /**
83
     * Whether the query state is locked
84
     * @var bool
85
     */
86
    protected bool $locked = false;
87
88
    /**
89
     * Create new instance
90
     * @param EntityManager<TEntity> $manager
91
     * @param EntityMapper<TEntity> $mapper
92
     * @param QueryStatement|null $queryStatement
93
     */
94
    public function __construct(
95
        EntityManager $manager,
96
        EntityMapper $mapper,
97
        ?QueryStatement $queryStatement = null
98
    ) {
99
        parent::__construct($queryStatement);
100
        $this->manager = $manager;
101
        $this->mapper = $mapper;
102
    }
103
104
    /**
105
     * Return the connection instance
106
     * @return Connection
107
     */
108
    public function getConnection(): Connection
109
    {
110
        return $this->manager->getConnection();
111
    }
112
113
    /**
114
     * Apply an filter(s) to the query
115
     * @param string|array<int, string>|array<string, mixed> $names
116
     * @return $this
117
     */
118
    public function filter(string|array $names): self
119
    {
120
        if (!is_array($names)) {
0 ignored issues
show
The condition is_array($names) is always true.
Loading history...
121
            $names = [$names];
122
        }
123
124
        $query = new Query($this->queryStatement);
125
        $filters = $this->mapper->getFilters();
126
127
        foreach ($names as $name => $args) {
128
            if (is_int($name)) {
129
                $name = $args;
130
                $args = null;
131
            }
132
133
            if (isset($filters[$name])) {
134
                $callback = $filters[$name];
135
                ($callback)($query, $args);
136
            }
137
        }
138
139
        return $this;
140
    }
141
142
    /**
143
     * Return one entity record
144
     * @param array<int, string> $columns
145
     * @param bool $primaryColumn
146
     * @return TEntity|null
0 ignored issues
show
The type Platine\Orm\Query\TEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
147
     */
148
    public function get(array $columns = [], bool $primaryColumn = true): ?Entity
149
    {
150
        $result = $this->query($columns, $primaryColumn)
151
                       ->fetchAssoc()
152
                       ->get();
153
154
        if ($result === false) {
155
            return null;
156
        }
157
158
        $class = $this->mapper->getEntityClass();
159
160
        return new $class(
161
            $this->manager,
162
            $this->mapper,
163
            $result,
164
            [],
165
            $this->isReadOnly(),
166
            false
167
        );
168
    }
169
170
    /**
171
     * Return the list of entities
172
     * @param array<int, string> $columns
173
     * @param bool $primaryColumn whether to return the primary key value
174
     * @return TEntity[]
175
     */
176
    public function all(array $columns = [], bool $primaryColumn = true): array
177
    {
178
        $results = $this->query($columns, $primaryColumn)
179
                        ->fetchAssoc()
180
                        ->all();
181
182
        $entities = [];
183
184
        if (is_array($results)) {
0 ignored issues
show
The condition is_array($results) is always true.
Loading history...
185
            $class = $this->mapper->getEntityClass();
186
            $isReadOnly = $this->isReadOnly();
187
            $loaders = $this->getRelationLoaders($results);
188
189
            foreach ($results as $result) {
190
                $entities[] = new $class(
191
                    $this->manager,
192
                    $this->mapper,
193
                    $result,
194
                    $loaders,
195
                    $isReadOnly,
196
                    false
197
                );
198
            }
199
        }
200
201
        return $entities;
202
    }
203
204
    /**
205
     * Delete entity record
206
     * @param bool $force whether to bypass soft delete
207
     * @param array<int, string> $tables
208
     * @return int the affected rows
209
     */
210
    public function delete(bool $force = false, array $tables = []): int
211
    {
212
        return (int) $this->transaction(function (Connection $connection) use ($tables, $force) {
213
            if (!$force && $this->mapper->hasSoftDelete()) {
214
                return (new Update($connection, $this->mapper->getTable(), $this->queryStatement))
215
                        ->set([
216
                            $this->mapper->getSoftDeleteColumn() => date($this->manager->getDateFormat())
217
                        ]);
218
            }
219
            return (new Delete($connection, $this->mapper->getTable(), $this->queryStatement))->delete($tables);
220
        });
221
    }
222
223
    /**
224
     * Update entity record
225
     * @param array<int, string> $columns
226
     * @return int
227
     */
228
    public function update(array $columns = []): int
229
    {
230
        return (int) $this->transaction(function (Connection $connection) use ($columns) {
231
            if ($this->mapper->hasTimestamp()) {
232
                list(, $updatedAtColumn) = $this->mapper->getTimestampColumns();
233
                $columns[$updatedAtColumn] = date($this->manager->getDateFormat());
234
            }
235
            return (new Update($connection, $this->mapper->getTable(), $this->queryStatement))
236
                    ->set($columns);
237
        });
238
    }
239
240
    /**
241
     * Update by incremented an column
242
     * @param string|array<string, mixed> $column
243
     * @param mixed $value
244
     * @return int
245
     */
246
    public function increment(string|array $column, mixed $value = 1): int
247
    {
248
        return (int) $this->transaction(function (Connection $connection) use ($column, $value) {
249
            if ($this->mapper->hasTimestamp()) {
250
                list(, $updatedAtColumn) = $this->mapper->getTimestampColumns();
251
                $this->queryStatement->addUpdateColumns([
252
                    $updatedAtColumn => date($this->manager->getDateFormat())
253
                ]);
254
            }
255
            return (new Update($connection, $this->mapper->getTable(), $this->queryStatement))
256
                    ->increment($column, $value);
257
        });
258
    }
259
260
    /**
261
     * Update by decremented an column
262
     * @param string|array<string, mixed> $column
263
     * @param mixed $value
264
     * @return int
265
     */
266
    public function decrement(string|array $column, mixed $value = 1): int
267
    {
268
        return (int) $this->transaction(function (Connection $connection) use ($column, $value) {
269
            if ($this->mapper->hasTimestamp()) {
270
                list(, $updatedAtColumn) = $this->mapper->getTimestampColumns();
271
                $this->queryStatement->addUpdateColumns([
272
                    $updatedAtColumn => date($this->manager->getDateFormat())
273
                ]);
274
            }
275
            return (new Update($connection, $this->mapper->getTable(), $this->queryStatement))
276
                    ->decrement($column, $value);
277
        });
278
    }
279
280
    /**
281
     * Find entity record using primary key value
282
     * @param string|int|float|array<string|int|float> $id
283
     *
284
     * @return TEntity|null
285
     */
286
    public function find(string|int|float|array $id): ?Entity
287
    {
288
        if (is_array($id)) {
0 ignored issues
show
The condition is_array($id) is always true.
Loading history...
289
            foreach ($id as $pkColumn => $pkValue) {
290
                $this->where($pkColumn)->is($pkValue);
291
            }
292
        } else {
293
            $columns = $this->mapper->getPrimaryKey()->columns();
294
            $this->where($columns[0])->is($id);
295
        }
296
297
        return $this->get();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get() also could return the type Platine\Orm\Entity which is incompatible with the documented return type Platine\Orm\Query\TEntity|null.
Loading history...
298
    }
299
300
/**
301
     * Find entities record using primary key values
302
     * @param mixed ...$ids
303
     *
304
     * @return TEntity[]
305
     */
306
    public function findAll(mixed ...$ids): array
307
    {
308
        if (is_array($ids[0])) {
309
            $keys = array_keys($ids[0]);
310
            $values = [];
311
312
            foreach ($ids as $pkValue) {
313
                foreach ($keys as $pkColumn) {
314
                    $values[$pkColumn][] = $pkValue[$pkColumn];
315
                }
316
            }
317
318
            foreach ($values as $pkColumn => $pkValues) {
319
                $this->where($pkColumn)->in($pkValues);
320
            }
321
        } else {
322
            $columns = $this->mapper->getPrimaryKey()->columns();
323
            $this->where($columns[0])->in($ids);
324
        }
325
326
        return $this->all();
327
    }
328
329
    /**
330
     *
331
     * @param string|Expression|Closure $column
332
     * @return mixed
333
     */
334
    public function column(string|Expression|Closure $column): mixed
335
    {
336
        (new ColumnExpression($this->queryStatement))->column($column);
337
338
        return $this->executeAggregate();
339
    }
340
341
    /**
342
     *
343
     * @param string|Expression|Closure $column
344
     * @param bool $distinct
345
     * @return mixed
346
     */
347
    public function count(
348
        string|Expression|Closure $column = '*',
349
        bool $distinct = false
350
    ): mixed {
351
        (new ColumnExpression($this->queryStatement))->count($column, null, $distinct);
352
353
        return $this->executeAggregate();
354
    }
355
356
    /**
357
     *
358
     * @param string|Expression|Closure $column
359
     * @param bool $distinct
360
     * @return mixed
361
     */
362
    public function avg(
363
        string|Expression|Closure $column,
364
        bool $distinct = false
365
    ): mixed {
366
        (new ColumnExpression($this->queryStatement))->avg($column, null, $distinct);
367
368
        return $this->executeAggregate();
369
    }
370
371
    /**
372
     *
373
     * @param string|Expression|Closure $column
374
     * @param bool $distinct
375
     * @return mixed
376
     */
377
    public function sum(
378
        string|Expression|Closure $column,
379
        bool $distinct = false
380
    ): mixed {
381
        (new ColumnExpression($this->queryStatement))->sum($column, null, $distinct);
382
383
        return $this->executeAggregate();
384
    }
385
386
    /**
387
     *
388
     * @param string|Expression|Closure $column
389
     * @param bool $distinct
390
     * @return mixed
391
     */
392
    public function min(string|Expression|Closure $column, bool $distinct = false): mixed
393
    {
394
        (new ColumnExpression($this->queryStatement))->min($column, null, $distinct);
395
396
        return $this->executeAggregate();
397
    }
398
399
    /**
400
     *
401
     * @param string|Expression|Closure $column
402
     * @param bool $distinct
403
     * @return mixed
404
     */
405
    public function max(
406
        string|Expression|Closure $column,
407
        bool $distinct = false
408
    ): mixed {
409
        (new ColumnExpression($this->queryStatement))->max($column, null, $distinct);
410
411
        return $this->executeAggregate();
412
    }
413
414
    /**
415
     * Clone of object
416
     */
417
    public function __clone(): void
418
    {
419
        parent::__clone();
420
        $this->havingStatement = new HavingStatement($this->queryStatement);
421
    }
422
423
    /**
424
     * Build the instance of EntityQuery
425
     * @return $this
426
     */
427
    protected function buildQuery(): self
428
    {
429
        $this->queryStatement->addTables([$this->mapper->getTable()]);
430
431
        return $this;
432
    }
433
434
    /**
435
     * Execute query and return the result set
436
     * @param array<int, string> $columns
437
     * @param bool $primaryColumn
438
     * @return ResultSet
439
     */
440
    protected function query(array $columns = [], bool $primaryColumn = true): ResultSet
441
    {
442
        if (!$this->buildQuery()->locked && !empty($columns) && $primaryColumn) {
443
            foreach ($this->mapper->getPrimaryKey()->columns() as $pkColumn) {
444
                $columns[] = $pkColumn;
445
            }
446
        }
447
448
        if ($this->mapper->hasSoftDelete()) {
449
            if (!$this->withSoftDeleted) {
450
                $this->where($this->mapper->getSoftDeleteColumn())->isNull();
451
            } elseif ($this->onlySoftDeleted) {
452
                $this->where($this->mapper->getSoftDeleteColumn())->isNotNull();
453
            }
454
        }
455
456
        $this->select($columns);
457
458
        $connection = $this->manager->getConnection();
459
        $driver = $connection->getDriver();
460
461
        return $connection->query(
462
            $driver->select($this->queryStatement),
463
            $driver->getParams()
464
        );
465
    }
466
467
    /**
468
     * Return the relations data loaders
469
     * @param array<int, mixed>|false $results
470
     * @return array<string, \Platine\Orm\Relation\RelationLoader<TEntity>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, \Platine\O...elationLoader<TEntity>> at position 4 could not be parsed: Expected '>' at position 4, but found '\Platine\Orm\Relation\RelationLoader'.
Loading history...
471
     */
472
    protected function getRelationLoaders(array|false $results): array
473
    {
474
        if (empty($this->with) || empty($results)) {
475
            return [];
476
        }
477
478
        $loaders = [];
479
        $attributes = $this->getWithAttributes();
480
        $relations = $this->mapper->getRelations();
481
482
        foreach ($attributes['with'] as $with => $callback) {
483
            if (!isset($relations[$with])) {
484
                continue;
485
            }
486
487
            /** @var RelationLoader<TEntity> $loader */
488
            $loader = $relations[$with]->getLoader($this->manager, $this->mapper, [
489
                'results' => $results,
490
                'callback' => $callback,
491
                'with' => isset($attributes['extra'][$with])
492
                            ? $attributes['extra'][$with]
493
                            : [],
494
                'immediate' => $this->immediate
495
            ]);
496
497
            $loaders[$with] = $loader;
498
        }
499
500
        return $loaders;
501
    }
502
503
    /**
504
     * Execute the aggregate
505
     * @return mixed
506
     */
507
    protected function executeAggregate(): mixed
508
    {
509
        $this->queryStatement->addTables([$this->mapper->getTable()]);
510
511
        if ($this->mapper->hasSoftDelete()) {
512
            if (!$this->withSoftDeleted) {
513
                $this->where($this->mapper->getSoftDeleteColumn())->isNull();
514
            } elseif ($this->onlySoftDeleted) {
515
                $this->where($this->mapper->getSoftDeleteColumn())->isNotNull();
516
            }
517
        }
518
519
        $connection = $this->manager->getConnection();
520
        $driver = $connection->getDriver();
521
522
        return $connection->column(
523
            $driver->select($this->queryStatement),
524
            $driver->getParams()
525
        );
526
    }
527
528
    /**
529
     * Check if the current entity query is read only
530
     * @return bool
531
     */
532
    protected function isReadOnly(): bool
533
    {
534
        return count($this->queryStatement->getJoins()) > 0;
535
    }
536
537
    /**
538
     * Execute the transaction
539
     * @param Closure $callback
540
     *
541
     * @return mixed
542
     */
543
    protected function transaction(Closure $callback): mixed
544
    {
545
        return $this->manager->getConnection()
546
                             ->transaction($callback);
547
    }
548
}
549