Passed
Push — master ( 474c12...0c0121 )
by Alexander
12:53
created

Sort::withoutDefaultSorting()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Data\Reader;
6
7
use InvalidArgumentException;
8
9
use function array_key_exists;
10
use function array_merge;
11
use function implode;
12
use function is_array;
13
use function is_int;
14
use function is_string;
15
use function preg_split;
16
use function strpos;
17
use function substr;
18
use function trim;
19
20
/**
21
 * Sort represents data sorting settings:
22
 *
23
 * - A config with a map of logical field => real fields along with their order. The config also contains default
24
 *   order for each logical field.
25
 * - Currently specified logical fields order such as field1 => asc, field2 => desc. Usually it is passed directly
26
 *   from end user.
27
 *
28
 * Logical fields are the ones user operates with. Real fields are the ones actually present in a data set.
29
 * Such a mapping helps when you need to sort by a single logical field that, in fact, consists of multiple fields
30
 * in underlying the data set. For example, we provide a user with a username which consists of first name and last name
31
 * fields in actual data set.
32
 *
33
 * Based on the settings, the class can produce a criteria to be applied to {@see SortableDataInterface}
34
 * when obtaining the data i.e. a list of real fields along with their order directions.
35
 *
36
 * There are two modes of forming a criteria available:
37
 *
38
 * - {@see Sort::only()} ignores user-specified order for logical fields that have no configuration.
39
 * - {@see Sort::any()} uses user-specified logical field name and order directly for fields that have no configuration.
40
 *
41
 * @psalm-type TOrder = array<string, "asc"|"desc">
42
 * @psalm-type TSortFieldItem = array<string, int>
43
 * @psalm-type TConfigItem = array{asc: TSortFieldItem, desc: TSortFieldItem, default: "asc"|"desc"}
44
 * @psalm-type TConfig = array<string, TConfigItem>
45
 * @psalm-type TUserConfig = array<int, string>|array<string, array<string, int|string>>
46
 */
47
final class Sort
48
{
49
    /**
50
     * Logical fields config.
51
     *
52
     * @psalm-var TConfig
53
     */
54
    private array $config;
55
56
    /**
57
     * @var bool Whether to ignore logical fields not present in the config when forming criteria.
58
     */
59
    private bool $ignoreExtraFields;
60
61
    /**
62
     * @var bool Whether to add default sorting when forming criteria.
63
     */
64
    private bool $withDefaultSorting = true;
65
66
    /**
67
     * @var array Logical fields to order by in form of [name => direction].
68
     * @psalm-var TOrder
69
     */
70
    private array $currentOrder = [];
71
72
    /**
73
     * @psalm-param TUserConfig $config
74
     *
75
     * @param array $config Logical fields config.
76
     * @param bool $ignoreExtraFields Whether to ignore logical fields not present in the config when forming criteria.
77
     */
78 64
    private function __construct(bool $ignoreExtraFields, array $config)
79
    {
80 64
        $this->ignoreExtraFields = $ignoreExtraFields;
81 64
        $normalizedConfig = [];
82
83 64
        foreach ($config as $fieldName => $fieldConfig) {
84
            if (
85 58
                !(is_int($fieldName) && is_string($fieldConfig))
86 58
                && !(is_string($fieldName) && is_array($fieldConfig))
87
            ) {
88 2
                throw new InvalidArgumentException('Invalid config format.');
89
            }
90
91 56
            if (is_string($fieldConfig)) {
92 52
                $fieldName = $fieldConfig;
93 52
                $fieldConfig = [];
94
            }
95
96
            /** @psalm-var TConfig $fieldConfig */
97 56
            $normalizedConfig[$fieldName] = array_merge([
98 56
                'asc' => [$fieldName => SORT_ASC],
99 56
                'desc' => [$fieldName => SORT_DESC],
100
                'default' => 'asc',
101
            ], $fieldConfig);
102
        }
103
104
        /** @psalm-var TConfig $normalizedConfig */
105 62
        $this->config = $normalizedConfig;
106
    }
107
108
    /**
109
     * Create a sort instance that ignores current order for extra logical fields that have no configuration.
110
     *
111
     * @psalm-param TUserConfig $config
112
     *
113
     * @param array $config Logical fields config.
114
     * ```php
115
     * [
116
     *     'age', // means will be sorted as is
117
     *     'name' => [
118
     *         'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
119
     *         'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
120
     *         'default' => 'desc',
121
     *     ],
122
     * ]
123
     * ```
124
     *
125
     * In the above, two fields are declared: `age` and `name`. The `age` field is
126
     * a simple field which is equivalent to the following:
127
     *
128
     * ```php
129
     * 'age' => [
130
     *     'asc' => ['age' => SORT_ASC],
131
     *     'desc' => ['age' => SORT_DESC],
132
     *     'default' => 'asc',
133
     * ]
134
     * ```
135
     *
136
     * The name field is a virtual field name that consists of two real fields, `first_name` and `last_name`. Virtual
137
     * field name is used in order string or order array while real fields are used in final sorting criteria.
138
     *
139
     * Each configuration has the following options:
140
     *
141
     * - `asc` - criteria for ascending sorting.
142
     * - `desc` - criteria for descending sorting.
143
     * - `default` - default sorting. Could be either `asc` or `desc`. If not specified, `asc` is used.
144
     *
145
     * @return self
146
     */
147 58
    public static function only(array $config): self
148
    {
149 58
        return new self(true, $config);
150
    }
151
152
    /**
153
     * Create a sort instance that uses logical field itself and direction provided when there is no configuration.
154
     *
155
     * @psalm-param TUserConfig $config
156
     *
157
     * @param array $config Logical fields config.
158
     * ```php
159
     * [
160
     *     'age', // means will be sorted as is
161
     *     'name' => [
162
     *         'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
163
     *         'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
164
     *         'default' => 'desc',
165
     *     ],
166
     * ]
167
     * ```
168
     *
169
     * In the above, two fields are declared: `age` and `name`. The `age` field is
170
     * a simple field which is equivalent to the following:
171
     *
172
     * ```php
173
     * 'age' => [
174
     *     'asc' => ['age' => SORT_ASC],
175
     *     'desc' => ['age' => SORT_DESC],
176
     *     'default' => 'asc',
177
     * ]
178
     * ```
179
     *
180
     * The name field is a virtual field name that consists of two real fields, `first_name` and `last_name`. Virtual
181
     * field name is used in order string or order array while real fields are used in final sorting criteria.
182
     *
183
     * Each configuration has the following options:
184
     *
185
     * - `asc` - criteria for ascending sorting.
186
     * - `desc` - criteria for descending sorting.
187
     * - `default` - default sorting. Could be either `asc` or `desc`. If not specified, `asc` is used.
188
     *
189
     * @return self
190
     */
191 7
    public static function any(array $config = []): self
192
    {
193 7
        return new self(false, $config);
194
    }
195
196
    /**
197
     * Change current logical fields order based on order string.
198
     *
199
     * The string consists of comma-separated field names.
200
     * If the name is prefixed with `-`, field order is descending.
201
     * Otherwise, the order is ascending.
202
     *
203
     * @param string $orderString Logical fields order as comma-separated string.
204
     *
205
     * @return self
206
     */
207 42
    public function withOrderString(string $orderString): self
208
    {
209 42
        $order = [];
210 42
        $parts = preg_split('/\s*,\s*/', trim($orderString), -1, PREG_SPLIT_NO_EMPTY);
211
212 42
        foreach ($parts as $part) {
213 42
            if (strpos($part, '-') === 0) {
214 2
                $order[substr($part, 1)] = 'desc';
215
            } else {
216 42
                $order[$part] = 'asc';
217
            }
218
        }
219
220 42
        return $this->withOrder($order);
221
    }
222
223
    /**
224
     * Change current logical fields order.
225
     *
226
     * @param array $order A map with logical field names to order by as keys, direction as values.
227
     * @psalm-param TOrder $order
228
     *
229
     * @return self
230
     */
231 59
    public function withOrder(array $order): self
232
    {
233 59
        $new = clone $this;
234 59
        $new->currentOrder = $order;
235 59
        return $new;
236
    }
237
238
    /**
239
     * Formation of criteria without default sorting.
240
     *
241
     * @return self
242
     */
243 1
    public function withoutDefaultSorting(): self
244
    {
245 1
        $new = clone $this;
246 1
        $new->withDefaultSorting = false;
247 1
        return $new;
248
    }
249
250
    /**
251
     * @psalm-return TOrder
252
     */
253 41
    public function getOrder(): array
254
    {
255 41
        return $this->currentOrder;
256
    }
257
258
    /**
259
     * Get an order string based on current logical fields order.
260
     *
261
     * The string consists of comma-separated field names.
262
     * If the name is prefixed with `-`, field order is descending.
263
     * Otherwise, the order is ascending.
264
     *
265
     * @return string An order string.
266
     */
267 1
    public function getOrderAsString(): string
268
    {
269 1
        $parts = [];
270
271 1
        foreach ($this->currentOrder as $field => $direction) {
272 1
            $parts[] = ($direction === 'desc' ? '-' : '') . $field;
273
        }
274
275 1
        return implode(',', $parts);
276
    }
277
278
    /**
279
     * A sorting criteria to be applied to {@see SortableDataInterface}
280
     * when obtaining the data i.e. a list of real fields along with their order directions.
281
     *
282
     * @psalm-return array<string, "asc"|"desc"|int>
283
     */
284 47
    public function getCriteria(): array
285
    {
286 47
        $criteria = [];
287 47
        $config = $this->config;
288
289 47
        foreach ($this->currentOrder as $field => $direction) {
290 45
            $fieldExists = array_key_exists($field, $config);
291
292 45
            if (!$fieldExists) {
293 4
                if ($this->ignoreExtraFields) {
294 2
                    continue;
295
                }
296
297 2
                $criteria += [$field => $direction];
298
            } else {
299 43
                $criteria += $config[$field][$direction];
300 43
                unset($config[$field]);
301
            }
302
        }
303
304 47
        if ($this->withDefaultSorting) {
305 46
            foreach ($config as $fieldConfig) {
306 30
                $criteria += $fieldConfig[$fieldConfig['default']];
307
            }
308
        }
309
310 47
        return $criteria;
311
    }
312
}
313