Skip to content

Commit

Permalink
feat(support): add more methods to ArrayHelper and StringHelper (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
innocenzi authored Nov 13, 2024
1 parent 6847a79 commit bdf5efc
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 11 deletions.
77 changes: 70 additions & 7 deletions src/Tempest/Support/src/ArrayHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public function __construct(
$this->array = $input;
} elseif ($input instanceof self) {
$this->array = $input->array;
} elseif ($input === null) {
$this->array = [];
} else {
$this->array = [$input];
}
Expand Down Expand Up @@ -432,6 +434,10 @@ public static function explode(string|Stringable $string, string $separator = '
return new self([(string) $string]);
}

if ((string) $string === '') {
return new self();
}

return new self(explode($separator, (string) $string));
}

Expand All @@ -449,7 +455,9 @@ public function equals(array|self $other): bool
* Returns the first item in the instance that matches the given `$filter`.
* If `$filter` is `null`, returns the first item.
*
* @param Closure(mixed $value, mixed $key): bool $filter
* @param null|Closure(TValue $value, TKey $key): bool $filter
*
* @return TValue
*/
public function first(?Closure $filter = null): mixed
{
Expand All @@ -474,7 +482,9 @@ public function first(?Closure $filter = null): mixed
* Returns the last item in the instance that matches the given `$filter`.
* If `$filter` is `null`, returns the last item.
*
* @param Closure(mixed $value, mixed $key): bool $filter
* @param null|Closure(TValue $value, TKey $key): bool $filter
*
* @return TValue
*/
public function last(?Closure $filter = null): mixed
{
Expand Down Expand Up @@ -608,7 +618,11 @@ public function each(Closure $each): self
/**
* Returns a new instance of the array, with each item transformed by the given callback.
*
* @param Closure(mixed $value, mixed $key): mixed $map
* @template TMapValue
*
* @param Closure(TValue, TKey): TMapValue $map
*
* @return static<TKey, TMapValue>
*/
public function map(Closure $map): self
{
Expand Down Expand Up @@ -654,11 +668,13 @@ public function mapWithKeys(Closure $map): self
*
* @return mixed|ArrayHelper
*/
public function get(string $key, mixed $default = null): mixed
public function get(int|string $key, mixed $default = null): mixed
{
$value = $this->array;

$keys = explode('.', $key);
$keys = is_int($key)
? [$key]
: explode('.', $key);

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

$keys = explode('.', $key);
$keys = is_int($key)
? [$key]
: explode('.', $key);

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

/**
* Flattens the instance to a single-level array, or until the specified `$depth` is reached.
*
* ### Example
* ```php
* arr(['foo', ['bar', 'baz']])->flatten(); // ['foo', 'bar', 'baz']
* ```
*/
public function flatten(int|float $depth = INF): self
{
$result = [];

foreach ($this->array as $item) {
if (! is_array($item)) {
$result[] = $item;

continue;
}

$values = $depth === 1
? array_values($item)
: arr($item)->flatten($depth - 1);

foreach ($values as $value) {
$result[] = $value;
}
}

return new self($result);
}

/**
* Returns a new instance of the array, with each item transformed by the given callback, then flattens it by the specified depth.
*
* @template TMapValue
*
* @param Closure(TValue, TKey): TMapValue[] $map
*
* @return static<TKey, TMapValue>
*/
public function flatMap(Closure $map, int|float $depth = 1): self
{
return $this->map($map)->flatten($depth);
}

/**
* Dumps the instance.
*/
Expand Down
126 changes: 123 additions & 3 deletions src/Tempest/Support/src/StringHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@

final readonly class StringHelper implements Stringable
{
public function __construct(
private string $string = '',
) {
private string $string;

public function __construct(?string $string = '')
{
$this->string = $string ?? '';
}

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

/**
* Replaces the portion of the specified `$length` at the specified `$position` with the specified `$replace`.
*
* ### Example
* ```php
* str('Lorem dolor')->replaceAt(6, 5, 'ipsum'); // Lorem ipsum
* ```
*/
public function replaceAt(int $position, int $length, Stringable|string $replace): self
{
if ($length < 0) {
$position += $length;
$length = abs($length);
}

return new self(substr_replace($this->string, (string) $replace, $position, $length));
}

/**
* Appends the given strings to the instance.
*/
Expand Down Expand Up @@ -636,6 +656,71 @@ public function excerpt(int $from, int $to, bool $asArray = false): self|ArrayHe
return new self(implode(PHP_EOL, $lines));
}

/**
* Truncates the instance to the specified amount of characters.
*
* ### Example
* ```php
* str('Lorem ipsum')->truncate(5, end: '...'); // Lorem...
* ```
*/
public function truncate(int $characters, string $end = ''): self
{
if (mb_strwidth($this->string, 'UTF-8') <= $characters) {
return $this;
}

return new self(rtrim(mb_strimwidth($this->string, 0, $characters, encoding: 'UTF-8')) . $end);
}

/**
* Gets parts of the instance.
*
* ### Example
* ```php
* str('Lorem ipsum')->substr(0, length: 5); // Lorem
* str('Lorem ipsum')->substr(6); // ipsum
* ```
*/
public function substr(int $start, ?int $length = null): self
{
return new self(mb_substr($this->string, $start, $length));
}

/**
* Takes the specified amount of characters. If `$length` is negative, starts from the end.
*/
public function take(int $length): self
{
if ($length < 0) {
return $this->substr($length);
}

return $this->substr(0, $length);
}

/**
* Splits the instance into chunks of the specified `$length`.
*/
public function split(int $length): ArrayHelper
{
if ($length <= 0) {
return new ArrayHelper();
}

if ($this->equals('')) {
return new ArrayHelper(['']);
}

$chunks = [];

foreach (str_split($this->string, $length) as $chunk) {
$chunks[] = $chunk;
}

return new ArrayHelper($chunks);
}

private function normalizeString(mixed $value): mixed
{
if ($value instanceof Stringable) {
Expand All @@ -653,6 +738,41 @@ public function explode(string $separator = ' '): ArrayHelper
return ArrayHelper::explode($this->string, $separator);
}

/**
* Strips HTML and PHP tags from the instance.
*
* @param null|string|string[] $allowed Allowed tags.
*
* ### Example
* ```php
* str('<p>Lorem ipsum</p>')->stripTags(); // Lorem ipsum
* str('<p>Lorem <strong>ipsum</strong></p>')->stripTags(allowed: 'strong'); // Lorem <strong>ipsum</strong>
* ```
*/
public function stripTags(null|string|array $allowed = null): self
{
$allowed = arr($allowed)
->map(fn (string $tag) => str($tag)->wrap('<', '>')->toString())
->toArray();

return new self(strip_tags($this->string, $allowed));
}

/**
* Inserts the specified `$string` at the specified `$position`.
*
* ### Example
* ```php
* str('Lorem ipsum sit amet')->insertAt(11, ' dolor'); // Lorem ipsum dolor sit amet
* ```
*/
public function insertAt(int $position, string $string): self
{
return new self(
mb_substr($this->string, 0, $position) . $string . mb_substr($this->string, $position)
);
}

/**
* Implodes the given array into a string by a separator.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Tempest/Support/src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/**
* Creates an instance of {@see StringHelper} using the given `$string`.
*/
function str(string $string = ''): StringHelper
function str(?string $string = ''): StringHelper
{
return new StringHelper($string);
}
Expand Down
60 changes: 60 additions & 0 deletions src/Tempest/Support/tests/ArrayHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,66 @@ public function test_sort_keys_by_callback(): void
);
}

public function test_flatten(): void
{
$this->assertTrue(arr(['#foo', '#bar', '#baz'])->flatten()->equals(['#foo', '#bar', '#baz']));
$this->assertTrue(arr([['#foo', '#bar'], '#baz'])->flatten()->equals(['#foo', '#bar', '#baz']));
$this->assertTrue(arr([['#foo', null], '#baz', null])->flatten()->equals(['#foo', null, '#baz', null]));
$this->assertTrue(arr([['#foo', '#bar'], ['#baz']])->flatten()->equals(['#foo', '#bar', '#baz']));
$this->assertTrue(arr([['#foo', ['#bar']], ['#baz']])->flatten()->equals(['#foo', '#bar', '#baz']));
$this->assertTrue(arr([['#foo', ['#bar', ['#baz']]], '#zap'])->flatten()->equals(['#foo', '#bar', '#baz', '#zap']));

$this->assertTrue(arr([['#foo', ['#bar', ['#baz']]], '#zap'])->flatten(depth: 1)->equals(['#foo', ['#bar', ['#baz']], '#zap']));
$this->assertTrue(arr([['#foo', ['#bar', ['#baz']]], '#zap'])->flatten(depth: 2)->equals(['#foo', '#bar', ['#baz'], '#zap']));
}

public function test_flatmap(): void
{
// basic
$this->assertTrue(
arr([
['name' => 'Makise', 'hobbies' => ['Science', 'Programming']],
['name' => 'Okabe', 'hobbies' => ['Science', 'Anime']],
])->flatMap(fn (array $person) => $person['hobbies'])
->equals(['Science', 'Programming', 'Science', 'Anime']),
);

// deeply nested
$likes = arr([
['name' => 'Enzo', 'likes' => [
'manga' => ['Tower of God', 'The Beginning After The End'],
'languages' => ['PHP', 'TypeScript'],
]],
['name' => 'Jon', 'likes' => [
'manga' => ['One Piece', 'Naruto'],
'languages' => ['Python'],
]],
]);

$this->assertTrue(
$likes->flatMap(fn (array $person) => $person['likes'], depth: 1)
->equals([
['Tower of God', 'The Beginning After The End'],
['PHP', 'TypeScript'],
['One Piece', 'Naruto'],
['Python'],
]),
);

$this->assertTrue(
$likes->flatMap(fn (array $person) => $person['likes'], depth: INF)
->equals([
'Tower of God',
'The Beginning After The End',
'PHP',
'TypeScript',
'One Piece',
'Naruto',
'Python',
]),
);
}

public function test_basic_reduce(): void
{
$collection = arr([
Expand Down
Loading

0 comments on commit bdf5efc

Please # to comment.