Passed
Pull Request — master (#179)
by Wilmer
12:49
created

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