Passed
Pull Request — master (#178)
by Wilmer
12:09
created

Sort::sortParam()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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