Passed
Push — 6.0 ( 70657e...b737f6 )
by Olivier
11:54
created

ErrorCollection::to_array()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ICanBoogie;
13
14
use ArrayAccess;
15
use Countable;
16
use InvalidArgumentException;
17
use IteratorAggregate;
18
use JsonSerializable;
19
use Throwable;
20
use Traversable;
21
22
use function get_debug_type;
23
24
/**
25
 * An error collection.
26
 *
27
 * @implements ArrayAccess<string, Error[]>
28
 * @implements IteratorAggregate<string, Error>
29
 */
30
class ErrorCollection implements ArrayAccess, IteratorAggregate, Countable, JsonSerializable, ToArray
31
{
32
    /**
33
     * Special identifier used when an error is not associated with a specific attribute.
34
     */
35
    public const GENERIC = '__generic__';
36
37
    /**
38
     * @var array<string, Error[]>
39
     */
40
    private array $collection = [];
41
42
    /**
43
     * Add an error associated with an attribute.
44
     *
45
     * @param string $attribute Attribute name.
46
     * @param Throwable|bool|string|Error $error_or_format_or_true A {@link Error} instance or
47
     * a format to create that instance, or `true`.
48
     * @param array<int|string, mixed> $args Only used if `$error_or_format_or_true` is not a {@link Error}
49
     * instance or `true`.
50
     *
51
     * @return $this
52
     */
53
    public function add(
54
        string $attribute,
55
        Throwable|bool|string|Error $error_or_format_or_true = true,
56
        array $args = []
57
    ): static {
58
        $this->assert_valid_error($error_or_format_or_true);
59
60
        $this->collection[$attribute][] = $this
61
            ->ensure_error_instance($error_or_format_or_true, $args);
62
63
        return $this;
64
    }
65
66
    /**
67
     * Add an error not associated with any attribute.
68
     *
69
     * @param Throwable|bool|string|Error $error_or_format_or_true A {@link Error} instance or
70
     * a format to create that instance, or `true`.
71
     * @param array<int|string, mixed> $args Only used if `$error_or_format_or_true` is not a {@link Error}
72
     * instance or `true`.
73
     *
74
     * @return $this
75
     */
76
    public function add_generic(
77
        Throwable|bool|string|Error $error_or_format_or_true = true,
78
        array $args = []
79
    ): static {
80
        return $this->add(self::GENERIC, $error_or_format_or_true, $args);
81
    }
82
83
    /**
84
     * Asserts that the error type is valid.
85
     *
86
     * @param mixed $error_or_format_or_true
87
     */
88
    private function assert_valid_error(mixed $error_or_format_or_true): void
89
    {
90
        if (
91
            $error_or_format_or_true === true
92
            || is_string($error_or_format_or_true)
93
            || $error_or_format_or_true instanceof Error
94
            || $error_or_format_or_true instanceof Throwable
95
        ) {
96
            return;
97
        }
98
99
        throw new InvalidArgumentException(sprintf(
100
            "\$error_or_format_or_true must be a an instance of `%s`, a string, or true. Given: `%s`",
101
            Error::class,
102
            get_debug_type($error_or_format_or_true)
103
        ));
104
    }
105
106
    /**
107
     * Ensures a {@link Error} instance.
108
     *
109
     * @param Throwable|bool|string|Error $error_or_format_or_true
110
     * @param array<int|string, mixed> $args
111
     */
112
    private function ensure_error_instance(
113
        Throwable|bool|string|Error $error_or_format_or_true,
114
        array $args = []
115
    ): Error {
116
        $error = $error_or_format_or_true;
117
118
        if (!$error instanceof Error) {
119
            $error = new Error($error === true ? "" : (string) $error, $args);
120
        }
121
122
        return $error;
123
    }
124
125
    /**
126
     * Adds an error.
127
     *
128
     * @param string|null $offset An attribute name or `null` for _generic_.
129
     * @param Throwable|Error|string|bool $value An error.
130
     *
131
     * @see add()
132
     *
133
     * @phpstan-ignore-next-line
134
     */
135
    public function offsetSet($offset, $value): void
136
    {
137
        $this->add($offset ?? self::GENERIC, $value);
138
    }
139
140
    /**
141
     * Clears the errors of an attribute.
142
     *
143
     * @param string|null $offset An attribute name or `null` for _generic_.
144
     */
145
    public function offsetUnset($offset): void
146
    {
147
        unset($this->collection[$offset ?? self::GENERIC]);
148
    }
149
150
    /**
151
     * Checks if an error is defined for an attribute.
152
     *
153
     * ```php
154
     * <?php
155
     *
156
     * use ICanBoogie\ErrorCollection
157
     *
158
     * $errors = new ErrorCollection;
159
     * isset($errors['username']);
160
     * // false
161
     * $errors->add('username');
162
     * isset($errors['username']);
163
     * // true
164
     * ```
165
     *
166
     * @param string|null $offset An attribute name or `null` for _generic_.
167
     */
168
    public function offsetExists($offset): bool
169
    {
170
        return isset($this->collection[$offset ?? self::GENERIC]);
171
    }
172
173
    /**
174
     * Returns errors associated with an attribute.
175
     *
176
     * ```php
177
     * <?php
178
     *
179
     * use ICanBoogie\ErrorCollection;
180
     *
181
     * $errors = new ErrorCollection;
182
     * $errors['password']
183
     * // []
184
     * $errors->add('password')
185
     * // [ Message ]
186
     * ```
187
     *
188
     * @param string|null $offset An attribute name or `null` for _generic_.
189
     *
190
     * @return Error[]
191
     */
192
    public function offsetGet($offset): array
193
    {
194
        if (!$this->offsetExists($offset)) {
195
            return [];
196
        }
197
198
        return $this->collection[$offset ?? self::GENERIC];
199
    }
200
201
    /**
202
     * Clears errors.
203
     *
204
     * @return $this
205
     */
206
    public function clear(): static
207
    {
208
        $this->collection = [];
209
210
        return $this;
211
    }
212
213
    /**
214
     * Merges with another error collection.
215
     */
216
    public function merge(ErrorCollection $collection): void
217
    {
218
        foreach ($collection as $attribute => $error) {
219
            $this->add($attribute, $error);
220
        }
221
    }
222
223
    /**
224
     * @return Traversable<string, Error>
225
     */
226
    public function getIterator(): Traversable
227
    {
228
        foreach ($this->to_array() as $attribute => $errors) {
229
            foreach ($errors as $error) {
230
                yield $attribute => $error;
231
            }
232
        }
233
    }
234
235
    /**
236
     * Iterates through errors using a callback.
237
     *
238
     * ```php
239
     * <?php
240
     *
241
     * use ICanBoogie\ErrorCollection;
242
     *
243
     * $errors = new ErrorCollection;
244
     * $errors->add('username', "Funny user name");
245
     * $errors->add('password', "Weak password");
246
     *
247
     * $errors->each(function ($error, $attribute, $errors) {
248
     *
249
     *     echo "$attribute => $error\n";
250
     *
251
     * });
252
     * ```
253
     *
254
     * @param callable(Error, string $attribute, ErrorCollection): void $callback
255
     *     Function to execute for each element, taking three arguments:
256
     *
257
     *     - `Error $error`: The current error.
258
     *     - `string $attribute`: The attribute or {@link self::GENERIC}.
259
     *     - `ErrorCollection $collection`: This instance.
260
     */
261
    public function each(callable $callback): void
262
    {
263
        foreach ($this as $attribute => $error) {
264
            $callback($error, $attribute, $this);
265
        }
266
    }
267
268
    /**
269
     * Returns the total number of errors.
270
     *
271
     * @inheritDoc
272
     */
273
    public function count(): int
274
    {
275
        return count($this->collection, COUNT_RECURSIVE) - count($this->collection);
276
    }
277
278
    /**
279
     * @return array<string, Error[]>
280
     */
281
    public function jsonSerialize(): array
282
    {
283
        return $this->to_array();
284
    }
285
286
    /**
287
     * Converts the object into an array.
288
     *
289
     * @return array<string, Error[]>
290
     */
291
    public function to_array(): array
292
    {
293
        return array_filter(array_merge([ self::GENERIC => [] ], $this->collection));
294
    }
295
}
296