Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/**
4
 * This file is part of the ramsey/collection library
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 *
9
 * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
10
 * @license http://opensource.org/licenses/MIT MIT
11
 */
12
 
13
declare(strict_types=1);
14
 
15
namespace Ramsey\Collection;
16
 
17
use Closure;
18
use Ramsey\Collection\Exception\CollectionMismatchException;
19
use Ramsey\Collection\Exception\InvalidArgumentException;
20
use Ramsey\Collection\Exception\InvalidPropertyOrMethod;
21
use Ramsey\Collection\Exception\NoSuchElementException;
22
use Ramsey\Collection\Exception\UnsupportedOperationException;
23
use Ramsey\Collection\Tool\TypeTrait;
24
use Ramsey\Collection\Tool\ValueExtractorTrait;
25
use Ramsey\Collection\Tool\ValueToStringTrait;
26
 
27
use function array_filter;
28
use function array_key_first;
29
use function array_key_last;
30
use function array_map;
31
use function array_merge;
32
use function array_reduce;
33
use function array_search;
34
use function array_udiff;
35
use function array_uintersect;
36
use function in_array;
37
use function is_int;
38
use function is_object;
39
use function spl_object_id;
40
use function sprintf;
41
use function usort;
42
 
43
/**
44
 * This class provides a basic implementation of `CollectionInterface`, to
45
 * minimize the effort required to implement this interface
46
 *
47
 * @template T
48
 * @extends AbstractArray<T>
49
 * @implements CollectionInterface<T>
50
 */
51
abstract class AbstractCollection extends AbstractArray implements CollectionInterface
52
{
53
    use TypeTrait;
54
    use ValueToStringTrait;
55
    use ValueExtractorTrait;
56
 
57
    /**
58
     * @throws InvalidArgumentException if $element is of the wrong type.
59
     */
60
    public function add(mixed $element): bool
61
    {
62
        $this[] = $element;
63
 
64
        return true;
65
    }
66
 
67
    public function contains(mixed $element, bool $strict = true): bool
68
    {
69
        return in_array($element, $this->data, $strict);
70
    }
71
 
72
    /**
73
     * @throws InvalidArgumentException if $element is of the wrong type.
74
     */
75
    public function offsetSet(mixed $offset, mixed $value): void
76
    {
77
        if ($this->checkType($this->getType(), $value) === false) {
78
            throw new InvalidArgumentException(
79
                'Value must be of type ' . $this->getType() . '; value is '
80
                . $this->toolValueToString($value),
81
            );
82
        }
83
 
84
        if ($offset === null) {
85
            $this->data[] = $value;
86
        } else {
87
            $this->data[$offset] = $value;
88
        }
89
    }
90
 
91
    public function remove(mixed $element): bool
92
    {
93
        if (($position = array_search($element, $this->data, true)) !== false) {
94
            unset($this[$position]);
95
 
96
            return true;
97
        }
98
 
99
        return false;
100
    }
101
 
102
    /**
103
     * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist
104
     *     on the elements in this collection.
105
     * @throws UnsupportedOperationException if unable to call column() on this
106
     *     collection.
107
     *
108
     * @inheritDoc
109
     */
110
    public function column(string $propertyOrMethod): array
111
    {
112
        $temp = [];
113
 
114
        foreach ($this->data as $item) {
115
            /** @psalm-suppress MixedAssignment */
116
            $temp[] = $this->extractValue($item, $propertyOrMethod);
117
        }
118
 
119
        return $temp;
120
    }
121
 
122
    /**
123
     * @return T
124
     *
125
     * @throws NoSuchElementException if this collection is empty.
126
     */
127
    public function first(): mixed
128
    {
129
        $firstIndex = array_key_first($this->data);
130
 
131
        if ($firstIndex === null) {
132
            throw new NoSuchElementException('Can\'t determine first item. Collection is empty');
133
        }
134
 
135
        return $this->data[$firstIndex];
136
    }
137
 
138
    /**
139
     * @return T
140
     *
141
     * @throws NoSuchElementException if this collection is empty.
142
     */
143
    public function last(): mixed
144
    {
145
        $lastIndex = array_key_last($this->data);
146
 
147
        if ($lastIndex === null) {
148
            throw new NoSuchElementException('Can\'t determine last item. Collection is empty');
149
        }
150
 
151
        return $this->data[$lastIndex];
152
    }
153
 
154
    /**
155
     * @return CollectionInterface<T>
156
     *
157
     * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist
158
     *     on the elements in this collection.
159
     * @throws UnsupportedOperationException if unable to call sort() on this
160
     *     collection.
161
     */
162
    public function sort(?string $propertyOrMethod = null, Sort $order = Sort::Ascending): CollectionInterface
163
    {
164
        $collection = clone $this;
165
 
166
        usort(
167
            $collection->data,
168
            /**
169
             * @param T $a
170
             * @param T $b
171
             */
172
            function (mixed $a, mixed $b) use ($propertyOrMethod, $order): int {
173
                /** @var mixed $aValue */
174
                $aValue = $this->extractValue($a, $propertyOrMethod);
175
 
176
                /** @var mixed $bValue */
177
                $bValue = $this->extractValue($b, $propertyOrMethod);
178
 
179
                return ($aValue <=> $bValue) * ($order === Sort::Descending ? -1 : 1);
180
            },
181
        );
182
 
183
        return $collection;
184
    }
185
 
186
    /**
187
     * @param callable(T): bool $callback A callable to use for filtering elements.
188
     *
189
     * @return CollectionInterface<T>
190
     */
191
    public function filter(callable $callback): CollectionInterface
192
    {
193
        $collection = clone $this;
194
        $collection->data = array_merge([], array_filter($collection->data, $callback));
195
 
196
        return $collection;
197
    }
198
 
199
    /**
200
     * @return CollectionInterface<T>
201
     *
202
     * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist
203
     *     on the elements in this collection.
204
     * @throws UnsupportedOperationException if unable to call where() on this
205
     *     collection.
206
     */
207
    public function where(?string $propertyOrMethod, mixed $value): CollectionInterface
208
    {
209
        return $this->filter(
210
            /**
211
             * @param T $item
212
             */
213
            function (mixed $item) use ($propertyOrMethod, $value): bool {
214
                /** @var mixed $accessorValue */
215
                $accessorValue = $this->extractValue($item, $propertyOrMethod);
216
 
217
                return $accessorValue === $value;
218
            },
219
        );
220
    }
221
 
222
    /**
223
     * @param callable(T): TCallbackReturn $callback A callable to apply to each
224
     *     item of the collection.
225
     *
226
     * @return CollectionInterface<TCallbackReturn>
227
     *
228
     * @template TCallbackReturn
229
     */
230
    public function map(callable $callback): CollectionInterface
231
    {
232
        /** @var Collection<TCallbackReturn> */
233
        return new Collection('mixed', array_map($callback, $this->data));
234
    }
235
 
236
    /**
237
     * @param callable(TCarry, T): TCarry $callback A callable to apply to each
238
     *     item of the collection to reduce it to a single value.
239
     * @param TCarry $initial This is the initial value provided to the callback.
240
     *
241
     * @return TCarry
242
     *
243
     * @template TCarry
244
     */
245
    public function reduce(callable $callback, mixed $initial): mixed
246
    {
247
        /** @var TCarry */
248
        return array_reduce($this->data, $callback, $initial);
249
    }
250
 
251
    /**
252
     * @param CollectionInterface<T> $other The collection to check for divergent
253
     *     items.
254
     *
255
     * @return CollectionInterface<T>
256
     *
257
     * @throws CollectionMismatchException if the compared collections are of
258
     *     differing types.
259
     */
260
    public function diff(CollectionInterface $other): CollectionInterface
261
    {
262
        $this->compareCollectionTypes($other);
263
 
264
        $diffAtoB = array_udiff($this->data, $other->toArray(), $this->getComparator());
265
        $diffBtoA = array_udiff($other->toArray(), $this->data, $this->getComparator());
266
 
267
        /** @var array<array-key, T> $diff */
268
        $diff = array_merge($diffAtoB, $diffBtoA);
269
 
270
        $collection = clone $this;
271
        $collection->data = $diff;
272
 
273
        return $collection;
274
    }
275
 
276
    /**
277
     * @param CollectionInterface<T> $other The collection to check for
278
     *     intersecting items.
279
     *
280
     * @return CollectionInterface<T>
281
     *
282
     * @throws CollectionMismatchException if the compared collections are of
283
     *     differing types.
284
     */
285
    public function intersect(CollectionInterface $other): CollectionInterface
286
    {
287
        $this->compareCollectionTypes($other);
288
 
289
        /** @var array<array-key, T> $intersect */
290
        $intersect = array_uintersect($this->data, $other->toArray(), $this->getComparator());
291
 
292
        $collection = clone $this;
293
        $collection->data = $intersect;
294
 
295
        return $collection;
296
    }
297
 
298
    /**
299
     * @param CollectionInterface<T> ...$collections The collections to merge.
300
     *
301
     * @return CollectionInterface<T>
302
     *
303
     * @throws CollectionMismatchException if unable to merge any of the given
304
     *     collections or items within the given collections due to type
305
     *     mismatch errors.
306
     */
307
    public function merge(CollectionInterface ...$collections): CollectionInterface
308
    {
309
        $mergedCollection = clone $this;
310
 
311
        foreach ($collections as $index => $collection) {
312
            if (!$collection instanceof static) {
313
                throw new CollectionMismatchException(
314
                    sprintf('Collection with index %d must be of type %s', $index, static::class),
315
                );
316
            }
317
 
318
            // When using generics (Collection.php, Set.php, etc),
319
            // we also need to make sure that the internal types match each other
320
            if ($this->getUniformType($collection) !== $this->getUniformType($this)) {
321
                throw new CollectionMismatchException(
322
                    sprintf(
323
                        'Collection items in collection with index %d must be of type %s',
324
                        $index,
325
                        $this->getType(),
326
                    ),
327
                );
328
            }
329
 
330
            foreach ($collection as $key => $value) {
331
                if (is_int($key)) {
332
                    $mergedCollection[] = $value;
333
                } else {
334
                    $mergedCollection[$key] = $value;
335
                }
336
            }
337
        }
338
 
339
        return $mergedCollection;
340
    }
341
 
342
    /**
343
     * @param CollectionInterface<T> $other
344
     *
345
     * @throws CollectionMismatchException
346
     */
347
    private function compareCollectionTypes(CollectionInterface $other): void
348
    {
349
        if (!$other instanceof static) {
350
            throw new CollectionMismatchException('Collection must be of type ' . static::class);
351
        }
352
 
353
        // When using generics (Collection.php, Set.php, etc),
354
        // we also need to make sure that the internal types match each other
355
        if ($this->getUniformType($other) !== $this->getUniformType($this)) {
356
            throw new CollectionMismatchException('Collection items must be of type ' . $this->getType());
357
        }
358
    }
359
 
360
    private function getComparator(): Closure
361
    {
362
        return /**
363
             * @param T $a
364
             * @param T $b
365
             */
366
            function (mixed $a, mixed $b): int {
367
                // If the two values are object, we convert them to unique scalars.
368
                // If the collection contains mixed values (unlikely) where some are objects
369
                // and some are not, we leave them as they are.
370
                // The comparator should still work and the result of $a < $b should
371
                // be consistent but unpredictable since not documented.
372
                if (is_object($a) && is_object($b)) {
373
                    $a = spl_object_id($a);
374
                    $b = spl_object_id($b);
375
                }
376
 
377
                return $a === $b ? 0 : ($a < $b ? 1 : -1);
378
            };
379
    }
380
 
381
    /**
382
     * @param CollectionInterface<mixed> $collection
383
     */
384
    private function getUniformType(CollectionInterface $collection): string
385
    {
386
        return match ($collection->getType()) {
387
            'integer' => 'int',
388
            'boolean' => 'bool',
389
            'double' => 'float',
390
            default => $collection->getType(),
391
        };
392
    }
393
}