Passed
Push — master ( ced247...8df200 )
by Alexander
02:13
created

Sort::only()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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