Passed
Push — dev_2x ( 1e248d...fedccd )
by Adrian
01:40
created

Query::subSelectForJoinWith()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Sirius\Orm;
5
6
use Sirius\Orm\Collection\Collection;
7
use Sirius\Orm\Collection\PaginatedCollection;
8
use Sirius\Orm\Contract\EntityInterface;
9
use Sirius\Orm\Entity\StateEnum;
10
use Sirius\Orm\Entity\Tracker;
11
use Sirius\Orm\Helpers\Arr;
12
use Sirius\Orm\Relation\Aggregate;
13
use Sirius\Sql\Bindings;
14
use Sirius\Sql\Select;
15
16
class Query extends Select
17
{
18
    /**
19
     * @var Mapper
20
     */
21
    protected $mapper;
22
23
    /**
24
     * @var MapperConfig
25
     */
26
    protected $mapperConfig;
27
28
    /**
29
     * @var string|array
30
     */
31
    protected $primaryKey;
32
33
    /**
34
     * @var string
35
     */
36
    protected $table;
37
38
    /**
39
     * @var string
40
     */
41
    protected $tableReference;
42
43
    /**
44
     * @var Connection
45
     */
46
    protected $connection;
47
48
    /**
49
     * @var array
50
     */
51
    protected $load = [];
52
53
    /**
54
     * @var array
55
     */
56
    protected $guards = [];
57
58
    /**
59
     * @var array
60
     */
61
    protected $scopes = [];
62
63
    /**
64
     * List of relation that were joined to prevent from joining them again
65
     * Designed for situation where you have conditions on atributes
66
     * of related entities
67
     *
68
     * @example $content->newQuery()->where('tags.name', 'cool')
69
     * @var array
70
     */
71
    protected $joinedWith = [];
72
73
    public function __construct(Connection $connection, Mapper $mapper, Bindings $bindings = null, string $indent = '')
74
    {
75
        parent::__construct($connection, $bindings, $indent);
76
        $this->mapper = $mapper;
77
78
        $mapperConfig         = $mapper->getConfig();
79
        $this->primaryKey     = $mapperConfig->getPrimaryKey();
80
        $this->tableReference = $mapperConfig->getTableReference();
81
        $this->table          = $mapperConfig->getTableAlias(true);
82
        $this->guards         = $mapperConfig->getGuards();
83
84
        $this->from($this->tableReference);
85
        $this->resetColumns();
86
        $this->columns($this->table . '.*');
87
88
        $this->init();
89
    }
90
91
    protected function init()
92
    {
93
94
    }
95
96
    public function __call(string $method, array $params)
97
    {
98
        $scope = $this->scopes[$method] ?? null;
99
        if ($scope && is_callable($scope)) {
100
            return $scope($this, ...$params);
101
        }
102
103
        return parent::__call($method, $params);
104
    }
105
106
    public function __clone()
107
    {
108
        $vars = get_object_vars($this);
109
        unset($vars['mapper']);
110
        foreach ($vars as $name => $prop) {
111
            if (is_object($prop) && method_exists($prop, '__clone')) {
112
                $this->$name = clone $prop;
113
            } else {
114
                $this->$name = $prop;
115
            }
116
        }
117
    }
118
119
120
    public function load(...$relations): self
121
    {
122
        foreach ($relations as $relation) {
123
            if (is_array($relation)) {
124
                $name     = key($relation);
125
                $callback = current($relation);
126
            } else {
127
                $name     = $relation;
128
                $callback = null;
129
            }
130
            $this->load[$name] = $callback;
131
        }
132
133
        return $this;
134
    }
135
136
    public function joinWith($name): Query
137
    {
138
        if (in_array($name, $this->joinedWith)) {
139
            return $this;
140
        }
141
142
        if ( ! $this->mapper->hasRelation($name)) {
143
            throw new \InvalidArgumentException(
144
                sprintf("Relation %s, not defined for %s", $name, $this->mapper->getConfig()->getTable())
145
            );
146
        }
147
148
        $relation = $this->mapper->getRelation($name);
149
150
        $this->joinedWith[] = $name;
151
152
        return $relation->joinSubselect($this, $name);
153
    }
154
155
    public function subSelectForJoinWith(Mapper $mapper): Query
156
    {
157
        return new Query($this->connection, $mapper, $this->bindings, $this->indent . '    ');
158
    }
159
160
    public function find($pk, array $load = [])
161
    {
162
        return $this->where($this->primaryKey, $pk)
163
                    ->load(...$load)
164
                    ->first();
165
    }
166
167
    public function first()
168
    {
169
        $row = $this->fetchOne();
170
171
        return $this->newEntityFromRow($row, $this->load);
172
    }
173
174
    public function get(): Collection
175
    {
176
        return $this->newCollectionFromRows(
177
            $this->connection->fetchAll($this->getStatement(), $this->getBindValues()),
178
            $this->load
179
        );
180
    }
181
182
    public function paginate(int $perPage, int $page = 1): PaginatedCollection
183
    {
184
        /** @var Query $countQuery */
185
        $countQuery = clone $this;
186
        $total      = $countQuery->count();
187
188
        if ($total == 0) {
189
            $this->newPaginatedCollectionFromRows([], $total, $perPage, $page, $this->load);
190
        }
191
192
        $this->perPage($perPage);
193
        $this->page($page);
194
195
        return $this->newPaginatedCollectionFromRows($this->fetchAll(), $total, $perPage, $page, $this->load);
196
    }
197
198
    protected function newEntityFromRow(array $data = null, array $load = [], Tracker $tracker = null)
199
    {
200
        if ($data == null) {
201
            return null;
202
        }
203
204
        $receivedTracker = ! ! $tracker;
205
        if ( ! $tracker) {
206
            $receivedTracker = false;
207
            $tracker         = new Tracker([$data]);
208
        }
209
210
        $entity = $this->mapper->newEntity($data);
211
        $this->injectRelations($entity, $tracker, $load);
212
        $this->injectAggregates($entity, $tracker, $load);
213
        $entity->setState(StateEnum::SYNCHRONIZED);
214
215
        if ( ! $receivedTracker) {
216
            $tracker->replaceRows([$entity]);
217
        }
218
219
        return $entity;
220
    }
221
222
    protected function newCollectionFromRows(array $rows, array $load = []): Collection
223
    {
224
        $entities = [];
225
        $tracker  = new Tracker($rows);
226
        foreach ($rows as $row) {
227
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
228
            $entities[] = $entity;
229
        }
230
        $tracker->replaceRows($entities);
231
232
        return new Collection($entities, $this->mapper->getHydrator());
233
    }
234
235
    protected function newPaginatedCollectionFromRows(
236
        array $rows,
237
        int $totalCount,
238
        int $perPage,
239
        int $currentPage,
240
        array $load = []
241
    ): PaginatedCollection {
242
        $entities = [];
243
        $tracker  = new Tracker($rows);
244
        foreach ($rows as $row) {
245
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
246
            $entities[] = $entity;
247
        }
248
        $tracker->replaceRows($entities);
249
250
        return new PaginatedCollection(
251
            $entities,
252
            $totalCount,
253
            $perPage,
254
            $currentPage,
255
            $this->mapper->getHydrator()
256
        );
257
    }
258
259
    protected function injectRelations(EntityInterface $entity, Tracker $tracker, array $eagerLoad = [])
260
    {
261
        foreach ($this->mapper->getRelations() as $name) {
262
            $relation      = $this->mapper->getRelation($name);
263
            $queryCallback = $eagerLoad[$name] ?? null;
264
            $nextLoad      = Arr::getChildren($eagerLoad, $name);
265
266
            if ( ! $tracker->hasRelation($name)) {
267
                $tracker->setRelation($name, $relation, $queryCallback, $nextLoad);
268
            }
269
270
            if (array_key_exists($name, $eagerLoad) || in_array($name, $eagerLoad) || $relation->isEagerLoad()) {
271
                $relation->attachMatchesToEntity($entity, $tracker->getResultsForRelation($name));
272
            } elseif ($relation->isLazyLoad()) {
273
                $relation->attachLazyRelationToEntity($entity, $tracker);
274
            }
275
        }
276
    }
277
278
    protected function injectAggregates(EntityInterface $entity, Tracker $tracker, array $eagerLoad = [])
0 ignored issues
show
Unused Code introduced by
The parameter $eagerLoad is not used and could be removed. ( Ignorable by Annotation )

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

278
    protected function injectAggregates(EntityInterface $entity, Tracker $tracker, /** @scrutinizer ignore-unused */ array $eagerLoad = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
279
    {
280
        foreach ($this->mapper->getRelations() as $name) {
281
            $relation = $this->mapper->getRelation($name);
282
            if ( ! method_exists($relation, 'getAggregates')) {
283
                continue;
284
            }
285
            $aggregates = $relation->getAggregates();
286
            foreach ($aggregates as $aggName => $aggregate) {
287
                /*
288
                 * aggregates don't have setters so we can't use the hydrator to inject
289
                 * the value using the old `attachAggregateToEntity()` method
290
                 *
291
                if (array_key_exists($aggName, $eagerLoad) || $aggregate->isEagerLoad()) {
292
                    $aggregate->attachAggregateToEntity($entity, $tracker->getAggregateResults($aggregate));
293
                } elseif ($aggregate->isLazyLoad()) {
294
                    $aggregate->attachLazyAggregateToEntity($entity, $tracker);
295
                }
296
                */
297
                /**
298
                 * @todo implement a mechanism to inject aggregates via a LazyValue which extends lazy loader
299
                 */
300
301
                /** @var $aggregate Aggregate */
302
                $aggregate->attachLazyAggregateToEntity($entity, $tracker);
303
            }
304
        }
305
    }
306
307
    /**
308
     * Executes the query with a limit of $size and applies the callback on each entity
309
     * The callback can change the DB in such a way that you can end up in an infinite loop
310
     * (depending on the sorting) so we set a limit on the number of chunks that can be processed
311
     *
312
     * @param int $size
313
     * @param callable $callback
314
     * @param int $limit
315
     */
316
    public function chunk(int $size, callable $callback, int $limit = 100000)
317
    {
318
        if ( ! $this->orderBy->build()) {
319
            $this->orderBy(...(array)$this->mapper->getConfig()->getPrimaryKey());
320
        }
321
322
        $query = clone $this;
323
        $query->applyGuards();
324
        $query->resetGuards();
325
326
        $run = 0;
327
        while ($run < $limit) {
328
            $query->limit($size);
329
            $query->offset($run * $size);
330
            $results = $query->get();
331
332
            if (count($results) === 0) {
333
                break;
334
            }
335
336
            foreach ($results as $entity) {
337
                $callback($entity);
338
            }
339
340
            $run++;
341
        }
342
    }
343
344
    public function count()
345
    {
346
        $this->resetOrderBy();
347
        $this->resetColumns();
348
        $this->columns('COUNT(*) AS total');
349
350
        return (int)$this->fetchValue();
351
    }
352
353
    public function where($column, $value = null, $condition = '=')
354
    {
355
        if (is_string($column) && ($dotPosition = strpos($column, '.'))) {
356
            $relationName = trim(substr($column, 0, $dotPosition), "'`");
357
            // the relationName could be a table so we need to make sure
358
            // beforehand that this is actually a relation
359
            if ($this->mapper->hasRelation($relationName)) {
360
                $this->joinWith($relationName);
361
            }
362
        }
363
364
        return parent::where($column, $value, $condition);
365
    }
366
367
    public function setGuards(array $guards)
368
    {
369
        foreach ($guards as $column => $value) {
370
            if (is_int($column)) {
371
                $this->guards[] = $value;
372
            } else {
373
                $this->guards[$column] = $value;
374
            }
375
        }
376
377
        return $this;
378
    }
379
380
    public function resetGuards()
381
    {
382
        $this->guards = [];
383
384
        return $this;
385
    }
386
387
    protected function applyGuards()
388
    {
389
390
        if (empty($this->guards)) {
391
            return;
392
        }
393
394
        $this->groupCurrentWhere();
395
        foreach ($this->guards as $column => $value) {
396
            if (is_int($column)) {
397
                $this->where($value, null, null);
398
            } else {
399
                $this->where($column, $value);
400
            }
401
        }
402
    }
403
404
    public function getStatement(): string
405
    {
406
        $this->applyGuards();
407
408
        return parent::getStatement();
409
    }
410
}
411