Passed
Push — dev_2x ( fedccd...fb9ebe )
by Adrian
02:16
created

Query   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 62
eloc 160
c 1
b 0
f 0
dl 0
loc 384
rs 3.44

23 Methods

Rating   Name   Duplication   Size   Complexity  
A resetGuards() 0 5 1
A chunk() 0 25 5
A setGuards() 0 11 3
A count() 0 7 1
A where() 0 12 4
A getStatement() 0 5 1
A __construct() 0 16 1
A init() 0 2 1
A paginate() 0 14 2
A newEntityFromRow() 0 22 4
A newPaginatedCollectionFromRows() 0 21 2
A first() 0 5 1
A get() 0 5 1
A load() 0 14 3
A applyGuards() 0 12 4
A find() 0 5 1
A subSelectForJoinWith() 0 3 1
A __call() 0 8 3
A joinWith() 0 17 3
A __clone() 0 9 4
B injectRelations() 0 15 7
A newCollectionFromRows() 0 11 2
B injectAggregates() 0 17 7

How to fix   Complexity   

Complex Class

Complex classes like Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Query, and based on these observations, apply Extract Interface, too.

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
    public function __call(string $method, array $params)
96
    {
97
        $scope = $this->scopes[$method] ?? null;
98
        if ($scope && is_callable($scope)) {
99
            return $scope($this, ...$params);
100
        }
101
102
        return parent::__call($method, $params);
103
    }
104
105
    public function __clone()
106
    {
107
        $vars = get_object_vars($this);
108
        unset($vars['mapper']);
109
        foreach ($vars as $name => $prop) {
110
            if (is_object($prop) && method_exists($prop, '__clone')) {
111
                $this->$name = clone $prop;
112
            } else {
113
                $this->$name = $prop;
114
            }
115
        }
116
    }
117
118
119
    public function load(...$relations): self
120
    {
121
        foreach ($relations as $relation) {
122
            if (is_array($relation)) {
123
                $name     = key($relation);
124
                $callback = current($relation);
125
            } else {
126
                $name     = $relation;
127
                $callback = null;
128
            }
129
            $this->load[$name] = $callback;
130
        }
131
132
        return $this;
133
    }
134
135
    public function joinWith($name): Query
136
    {
137
        if (in_array($name, $this->joinedWith)) {
138
            return $this;
139
        }
140
141
        if (! $this->mapper->hasRelation($name)) {
142
            throw new \InvalidArgumentException(
143
                sprintf("Relation %s, not defined for %s", $name, $this->mapper->getConfig()->getTable())
144
            );
145
        }
146
147
        $relation = $this->mapper->getRelation($name);
148
149
        $this->joinedWith[] = $name;
150
151
        return $relation->joinSubselect($this, $name);
152
    }
153
154
    public function subSelectForJoinWith(Mapper $mapper): Query
155
    {
156
        return new Query($this->connection, $mapper, $this->bindings, $this->indent . '    ');
157
    }
158
159
    public function find($pk, array $load = [])
160
    {
161
        return $this->where($this->primaryKey, $pk)
162
                    ->load(...$load)
163
                    ->first();
164
    }
165
166
    public function first()
167
    {
168
        $row = $this->fetchOne();
169
170
        return $this->newEntityFromRow($row, $this->load);
171
    }
172
173
    public function get(): Collection
174
    {
175
        return $this->newCollectionFromRows(
176
            $this->connection->fetchAll($this->getStatement(), $this->getBindValues()),
177
            $this->load
178
        );
179
    }
180
181
    public function paginate(int $perPage, int $page = 1): PaginatedCollection
182
    {
183
        /** @var Query $countQuery */
184
        $countQuery = clone $this;
185
        $total      = $countQuery->count();
186
187
        if ($total == 0) {
188
            $this->newPaginatedCollectionFromRows([], $total, $perPage, $page, $this->load);
189
        }
190
191
        $this->perPage($perPage);
192
        $this->page($page);
193
194
        return $this->newPaginatedCollectionFromRows($this->fetchAll(), $total, $perPage, $page, $this->load);
195
    }
196
197
    protected function newEntityFromRow(array $data = null, array $load = [], Tracker $tracker = null)
198
    {
199
        if ($data == null) {
200
            return null;
201
        }
202
203
        $receivedTracker = ! ! $tracker;
204
        if (! $tracker) {
205
            $receivedTracker = false;
206
            $tracker         = new Tracker([$data]);
207
        }
208
209
        $entity = $this->mapper->newEntity($data);
210
        $this->injectRelations($entity, $tracker, $load);
211
        $this->injectAggregates($entity, $tracker, $load);
212
        $entity->setState(StateEnum::SYNCHRONIZED);
213
214
        if (! $receivedTracker) {
215
            $tracker->replaceRows([$entity]);
216
        }
217
218
        return $entity;
219
    }
220
221
    protected function newCollectionFromRows(array $rows, array $load = []): Collection
222
    {
223
        $entities = [];
224
        $tracker  = new Tracker($rows);
225
        foreach ($rows as $row) {
226
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
227
            $entities[] = $entity;
228
        }
229
        $tracker->replaceRows($entities);
230
231
        return new Collection($entities, $this->mapper->getHydrator());
232
    }
233
234
    protected function newPaginatedCollectionFromRows(
235
        array $rows,
236
        int $totalCount,
237
        int $perPage,
238
        int $currentPage,
239
        array $load = []
240
    ): PaginatedCollection {
241
        $entities = [];
242
        $tracker  = new Tracker($rows);
243
        foreach ($rows as $row) {
244
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
245
            $entities[] = $entity;
246
        }
247
        $tracker->replaceRows($entities);
248
249
        return new PaginatedCollection(
250
            $entities,
251
            $totalCount,
252
            $perPage,
253
            $currentPage,
254
            $this->mapper->getHydrator()
255
        );
256
    }
257
258
    protected function injectRelations(EntityInterface $entity, Tracker $tracker, array $eagerLoad = [])
259
    {
260
        foreach ($this->mapper->getRelations() as $name) {
261
            $relation      = $this->mapper->getRelation($name);
262
            $queryCallback = $eagerLoad[$name] ?? null;
263
            $nextLoad      = Arr::getChildren($eagerLoad, $name);
264
265
            if (! $tracker->hasRelation($name)) {
266
                $tracker->setRelation($name, $relation, $queryCallback, $nextLoad);
267
            }
268
269
            if (array_key_exists($name, $eagerLoad) || in_array($name, $eagerLoad) || $relation->isEagerLoad()) {
270
                $relation->attachMatchesToEntity($entity, $tracker->getResultsForRelation($name));
271
            } elseif ($relation->isLazyLoad()) {
272
                $relation->attachLazyRelationToEntity($entity, $tracker);
273
            }
274
        }
275
    }
276
277
    protected function injectAggregates(EntityInterface $entity, Tracker $tracker, array $eagerLoad = [])
278
    {
279
        foreach ($this->mapper->getRelations() as $name) {
280
            $relation = $this->mapper->getRelation($name);
281
            if (! method_exists($relation, 'getAggregates')) {
282
                continue;
283
            }
284
            $aggregates = $relation->getAggregates();
285
            /**
286
             * @var string $aggName
287
             * @var Aggregate $aggregate
288
             */
289
            foreach ($aggregates as $aggName => $aggregate) {
290
                if (array_key_exists($aggName, $eagerLoad) || $aggregate->isEagerLoad()) {
291
                    $aggregate->attachAggregateToEntity($entity, $tracker->getAggregateResults($aggregate));
292
                } elseif ($aggregate->isLazyLoad()) {
293
                    $aggregate->attachLazyAggregateToEntity($entity, $tracker);
294
                }
295
            }
296
        }
297
    }
298
299
    /**
300
     * Executes the query with a limit of $size and applies the callback on each entity
301
     * The callback can change the DB in such a way that you can end up in an infinite loop
302
     * (depending on the sorting) so we set a limit on the number of chunks that can be processed
303
     *
304
     * @param int $size
305
     * @param callable $callback
306
     * @param int $limit
307
     */
308
    public function chunk(int $size, callable $callback, int $limit = 100000)
309
    {
310
        if (! $this->orderBy->build()) {
311
            $this->orderBy(...(array)$this->mapper->getConfig()->getPrimaryKey());
312
        }
313
314
        $query = clone $this;
315
        $query->applyGuards();
316
        $query->resetGuards();
317
318
        $run = 0;
319
        while ($run < $limit) {
320
            $query->limit($size);
321
            $query->offset($run * $size);
322
            $results = $query->get();
323
324
            if (count($results) === 0) {
325
                break;
326
            }
327
328
            foreach ($results as $entity) {
329
                $callback($entity);
330
            }
331
332
            $run++;
333
        }
334
    }
335
336
    public function count()
337
    {
338
        $this->resetOrderBy();
339
        $this->resetColumns();
340
        $this->columns('COUNT(*) AS total');
341
342
        return (int)$this->fetchValue();
343
    }
344
345
    public function where($column, $value = null, $condition = '=')
346
    {
347
        if (is_string($column) && ($dotPosition = strpos($column, '.'))) {
348
            $relationName = trim(substr($column, 0, $dotPosition), "'`");
349
            // the relationName could be a table so we need to make sure
350
            // beforehand that this is actually a relation
351
            if ($this->mapper->hasRelation($relationName)) {
352
                $this->joinWith($relationName);
353
            }
354
        }
355
356
        return parent::where($column, $value, $condition);
357
    }
358
359
    public function setGuards(array $guards)
360
    {
361
        foreach ($guards as $column => $value) {
362
            if (is_int($column)) {
363
                $this->guards[] = $value;
364
            } else {
365
                $this->guards[$column] = $value;
366
            }
367
        }
368
369
        return $this;
370
    }
371
372
    public function resetGuards()
373
    {
374
        $this->guards = [];
375
376
        return $this;
377
    }
378
379
    protected function applyGuards()
380
    {
381
        if (empty($this->guards)) {
382
            return;
383
        }
384
385
        $this->groupCurrentWhere();
386
        foreach ($this->guards as $column => $value) {
387
            if (is_int($column)) {
388
                $this->where($value, null, null);
389
            } else {
390
                $this->where($column, $value);
391
            }
392
        }
393
    }
394
395
    public function getStatement(): string
396
    {
397
        $this->applyGuards();
398
399
        return parent::getStatement();
400
    }
401
}
402