Completed
Push — master ( 3f3907...70cb28 )
by Jared
01:46
created

Query::execute()   C

Complexity

Conditions 16
Paths 77

Size

Total Lines 73
Code Lines 40

Duplication

Lines 18
Ratio 24.66 %

Importance

Changes 0
Metric Value
dl 18
loc 73
rs 5.4156
c 0
b 0
f 0
cc 16
eloc 40
nc 77
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
/**
15
 * Represents a query against a model type.
16
 */
17
class Query
18
{
19
    const DEFAULT_LIMIT = 100;
20
    const MAX_LIMIT = 1000;
21
22
    /**
23
     * @var string
24
     */
25
    private $model;
26
27
    /**
28
     * @var array
29
     */
30
    private $joins;
31
32
    /**
33
     * @var array
34
     */
35
    private $eagerLoaded;
36
37
    /**
38
     * @var array
39
     */
40
    private $where;
41
42
    /**
43
     * @var int
44
     */
45
    private $limit;
46
47
    /**
48
     * @var int
49
     */
50
    private $start;
51
52
    /**
53
     * @var array
54
     */
55
    private $sort;
56
57
    /**
58
     * @param string $model model class
59
     */
60
    public function __construct($model = '')
61
    {
62
        $this->model = $model;
63
        $this->joins = [];
64
        $this->eagerLoaded = [];
65
        $this->where = [];
66
        $this->start = 0;
67
        $this->limit = self::DEFAULT_LIMIT;
68
        $this->sort = [];
69
    }
70
71
    /**
72
     * Gets the model class associated with this query.
73
     *
74
     * @return string
75
     */
76
    public function getModel()
77
    {
78
        return $this->model;
79
    }
80
81
    /**
82
     * Sets the limit for this query.
83
     *
84
     * @param int $limit
85
     *
86
     * @return $this
87
     */
88
    public function limit($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
     * @return int
99
     */
100
    public function getLimit()
101
    {
102
        return $this->limit;
103
    }
104
105
    /**
106
     * Sets the start offset.
107
     *
108
     * @param int $start
109
     *
110
     * @return $this
111
     */
112
    public function start($start)
113
    {
114
        $this->start = max($start, 0);
115
116
        return $this;
117
    }
118
119
    /**
120
     * Gets the start offset.
121
     *
122
     * @return int
123
     */
124
    public function getStart()
125
    {
126
        return $this->start;
127
    }
128
129
    /**
130
     * Sets the sort pattern for the query.
131
     *
132
     * @param array|string $sort
133
     *
134
     * @return $this
135
     */
136
    public function sort($sort)
137
    {
138
        $columns = explode(',', $sort);
139
140
        $sortParams = [];
141
        foreach ($columns as $column) {
142
            $c = explode(' ', trim($column));
143
144
            if (2 != count($c)) {
145
                continue;
146
            }
147
148
            // validate direction
149
            $direction = strtolower($c[1]);
150
            if (!in_array($direction, ['asc', 'desc'])) {
151
                continue;
152
            }
153
154
            $sortParams[] = [$c[0], $direction];
155
        }
156
157
        $this->sort = $sortParams;
158
159
        return $this;
160
    }
161
162
    /**
163
     * Gets the sort parameters.
164
     *
165
     * @return array
166
     */
167
    public function getSort()
168
    {
169
        return $this->sort;
170
    }
171
172
    /**
173
     * Sets the where parameters.
174
     * Accepts the following forms:
175
     *   i)   where(['name' => 'Bob'])
176
     *   ii)  where('name', 'Bob')
177
     *   iii) where('balance', 100, '>')
178
     *   iv)  where('balance > 100').
179
     *
180
     * @param array|string $where
181
     * @param mixed        $value     optional value
182
     * @param string|null  $condition optional condition
183
     *
184
     * @return $this
185
     */
186
    public function where($where, $value = null, $condition = null)
187
    {
188
        // handles i.
189
        if (is_array($where)) {
190
            $this->where = array_merge($this->where, $where);
191
        } else {
192
            // handles iii.
193
            $args = func_num_args();
194
            if ($args > 2) {
195
                $this->where[] = [$where, $value, $condition];
196
            // handles ii.
197
            } elseif (2 == $args) {
198
                $this->where[$where] = $value;
199
            // handles iv.
200
            } else {
201
                $this->where[] = $where;
202
            }
203
        }
204
205
        return $this;
206
    }
207
208
    /**
209
     * Gets the where parameters.
210
     *
211
     * @return array
212
     */
213
    public function getWhere()
214
    {
215
        return $this->where;
216
    }
217
218
    /**
219
     * Adds a join to the query. Matches a property on this model
220
     * to the ID of the model we are joining.
221
     *
222
     * @param string $model      model being joined
223
     * @param string $column     name of local property
224
     * @param string $foreignKey
225
     *
226
     * @return $this
227
     */
228
    public function join($model, $column, $foreignKey)
229
    {
230
        $this->joins[] = [$model, $column, $foreignKey];
231
232
        return $this;
233
    }
234
235
    /**
236
     * Gets the joins.
237
     *
238
     * @return array
239
     */
240
    public function getJoins()
241
    {
242
        return $this->joins;
243
    }
244
245
    /**
246
     * Marks a relationship property on the model that should be eager loaded.
247
     *
248
     * @param string $k local property containing the relationship
249
     *
250
     * @return $this
251
     */
252
    public function with($k)
253
    {
254
        if (!in_array($k, $this->eagerLoaded)) {
255
            $this->eagerLoaded[] = $k;
256
        }
257
258
        return $this;
259
    }
260
261
    /**
262
     * Gets the relationship properties that are going to be eager-loaded.
263
     *
264
     * @return array
265
     */
266
    public function getWith()
267
    {
268
        return $this->eagerLoaded;
269
    }
270
271
    /**
272
     * Executes the query against the model's driver.
273
     *
274
     * @return array results
275
     */
276
    public function execute()
277
    {
278
        $model = $this->model;
279
        $driver = $model::getDriver();
280
281
        $eagerLoadedProperties = [];
282
        $ids = array_fill_keys($this->eagerLoaded, []);
283
284
        // fetch the models matching the query
285
        $models = [];
286
        foreach ($driver->queryModels($this) as $row) {
287
            // get the model's ID
288
            $id = [];
289
            foreach ($model::getIDProperties() as $k) {
290
                $id[] = $row[$k];
291
            }
292
293
            // create the model and cache the loaded values
294
            $models[] = new $model($id, $row);
295
            foreach ($this->eagerLoaded as $k) {
296
                if (!isset($eagerLoadedProperties[$k])) {
297
                    $eagerLoadedProperties[$k] = $model::getProperty($k);
298
                }
299
300
                $localKey = $eagerLoadedProperties[$k]['local_key'];
301
                if ($row[$localKey]) {
302
                    $ids[$k][] = $row[$localKey];
303
                }
304
            }
305
        }
306
307
        // hydrate the eager loaded relationships
308
        foreach ($this->eagerLoaded as $k) {
309
            $property = $eagerLoadedProperties[$k];
310
            $relationModelClass = $property['relation'];
311
312
            if (Model::RELATIONSHIP_BELONGS_TO == $property['relation_type']) {
313
                $relationships = $this->fetchRelationships($relationModelClass, $ids[$k], $property['foreign_key'], false);
314
315
                foreach ($ids[$k] as $j => $id) {
316
                    if (isset($relationships[$id])) {
317
                        $models[$j]->setRelation($k, $relationships[$id]);
318
                    }
319
                }
320
            } elseif (Model::RELATIONSHIP_HAS_ONE == $property['relation_type']) {
321
                $relationships = $this->fetchRelationships($relationModelClass, $ids[$k], $property['foreign_key'], false);
322
323 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...
324
                    if (isset($relationships[$id])) {
325
                        $models[$j]->setRelation($k, $relationships[$id]);
326
                    } else {
327
                        // when using has one eager loading we must
328
                        // explicitly mark the relationship as null
329
                        // for models not found during eager loading
330
                        // or else it will trigger another DB call
331
                        $models[$j]->clearRelation($k);
332
                    }
333
                }
334
            } elseif (Model::RELATIONSHIP_HAS_MANY == $property['relation_type']) {
335
                $relationships = $this->fetchRelationships($relationModelClass, $ids[$k], $property['foreign_key'], true);
336
337 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...
338
                    if (isset($relationships[$id])) {
339
                        $models[$j]->setRelationCollection($k, $relationships[$id]);
340
                    } else {
341
                        $models[$j]->setRelationCollection($k, []);
342
                    }
343
                }
344
            }
345
        }
346
347
        return $models;
348
    }
349
350
    /**
351
     * Creates an iterator for a search.
352
     *
353
     * @return Iterator
354
     */
355
    public function all()
356
    {
357
        return new Iterator($this);
358
    }
359
360
    /**
361
     * Executes the query against the model's driver and returns the first result.
362
     *
363
     * @param int $limit
364
     *
365
     * @return array|Model|null when $limit = 1, returns a single model or null, otherwise returns an array
366
     */
367
    public function first($limit = 1)
368
    {
369
        $models = $this->limit($limit)->execute();
370
371
        if (1 == $limit) {
372
            return (1 == count($models)) ? $models[0] : null;
373
        }
374
375
        return $models;
376
    }
377
378
    /**
379
     * Gets the number of models matching the query.
380
     *
381
     * @return int
382
     */
383
    public function count()
384
    {
385
        $model = $this->model;
386
        $driver = $model::getDriver();
387
388
        return $driver->count($this);
389
    }
390
391
    /**
392
     * @deprecated
393
     *
394
     * Gets the total number of records matching an optional criteria
395
     *
396
     * @param array $where criteria
397
     *
398
     * @return int
399
     */
400
    public function totalRecords(array $where = [])
401
    {
402
        return $this->where($where)->count();
403
    }
404
405
    /**
406
     * Gets the sum of a property matching the query.
407
     *
408
     * @param string $property
409
     *
410
     * @return int
411
     */
412
    public function sum($property)
413
    {
414
        $model = $this->model;
415
        $driver = $model::getDriver();
416
417
        return $driver->sum($this, $property);
418
    }
419
420
    /**
421
     * Gets the average of a property matching the query.
422
     *
423
     * @param string $property
424
     *
425
     * @return int
426
     */
427
    public function average($property)
428
    {
429
        $model = $this->model;
430
        $driver = $model::getDriver();
431
432
        return $driver->average($this, $property);
433
    }
434
435
    /**
436
     * Gets the max of a property matching the query.
437
     *
438
     * @param string $property
439
     *
440
     * @return int
441
     */
442
    public function max($property)
443
    {
444
        $model = $this->model;
445
        $driver = $model::getDriver();
446
447
        return $driver->max($this, $property);
448
    }
449
450
    /**
451
     * Gets the min of a property matching the query.
452
     *
453
     * @param string $property
454
     *
455
     * @return int
456
     */
457
    public function min($property)
458
    {
459
        $model = $this->model;
460
        $driver = $model::getDriver();
461
462
        return $driver->min($this, $property);
463
    }
464
465
    /**
466
     * Updates all of the models matched by this query.
467
     *
468
     * @todo should be optimized to be done in a single call to the data layer
469
     *
470
     * @param array $params key-value update parameters
471
     *
472
     * @return int # of models updated
473
     */
474
    public function set(array $params)
475
    {
476
        $n = 0;
477
        foreach ($this->all() as $model) {
478
            $model->set($params);
479
            ++$n;
480
        }
481
482
        return $n;
483
    }
484
485
    /**
486
     * Deletes all of the models matched by this query.
487
     *
488
     * @todo should be optimized to be done in a single call to the data layer
489
     *
490
     * @return int # of models deleted
491
     */
492
    public function delete()
493
    {
494
        $n = 0;
495
        foreach ($this->all() as $model) {
496
            $model->delete();
497
            ++$n;
498
        }
499
500
        return $n;
501
    }
502
503
    /**
504
     * Hydrates the eager-loaded relationships for a given set of IDs.
505
     *
506
     * @param string $modelClass
507
     * @param array  $ids
508
     * @param string $foreignKey
509
     * @param bool   $multiple   when true will condense
510
     *
511
     * @return array
512
     */
513
    private function fetchRelationships($modelClass, array $ids, $foreignKey, $multiple)
514
    {
515
        $uniqueIds = array_unique($ids);
516
        if (0 === count($uniqueIds)) {
517
            return [];
518
        }
519
520
        $in = $foreignKey.' IN ('.implode(',', $uniqueIds).')';
521
        $models = $modelClass::where($in)
522
                             ->first(self::MAX_LIMIT);
523
524
        $result = [];
525
        foreach ($models as $model) {
526
            if ($multiple) {
527
                if (!isset($result[$model->$foreignKey])) {
528
                    $result[$model->$foreignKey] = [];
529
                }
530
                $result[$model->$foreignKey][] = $model;
531
            } else {
532
                $result[$model->$foreignKey] = $model;
533
            }
534
        }
535
536
        return $result;
537
    }
538
}
539