Completed
Push — master ( 40c610...9750f1 )
by Jared
01:30
created

Query::with()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @see http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
12
namespace Pulsar;
13
14
use Pulsar\Relation\Relationship;
15
16
/**
17
 * Represents a query against a model type.
18
 */
19
class Query
20
{
21
    const DEFAULT_LIMIT = 100;
22
    const MAX_LIMIT = 1000;
23
24
    /**
25
     * @var Model|string
26
     */
27
    private $model;
28
29
    /**
30
     * @var array
31
     */
32
    private $joins;
33
34
    /**
35
     * @var array
36
     */
37
    private $eagerLoaded;
38
39
    /**
40
     * @var array
41
     */
42
    private $where;
43
44
    /**
45
     * @var int
46
     */
47
    private $limit;
48
49
    /**
50
     * @var int
51
     */
52
    private $start;
53
54
    /**
55
     * @var array
56
     */
57
    private $sort;
58
59
    /**
60
     * @param Model|string $model
61
     */
62
    public function __construct($model = '')
63
    {
64
        $this->model = $model;
65
        $this->joins = [];
66
        $this->eagerLoaded = [];
67
        $this->where = [];
68
        $this->start = 0;
69
        $this->limit = self::DEFAULT_LIMIT;
70
        $this->sort = [];
71
    }
72
73
    /**
74
     * Gets the model class associated with this query.
75
     *
76
     * @return Model|string
77
     */
78
    public function getModel()
79
    {
80
        return $this->model;
81
    }
82
83
    /**
84
     * Sets the limit for this query.
85
     *
86
     * @return $this
87
     */
88
    public function limit(int $limit)
89
    {
90
        $this->limit = min($limit, self::MAX_LIMIT);
91
92
        return $this;
93
    }
94
95
    /**
96
     * Gets the limit for this query.
97
     */
98
    public function getLimit(): int
99
    {
100
        return $this->limit;
101
    }
102
103
    /**
104
     * Sets the start offset.
105
     *
106
     * @return $this
107
     */
108
    public function start(int $start)
109
    {
110
        $this->start = max($start, 0);
111
112
        return $this;
113
    }
114
115
    /**
116
     * Gets the start offset.
117
     */
118
    public function getStart(): int
119
    {
120
        return $this->start;
121
    }
122
123
    /**
124
     * Sets the sort pattern for the query.
125
     *
126
     * @param array|string $sort
127
     *
128
     * @return $this
129
     */
130
    public function sort($sort)
131
    {
132
        $columns = explode(',', $sort);
133
134
        $sortParams = [];
135
        foreach ($columns as $column) {
136
            $c = explode(' ', trim($column));
137
138
            if (2 != count($c)) {
139
                continue;
140
            }
141
142
            // validate direction
143
            $direction = strtolower($c[1]);
144
            if (!in_array($direction, ['asc', 'desc'])) {
145
                continue;
146
            }
147
148
            $sortParams[] = [$c[0], $direction];
149
        }
150
151
        $this->sort = $sortParams;
152
153
        return $this;
154
    }
155
156
    /**
157
     * Gets the sort parameters.
158
     */
159
    public function getSort(): array
160
    {
161
        return $this->sort;
162
    }
163
164
    /**
165
     * Sets the where parameters.
166
     * Accepts the following forms:
167
     *   i)   where(['name' => 'Bob'])
168
     *   ii)  where('name', 'Bob')
169
     *   iii) where('balance', 100, '>')
170
     *   iv)  where('balance > 100').
171
     *
172
     * @param array|string $where
173
     * @param mixed        $value     optional value
174
     * @param string|null  $condition optional condition
175
     *
176
     * @return $this
177
     */
178
    public function where($where, $value = null, $condition = null)
179
    {
180
        // handles i.
181
        if (is_array($where)) {
182
            $this->where = array_merge($this->where, $where);
183
        } else {
184
            // handles iii.
185
            $args = func_num_args();
186
            if ($args > 2) {
187
                $this->where[] = [$where, $value, $condition];
188
            // handles ii.
189
            } elseif (2 == $args) {
190
                $this->where[$where] = $value;
191
            // handles iv.
192
            } else {
193
                $this->where[] = $where;
194
            }
195
        }
196
197
        return $this;
198
    }
199
200
    /**
201
     * Gets the where parameters.
202
     */
203
    public function getWhere(): array
204
    {
205
        return $this->where;
206
    }
207
208
    /**
209
     * Adds a join to the query. Matches a property on this model
210
     * to the ID of the model we are joining.
211
     *
212
     * @param string $model  model being joined
213
     * @param string $column name of local property
214
     *
215
     * @return $this
216
     */
217
    public function join($model, string $column, string $foreignKey)
218
    {
219
        $this->joins[] = [$model, $column, $foreignKey];
220
221
        return $this;
222
    }
223
224
    /**
225
     * Gets the joins.
226
     */
227
    public function getJoins(): array
228
    {
229
        return $this->joins;
230
    }
231
232
    /**
233
     * Marks a relationship property on the model that should be eager loaded.
234
     *
235
     * @param string $k local property containing the relationship
236
     *
237
     * @return $this
238
     */
239
    public function with(string $k)
240
    {
241
        if (!in_array($k, $this->eagerLoaded)) {
242
            $this->eagerLoaded[] = $k;
243
        }
244
245
        return $this;
246
    }
247
248
    /**
249
     * Gets the relationship properties that are going to be eager-loaded.
250
     */
251
    public function getWith(): array
252
    {
253
        return $this->eagerLoaded;
254
    }
255
256
    /**
257
     * Executes the query against the model's driver.
258
     *
259
     * @return array results
260
     */
261
    public function execute(): array
262
    {
263
        $modelClass = $this->model;
264
        $driver = $modelClass::getDriver();
265
266
        $eagerLoadedProperties = [];
267
        // instantiate a model so that initialize() is called and properties are filled in
268
        // otherwise this empty model is not used
269
        $model = new $modelClass();
0 ignored issues
show
Unused Code introduced by
$model is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
270
        $ids = [];
271
        foreach ($this->eagerLoaded as $k) {
272
            $eagerLoadedProperties[$k] = $modelClass::definition()->get($k);
273
            $ids[$k] = [];
274
        }
275
276
        // fetch the models matching the query
277
        /** @var Model[] $models */
278
        $models = [];
279
        foreach ($driver->queryModels($this) as $j => $row) {
280
            // type-cast the values because they came from the database
281
            foreach ($row as $k => &$v) {
282
                if ($property = $modelClass::definition()->get($k)) {
283
                    $v = Type::cast($property, $v);
284
                }
285
            }
286
287
            // create the model and cache the loaded values
288
            $models[] = new $modelClass($row);
289
290
            // capture any local ids for eager loading relationships
291
            foreach ($this->eagerLoaded as $k) {
292
                $localKey = $eagerLoadedProperties[$k]['local_key'];
293
                if (isset($row[$localKey])) {
294
                    $ids[$k][$j] = $row[$localKey];
295
                }
296
            }
297
        }
298
299
        // hydrate the eager loaded relationships
300
        foreach ($this->eagerLoaded as $k) {
301
            $property = $eagerLoadedProperties[$k];
302
            $relationModelClass = $property->getForeignModelClass();
303
            $type = $property->getRelationshipType();
304
305
            if (Relationship::BELONGS_TO == $type) {
306
                $relationships = $this->fetchRelationships($relationModelClass, $ids[$k], $property->getForeignKey(), false);
307
308
                foreach ($ids[$k] as $j => $id) {
309
                    if (isset($relationships[$id])) {
310
                        $models[$j]->setRelation($k, $relationships[$id]);
311
                        // older style properties do not support this type of hydration
312
                        if (!$property->isPersisted()) {
313
                            $models[$j]->hydrateValue($k, $relationships[$id]);
314
                        }
315
                    }
316
                }
317
            } elseif (Relationship::HAS_ONE == $type) {
318
                $relationships = $this->fetchRelationships($relationModelClass, $ids[$k], $property->getForeignKey(), false);
319
320 View Code Duplication
                foreach ($ids[$k] as $j => $id) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
321
                    if (isset($relationships[$id])) {
322
                        $models[$j]->setRelation($k, $relationships[$id]);
323
                        // older style properties do not support this type of hydration
324
                        if (!$property->isPersisted()) {
325
                            $models[$j]->hydrateValue($k, $relationships[$id]);
326
                        }
327
                    } else {
328
                        // when using has one eager loading we must
329
                        // explicitly mark the relationship as null
330
                        // for models not found during eager loading
331
                        // or else it will trigger another DB call
332
                        $models[$j]->clearRelation($k);
333
334
                        // older style properties do not support this type of hydration
335
                        if (!$property->isPersisted()) {
336
                            $models[$j]->hydrateValue($k, null);
337
                        }
338
                    }
339
                }
340
            } elseif (Relationship::HAS_MANY == $type) {
341
                $relationships = $this->fetchRelationships($relationModelClass, $ids[$k], $property->getForeignKey(), true);
342
343 View Code Duplication
                foreach ($ids[$k] as $j => $id) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
344
                    if (isset($relationships[$id])) {
345
                        $models[$j]->setRelationCollection($k, $relationships[$id]);
346
                        // older style properties do not support this type of hydration
347
                        if (!$property->isPersisted()) {
348
                            $models[$j]->hydrateValue($k, $relationships[$id]);
349
                        }
350
                    } else {
351
                        $models[$j]->setRelationCollection($k, []);
352
                        // older style properties do not support this type of hydration
353
                        if (!$property->isPersisted()) {
354
                            $models[$j]->hydrateValue($k, []);
355
                        }
356
                    }
357
                }
358
            }
359
        }
360
361
        return $models;
362
    }
363
364
    /**
365
     * Creates an iterator for a search.
366
     *
367
     * @return Iterator
368
     */
369
    public function all()
370
    {
371
        return new Iterator($this);
372
    }
373
374
    /**
375
     * Executes the query against the model's driver and returns the first result.
376
     *
377
     * @return array|Model|null when $limit = 1, returns a single model or null, otherwise returns an array
378
     */
379
    public function first(int $limit = 1)
380
    {
381
        $models = $this->limit($limit)->execute();
382
383
        if (1 == $limit) {
384
            return (1 == count($models)) ? $models[0] : null;
385
        }
386
387
        return $models;
388
    }
389
390
    /**
391
     * Gets the number of models matching the query.
392
     */
393
    public function count(): int
394
    {
395
        $model = $this->model;
396
        $driver = $model::getDriver();
397
398
        return $driver->count($this);
399
    }
400
401
    /**
402
     * Gets the sum of a property matching the query.
403
     *
404
     * @return number
405
     */
406
    public function sum(string $property)
407
    {
408
        $model = $this->model;
409
        $driver = $model::getDriver();
410
411
        return $driver->sum($this, $property);
412
    }
413
414
    /**
415
     * Gets the average of a property matching the query.
416
     *
417
     * @return number
418
     */
419
    public function average(string $property)
420
    {
421
        $model = $this->model;
422
        $driver = $model::getDriver();
423
424
        return $driver->average($this, $property);
425
    }
426
427
    /**
428
     * Gets the max of a property matching the query.
429
     *
430
     * @return number
431
     */
432
    public function max(string $property)
433
    {
434
        $model = $this->model;
435
        $driver = $model::getDriver();
436
437
        return $driver->max($this, $property);
438
    }
439
440
    /**
441
     * Gets the min of a property matching the query.
442
     *
443
     * @return number
444
     */
445
    public function min(string $property)
446
    {
447
        $model = $this->model;
448
        $driver = $model::getDriver();
449
450
        return $driver->min($this, $property);
451
    }
452
453
    /**
454
     * Updates all of the models matched by this query.
455
     *
456
     * @todo should be optimized to be done in a single call to the data layer
457
     *
458
     * @param array $params key-value update parameters
459
     *
460
     * @return int # of models updated
461
     */
462
    public function set(array $params): int
463
    {
464
        $n = 0;
465
        foreach ($this->all() as $model) {
466
            $model->set($params);
467
            ++$n;
468
        }
469
470
        return $n;
471
    }
472
473
    /**
474
     * Deletes all of the models matched by this query.
475
     *
476
     * @todo should be optimized to be done in a single call to the data layer
477
     *
478
     * @return int # of models deleted
479
     */
480
    public function delete(): int
481
    {
482
        $n = 0;
483
        foreach ($this->all() as $model) {
484
            $model->delete();
485
            ++$n;
486
        }
487
488
        return $n;
489
    }
490
491
    /**
492
     * Hydrates the eager-loaded relationships for a given set of IDs.
493
     *
494
     * @param string $modelClass
495
     * @param bool   $multiple   when true will condense
496
     *
497
     * @return Model[]
498
     */
499
    private function fetchRelationships($modelClass, array $ids, string $foreignKey, bool $multiple): array
500
    {
501
        $uniqueIds = array_unique($ids);
502
        if (0 === count($uniqueIds)) {
503
            return [];
504
        }
505
506
        $in = $foreignKey.' IN ('.implode(',', $uniqueIds).')';
507
        $models = $modelClass::where($in)
508
                             ->first(self::MAX_LIMIT);
509
510
        $result = [];
511
        foreach ($models as $model) {
512
            if ($multiple) {
513
                if (!isset($result[$model->$foreignKey])) {
514
                    $result[$model->$foreignKey] = [];
515
                }
516
                $result[$model->$foreignKey][] = $model;
517
            } else {
518
                $result[$model->$foreignKey] = $model;
519
            }
520
        }
521
522
        return $result;
523
    }
524
}
525