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