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