1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Neurony\Sort\Traits; |
4
|
|
|
|
5
|
|
|
use Illuminate\Support\Arr; |
6
|
|
|
use Illuminate\Support\Str; |
7
|
|
|
use Neurony\Sort\Objects\Sort; |
8
|
|
|
use Illuminate\Database\Eloquent\Model; |
9
|
|
|
use Illuminate\Database\Eloquent\Builder; |
10
|
|
|
use Neurony\Sort\Exceptions\SortException; |
11
|
|
|
use Illuminate\Database\Eloquent\Relations\HasOne; |
12
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo; |
13
|
|
|
|
14
|
|
|
trait IsSortable |
15
|
|
|
{ |
16
|
|
|
/** |
17
|
|
|
* @var array |
18
|
|
|
*/ |
19
|
|
|
protected $sort = [ |
20
|
|
|
/* |
21
|
|
|
* The query builder instance from the Sorted scope. |
22
|
|
|
* |
23
|
|
|
* @var Builder |
24
|
|
|
*/ |
25
|
|
|
'query' => null, |
26
|
|
|
|
27
|
|
|
/* |
28
|
|
|
* The data applying the "sorted" scope on a model. |
29
|
|
|
* |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
'data' => null, |
33
|
|
|
|
34
|
|
|
/* |
35
|
|
|
* The Neurony\Sort\Objects\Sort instance. |
36
|
|
|
* This is used to get the sorting rules, just like a request. |
37
|
|
|
* |
38
|
|
|
* @var Sort |
39
|
|
|
*/ |
40
|
|
|
'instance' => null, |
41
|
|
|
|
42
|
|
|
/* |
43
|
|
|
* The field to sort by. |
44
|
|
|
* |
45
|
|
|
* @var string |
46
|
|
|
*/ |
47
|
|
|
'field' => Sort::DEFAULT_SORT_FIELD, |
48
|
|
|
|
49
|
|
|
/* |
50
|
|
|
* The direction to sort in. |
51
|
|
|
* |
52
|
|
|
* @var string |
53
|
|
|
*/ |
54
|
|
|
'direction' => Sort::DEFAULT_DIRECTION_FIELD, |
55
|
|
|
]; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* The filter scope. |
59
|
|
|
* Should be called on the model when building the query. |
60
|
|
|
* |
61
|
|
|
* @param Builder $query |
62
|
|
|
* @param array $data |
63
|
|
|
* @param Sort $sort |
64
|
|
|
*/ |
65
|
|
|
public function scopeSorted($query, array $data, Sort $sort = null) |
66
|
|
|
{ |
67
|
|
|
$this->sort['query'] = $query; |
68
|
|
|
$this->sort['data'] = $data; |
69
|
|
|
$this->sort['instance'] = $sort; |
70
|
|
|
|
71
|
|
|
$this->setFieldToSortBy(); |
72
|
|
|
$this->setDirectionToSortIn(); |
73
|
|
|
|
74
|
|
|
if ($this->isValidSort()) { |
75
|
|
|
$this->checkSortingDirection(); |
76
|
|
|
|
77
|
|
|
switch ($this->sort['data'][$this->sort['direction']]) { |
78
|
|
|
case Sort::DIRECTION_RANDOM: |
79
|
|
|
$this->sort['query']->inRandomOrder(); |
80
|
|
|
break; |
81
|
|
|
default: |
82
|
|
|
if ($this->shouldSortByRelation()) { |
83
|
|
|
$this->sortByRelation(); |
84
|
|
|
} else { |
85
|
|
|
$this->sortNormally(); |
86
|
|
|
} |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Verify if all sorting conditions are met. |
93
|
|
|
* |
94
|
|
|
* @return bool |
95
|
|
|
*/ |
96
|
|
|
protected function isValidSort() |
97
|
|
|
{ |
98
|
|
|
return |
99
|
|
|
isset($this->sort['data'][$this->sort['field']]) && |
100
|
|
|
isset($this->sort['data'][$this->sort['direction']]); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Set the sort field if an Neurony\Sort\Objects\Sort instance has been provided as a parameter for the sorted scope. |
105
|
|
|
* |
106
|
|
|
* @return void |
107
|
|
|
*/ |
108
|
|
|
protected function setFieldToSortBy() |
109
|
|
|
{ |
110
|
|
|
if ($this->sort['instance'] instanceof Sort) { |
111
|
|
|
$this->sort['field'] = $this->sort['instance']->field(); |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Set the sort direction if an Neurony\Sort\Objects\Sort instance has been provided as a parameter for the sorted scope. |
117
|
|
|
* |
118
|
|
|
* @return void |
119
|
|
|
*/ |
120
|
|
|
protected function setDirectionToSortIn() |
121
|
|
|
{ |
122
|
|
|
if ($this->sort['instance'] instanceof Sort) { |
123
|
|
|
$this->sort['direction'] = $this->sort['instance']->direction(); |
124
|
|
|
} |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Sort model records using columns from the model's table itself. |
129
|
|
|
* |
130
|
|
|
* @return void |
131
|
|
|
*/ |
132
|
|
|
protected function sortNormally() |
133
|
|
|
{ |
134
|
|
|
$this->sort['query']->orderBy( |
135
|
|
|
$this->sort['data'][$this->sort['field']], |
136
|
|
|
$this->sort['data'][$this->sort['direction']] |
137
|
|
|
); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Sort model records using columns from the model relation's table. |
142
|
|
|
* |
143
|
|
|
* @return void |
144
|
|
|
*/ |
145
|
|
|
protected function sortByRelation() |
146
|
|
|
{ |
147
|
|
|
$parts = explode('.', $this->sort['data'][$this->sort['field']]); |
148
|
|
|
$models = []; |
149
|
|
|
|
150
|
|
|
if (count($parts) > 2) { |
151
|
|
|
$field = array_pop($parts); |
152
|
|
|
$relations = $parts; |
153
|
|
|
} else { |
154
|
|
|
$field = Arr::last($parts); |
155
|
|
|
$relations = (array) Arr::first($parts); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
foreach ($relations as $index => $relation) { |
159
|
|
|
$previousModel = $this; |
160
|
|
|
|
161
|
|
|
if (isset($models[$index - 1])) { |
162
|
|
|
$previousModel = $models[$index - 1]; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
$this->checkRelationToSortBy($previousModel, $relation); |
166
|
|
|
|
167
|
|
|
$models[] = $previousModel->{$relation}()->getModel(); |
168
|
|
|
|
169
|
|
|
$modelTable = $previousModel->getTable(); |
170
|
|
|
$relationTable = $previousModel->{$relation}()->getModel()->getTable(); |
171
|
|
|
$foreignKey = $previousModel->{$relation}()->getForeignKeyName(); |
172
|
|
|
|
173
|
|
|
if (! $this->alreadyJoinedForSorting($relationTable)) { |
174
|
|
|
switch (get_class($previousModel->{$relation}())) { |
175
|
|
View Code Duplication |
case BelongsTo::class: |
|
|
|
|
176
|
|
|
$this->sort['query']->join($relationTable, $modelTable.'.'.$foreignKey, '=', $relationTable.'.id'); |
177
|
|
|
break; |
178
|
|
View Code Duplication |
case HasOne::class: |
|
|
|
|
179
|
|
|
$this->sort['query']->join($relationTable, $modelTable.'.id', '=', $relationTable.'.'.$foreignKey); |
180
|
|
|
break; |
181
|
|
|
} |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
$alias = implode('_', $relations).'_'.$field; |
186
|
|
|
|
187
|
|
|
if (isset($relationTable)) { |
188
|
|
|
$this->sort['query']->addSelect([ |
189
|
|
|
$this->getTable().'.*', |
|
|
|
|
190
|
|
|
$relationTable.'.'.$field.' AS '.$alias, |
191
|
|
|
]); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
$this->sort['query']->orderBy( |
195
|
|
|
$alias, $this->sort['data'][$this->sort['direction']] |
196
|
|
|
); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* @return bool |
201
|
|
|
*/ |
202
|
|
|
protected function shouldSortByRelation() |
203
|
|
|
{ |
204
|
|
|
return Str::contains($this->sort['data'][$this->sort['field']], '.'); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Verify if the desired join exists already, possibly included by a global scope. |
209
|
|
|
* |
210
|
|
|
* @param string $table |
211
|
|
|
* |
212
|
|
|
* @return bool |
213
|
|
|
*/ |
214
|
|
|
protected function alreadyJoinedForSorting($table) |
215
|
|
|
{ |
216
|
|
|
return Str::contains(strtolower($this->sort['query']->toSql()), 'join `'.$table.'`'); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* Verify if the direction provided matches one of the directions from: |
221
|
|
|
* Neurony\Sort\Objects\Sort::$directions. |
222
|
|
|
* |
223
|
|
|
* @return void |
224
|
|
|
*/ |
225
|
|
|
protected function checkSortingDirection() |
226
|
|
|
{ |
227
|
|
|
if (! in_array(strtolower($this->sort['data'][$this->sort['direction']]), array_map('strtolower', Sort::$directions))) { |
228
|
|
|
throw SortException::invalidDirectionSupplied($this->sort['data'][$this->sort['direction']]); |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Verify if the desired relation to sort by is one of: HasOne or BelongsTo. |
234
|
|
|
* Sorting by "many" relations or "morph" ones is not possible. |
235
|
|
|
* |
236
|
|
|
* @param Model $model |
237
|
|
|
* @param string $relation |
238
|
|
|
*/ |
239
|
|
|
protected function checkRelationToSortBy(Model $model, $relation) |
240
|
|
|
{ |
241
|
|
|
if (! ($model->{$relation}() instanceof HasOne) && ! ($model->{$relation}() instanceof BelongsTo)) { |
242
|
|
|
throw SortException::wrongRelationToSort($relation, get_class($model->{$relation}())); |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
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.