Completed
Push — master ( 56d3b9...264ef8 )
by Jared
01:47
created

src/Query.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
$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) {
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) {
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