• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tempestphp / tempest-framework / 11295726163

11 Oct 2024 04:14PM UTC coverage: 82.143% (+0.009%) from 82.134%
11295726163

Pull #566

github

web-flow
Merge 07fdb0e06 into c1233aa56
Pull Request #566: feat: add `isList()` and `isAssoc()` methods in ArrayHelper for array type checking

3 of 3 new or added lines in 1 file covered. (100.0%)

9 existing lines in 1 file now uncovered.

6762 of 8232 relevant lines covered (82.14%)

38.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.66
/src/Tempest/Support/src/ArrayHelper.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Support;
6

7
use ArrayAccess;
8
use Closure;
9
use Countable;
10
use Generator;
11
use InvalidArgumentException;
12
use Iterator;
13
use Random\Randomizer;
14
use Serializable;
15
use Stringable;
16
use function Tempest\map;
17

18
/**
19
 * @template TKey of array-key
20
 * @template TValue
21
 *
22
 * @implements ArrayAccess<TKey, TValue>
23
 * @implements Iterator<TKey, TValue>
24
 */
25
final class ArrayHelper implements Iterator, ArrayAccess, Serializable, Countable
26
{
27
    use IsIterable;
28

29
    /**
30
     * The underlying array.
31
     *
32
     * @var array<TKey, TValue>
33
     */
34
    private array $array;
35

36
    /**
37
     * @param array<TKey, TValue>|self<TKey, TValue>|TValue $input
38
     */
39
    public function __construct(
375✔
40
        mixed $input = [],
41
    ) {
42
        if (is_array($input)) {
375✔
43
            $this->array = $input;
365✔
44
        } elseif ($input instanceof self) {
69✔
45
            $this->array = $input->array;
7✔
46
        } else {
47
            $this->array = [$input];
69✔
48
        }
49
    }
50

51
    /**
52
     * Determines if the array is a list.
53
     *
54
     * An array is a list if its keys consist of consecutive numbers.
55
     *
56
     * @return bool
57
     */
58
    public function isList(): bool
2✔
59
    {
60
        return array_is_list($this->array);
2✔
61
    }
62

63
    /**
64
     * Determines if the array is an associative.
65
     *
66
     * An array is associative if its keys doesn't consist of consecutive numbers.
67
     *
68
     * @return bool
69
     */
70
    public function isAssoc(): bool
1✔
71
    {
72
        return !$this->isList();
1✔
73
    }
74

75
    /**
76
     * Get one or a specified number of random values from the array.
77
     *
78
     * @param int $number The number of random values to get.
79
     * @param bool $preserveKey Whether to preserve the keys of the original array. ( won't work if $number is 1 as it will return a single value )
80
     *
81
     * @return self<TKey, TValue>|mixed The random values or single value if $number is 1.
82
     */
83
    public function random(int $number = 1, bool $preserveKey = false): mixed
5✔
84
    {
85
        $count = count($this->array);
5✔
86

87
        if ($number > $count) {
5✔
88
            throw new InvalidArgumentException("Cannot retrive {$number} items from an array of {$count} items.");
2✔
89
        }
90

91
        if ($number < 1) {
3✔
92
            throw new InvalidArgumentException("Random value only accepts positive integers, {$number} requested.");
1✔
93
        }
94

95
        $keys = (new Randomizer())->pickArrayKeys($this->array, $number);
2✔
96

97
        $randomValues = [];
2✔
98
        foreach ($keys as $key) {
2✔
99
            $preserveKey
2✔
100
                ? $randomValues[$key] = $this->array[$key]
1✔
101
                : $randomValues[] = $this->array[$key];
1✔
102
        }
103

104
        if ($preserveKey === false) {
2✔
105
            shuffle($randomValues);
1✔
106
        }
107

108
        return count($randomValues) > 1
2✔
109
            ? new self($randomValues)
2✔
110
            : $randomValues[0];
2✔
111
    }
112

113
    /**
114
     * Retrieve values from a given key in each sub-array of the current array.
115
     * Optionally, you can pass a second parameter to also get the keys following the same pattern.
116
     *
117
     * @param string $value The key to assign the values from, support dot notation.
118
     * @param string|null $key The key to assign the keys from, support dot notation.
119
     *
120
     * @return self<TKey, TValue>
121
     */
122
    public function pluck(string $value, ?string $key = null): self
3✔
123
    {
124
        $results = [];
3✔
125

126
        foreach ($this->array as $item) {
3✔
127
            if (! is_array($item)) {
3✔
128
                continue;
1✔
129
            }
130

131
            $itemValue = arr($item)->get($value);
2✔
132

133
            /**
134
             * Perform basic pluck if no key is given.
135
             * Otherwise, also pluck the key as well.
136
             */
137
            if (is_null($key)) {
2✔
138
                $results[] = $itemValue;
2✔
139
            } else {
140
                $itemKey = arr($item)->get($key);
2✔
141
                $results[$itemKey] = $itemValue;
2✔
142
            }
143
        }
144

145
        return new self($results);
3✔
146
    }
147

148
    /**
149
     * @alias of add.
150
     */
151
    public function push(mixed $value): self
1✔
152
    {
153
        return $this->add($value);
1✔
154
    }
155

156
    /**
157
     * Add an item at the end of the array.
158
     *
159
     *
160
     * @return self<TKey, TValue>
161
     */
162
    public function add(mixed $value): self
2✔
163
    {
164
        $this->array[] = $value;
2✔
165

166
        return $this;
2✔
167
    }
168

169
    /**
170
     * Pad the array to the specified size with a value.
171
     *
172
     *
173
     * @return self<TKey, TValue>
174
     */
175
    public function pad(int $size, mixed $value): self
1✔
176
    {
177
        return new self(array_pad($this->array, $size, $value));
1✔
178
    }
179

180
    /**
181
     * Reverse the keys and values of the array.
182
     *
183
     * @return self<TValue&array-key, TKey>
184
     */
185
    public function flip(): self
1✔
186
    {
187
        return new self(array_flip($this->array));
1✔
188
    }
189

190
    /**
191
     * Keep only the unique items in the array.
192
     *
193
     * @param string|null $key The key to use as the uniqueness criteria in nested arrays.
194
     * @param bool $should_be_strict Whether the comparison should be strict, only used when giving a key parameter.
195
     *
196
     * @return self<TKey, TValue>
197
     */
198
    public function unique(?string $key = null, bool $should_be_strict = false): self
9✔
199
    {
200
        if (is_null($key) && $should_be_strict === false) {
9✔
201
            return new self(array_unique($this->array, flags: SORT_REGULAR));
4✔
202
        }
203

204
        $uniqueItems = [];
5✔
205
        $uniqueFilteredValues = [];
5✔
206
        foreach ($this->array as $item) {
5✔
207
            // Ensure we don't check raw values with key filter
208
            if (! is_null($key) && ! is_array($item)) {
5✔
209
                continue;
1✔
210
            }
211

212
            $filterValue = is_array($item)
5✔
213
                ? arr($item)->get($key)
4✔
214
                : $item;
1✔
215

216
            if (is_null($filterValue)) {
5✔
UNCOV
217
                continue;
×
218
            }
219

220
            if (in_array($filterValue, $uniqueFilteredValues, strict: $should_be_strict)) {
5✔
221
                continue;
4✔
222
            }
223

224
            $uniqueItems[] = $item;
5✔
225
            $uniqueFilteredValues[] = $filterValue;
5✔
226
        }
227

228
        return new self($uniqueItems);
5✔
229
    }
230

231
    /**
232
     * Keep only the items that are not present in any of the given arrays.
233
     *
234
     * @param array<TKey, TValue>|self<TKey, TValue> ...$arrays
235
     *
236
     * @return self<TKey, TValue>
237
     */
238
    public function diff(array|self ...$arrays): self
1✔
239
    {
240
        $arrays = array_map(fn (array|self $array) => $array instanceof self ? $array->toArray() : $array, $arrays);
1✔
241

242
        return new self(array_diff($this->array, ...$arrays));
1✔
243
    }
244

245
    /**
246
     * Keep only the items whose keys are not present in any of the given arrays.
247
     *
248
     * @param array<TKey, TValue>|self<TKey, TValue> ...$arrays
249
     *
250
     * @return self<TKey, TValue>
251
     */
252
    public function diffKeys(array|self ...$arrays): self
1✔
253
    {
254
        $arrays = array_map(fn (array|self $array) => $array instanceof self ? $array->toArray() : $array, $arrays);
1✔
255

256
        return new self(array_diff_key($this->array, ...$arrays));
1✔
257
    }
258

259
    /**
260
     * Keep only the items that are present in all of the given arrays.
261
     *
262
     * @param array<TKey, TValue>|self<TKey, TValue> ...$arrays
263
     *
264
     * @return self<TKey, TValue>
265
     */
266
    public function intersect(array|self ...$arrays): self
1✔
267
    {
268
        $arrays = array_map(fn (array|self $array) => $array instanceof self ? $array->toArray() : $array, $arrays);
1✔
269

270
        return new self(array_intersect($this->array, ...$arrays));
1✔
271
    }
272

273
    /**
274
     * Keep only the items whose keys are present in all of the given arrays.
275
     *
276
     * @param array<TKey, TValue>|self<TKey, TValue> ...$arrays
277
     *
278
     * @return self<TKey, TValue>
279
     */
280
    public function intersectKeys(array|self ...$arrays): self
1✔
281
    {
282
        $arrays = array_map(fn (array|self $array) => $array instanceof self ? $array->toArray() : $array, $arrays);
1✔
283

284
        return new self(array_intersect_key($this->array, ...$arrays));
1✔
285
    }
286

287
    /**
288
     * Merge the array with the given arrays.
289
     *
290
     * @param array<TKey, TValue>|self<TKey, TValue> ...$arrays The arrays to merge.
291
     *
292
     * @return self<TKey, TValue>
293
     */
294
    public function merge(array|self ...$arrays): self
2✔
295
    {
296
        $arrays = array_map(fn (array|self $array) => $array instanceof self ? $array->toArray() : $array, $arrays);
2✔
297

298
        return new self(array_merge($this->array, ...$arrays));
2✔
299
    }
300

301
    /**
302
     * Create a new array with this current array values as keys and the given values as values.
303
     *
304
     * @template TCombineValue
305
     *
306
     * @param array<array-key, TCombineValue>|self<array-key, TCombineValue> $values
307
     *
308
     * @return self<array-key, TCombineValue>
309
     */
310
    public function combine(array|self $values): self
4✔
311
    {
312
        $values = $values instanceof self
4✔
313
            ? $values->toArray()
1✔
314
            : $values;
3✔
315

316
        return new self(array_combine($this->array, $values));
4✔
317
    }
318

319
    public static function explode(string|Stringable $string, string $separator = ' '): self
2✔
320
    {
321
        if ($separator === '') {
2✔
322
            return new self([(string) $string]);
1✔
323
        }
324

325
        return new self(explode($separator, (string) $string));
2✔
326
    }
327

328
    public function equals(array|self $other): bool
13✔
329
    {
330
        $other = is_array($other) ? $other : $other->array;
13✔
331

332
        return $this->array === $other;
13✔
333
    }
334

335
    /** @param Closure(mixed $value, mixed $key): bool $filter */
336
    public function first(?Closure $filter = null): mixed
8✔
337
    {
338
        if ($filter === null) {
8✔
339
            return $this->array[array_key_first($this->array)];
2✔
340
        }
341

342
        foreach ($this as $key => $value) {
6✔
343
            if ($filter($value, $key)) {
6✔
344
                return $value;
6✔
345
            }
346
        }
347

348
        return null;
6✔
349
    }
350

351
    /** @param Closure(mixed $value, mixed $key): bool $filter */
352
    public function last(?Closure $filter = null): mixed
10✔
353
    {
354
        if ($filter === null) {
10✔
355
            return $this->array[array_key_last($this->array)];
10✔
356
        }
357

UNCOV
358
        foreach ($this->reverse() as $key => $value) {
×
UNCOV
359
            if ($filter($value, $key)) {
×
360
                return $value;
×
361
            }
362
        }
363

UNCOV
364
        return null;
×
365
    }
366

367
    /** @param mixed $value The popped value will be stored in this variable */
368
    public function pop(mixed &$value): self
9✔
369
    {
370
        $value = $this->last();
9✔
371

372
        return new self(array_slice($this->array, 0, -1));
9✔
373
    }
374

375
    /** @param mixed $value The unshifted value will be stored in this variable */
376
    public function unshift(mixed &$value): self
1✔
377
    {
378
        $value = $this->first();
1✔
379

380
        return new self(array_slice($this->array, 1));
1✔
381
    }
382

383
    public function reverse(): self
1✔
384
    {
385
        return new self(array_reverse($this->array));
1✔
386
    }
387

388
    public function isEmpty(): bool
9✔
389
    {
390
        return empty($this->array);
9✔
391
    }
392

393
    public function isNotEmpty(): bool
8✔
394
    {
395
        return ! $this->isEmpty();
8✔
396
    }
397

398
    public function implode(string $glue): StringHelper
51✔
399
    {
400
        return str(implode($glue, $this->array));
51✔
401
    }
402

403
    /**
404
     * Create a new array with the keys of this array as values.
405
     *
406
     * @return self<array-key, TKey>
407
     */
408
    public function keys(): self
1✔
409
    {
410
        return new self(array_keys($this->array));
1✔
411
    }
412

413
    public function values(): self
289✔
414
    {
415
        return new self(array_values($this->array));
289✔
416
    }
417

418
    /** @param null|Closure(mixed $value, mixed $key): bool $filter */
419
    public function filter(?Closure $filter = null): self
1✔
420
    {
421
        $array = [];
1✔
422
        $filter ??= static fn (mixed $value, mixed $_) => ! in_array($value, [false, null], strict: true);
1✔
423

424
        foreach ($this->array as $key => $value) {
1✔
425
            if ($filter($value, $key)) {
1✔
426
                $array[$key] = $value;
1✔
427
            }
428
        }
429

430
        return new self($array);
1✔
431
    }
432

433
    /** @param Closure(mixed $value, mixed $key): void $each */
434
    public function each(Closure $each): self
53✔
435
    {
436
        foreach ($this as $key => $value) {
53✔
437
            $each($value, $key);
51✔
438
        }
439

440
        return $this;
53✔
441
    }
442

443
    /** @param Closure(mixed $value, mixed $key): mixed $map */
444
    public function map(Closure $map): self
282✔
445
    {
446
        $array = [];
282✔
447

448
        foreach ($this->array as $key => $value) {
282✔
449
            $array[$key] = $map($value, $key);
282✔
450
        }
451

452
        return new self($array);
282✔
453
    }
454

455
    /** @param Closure(mixed $value, mixed $key): Generator $map */
456
    public function mapWithKeys(Closure $map): self
3✔
457
    {
458
        $array = [];
3✔
459

460
        foreach ($this->array as $key => $value) {
3✔
461
            $generator = $map($value, $key);
3✔
462

463
            if (! $generator instanceof Generator) {
3✔
464
                throw new InvalidMapWithKeysUsage();
1✔
465
            }
466

467
            $array[$generator->key()] = $generator->current();
2✔
468
        }
469

470
        return new self($array);
2✔
471
    }
472

473
    /** @return mixed|ArrayHelper */
474
    public function get(string $key, mixed $default = null): mixed
292✔
475
    {
476
        $value = $this->array;
292✔
477

478
        $keys = explode('.', $key);
292✔
479

480
        foreach ($keys as $key) {
292✔
481
            if (! isset($value[$key])) {
292✔
482
                return $default;
12✔
483
            }
484

485
            $value = $value[$key];
290✔
486
        }
487

488
        if (is_array($value)) {
290✔
489
            return new self($value);
280✔
490
        }
491

492
        return $value;
14✔
493
    }
494

495
    public function has(string $key): bool
1✔
496
    {
497
        $array = $this->array;
1✔
498

499
        $keys = explode('.', $key);
1✔
500

501
        foreach ($keys as $key) {
1✔
502
            if (! isset($array[$key])) {
1✔
503
                return false;
1✔
504
            }
505

506
            $array = &$array[$key];
1✔
507
        }
508

509
        return true;
1✔
510
    }
511

512
    public function contains(mixed $search): bool
1✔
513
    {
514
        return $this->first(fn ($value) => $value === $search) !== null;
1✔
515
    }
516

517
    public function set(string $key, mixed $value): self
2✔
518
    {
519
        $array = $this->array;
2✔
520

521
        $current = &$array;
2✔
522

523
        $keys = explode('.', $key);
2✔
524

525
        foreach ($keys as $i => $key) {
2✔
526
            // If this is the last key in dot notation, we don't
527
            // need to go through the next steps.
528
            if (count($keys) === 1) {
2✔
529
                break;
2✔
530
            }
531

532
            // Remove the current key from our keys array
533
            // so that later we can use the first value
534
            // from that array as our key.
535
            unset($keys[$i]);
2✔
536

537
            // If we know this key is not an array, make it one.
538
            if (! isset($current[$key]) || ! is_array($current[$key])) {
2✔
539
                $current[$key] = [];
2✔
540
            }
541

542
            // Set the context to this key.
543
            $current = &$current[$key];
2✔
544
        }
545

546
        // Pull the first key out of the array
547
        // and use it to set the value.
548
        $current[array_shift($keys)] = $value;
2✔
549

550
        return new self($array);
2✔
551
    }
552

553
    /**
554
     * @alias self::set()
555
     */
556
    public function put(string $key, mixed $value): self
1✔
557
    {
558
        return $this->set($key, $value);
1✔
559
    }
560

561
    public function unwrap(): self
86✔
562
    {
563
        $unwrapValue = function (string|int $key, mixed $value) {
86✔
564
            if (is_int($key)) {
82✔
UNCOV
565
                return [$key => $value];
×
566
            }
567

568
            $keys = explode('.', $key);
82✔
569

570
            for ($i = array_key_last($keys); $i >= 0; $i--) {
82✔
571
                $currentKey = $keys[$i];
82✔
572

573
                $value = [$currentKey => $value];
82✔
574
            }
575

576
            return $value;
82✔
577
        };
86✔
578

579
        $array = [];
86✔
580

581
        foreach ($this->array as $key => $value) {
86✔
582
            $array = array_merge_recursive($array, $unwrapValue($key, $value));
82✔
583
        }
584

585
        return new self($array);
86✔
586
    }
587

588
    public function dump(mixed ...$dumps): self
×
589
    {
UNCOV
590
        lw($this->array, ...$dumps); // @phpstan-ignore-line
×
591

UNCOV
592
        return $this;
×
593
    }
594

UNCOV
595
    public function dd(mixed ...$dd): void
×
596
    {
UNCOV
597
        ld($this->array, ...$dd); // @phpstan-ignore-line
×
598
    }
599

600
    /**
601
     * @return array<TKey, TValue>
602
     */
603
    public function toArray(): array
313✔
604
    {
605
        return $this->array;
313✔
606
    }
607

608
    /**
609
     * @template T
610
     * @param class-string<T> $to
611
     * @return self<T>
612
     */
613
    public function mapTo(string $to): self
1✔
614
    {
615
        return new self(map($this->array)->collection()->to($to));
1✔
616
    }
617
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc