Skip to content

Commit bdf5efc

Browse files
authored
feat(support): add more methods to ArrayHelper and StringHelper (#721)
1 parent 6847a79 commit bdf5efc

File tree

5 files changed

+330
-11
lines changed

5 files changed

+330
-11
lines changed

src/Tempest/Support/src/ArrayHelper.php

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public function __construct(
3939
$this->array = $input;
4040
} elseif ($input instanceof self) {
4141
$this->array = $input->array;
42+
} elseif ($input === null) {
43+
$this->array = [];
4244
} else {
4345
$this->array = [$input];
4446
}
@@ -432,6 +434,10 @@ public static function explode(string|Stringable $string, string $separator = '
432434
return new self([(string) $string]);
433435
}
434436

437+
if ((string) $string === '') {
438+
return new self();
439+
}
440+
435441
return new self(explode($separator, (string) $string));
436442
}
437443

@@ -449,7 +455,9 @@ public function equals(array|self $other): bool
449455
* Returns the first item in the instance that matches the given `$filter`.
450456
* If `$filter` is `null`, returns the first item.
451457
*
452-
* @param Closure(mixed $value, mixed $key): bool $filter
458+
* @param null|Closure(TValue $value, TKey $key): bool $filter
459+
*
460+
* @return TValue
453461
*/
454462
public function first(?Closure $filter = null): mixed
455463
{
@@ -474,7 +482,9 @@ public function first(?Closure $filter = null): mixed
474482
* Returns the last item in the instance that matches the given `$filter`.
475483
* If `$filter` is `null`, returns the last item.
476484
*
477-
* @param Closure(mixed $value, mixed $key): bool $filter
485+
* @param null|Closure(TValue $value, TKey $key): bool $filter
486+
*
487+
* @return TValue
478488
*/
479489
public function last(?Closure $filter = null): mixed
480490
{
@@ -608,7 +618,11 @@ public function each(Closure $each): self
608618
/**
609619
* Returns a new instance of the array, with each item transformed by the given callback.
610620
*
611-
* @param Closure(mixed $value, mixed $key): mixed $map
621+
* @template TMapValue
622+
*
623+
* @param Closure(TValue, TKey): TMapValue $map
624+
*
625+
* @return static<TKey, TMapValue>
612626
*/
613627
public function map(Closure $map): self
614628
{
@@ -654,11 +668,13 @@ public function mapWithKeys(Closure $map): self
654668
*
655669
* @return mixed|ArrayHelper
656670
*/
657-
public function get(string $key, mixed $default = null): mixed
671+
public function get(int|string $key, mixed $default = null): mixed
658672
{
659673
$value = $this->array;
660674

661-
$keys = explode('.', $key);
675+
$keys = is_int($key)
676+
? [$key]
677+
: explode('.', $key);
662678

663679
foreach ($keys as $key) {
664680
if (! isset($value[$key])) {
@@ -678,11 +694,13 @@ public function get(string $key, mixed $default = null): mixed
678694
/**
679695
* Asserts whether a value identified by the specified `$key` exists.
680696
*/
681-
public function has(string $key): bool
697+
public function has(int|string $key): bool
682698
{
683699
$array = $this->array;
684700

685-
$keys = explode('.', $key);
701+
$keys = is_int($key)
702+
? [$key]
703+
: explode('.', $key);
686704

687705
foreach ($keys as $key) {
688706
if (! isset($array[$key])) {
@@ -802,6 +820,51 @@ public function join(string $glue = ', ', ?string $finalGlue = ' and '): StringH
802820
return str($last);
803821
}
804822

823+
/**
824+
* Flattens the instance to a single-level array, or until the specified `$depth` is reached.
825+
*
826+
* ### Example
827+
* ```php
828+
* arr(['foo', ['bar', 'baz']])->flatten(); // ['foo', 'bar', 'baz']
829+
* ```
830+
*/
831+
public function flatten(int|float $depth = INF): self
832+
{
833+
$result = [];
834+
835+
foreach ($this->array as $item) {
836+
if (! is_array($item)) {
837+
$result[] = $item;
838+
839+
continue;
840+
}
841+
842+
$values = $depth === 1
843+
? array_values($item)
844+
: arr($item)->flatten($depth - 1);
845+
846+
foreach ($values as $value) {
847+
$result[] = $value;
848+
}
849+
}
850+
851+
return new self($result);
852+
}
853+
854+
/**
855+
* Returns a new instance of the array, with each item transformed by the given callback, then flattens it by the specified depth.
856+
*
857+
* @template TMapValue
858+
*
859+
* @param Closure(TValue, TKey): TMapValue[] $map
860+
*
861+
* @return static<TKey, TMapValue>
862+
*/
863+
public function flatMap(Closure $map, int|float $depth = 1): self
864+
{
865+
return $this->map($map)->flatten($depth);
866+
}
867+
805868
/**
806869
* Dumps the instance.
807870
*/

src/Tempest/Support/src/StringHelper.php

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414

1515
final readonly class StringHelper implements Stringable
1616
{
17-
public function __construct(
18-
private string $string = '',
19-
) {
17+
private string $string;
18+
19+
public function __construct(?string $string = '')
20+
{
21+
$this->string = $string ?? '';
2022
}
2123

2224
/**
@@ -481,6 +483,24 @@ public function replaceStart(Stringable|string $search, Stringable|string $repla
481483
return $this->replaceFirst($search, $replace);
482484
}
483485

486+
/**
487+
* Replaces the portion of the specified `$length` at the specified `$position` with the specified `$replace`.
488+
*
489+
* ### Example
490+
* ```php
491+
* str('Lorem dolor')->replaceAt(6, 5, 'ipsum'); // Lorem ipsum
492+
* ```
493+
*/
494+
public function replaceAt(int $position, int $length, Stringable|string $replace): self
495+
{
496+
if ($length < 0) {
497+
$position += $length;
498+
$length = abs($length);
499+
}
500+
501+
return new self(substr_replace($this->string, (string) $replace, $position, $length));
502+
}
503+
484504
/**
485505
* Appends the given strings to the instance.
486506
*/
@@ -636,6 +656,71 @@ public function excerpt(int $from, int $to, bool $asArray = false): self|ArrayHe
636656
return new self(implode(PHP_EOL, $lines));
637657
}
638658

659+
/**
660+
* Truncates the instance to the specified amount of characters.
661+
*
662+
* ### Example
663+
* ```php
664+
* str('Lorem ipsum')->truncate(5, end: '...'); // Lorem...
665+
* ```
666+
*/
667+
public function truncate(int $characters, string $end = ''): self
668+
{
669+
if (mb_strwidth($this->string, 'UTF-8') <= $characters) {
670+
return $this;
671+
}
672+
673+
return new self(rtrim(mb_strimwidth($this->string, 0, $characters, encoding: 'UTF-8')) . $end);
674+
}
675+
676+
/**
677+
* Gets parts of the instance.
678+
*
679+
* ### Example
680+
* ```php
681+
* str('Lorem ipsum')->substr(0, length: 5); // Lorem
682+
* str('Lorem ipsum')->substr(6); // ipsum
683+
* ```
684+
*/
685+
public function substr(int $start, ?int $length = null): self
686+
{
687+
return new self(mb_substr($this->string, $start, $length));
688+
}
689+
690+
/**
691+
* Takes the specified amount of characters. If `$length` is negative, starts from the end.
692+
*/
693+
public function take(int $length): self
694+
{
695+
if ($length < 0) {
696+
return $this->substr($length);
697+
}
698+
699+
return $this->substr(0, $length);
700+
}
701+
702+
/**
703+
* Splits the instance into chunks of the specified `$length`.
704+
*/
705+
public function split(int $length): ArrayHelper
706+
{
707+
if ($length <= 0) {
708+
return new ArrayHelper();
709+
}
710+
711+
if ($this->equals('')) {
712+
return new ArrayHelper(['']);
713+
}
714+
715+
$chunks = [];
716+
717+
foreach (str_split($this->string, $length) as $chunk) {
718+
$chunks[] = $chunk;
719+
}
720+
721+
return new ArrayHelper($chunks);
722+
}
723+
639724
private function normalizeString(mixed $value): mixed
640725
{
641726
if ($value instanceof Stringable) {
@@ -653,6 +738,41 @@ public function explode(string $separator = ' '): ArrayHelper
653738
return ArrayHelper::explode($this->string, $separator);
654739
}
655740

741+
/**
742+
* Strips HTML and PHP tags from the instance.
743+
*
744+
* @param null|string|string[] $allowed Allowed tags.
745+
*
746+
* ### Example
747+
* ```php
748+
* str('<p>Lorem ipsum</p>')->stripTags(); // Lorem ipsum
749+
* str('<p>Lorem <strong>ipsum</strong></p>')->stripTags(allowed: 'strong'); // Lorem <strong>ipsum</strong>
750+
* ```
751+
*/
752+
public function stripTags(null|string|array $allowed = null): self
753+
{
754+
$allowed = arr($allowed)
755+
->map(fn (string $tag) => str($tag)->wrap('<', '>')->toString())
756+
->toArray();
757+
758+
return new self(strip_tags($this->string, $allowed));
759+
}
760+
761+
/**
762+
* Inserts the specified `$string` at the specified `$position`.
763+
*
764+
* ### Example
765+
* ```php
766+
* str('Lorem ipsum sit amet')->insertAt(11, ' dolor'); // Lorem ipsum dolor sit amet
767+
* ```
768+
*/
769+
public function insertAt(int $position, string $string): self
770+
{
771+
return new self(
772+
mb_substr($this->string, 0, $position) . $string . mb_substr($this->string, $position)
773+
);
774+
}
775+
656776
/**
657777
* Implodes the given array into a string by a separator.
658778
*/

src/Tempest/Support/src/functions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
/**
77
* Creates an instance of {@see StringHelper} using the given `$string`.
88
*/
9-
function str(string $string = ''): StringHelper
9+
function str(?string $string = ''): StringHelper
1010
{
1111
return new StringHelper($string);
1212
}

src/Tempest/Support/tests/ArrayHelperTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,66 @@ public function test_sort_keys_by_callback(): void
13821382
);
13831383
}
13841384

1385+
public function test_flatten(): void
1386+
{
1387+
$this->assertTrue(arr(['#foo', '#bar', '#baz'])->flatten()->equals(['#foo', '#bar', '#baz']));
1388+
$this->assertTrue(arr([['#foo', '#bar'], '#baz'])->flatten()->equals(['#foo', '#bar', '#baz']));
1389+
$this->assertTrue(arr([['#foo', null], '#baz', null])->flatten()->equals(['#foo', null, '#baz', null]));
1390+
$this->assertTrue(arr([['#foo', '#bar'], ['#baz']])->flatten()->equals(['#foo', '#bar', '#baz']));
1391+
$this->assertTrue(arr([['#foo', ['#bar']], ['#baz']])->flatten()->equals(['#foo', '#bar', '#baz']));
1392+
$this->assertTrue(arr([['#foo', ['#bar', ['#baz']]], '#zap'])->flatten()->equals(['#foo', '#bar', '#baz', '#zap']));
1393+
1394+
$this->assertTrue(arr([['#foo', ['#bar', ['#baz']]], '#zap'])->flatten(depth: 1)->equals(['#foo', ['#bar', ['#baz']], '#zap']));
1395+
$this->assertTrue(arr([['#foo', ['#bar', ['#baz']]], '#zap'])->flatten(depth: 2)->equals(['#foo', '#bar', ['#baz'], '#zap']));
1396+
}
1397+
1398+
public function test_flatmap(): void
1399+
{
1400+
// basic
1401+
$this->assertTrue(
1402+
arr([
1403+
['name' => 'Makise', 'hobbies' => ['Science', 'Programming']],
1404+
['name' => 'Okabe', 'hobbies' => ['Science', 'Anime']],
1405+
])->flatMap(fn (array $person) => $person['hobbies'])
1406+
->equals(['Science', 'Programming', 'Science', 'Anime']),
1407+
);
1408+
1409+
// deeply nested
1410+
$likes = arr([
1411+
['name' => 'Enzo', 'likes' => [
1412+
'manga' => ['Tower of God', 'The Beginning After The End'],
1413+
'languages' => ['PHP', 'TypeScript'],
1414+
]],
1415+
['name' => 'Jon', 'likes' => [
1416+
'manga' => ['One Piece', 'Naruto'],
1417+
'languages' => ['Python'],
1418+
]],
1419+
]);
1420+
1421+
$this->assertTrue(
1422+
$likes->flatMap(fn (array $person) => $person['likes'], depth: 1)
1423+
->equals([
1424+
['Tower of God', 'The Beginning After The End'],
1425+
['PHP', 'TypeScript'],
1426+
['One Piece', 'Naruto'],
1427+
['Python'],
1428+
]),
1429+
);
1430+
1431+
$this->assertTrue(
1432+
$likes->flatMap(fn (array $person) => $person['likes'], depth: INF)
1433+
->equals([
1434+
'Tower of God',
1435+
'The Beginning After The End',
1436+
'PHP',
1437+
'TypeScript',
1438+
'One Piece',
1439+
'Naruto',
1440+
'Python',
1441+
]),
1442+
);
1443+
}
1444+
13851445
public function test_basic_reduce(): void
13861446
{
13871447
$collection = arr([

0 commit comments

Comments
 (0)