Completed
Push — master ( 6a0fed...477afe )
by Andrei
08:07
created

IsSortable::scopeSorted()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 9.52
c 0
b 0
f 0
cc 4
nc 4
nop 3
1
<?php
2
3
namespace Neurony\Sort\Traits;
4
5
use Neurony\Sort\Objects\Sort;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Eloquent\Builder;
8
use Neurony\Sort\Exceptions\SortException;
9
use Illuminate\Database\Eloquent\Relations\HasOne;
10
use Illuminate\Database\Eloquent\Relations\BelongsTo;
11
12
trait IsSortable
13
{
14
    /**
15
     * @var array
16
     */
17
    protected $sort = [
18
        /*
19
         * The query builder instance from the Sorted scope.
20
         *
21
         * @var Builder
22
         */
23
        'query' => null,
24
25
        /*
26
         * The data applying the "sorted" scope on a model.
27
         *
28
         * @var array
29
         */
30
        'data' => null,
31
32
        /*
33
         * The Neurony\Sort\Objects\Sort instance.
34
         * This is used to get the sorting rules, just like a request.
35
         *
36
         * @var Sort
37
         */
38
        'instance' => null,
39
40
        /*
41
         * The field to sort by.
42
         *
43
         * @var string
44
         */
45
        'field' => Sort::DEFAULT_SORT_FIELD,
46
47
        /*
48
         * The direction to sort in.
49
         *
50
         * @var string
51
         */
52
        'direction' => Sort::DEFAULT_DIRECTION_FIELD,
53
    ];
54
55
    /**
56
     * The filter scope.
57
     * Should be called on the model when building the query.
58
     *
59
     * @param Builder $query
60
     * @param array $data
61
     * @param Sort $sort
62
     */
63
    public function scopeSorted($query, array $data, Sort $sort = null)
64
    {
65
        $this->sort['query'] = $query;
66
        $this->sort['data'] = $data;
67
        $this->sort['instance'] = $sort;
68
69
        $this->setFieldToSortBy();
70
        $this->setDirectionToSortIn();
71
72
        if ($this->isValidSort()) {
73
            $this->checkSortingDirection();
74
75
            switch ($this->sort['data'][$this->sort['direction']]) {
76
                case Sort::DIRECTION_RANDOM:
77
                    $this->sort['query']->inRandomOrder();
78
                    break;
79
                default:
80
                    if ($this->shouldSortByRelation()) {
81
                        $this->sortByRelation();
82
                    } else {
83
                        $this->sortNormally();
84
                    }
85
            }
86
        }
87
    }
88
89
    /**
90
     * Verify if all sorting conditions are met.
91
     *
92
     * @return bool
93
     */
94
    protected function isValidSort()
95
    {
96
        return
97
            isset($this->sort['data'][$this->sort['field']]) &&
98
            isset($this->sort['data'][$this->sort['direction']]);
99
    }
100
101
    /**
102
     * Set the sort field if an Neurony\Sort\Objects\Sort instance has been provided as a parameter for the sorted scope.
103
     *
104
     * @return void
105
     */
106
    protected function setFieldToSortBy()
107
    {
108
        if ($this->sort['instance'] instanceof Sort) {
109
            $this->sort['field'] = $this->sort['instance']->field();
110
        }
111
    }
112
113
    /**
114
     * Set the sort direction if an Neurony\Sort\Objects\Sort instance has been provided as a parameter for the sorted scope.
115
     *
116
     * @return void
117
     */
118
    protected function setDirectionToSortIn()
119
    {
120
        if ($this->sort['instance'] instanceof Sort) {
121
            $this->sort['direction'] = $this->sort['instance']->direction();
122
        }
123
    }
124
125
    /**
126
     * Sort model records using columns from the model's table itself.
127
     *
128
     * @return void
129
     */
130
    protected function sortNormally()
131
    {
132
        $this->sort['query']->orderBy(
133
            $this->sort['data'][$this->sort['field']],
134
            $this->sort['data'][$this->sort['direction']]
135
        );
136
    }
137
138
    /**
139
     * Sort model records using columns from the model relation's table.
140
     *
141
     * @return void
142
     */
143
    protected function sortByRelation()
144
    {
145
        $parts = explode('.', $this->sort['data'][$this->sort['field']]);
146
        $models = [];
147
148
        if (count($parts) > 2) {
149
            $field = array_pop($parts);
150
            $relations = $parts;
151
        } else {
152
            $field = array_last($parts);
153
            $relations = (array) array_first($parts);
154
        }
155
156
        foreach ($relations as $index => $relation) {
157
            $previousModel = $this;
158
159
            if (isset($models[$index - 1])) {
160
                $previousModel = $models[$index - 1];
161
            }
162
163
            $this->checkRelationToSortBy($previousModel, $relation);
164
165
            $models[] = $previousModel->{$relation}()->getModel();
166
167
            $modelTable = $previousModel->getTable();
168
            $relationTable = $previousModel->{$relation}()->getModel()->getTable();
169
            $foreignKey = $previousModel->{$relation}() instanceof HasOne ?
170
                $previousModel->{$relation}()->getForeignKeyName() :
171
                $previousModel->{$relation}()->getForeignKey();
172
173
            if (! $this->alreadyJoinedForSorting($relationTable)) {
174
                switch (get_class($previousModel->{$relation}())) {
175 View Code Duplication
                    case BelongsTo::class:
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...
176
                        $this->sort['query']->join($relationTable, $modelTable.'.'.$foreignKey, '=', $relationTable.'.id');
177
                        break;
178 View Code Duplication
                    case HasOne::class:
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...
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().'.*',
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
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