1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Sofa\Eloquence; |
4
|
|
|
|
5
|
|
|
use Sofa\Eloquence\Metable\Hooks; |
6
|
|
|
use Sofa\Eloquence\Metable\Attribute; |
7
|
|
|
use Sofa\Eloquence\Metable\AttributeBag; |
8
|
|
|
use Sofa\Hookable\Contracts\ArgumentBag; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* @property array $allowedMeta |
12
|
|
|
*/ |
13
|
|
|
trait Metable |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* Query methods customizable by this trait. |
17
|
|
|
* |
18
|
|
|
* @var array |
19
|
|
|
*/ |
20
|
|
|
protected $metaQueryable = [ |
21
|
|
|
'where', 'whereBetween', 'whereIn', 'whereNull', |
22
|
|
|
'whereDate', 'whereYear', 'whereMonth', 'whereDay', |
23
|
|
|
'orderBy', 'pluck', 'value', 'aggregate', 'lists' |
24
|
|
|
]; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Register hooks for the trait. |
28
|
|
|
* |
29
|
|
|
* @codeCoverageIgnore |
30
|
|
|
* |
31
|
|
|
* @return void |
32
|
|
|
*/ |
33
|
|
View Code Duplication |
public static function bootMetable() |
|
|
|
|
34
|
|
|
{ |
35
|
|
|
$hooks = new Hooks; |
36
|
|
|
|
37
|
|
|
foreach ([ |
38
|
|
|
'setAttribute', |
39
|
|
|
'getAttribute', |
40
|
|
|
'toArray', |
41
|
|
|
'replicate', |
42
|
|
|
'save', |
43
|
|
|
'__isset', |
44
|
|
|
'__unset', |
45
|
|
|
'queryHook', |
46
|
|
|
] as $method) { |
47
|
|
|
static::hook($method, $hooks->{$method}()); |
48
|
|
|
} |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Determine wheter method called on the query is customizable by this trait. |
53
|
|
|
* |
54
|
|
|
* @param string $method |
55
|
|
|
* @return boolean |
56
|
|
|
*/ |
57
|
|
|
protected function isMetaQueryable($method) |
58
|
|
|
{ |
59
|
|
|
return in_array($method, $this->metaQueryable); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Custom query handler for querying meta attributes. |
64
|
|
|
* |
65
|
|
|
* @param \Sofa\Eloquence\Builder $query |
66
|
|
|
* @param string $method |
67
|
|
|
* @param \Sofa\Hookable\Contracts\ArgumentBag $args |
68
|
|
|
* @return mixed |
69
|
|
|
*/ |
70
|
|
|
protected function metaQuery(Builder $query, $method, ArgumentBag $args) |
71
|
|
|
{ |
72
|
|
View Code Duplication |
if (in_array($method, ['pluck', 'value', 'aggregate', 'orderBy', 'lists'])) { |
|
|
|
|
73
|
|
|
return $this->metaJoinQuery($query, $method, $args); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
return $this->metaHasQuery($query, $method, $args); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Adjust meta columns for select statement. |
81
|
|
|
* |
82
|
|
|
* @param \Sofa\Eloquence\Builder $query |
83
|
|
|
* @param \Sofa\Hookable\Contracts\ArgumentBag $args |
84
|
|
|
* @return void |
85
|
|
|
*/ |
86
|
|
|
protected function metaSelect(Builder $query, ArgumentBag $args) |
87
|
|
|
{ |
88
|
|
|
$columns = $args->get('columns'); |
89
|
|
|
|
90
|
|
|
foreach ($columns as $key => $column) { |
91
|
|
|
list($column, $alias) = $this->extractColumnAlias($column); |
|
|
|
|
92
|
|
|
|
93
|
|
|
if ($this->hasColumn($column)) { |
|
|
|
|
94
|
|
|
$select = "{$this->getTable()}.{$column}"; |
|
|
|
|
95
|
|
|
|
96
|
|
|
if ($column !== $alias) { |
97
|
|
|
$select .= " as {$alias}"; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
$columns[$key] = $select; |
101
|
|
|
} elseif (is_string($column) && $column != '*' && strpos($column, '.') === false) { |
102
|
|
|
$table = $this->joinMeta($query, $column); |
103
|
|
|
|
104
|
|
|
$columns[$key] = "{$table}.meta_value as {$alias}"; |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
$args->set('columns', $columns); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Join meta attributes table in order to call provided method. |
113
|
|
|
* |
114
|
|
|
* @param \Sofa\Eloquence\Builder $query |
115
|
|
|
* @param string $method |
116
|
|
|
* @param \Sofa\Hookable\Contracts\ArgumentBag $args |
117
|
|
|
* @return mixed |
118
|
|
|
*/ |
119
|
|
|
protected function metaJoinQuery(Builder $query, $method, ArgumentBag $args) |
120
|
|
|
{ |
121
|
|
|
$alias = $this->joinMeta($query, $args->get('column')); |
122
|
|
|
|
123
|
|
|
// For aggregates we need the actual function name |
124
|
|
|
// so it can be called directly on the builder. |
125
|
|
|
$method = $args->get('function') ?: $method; |
126
|
|
|
|
127
|
|
|
return (in_array($method, ['orderBy', 'lists', 'pluck'])) |
128
|
|
|
? $this->{"{$method}Meta"}($query, $args, $alias) |
129
|
|
|
: $this->metaSingleResult($query, $method, $alias); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Order query by meta attribute. |
134
|
|
|
* |
135
|
|
|
* @param \Sofa\Eloquence\Builder $query |
136
|
|
|
* @param \Sofa\Hookable\Contracts\ArgumentBag $args |
137
|
|
|
* @param string $alias |
138
|
|
|
* @return \Sofa\Eloquence\Builder |
139
|
|
|
*/ |
140
|
|
|
protected function orderByMeta(Builder $query, $args, $alias) |
141
|
|
|
{ |
142
|
|
|
$query->with('metaAttributes')->getQuery()->orderBy("{$alias}.meta_value", $args->get('direction')); |
143
|
|
|
|
144
|
|
|
return $query; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
protected function listsMeta(Builder $query, ArgumentBag $args, $alias) |
148
|
|
|
{ |
149
|
|
|
return $this->pluckMeta($query, $args, $alias); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Get an array with the values of given meta attribute. |
154
|
|
|
* |
155
|
|
|
* @param \Sofa\Eloquence\Builder $query |
156
|
|
|
* @param \Sofa\Hookable\Contracts\ArgumentBag $args |
157
|
|
|
* @param string $alias |
158
|
|
|
* @return array |
159
|
|
|
*/ |
160
|
|
|
protected function pluckMeta(Builder $query, ArgumentBag $args, $alias) |
161
|
|
|
{ |
162
|
|
|
list($column, $key) = [$args->get('column'), $args->get('key')]; |
163
|
|
|
|
164
|
|
|
$query->select("{$alias}.meta_value as {$column}"); |
165
|
|
|
|
166
|
|
|
if (!is_null($key)) { |
167
|
|
|
$this->metaSelectListsKey($query, $key); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
return $query->callParent('pluck', $args->all()); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Add select clause for key of the list array. |
175
|
|
|
* |
176
|
|
|
* @param \Sofa\Eloquence\Builder $query |
177
|
|
|
* @param string $key |
178
|
|
|
* @return \Sofa\Eloquence\Builder |
179
|
|
|
*/ |
180
|
|
|
protected function metaSelectListsKey(Builder $query, $key) |
181
|
|
|
{ |
182
|
|
|
if (strpos($key, '.') !== false) { |
183
|
|
|
return $query->addSelect($key); |
|
|
|
|
184
|
|
|
} elseif ($this->hasColumn($key)) { |
|
|
|
|
185
|
|
|
return $query->addSelect($this->getTable() . '.' . $key); |
|
|
|
|
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
$alias = $this->joinMeta($query, $key); |
189
|
|
|
|
190
|
|
|
return $query->addSelect("{$alias}.meta_value as {$key}"); |
|
|
|
|
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Get single value result from the meta attribute. |
195
|
|
|
* |
196
|
|
|
* @param \Sofa\Eloquence\Builder $query |
197
|
|
|
* @param string $method |
198
|
|
|
* @param string $alias |
199
|
|
|
* @return mixed |
200
|
|
|
*/ |
201
|
|
|
protected function metaSingleResult(Builder $query, $method, $alias) |
202
|
|
|
{ |
203
|
|
|
return $query->getQuery()->select("{$alias}.meta_value")->{$method}("{$alias}.meta_value"); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Join meta attributes table. |
209
|
|
|
* |
210
|
|
|
* @param \Sofa\Eloquence\Builder $query |
211
|
|
|
* @param string $column |
212
|
|
|
* @return string |
213
|
|
|
*/ |
214
|
|
|
protected function joinMeta(Builder $query, $column) |
215
|
|
|
{ |
216
|
|
|
$query->prefixColumnsForJoin(); |
217
|
|
|
|
218
|
|
|
$alias = $this->generateMetaAlias(); |
219
|
|
|
|
220
|
|
|
$table = (new Attribute)->getTable(); |
221
|
|
|
|
222
|
|
|
$query->leftJoin("{$table} as {$alias}", function ($join) use ($alias, $column) { |
|
|
|
|
223
|
|
|
$join->on("{$alias}.metable_id", '=', $this->getQualifiedKeyName()) |
|
|
|
|
224
|
|
|
->where("{$alias}.metable_type", '=', $this->getMorphClass()) |
|
|
|
|
225
|
|
|
->where("{$alias}.meta_key", '=', $column); |
226
|
|
|
}); |
227
|
|
|
|
228
|
|
|
return $alias; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Generate unique alias for meta attributes table. |
233
|
|
|
* |
234
|
|
|
* @return string |
235
|
|
|
*/ |
236
|
|
|
protected function generateMetaAlias() |
237
|
|
|
{ |
238
|
|
|
return md5(microtime(true)) . '_meta_alias'; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Add whereHas subquery on the meta attributes relation. |
243
|
|
|
* |
244
|
|
|
* @param \Sofa\Eloquence\Builder $query |
245
|
|
|
* @param string $method |
246
|
|
|
* @param \Sofa\Hookable\Contracts\ArgumentBag $args |
247
|
|
|
* @return \Sofa\Eloquence\Builder |
248
|
|
|
*/ |
249
|
|
|
protected function metaHasQuery(Builder $query, $method, ArgumentBag $args) |
250
|
|
|
{ |
251
|
|
|
$boolean = $this->getMetaBoolean($args); |
252
|
|
|
|
253
|
|
|
$operator = $this->getMetaOperator($method, $args); |
254
|
|
|
|
255
|
|
|
if (in_array($method, ['whereBetween', 'where'])) { |
256
|
|
|
$this->unbindNumerics($args); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
return $query |
260
|
|
|
->has('metaAttributes', $operator, 1, $boolean, $this->getMetaWhereConstraint($method, $args)) |
261
|
|
|
->with('metaAttributes'); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* Get boolean called on the original method and set it to default. |
266
|
|
|
* |
267
|
|
|
* @param \Sofa\EloquenceArgumentBag $args |
268
|
|
|
* @return string |
269
|
|
|
*/ |
270
|
|
|
protected function getMetaBoolean(ArgumentBag $args) |
271
|
|
|
{ |
272
|
|
|
$boolean = $args->get('boolean'); |
273
|
|
|
|
274
|
|
|
$args->set('boolean', 'and'); |
275
|
|
|
|
276
|
|
|
return $boolean; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* Determine the operator for count relation query. |
281
|
|
|
* |
282
|
|
|
* @param string $method |
283
|
|
|
* @param \Sofa\Hookable\Contracts\ArgumentBag $args |
284
|
|
|
* @return string |
285
|
|
|
*/ |
286
|
|
|
protected function getMetaOperator($method, ArgumentBag $args) |
287
|
|
|
{ |
288
|
|
|
if ($not = $args->get('not')) { |
289
|
|
|
$args->set('not', false); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
return ($not ^ $this->isWhereNull($method, $args)) ? '<' : '>='; |
|
|
|
|
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Integers and floats must be passed in raw form in order to avoid string |
297
|
|
|
* comparison, due to the fact that all meta values are stored as strings. |
298
|
|
|
* |
299
|
|
|
* @param \Sofa\Hookable\Contracts\ArgumentBag $args |
300
|
|
|
* @return void |
301
|
|
|
*/ |
302
|
|
|
protected function unbindNumerics(ArgumentBag $args) |
303
|
|
|
{ |
304
|
|
|
if (($value = $args->get('value')) && (is_int($value) || is_float($value))) { |
305
|
|
|
$args->set('value', $this->raw($value)); |
|
|
|
|
306
|
|
|
} elseif ($values = $args->get('values')) { |
307
|
|
|
foreach ($values as $key => $value) { |
308
|
|
|
if (is_int($value) || is_float($value)) { |
309
|
|
|
$values[$key] = $this->raw($value); |
|
|
|
|
310
|
|
|
} |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
$args->set('values', $values); |
314
|
|
|
} |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* Get the relation constraint closure. |
319
|
|
|
* |
320
|
|
|
* @param string $method |
321
|
|
|
* @param \Sofa\Hookable\Contracts\ArgumentBag $args |
322
|
|
|
* @return \Closure |
323
|
|
|
*/ |
324
|
|
|
protected function getMetaWhereConstraint($method, ArgumentBag $args) |
325
|
|
|
{ |
326
|
|
|
$column = $args->get('column'); |
327
|
|
|
|
328
|
|
|
$args->set('column', 'meta_value'); |
329
|
|
|
|
330
|
|
|
if ($method === 'whereBetween') { |
331
|
|
|
return $this->getMetaBetweenConstraint($column, $args->get('values')); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
return function ($query) use ($column, $method, $args) { |
335
|
|
|
$query->where('meta_key', $column); |
336
|
|
|
|
337
|
|
|
if ($args->get('value') || $args->get('values')) { |
338
|
|
|
call_user_func_array([$query, $method], $args->all()); |
339
|
|
|
} |
340
|
|
|
}; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Query Builder whereBetween override required to pass raw numeric values. |
345
|
|
|
* |
346
|
|
|
* @param string $column |
347
|
|
|
* @param array $values |
348
|
|
|
* @return \Closure |
349
|
|
|
*/ |
350
|
|
|
protected function getMetaBetweenConstraint($column, array $values) |
351
|
|
|
{ |
352
|
|
|
$min = $values[0]; |
353
|
|
|
$max = $values[1]; |
354
|
|
|
|
355
|
|
|
return function ($query) use ($column, $min, $max) { |
356
|
|
|
$query->where('meta_key', $column) |
357
|
|
|
->where('meta_value', '>=', $min) |
358
|
|
|
->where('meta_value', '<=', $max); |
359
|
|
|
}; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* Save new or updated meta attributes and delete the ones that were unset. |
364
|
|
|
* |
365
|
|
|
* @return void |
366
|
|
|
*/ |
367
|
|
|
protected function saveMeta() |
368
|
|
|
{ |
369
|
|
|
foreach ($this->getMetaAttributes() as $attribute) { |
370
|
|
|
if (is_null($attribute->getValue())) { |
371
|
|
|
$attribute->delete(); |
372
|
|
|
} else { |
373
|
|
|
$this->metaAttributes()->save($attribute); |
374
|
|
|
} |
375
|
|
|
} |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* Determine whether meta attribute is allowed for the model. |
380
|
|
|
* |
381
|
|
|
* @param string $key |
382
|
|
|
* @return boolean |
383
|
|
|
*/ |
384
|
|
|
public function allowsMeta($key) |
385
|
|
|
{ |
386
|
|
|
$allowed = $this->getAllowedMeta(); |
387
|
|
|
|
388
|
|
|
return empty($allowed) || in_array($key, $allowed); |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* Determine whether meta attribute exists on the model. |
393
|
|
|
* |
394
|
|
|
* @param string $key |
395
|
|
|
* @return boolean |
396
|
|
|
*/ |
397
|
|
|
public function hasMeta($key) |
398
|
|
|
{ |
399
|
|
|
return array_key_exists($key, $this->getMetaAttributesArray()); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
/** |
403
|
|
|
* Get meta attribute value. |
404
|
|
|
* |
405
|
|
|
* @param string $key |
406
|
|
|
* @return mixed |
407
|
|
|
*/ |
408
|
|
|
public function getMeta($key) |
409
|
|
|
{ |
410
|
|
|
return $this->getMetaAttributes()->getValue($key); |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
/** |
414
|
|
|
* Set meta attribute. |
415
|
|
|
* |
416
|
|
|
* @param string $key |
417
|
|
|
* @param mixed $value |
418
|
|
|
* @return void |
419
|
|
|
*/ |
420
|
|
|
public function setMeta($key, $value) |
421
|
|
|
{ |
422
|
|
|
$this->getMetaAttributes()->set($key, $value); |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* Meta attributes relation. |
427
|
|
|
* |
428
|
|
|
* @codeCoverageIgnore |
429
|
|
|
* |
430
|
|
|
* @return \Illuminate\Database\Eloquent\Relations\MorphMany |
431
|
|
|
*/ |
432
|
|
|
public function metaAttributes() |
433
|
|
|
{ |
434
|
|
|
return $this->morphMany('Sofa\Eloquence\Metable\Attribute', 'metable'); |
|
|
|
|
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* Get meta attributes as collection. |
439
|
|
|
* |
440
|
|
|
* @return \Sofa\Eloquence\Metable\AttributeBag |
441
|
|
|
*/ |
442
|
|
|
public function getMetaAttributes() |
443
|
|
|
{ |
444
|
|
|
$this->loadMetaAttributes(); |
445
|
|
|
|
446
|
|
|
return $this->getRelation('metaAttributes'); |
|
|
|
|
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* Accessor for metaAttributes property |
451
|
|
|
* |
452
|
|
|
* @return \Sofa\Eloquence\Metable\AttributeBag |
453
|
|
|
*/ |
454
|
|
|
public function getMetaAttributesAttribute() |
455
|
|
|
{ |
456
|
|
|
return $this->getMetaAttributes(); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
/** |
460
|
|
|
* Get meta attributes as associative array. |
461
|
|
|
* |
462
|
|
|
* @return array |
463
|
|
|
*/ |
464
|
|
|
public function getMetaAttributesArray() |
465
|
|
|
{ |
466
|
|
|
return $this->getMetaAttributes()->toArray(); |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* Load meta attributes relation. |
471
|
|
|
* |
472
|
|
|
* @return void |
473
|
|
|
*/ |
474
|
|
|
protected function loadMetaAttributes() |
475
|
|
|
{ |
476
|
|
|
if (!array_key_exists('metaAttributes', $this->relations)) { |
|
|
|
|
477
|
|
|
$this->reloadMetaAttributes(); |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
$attributes = $this->getRelation('metaAttributes'); |
|
|
|
|
481
|
|
|
|
482
|
|
|
if (!$attributes instanceof AttributeBag) { |
483
|
|
|
$this->setRelation('metaAttributes', (new Attribute)->newBag($attributes->all())); |
|
|
|
|
484
|
|
|
} |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* Reload meta attributes from db or set empty bag for newly created model. |
489
|
|
|
* |
490
|
|
|
* @return $this |
491
|
|
|
*/ |
492
|
|
|
protected function reloadMetaAttributes() |
493
|
|
|
{ |
494
|
|
|
return ($this->exists) |
|
|
|
|
495
|
|
|
? $this->load('metaAttributes') |
|
|
|
|
496
|
|
|
: $this->setRelation('metaAttributes', (new Attribute)->newBag()); |
|
|
|
|
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
/** |
500
|
|
|
* Get allowed meta attributes array. |
501
|
|
|
* |
502
|
|
|
* @return array |
503
|
|
|
*/ |
504
|
|
|
public function getAllowedMeta() |
505
|
|
|
{ |
506
|
|
|
return (property_exists($this, 'allowedMeta')) ? $this->allowedMeta : []; |
507
|
|
|
} |
508
|
|
|
} |
509
|
|
|
|
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.