IsSortable::checkSortingDirection()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
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:
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