Skip to content

Commit 667e740

Browse files
committed
feat: add new dot-syntax helper functions
1 parent 651988a commit 667e740

8 files changed

Lines changed: 1102 additions & 58 deletions

File tree

system/Helpers/Array/ArrayHelper.php

Lines changed: 327 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
use CodeIgniter\Exceptions\InvalidArgumentException;
1717

1818
/**
19-
* @interal This is internal implementation for the framework.
19+
* @internal This is internal implementation for the framework.
2020
*
2121
* If there are any methods that should be provided, make them
2222
* public APIs via helper functions.
2323
*
24-
* @see \CodeIgniter\Helpers\Array\ArrayHelperDotKeyExistsTest
24+
* @see \CodeIgniter\Helpers\Array\ArrayHelperDotHasTest
25+
* @see \CodeIgniter\Helpers\Array\ArrayHelperDotModifyTest
2526
* @see \CodeIgniter\Helpers\Array\ArrayHelperRecursiveDiffTest
2627
* @see \CodeIgniter\Helpers\Array\ArrayHelperSortValuesByNaturalTest
2728
*/
@@ -125,53 +126,174 @@ private static function arraySearchDot(array $indexes, array $array)
125126
* array_key_exists() with dot array syntax.
126127
*
127128
* If wildcard `*` is used, all items for the key after it must have the key.
129+
*
130+
* @param array<array-key, mixed> $array
128131
*/
129-
public static function dotKeyExists(string $index, array $array): bool
132+
public static function dotHas(string $index, array $array): bool
130133
{
131-
if (str_ends_with($index, '*') || str_contains($index, '*.*')) {
132-
throw new InvalidArgumentException(
133-
'You must set key right after "*". Invalid index: "' . $index . '"',
134-
);
135-
}
134+
self::ensureValidWildcardPattern($index);
136135

137136
$indexes = self::convertToArray($index);
138137

139-
// If indexes is empty, returns false.
140138
if ($indexes === []) {
141139
return false;
142140
}
143141

144-
$currentArray = $array;
142+
return self::hasByDotPath($array, $indexes);
143+
}
145144

146-
// Grab the current index
147-
while ($currentIndex = array_shift($indexes)) {
148-
if ($currentIndex === '*') {
149-
$currentIndex = array_shift($indexes);
150-
151-
foreach ($currentArray as $item) {
152-
if (! array_key_exists($currentIndex, $item)) {
153-
return false;
154-
}
155-
}
145+
/**
146+
* Recursively check key existence by dot path, including wildcard support.
147+
*
148+
* @param array<array-key, mixed> $array
149+
* @param list<string> $indexes
150+
*/
151+
private static function hasByDotPath(array $array, array $indexes): bool
152+
{
153+
if ($indexes === []) {
154+
return true;
155+
}
156+
157+
$currentIndex = array_shift($indexes);
156158

157-
// If indexes is empty, all elements are checked.
158-
if ($indexes === []) {
159-
return true;
159+
if ($currentIndex === '*') {
160+
foreach ($array as $item) {
161+
if (! is_array($item) || ! self::hasByDotPath($item, $indexes)) {
162+
return false;
160163
}
164+
}
165+
166+
return true;
167+
}
168+
169+
if (! array_key_exists($currentIndex, $array)) {
170+
return false;
171+
}
161172

162-
$currentArray = self::dotSearch('*.' . $currentIndex, $currentArray);
173+
if ($indexes === []) {
174+
return true;
175+
}
176+
177+
if (! is_array($array[$currentIndex])) {
178+
return false;
179+
}
180+
181+
return self::hasByDotPath($array[$currentIndex], $indexes);
182+
}
183+
184+
/**
185+
* Sets a value by dot array syntax.
186+
*
187+
* @param array<array-key, mixed> $array
188+
*/
189+
public static function dotSet(array &$array, string $index, mixed $value): void
190+
{
191+
self::ensureValidWildcardPattern($index);
192+
193+
$indexes = self::convertToArray($index);
194+
195+
if ($indexes === []) {
196+
return;
197+
}
198+
199+
self::setByDotPath($array, $indexes, $value);
200+
}
201+
202+
/**
203+
* Removes a value by dot array syntax.
204+
*
205+
* @param array<array-key, mixed> $array
206+
*/
207+
public static function dotUnset(array &$array, string $index): bool
208+
{
209+
self::ensureValidWildcardPattern($index, true);
210+
211+
if ($index === '*') {
212+
return self::clearByDotPath($array, []) > 0;
213+
}
214+
215+
$indexes = self::convertToArray($index);
216+
217+
if ($indexes === []) {
218+
return false;
219+
}
220+
221+
if (str_ends_with($index, '*')) {
222+
return self::clearByDotPath($array, $indexes) > 0;
223+
}
224+
225+
return self::unsetByDotPath($array, $indexes) > 0;
226+
}
227+
228+
/**
229+
* Gets only the specified keys using dot syntax.
230+
*
231+
* @param array<array-key, mixed> $array
232+
* @param list<string>|string $indexes
233+
*
234+
* @return array<array-key, mixed>
235+
*/
236+
public static function dotOnly(array $array, array|string $indexes): array
237+
{
238+
$indexes = is_string($indexes) ? [$indexes] : $indexes;
239+
$result = [];
240+
241+
foreach ($indexes as $index) {
242+
self::ensureValidWildcardPattern($index, true);
243+
244+
if ($index === '*') {
245+
$result = [...$result, ...$array];
246+
247+
continue;
248+
}
249+
250+
$segments = self::convertToArray($index);
251+
if ($segments === []) {
252+
continue;
253+
}
254+
255+
self::projectByDotPath($array, $segments, $result);
256+
}
257+
258+
return $result;
259+
}
260+
261+
/**
262+
* Gets all keys except the specified ones using dot syntax.
263+
*
264+
* @param array<array-key, mixed> $array
265+
* @param list<string>|string $indexes
266+
*
267+
* @return array<array-key, mixed>
268+
*/
269+
public static function dotExcept(array $array, array|string $indexes): array
270+
{
271+
$indexes = is_string($indexes) ? [$indexes] : $indexes;
272+
$result = $array;
273+
274+
foreach ($indexes as $index) {
275+
self::ensureValidWildcardPattern($index, true);
276+
277+
if ($index === '*') {
278+
$result = [];
163279

164280
continue;
165281
}
166282

167-
if (! array_key_exists($currentIndex, $currentArray)) {
168-
return false;
283+
if (str_ends_with($index, '*')) {
284+
$segments = self::convertToArray($index);
285+
self::clearByDotPath($result, $segments);
286+
287+
continue;
169288
}
170289

171-
$currentArray = $currentArray[$currentIndex];
290+
$segments = self::convertToArray($index);
291+
if ($segments !== []) {
292+
self::unsetByDotPath($result, $segments);
293+
}
172294
}
173295

174-
return true;
296+
return $result;
175297
}
176298

177299
/**
@@ -315,4 +437,181 @@ public static function sortValuesByNatural(array &$array, $sortByIndex = null):
315437
return strnatcmp((string) $currentValue, (string) $nextValue);
316438
});
317439
}
440+
441+
/**
442+
* Throws exception for invalid wildcard patterns.
443+
*/
444+
private static function ensureValidWildcardPattern(string $index, bool $allowTrailingWildcard = false): void
445+
{
446+
if ((! $allowTrailingWildcard && str_ends_with($index, '*')) || str_contains($index, '*.*')) {
447+
throw new InvalidArgumentException(
448+
'You must set key right after "*". Invalid index: "' . $index . '"',
449+
);
450+
}
451+
}
452+
453+
/**
454+
* Set value recursively by dot path, including wildcard support.
455+
*
456+
* @param array<array-key, mixed> $array
457+
* @param list<string> $indexes
458+
*/
459+
private static function setByDotPath(array &$array, array $indexes, mixed $value): void
460+
{
461+
if ($indexes === []) {
462+
return;
463+
}
464+
465+
$currentIndex = array_shift($indexes);
466+
467+
if ($currentIndex === '*') {
468+
foreach ($array as &$item) {
469+
if (! is_array($item)) {
470+
continue;
471+
}
472+
473+
self::setByDotPath($item, $indexes, $value);
474+
}
475+
unset($item);
476+
477+
return;
478+
}
479+
480+
if ($indexes === []) {
481+
$array[$currentIndex] = $value;
482+
483+
return;
484+
}
485+
486+
if (! isset($array[$currentIndex]) || ! is_array($array[$currentIndex])) {
487+
$array[$currentIndex] = [];
488+
}
489+
490+
self::setByDotPath($array[$currentIndex], $indexes, $value);
491+
}
492+
493+
/**
494+
* Unset value recursively by dot path, including wildcard support.
495+
*
496+
* @param array<array-key, mixed> $array
497+
* @param list<string> $indexes
498+
*/
499+
private static function unsetByDotPath(array &$array, array $indexes): int
500+
{
501+
if ($indexes === []) {
502+
return 0;
503+
}
504+
505+
$currentIndex = array_shift($indexes);
506+
507+
if ($currentIndex === '*') {
508+
$removed = 0;
509+
510+
foreach ($array as &$item) {
511+
if (! is_array($item)) {
512+
continue;
513+
}
514+
515+
$removed += self::unsetByDotPath($item, $indexes);
516+
}
517+
unset($item);
518+
519+
return $removed;
520+
}
521+
522+
if ($indexes === []) {
523+
if (! array_key_exists($currentIndex, $array)) {
524+
return 0;
525+
}
526+
527+
unset($array[$currentIndex]);
528+
529+
return 1;
530+
}
531+
532+
if (! isset($array[$currentIndex]) || ! is_array($array[$currentIndex])) {
533+
return 0;
534+
}
535+
536+
return self::unsetByDotPath($array[$currentIndex], $indexes);
537+
}
538+
539+
/**
540+
* Clears all children under the specified path.
541+
*
542+
* @param array<array-key, mixed> $array
543+
* @param list<string> $indexes
544+
*/
545+
private static function clearByDotPath(array &$array, array $indexes): int
546+
{
547+
if ($indexes === []) {
548+
$count = count($array);
549+
$array = [];
550+
551+
return $count;
552+
}
553+
554+
$currentIndex = array_shift($indexes);
555+
556+
if ($currentIndex === '*') {
557+
$cleared = 0;
558+
559+
foreach ($array as &$item) {
560+
if (! is_array($item)) {
561+
continue;
562+
}
563+
564+
$cleared += self::clearByDotPath($item, $indexes);
565+
}
566+
unset($item);
567+
568+
return $cleared;
569+
}
570+
571+
if (! array_key_exists($currentIndex, $array) || ! is_array($array[$currentIndex])) {
572+
return 0;
573+
}
574+
575+
return self::clearByDotPath($array[$currentIndex], $indexes);
576+
}
577+
578+
/**
579+
* Projects matching paths from source array into result with preserved structure.
580+
*
581+
* @param list<string> $indexes
582+
* @param list<string> $prefix
583+
* @param array<array-key, mixed> $result
584+
*/
585+
private static function projectByDotPath(
586+
mixed $source,
587+
array $indexes,
588+
array &$result,
589+
array $prefix = [],
590+
): void {
591+
if ($indexes === []) {
592+
self::setByDotPath($result, $prefix, $source);
593+
594+
return;
595+
}
596+
597+
$currentIndex = array_shift($indexes);
598+
599+
if ($currentIndex === '*') {
600+
if (! is_array($source)) {
601+
return;
602+
}
603+
604+
foreach ($source as $key => $value) {
605+
self::projectByDotPath($value, $indexes, $result, [...$prefix, (string) $key]);
606+
}
607+
608+
return;
609+
}
610+
611+
if (! is_array($source) || ! array_key_exists($currentIndex, $source)) {
612+
return;
613+
}
614+
615+
self::projectByDotPath($source[$currentIndex], $indexes, $result, [...$prefix, $currentIndex]);
616+
}
318617
}

0 commit comments

Comments
 (0)