Passed
Push — master ( 9607e5...058f8b )
by Wilmer
09:15
created

Sort::getAttributeOrders()   B

Complexity

Conditions 11
Paths 9

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 32.5354

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 15
c 1
b 0
f 0
nc 9
nop 1
dl 0
loc 28
ccs 7
cts 16
cp 0.4375
crap 32.5354
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Data;
6
7
use function array_merge;
8
9
use function explode;
10
use function implode;
11
use function is_array;
12
use function is_iterable;
13
use function is_scalar;
14
use function strncmp;
15
use function substr;
16
use Yiisoft\Db\Exception\InvalidConfigException;
17
18
/**
19
 * Sort represents information relevant to sorting.
20
 *
21
 * When data needs to be sorted according to one or several attributes,
22
 * we can use Sort to represent the sorting information and generate
23
 * appropriate hyperlinks that can lead to sort actions.
24
 *
25
 * A typical usage example is as follows,
26
 *
27
 * ```php
28
 * public function actionIndex()
29
 * {
30
 *     $sort = new Sort();
31
 *
32
 *     $sort->attributes(
33
 *         [
34
 *             'age',
35
 *             'name' => [
36
 *                  'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
37
 *                  'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
38
 *             ]
39
 *         ]
40
 *     )->params(['sort' => 'age,-name'])->enableMultiSort(true);
41
 * }
42
 * ```
43
 *
44
 * In the above, we declare two {@see attributes]] that support sorting: `name` and `age`.
45
 *
46
 * For more details and usage information on Sort, see the [guide article on sorting](guide:output-sorting).
47
 *
48
 * @property array $attributeOrders Sort directions indexed by attribute names. Sort direction can be either `SORT_ASC`
49
 * for ascending order or `SORT_DESC` for descending order. Note that the type of this property differs in getter and
50
 * setter. See {@see getAttributeOrders()} and {@see attributeOrders()} for details.
51
 * @property array $orders The columns (keys) and their corresponding sort directions (values). This can be passed to
52
 * {@see \Yiisoft\Db\Query\Query::orderBy()]] to construct a DB query. This property is read-only.
53
 */
54
final class Sort
55
{
56
    /**
57
     * @var array|null the currently requested sort order as computed by {@see getAttributeOrders}.
58
     */
59
    private ?array $attributeOrders = null;
60
    private bool $enableMultiSort = false;
61
    private array $attributes = [];
62
    private string $sortParam = 'sort';
63
    private array $defaultOrder = [];
64
    private string $separator = ',';
65
    private array $params = [];
66
67
    /**
68
     * @param array $value list of attributes that are allowed to be sorted. Its syntax can be described using the
69
     * following example:
70
     *
71
     * ```php
72
     * [
73
     *     'age',
74
     *     'name' => [
75
     *         'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
76
     *         'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
77
     *         'default' => SORT_DESC,
78
     *         'label' => 'Name',
79
     *     ],
80
     * ]
81
     * ```
82
     *
83
     * In the above, two attributes are declared: `age` and `name`. The `age` attribute is a simple attribute which is
84
     * equivalent to the following:
85
     *
86
     * ```php
87
     * [
88
     *     'age' => [
89
     *         'asc' => ['age' => SORT_ASC],
90
     *         'desc' => ['age' => SORT_DESC],
91
     *     ],
92
     *     'default' => SORT_ASC,
93
     *     'label' => Inflector::camel2words('age'),
94
     * ]
95
     * ```
96
     *
97
     * ```php
98
     * 'name' => [
99
     *     'asc' => '[[last_name]] ASC NULLS FIRST', // PostgreSQL specific feature
100
     *     'desc' => '[[last_name]] DESC NULLS LAST',
101
     * ]
102
     * ```
103
     *
104
     * The `name` attribute is a composite attribute:
105
     *
106
     * - The `name` key represents the attribute name which will appear in the URLs leading to sort actions.
107
     * - The `asc` and `desc` elements specify how to sort by the attribute in ascending and descending orders,
108
     *   respectively. Their values represent the actual columns and the directions by which the data should be sorted
109
     *   by.
110
     * - The `default` element specifies by which direction the attribute should be sorted if it is not currently sorted
111
     *   (the default value is ascending order).
112
     * - The `label` element specifies what label should be used when calling {@see link()} to create a sort link.
113
     *   If not set, {@see Inflector::toWords()} will be called to get a label. Note that it will not be HTML-encoded.
114
     *
115
     * Note that if the Sort object is already created, you can only use the full format to configure every attribute.
116
     * Each attribute must include these elements: `asc` and `desc`.
117
     *
118
     * @return self
119
     */
120 4
    public function attributes(array $value = []): self
121
    {
122 4
        $attributes = [];
123
124 4
        foreach ($value as $name => $attribute) {
125 4
            if (!is_array($attribute)) {
126 4
                $attributes[$attribute] = [
127 4
                    'asc' => [$attribute => SORT_ASC],
128 4
                    'desc' => [$attribute => SORT_DESC],
129
                ];
130
            } elseif (!isset($attribute['asc'], $attribute['desc'])) {
131
                $attributes[$name] = array_merge([
132
                    'asc' => [$name => SORT_ASC],
133
                    'desc' => [$name => SORT_DESC],
134
                ], $attribute);
135
            } else {
136
                $attributes[$name] = $attribute;
137
            }
138
        }
139
140 4
        $this->attributes = $attributes;
141
142 4
        return $this;
143
    }
144
145
    /**
146
     * Sets up the currently sort information.
147
     *
148
     * @param array $attributeOrders sort directions indexed by attribute names. Sort direction can be either `SORT_ASC`
149
     * for ascending order or `SORT_DESC` for descending order.
150
     * @param bool $validate whether to validate given attribute orders against {@see attributes} and {enableMultiSort}.
151
     * If validation is enabled incorrect entries will be removed.
152
     */
153
    public function attributeOrders(array $attributeOrders = [], bool $validate = true): void
154
    {
155
        if ($attributeOrders === [] || !$validate) {
156
            $this->attributeOrders = $attributeOrders;
157
        } else {
158
            $this->attributeOrders = [];
159
            foreach ($attributeOrders as $attribute => $order) {
160
                if (isset($this->attributes[$attribute])) {
161
                    $this->attributeOrders[$attribute] = $order;
162
                    if (!$this->enableMultiSort) {
163
                        break;
164
                    }
165
                }
166
            }
167
        }
168
    }
169
170
    /**
171
     * Creates the sort variable for the specified attribute.
172
     *
173
     * The newly created sort variable can be used to create a URL that will lead to sorting by the specified attribute.
174
     *
175
     * @param string $attribute the attribute name.
176
     *
177
     * @throws InvalidConfigException if the specified attribute is not defined in {@see attributes}
178
     *
179
     * @return string the value of the sort variable.
180
     */
181
    public function createSortParam(string $attribute): string
182
    {
183
        if (!isset($this->attributes[$attribute])) {
184
            throw new InvalidConfigException("Unknown attribute: $attribute");
185
        }
186
187
        $definition = $this->attributes[$attribute];
188
189
        $directions = $this->getAttributeOrders();
190
191
        if (isset($directions[$attribute])) {
192
            $direction = $directions[$attribute] === SORT_DESC ? SORT_ASC : SORT_DESC;
193
            unset($directions[$attribute]);
194
        } else {
195
            $direction = $definition['default'] ?? SORT_ASC;
196
        }
197
198
        if ($this->enableMultiSort) {
199
            $directions = array_merge([$attribute => $direction], $directions);
200
        } else {
201
            $directions = [$attribute => $direction];
202
        }
203
204
        $sorts = [];
205
206
        foreach ($directions as $attribute => $direction) {
207
            $sorts[] = $direction === SORT_DESC ? '-' . $attribute : $attribute;
208
        }
209
210
        return implode($this->separator, $sorts);
211
    }
212
213
    /**
214
     * @param array $value the order that should be used when the current request does not specify any order.
215
     * The array keys are attribute names and the array values are the corresponding sort directions. For example,
216
     *
217
     * ```php
218
     * [
219
     *     'name' => SORT_ASC,
220
     *     'created_at' => SORT_DESC,
221
     * ]
222
     * ```
223
     *
224
     * {@see attributeOrders}
225
     *
226
     * @return $this
227
     */
228 4
    public function defaultOrder(array $value): self
229
    {
230 4
        $this->defaultOrder = $value;
231
232 4
        return $this;
233
    }
234
235
    /**
236
     * @param bool $value whether the sorting can be applied to multiple attributes simultaneously.
237
     *
238
     * Defaults to `false`, which means each time the data can only be sorted by one attribute.
239
     *
240
     * @return self
241
     */
242
    public function enableMultiSort(bool $value): self
243
    {
244
        $this->enableMultiSort = $value;
245
246
        return $this;
247
    }
248
249
    /**
250
     * Returns the sort direction of the specified attribute in the current request.
251
     *
252
     * @param string $attribute the attribute name.
253
     *
254
     * @return int|null Sort direction of the attribute. Can be either `SORT_ASC` for ascending order or `SORT_DESC` for
255
     * descending order. Null is returned if the attribute is invalid or does not need to be sorted.
256
     */
257
    public function getAttributeOrder(string $attribute): ?int
258
    {
259
        $orders = $this->getAttributeOrders();
260
261
        return $orders[$attribute] ?? null;
262
    }
263
264
    /**
265
     * Returns the currently requested sort information.
266
     *
267
     * @param bool $recalculate whether to recalculate the sort directions.
268
     *
269
     * @return array sort directions indexed by attribute names. Sort direction can be either `SORT_ASC` for ascending
270
     * order or `SORT_DESC` for descending order.
271
     */
272 4
    public function getAttributeOrders(bool $recalculate = false): array
273
    {
274 4
        if ($this->attributeOrders === null || $recalculate) {
275 4
            $this->attributeOrders = [];
276
277 4
            if (isset($this->params[$this->sortParam])) {
278
                foreach ($this->parseSortParam($this->params[$this->sortParam]) as $attribute) {
279
                    $descending = false;
280
                    if (strncmp($attribute, '-', 1) === 0) {
281
                        $descending = true;
282
                        $attribute = substr($attribute, 1);
283
                    }
284
285
                    if (isset($this->attributes[$attribute])) {
286
                        $this->attributeOrders[$attribute] = $descending ? SORT_DESC : SORT_ASC;
287
                        if (!$this->enableMultiSort) {
288
                            return $this->attributeOrders;
289
                        }
290
                    }
291
                }
292
            }
293
294 4
            if (empty($this->attributeOrders) && !empty($this->defaultOrder)) {
295 4
                $this->attributeOrders = $this->defaultOrder;
296
            }
297
        }
298
299 4
        return $this->attributeOrders;
300
    }
301
302
    /**
303
     * Returns the columns and their corresponding sort directions.
304
     *
305
     * @param bool $recalculate whether to recalculate the sort directions.
306
     *
307
     * @return array the columns (keys) and their corresponding sort directions (values). This can be passed to
308
     * {@see \Yiisoft\Db\Query\Query::orderBy()} to construct a DB query.
309
     */
310 4
    public function getOrders(bool $recalculate = false): array
311
    {
312 4
        $attributeOrders = $this->getAttributeOrders($recalculate);
313
314 4
        $orders = [];
315
316 4
        foreach ($attributeOrders as $attribute => $direction) {
317 4
            $definition = $this->attributes[$attribute];
318 4
            $columns = $definition[$direction === SORT_ASC ? 'asc' : 'desc'];
319 4
            if (is_iterable($columns)) {
320 4
                foreach ($columns as $name => $dir) {
321 4
                    $orders[$name] = $dir;
322
                }
323
            } else {
324
                $orders[] = $columns;
325
            }
326
        }
327
328 4
        return $orders;
329
    }
330
331
    /**
332
     * Returns a value indicating whether the sort definition supports sorting by the named attribute.
333
     *
334
     * @param string $name the attribute name.
335
     *
336
     * @return bool whether the sort definition supports sorting by the named attribute.
337
     */
338
    public function hasAttribute(string $name): bool
339
    {
340
        return isset($this->attributes[$name]);
341
    }
342
343
    /**
344
     * @param string $value the character used to separate different attributes that need to be sorted by.
345
     *
346
     * @return self
347
     */
348
    public function separator(string $value): self
349
    {
350
        $this->separator = $value;
351
352
        return $this;
353
    }
354
355
    /**
356
     * @param string $value the name of the parameter that specifies which attributes to be sorted in which direction.
357
     * Defaults to `sort`.
358
     *
359
     * {@see params}
360
     *
361
     * @return self
362
     */
363
    public function sortParam(string $value): self
364
    {
365
        $this->sortParam = $value;
366
367
        return $this;
368
    }
369
370
    /**
371
     * @param array $value parameters (name => value) that should be used to obtain the current sort directions and to
372
     * create new sort URLs. If not set, `$_GET` will be used instead.
373
     *
374
     * In order to add hash to all links use `array_merge($_GET, ['#' => 'my-hash'])`.
375
     *
376
     * The array element indexed by {@see sortParam} is considered to be the current sort directions. If the element
377
     * does not exist, the {@see defaultOrder|default order} will be used.
378
     *
379
     * @return self
380
     *
381
     * {@see sortParam}
382
     * {@see defaultOrder}
383
     */
384
    public function params(array $value): self
385
    {
386
        $this->params = $value;
387
388
        return $this;
389
    }
390
391
    /**
392
     * Parses the value of {@see sortParam} into an array of sort attributes.
393
     *
394
     * The format must be the attribute name only for ascending or the attribute name prefixed with `-` for descending.
395
     *
396
     * For example the following return value will result in ascending sort by `category` and descending sort by
397
     * `created_at`:
398
     *
399
     * ```php
400
     * [
401
     *     'category',
402
     *     '-created_at'
403
     * ]
404
     * ```
405
     *
406
     * @param string $param the value of the {@see sortParam}.
407
     *
408
     * @return array the valid sort attributes.
409
     */
410
    protected function parseSortParam(string $param): array
411
    {
412
        return is_scalar($param) ? explode($this->separator, $param) : [];
0 ignored issues
show
introduced by
The condition is_scalar($param) is always true.
Loading history...
413
    }
414
}
415