diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index 37382af4d012..5b359a761384 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -11,6 +11,8 @@ namespace CodeIgniter\Helpers\Array; +use InvalidArgumentException; + /** * @interal This is internal implementation for the framework. * @@ -27,9 +29,21 @@ final class ArrayHelper * * @used-by dot_array_search() * + * @param string $index The index as dot array syntax. + * * @return array|bool|int|object|string|null */ public static function dotSearch(string $index, array $array) + { + return self::arraySearchDot(self::convertToArray($index), $array); + } + + /** + * @param string $index The index as dot array syntax. + * + * @return list The index as an array. + */ + private static function convertToArray(string $index): array { // See https://regex101.com/r/44Ipql/1 $segments = preg_split( @@ -39,9 +53,10 @@ public static function dotSearch(string $index, array $array) PREG_SPLIT_NO_EMPTY ); - $segments = array_map(static fn ($key) => str_replace('\.', '.', $key), $segments); - - return self::arraySearchDot($segments, $array); + return array_map( + static fn ($key) => str_replace('\.', '.', $key), + $segments + ); } /** @@ -106,6 +121,59 @@ private static function arraySearchDot(array $indexes, array $array) return null; } + /** + * array_key_exists() with dot array syntax. + * + * If wildcard `*` is used, all items for the key after it must have the key. + */ + public static function dotKeyExists(string $index, array $array): bool + { + if (str_ends_with($index, '*') || str_contains($index, '*.*')) { + throw new InvalidArgumentException( + 'You must set key right after "*". Invalid index: "' . $index . '"' + ); + } + + $indexes = self::convertToArray($index); + + // If indexes is empty, returns false. + if ($indexes === []) { + return false; + } + + $currentArray = $array; + + // Grab the current index + while ($currentIndex = array_shift($indexes)) { + if ($currentIndex === '*') { + $currentIndex = array_shift($indexes); + + foreach ($currentArray as $item) { + if (! array_key_exists($currentIndex, $item)) { + return false; + } + } + + // If indexes is empty, all elements are checked. + if ($indexes === []) { + return true; + } + + $currentArray = self::dotSearch('*.' . $currentIndex, $currentArray); + + continue; + } + + if (! array_key_exists($currentIndex, $currentArray)) { + return false; + } + + $currentArray = $currentArray[$currentIndex]; + } + + return true; + } + /** * Groups all rows by their index values. Result's depth equals number of indexes * diff --git a/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php b/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php new file mode 100644 index 000000000000..c4168688cf53 --- /dev/null +++ b/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Helpers\Array; + +use CodeIgniter\Test\CIUnitTestCase; +use InvalidArgumentException; + +/** + * @group Others + * + * @internal + */ +final class ArrayHelperDotKeyExistsTest extends CIUnitTestCase +{ + private array $array = [ + 'contacts' => [ + 'friends' => [ + ['name' => 'Fred Flinstone', 'age' => 20], + ['age' => 21], // 'name' key does not exist + ], + ], + ]; + + public function testDotKeyExists(): void + { + $this->assertFalse(ArrayHelper::dotKeyExists('', $this->array)); + $this->assertTrue(ArrayHelper::dotKeyExists('contacts', $this->array)); + $this->assertFalse(ArrayHelper::dotKeyExists('not', $this->array)); + $this->assertTrue(ArrayHelper::dotKeyExists('contacts.friends', $this->array)); + $this->assertFalse(ArrayHelper::dotKeyExists('not.friends', $this->array)); + $this->assertTrue(ArrayHelper::dotKeyExists('contacts.friends.0.name', $this->array)); + $this->assertFalse(ArrayHelper::dotKeyExists('contacts.friends.1.name', $this->array)); + } + + public function testDotKeyExistsWithEndingWildCard(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You must set key right after "*". Invalid index: "contacts.*"'); + + $this->assertTrue(ArrayHelper::dotKeyExists('contacts.*', $this->array)); + } + + public function testDotKeyExistsWithDoubleWildCard(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You must set key right after "*". Invalid index: "contacts.*.*.age"'); + + $this->assertTrue(ArrayHelper::dotKeyExists('contacts.*.*.age', $this->array)); + } + + public function testDotKeyExistsWithWildCard(): void + { + $this->assertTrue(ArrayHelper::dotKeyExists('*.friends', $this->array)); + $this->assertTrue(ArrayHelper::dotKeyExists('contacts.friends.*.age', $this->array)); + $this->assertFalse(ArrayHelper::dotKeyExists('contacts.friends.*.name', $this->array)); + $this->assertTrue(ArrayHelper::dotKeyExists('*.friends.*.age', $this->array)); + $this->assertFalse(ArrayHelper::dotKeyExists('*.friends.*.name', $this->array)); + $this->assertTrue(ArrayHelper::dotKeyExists('contacts.*.0.age', $this->array)); + $this->assertTrue(ArrayHelper::dotKeyExists('contacts.*.1.age', $this->array)); + $this->assertTrue(ArrayHelper::dotKeyExists('contacts.*.0.name', $this->array)); + $this->assertFalse(ArrayHelper::dotKeyExists('contacts.*.1.name', $this->array)); + } +}