diff --git a/README.md b/README.md index 6739e77..2a5ad81 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ A library for array manipulations. * [get_first_set](/docs/classes/StellarWP/Arrays/Arr.md#get_first_set) * [get_in_any](/docs/classes/StellarWP/Arrays/Arr.md#get_in_any) * [has](/docs/classes/StellarWP/Arrays/Arr.md#has) + * [has_shape]/docs/classes/StellarWP/Arrays/Arr.md#has_shape) * [insert_after_key](/docs/classes/StellarWP/Arrays/Arr.md#insert_after_key) * [insert_before_key](/docs/classes/StellarWP/Arrays/Arr.md#insert_before_key) * [is_assoc](/docs/classes/StellarWP/Arrays/Arr.md#is_assoc) diff --git a/composer.json b/composer.json index 67f7a1b..a82df5e 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,11 @@ "platform": { "php": "7.4" }, + "config": { + "platform": { + "php": "7.4.33" + } + }, "autoload": { "psr-4": { "StellarWP\\Arrays\\": "src/Arrays/" @@ -27,7 +32,9 @@ } ], "minimum-stability": "stable", - "require": {}, + "require": { + "php": ">=7.4" + }, "require-dev": { "codeception/module-asserts": "^1.0", "codeception/module-cli": "^1.0", @@ -42,7 +49,8 @@ "szepeviktor/phpstan-wordpress": "^1.1", "symfony/event-dispatcher-contracts": "^2.5.1", "symfony/string": "^5.4", - "saggre/phpdocumentor-markdown": "^0.1.3" + "saggre/phpdocumentor-markdown": "^0.1.3", + "illuminate/support": "^8.83" }, "scripts": { "create-docs": [ diff --git a/docs/classes/StellarWP/Arrays/Arr.md b/docs/classes/StellarWP/Arrays/Arr.md index 8a3cf0a..25b5d0f 100644 --- a/docs/classes/StellarWP/Arrays/Arr.md +++ b/docs/classes/StellarWP/Arrays/Arr.md @@ -274,7 +274,7 @@ The sanitized array **See Also:** -* https://gist.github.com/esthezia/5804445 - +* https://gist.github.com/esthezia/5804445 - *** @@ -604,7 +604,25 @@ public static has(\ArrayAccess|array $array, array|string|int|null $indexes): bo | `$indexes` | **array|string|int|null** | The indexes to search; in order the function will look from the first to the last. | +### has_shape +Check if an array has a specific shape. + +```php +public static has_shape(mixed $array, array $shape): bool +``` + +* This method is **static**. + + + + +**Parameters:** + +| Parameter | Type | Description | +|-----------|-----------|-----------------------------------------------------------------------------------| +| `$array` | **mixed** | The array to check. | +| `$shape` | **array** | The shape to check for. A map from keys to the callable or Closure to check them. | *** @@ -865,7 +883,7 @@ public merge_recursive(array& $array1, array& $array2): array **See Also:** -* http://php.net/manual/en/function.array-merge-recursive.php#92195 - +* http://php.net/manual/en/function.array-merge-recursive.php#92195 - *** @@ -1415,7 +1433,7 @@ Integer position of first needle occurrence. **See Also:** -* \StellarWP\Arrays\strpos() - +* \StellarWP\Arrays\strpos() - *** diff --git a/src/Arrays/Arr.php b/src/Arrays/Arr.php index 24303de..f6ed686 100644 --- a/src/Arrays/Arr.php +++ b/src/Arrays/Arr.php @@ -299,8 +299,8 @@ public static function except( $array, $keys ) { /** * Determine if the given key exists in the provided array. * - * @param \ArrayAccess|array $array - * @param string|int|float $key + * @param ArrayAccess|array $array + * @param string|int|float $key * * @return bool */ @@ -357,7 +357,7 @@ public static function filter_prefixed( array $array, string $prefix ): array { public static function first( $array, callable $callback = null, $default = null ) { if ( is_null( $callback ) ) { if ( empty( $array ) ) { - return value( $default ); + return $default; } foreach ( $array as $item ) { @@ -371,7 +371,7 @@ public static function first( $array, callable $callback = null, $default = null } } - return value( $default ); + return $default; } /** @@ -543,7 +543,7 @@ public static function get_in_any( array $variables, $indexes, $default = null ) /** * Check if an item or items exist in an array using "dot" notation. * - * @param \ArrayAccess|array $array + * @param ArrayAccess|array $array * @param array|string|int|null $indexes The indexes to search; in order the function will look from the first to the last. * * @return bool @@ -691,7 +691,7 @@ public static function join( $array, $glue, $finalGlue = '' ) { */ public static function last( $array, callable $callback = null, $default = null ) { if ( is_null( $callback ) ) { - return empty( $array ) ? value( $default ) : end( $array ); + return empty( $array ) ? $default : end( $array ); } return static::first( array_reverse( $array, true ), $callback, $default ); @@ -942,7 +942,7 @@ public static function query( $array ) { * * @return mixed * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public static function random( $array, $number = null, $preserveKeys = false ) { $requested = is_null( $number ) ? 1 : $number; @@ -1386,4 +1386,54 @@ public static function wrap( $value ) { return is_array( $value ) ? $value : [ $value ]; } + + /** + * Checks if an array has a specific shape. + * + * @since TBD + * + * @param array $array The array to check. + * @param array $shape The shape to check for. Each key, either a string or an integer, + * maps to a callable that will be used to validate the value at that key. + * The callable must have the signature `fn( mixed $value ) :bool`. + * @param bool $strict Whether the array should only contain the keys specified in the shape. + * + * @return bool Whether the array has the specified shape. + */ + public static function has_shape( $array, array $shape, bool $strict = false ): bool { + if ( ! is_array( $array ) ) { + return false; + } + + if ( + $strict + && ( + array_intersect_key( $array, $shape ) !== $array + || + array_diff_key( $array, $shape ) !== [] + ) + ) { + return false; + } + + if ( count( array_intersect_key( $shape, $array ) ) < count( $shape ) ) { + return false; + } + + foreach ( $shape as $key => $check ) { + if ( ! is_callable( $check ) ) { + throw new \BadMethodCallException( 'The shape array must contain only callables as values.' ); + } + + try { + if ( ! $check( $array[ $key ] ) ) { + return false; + } + } catch ( \Throwable $th ) { + return false; + } + } + + return true; + } } diff --git a/tests/wpunit/ArraysTest.php b/tests/wpunit/ArraysTest.php index 078e725..b08afc5 100644 --- a/tests/wpunit/ArraysTest.php +++ b/tests/wpunit/ArraysTest.php @@ -766,10 +766,135 @@ public function array_visit_recursive_data_provider() { ]; } + public function has_shape_data_provider(): array { + return [ + 'not an array' => [ 'foo', [], true, false ], + 'empty array, empty shape' => [ [], [], true, true ], + 'empty array, non-empty shape, strict' => [ + [], + [ 'foo' => 'is_string' ], + true, + false + ], + 'empty array, non-empty shape, non-strict' => [ + [], + [ 'foo' => 'is_string' ], + false, + false + ], + 'non-empty array, function shape, missing key, strict' => [ + [ 'foo' => 23 ], + [ 'bar' => 'is_string' ], + true, + false + ], + 'non-empty array, function shape, missing key, non-strict' => [ + [ 'foo' => 23 ], + [ 'bar' => 'is_string' ], + false, + false + ], + 'non-empty array, function shape, extra key, strict' => [ + [ 'foo' => 23, 'bar' => 'baz' ], + [ 'foo' => 'is_int' ], + true, + false + ], + 'non-empty array, function shape, extra key, non-strict' => [ + [ 'foo' => 23, 'bar' => 'baz' ], + [ 'foo' => 'is_int' ], + false, + true + ], + 'non-empty array, closure shape, all key fail failure, strict' => [ + [ 'foo' => 23, 'bar' => 89 ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + true, + false + ], + 'non-empty array, closure shape, all key fail failure, non-strict' => [ + [ 'foo' => 23, 'bar' => 89 ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + false, + false + ], + 'non-empty array, closure shape, all key pass, strict' => [ + [ 'foo' => 'hello', 'bar' => 'world' ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + true, + true + ], + 'non-empty array, closure shape, all key pass, non-strict ' => [ + [ 'foo' => 'hello', 'bar' => 'world' ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + false, + true + ], + 'non-empty array, closure shape, some key pass, strict' => [ + [ 'foo' => 'hello', 'bar' => 89 ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + true, + false + ], + 'non-empty array, closure shape, some key pass, non-strict' => [ + [ 'foo' => 'hello', 'bar' => 89 ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + false, + false + ], + ]; + } + /** - * @dataProvider array_visit_recursive_data_provider + * @dataProvider has_shape_data_provider */ - public function test_array_visit_recursive( $input, $visitor, $expected ) { - $this->assertEqualSets( $expected, Arr::array_visit_recursive( $input, $visitor ) ); + public function test_has_shape( $input, $shape, $strict, $expected ): void { + $this->assertEquals( $expected, Arr::has_shape( $input, $shape, $strict ) ); + } + + public function test_first(): void { + $this->assertEquals( + 'lorem', + Arr::first(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'lorem', 'default') + ); + $this->assertEquals( + 'dolor', + Arr::first(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'dolor', 'default') + ); + $this->assertEquals( + 'default', + Arr::first(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'foo', 'default') + ); + $this->assertEquals( + 'default', + Arr::first(['ipsum', 'lorem', 'dolor'], fn($value) => str_starts_with($value,'p'), 'default') + ); + $this->assertEquals( + 'lorem', + Arr::first(['ipsum', 'lorem', 'dolor', 'loller'], fn($value) => str_starts_with($value,'l'), 'default') + ); + } + + public function test_last():void{ + $this->assertEquals( + 'lorem', + Arr::last(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'lorem', 'default') + ); + $this->assertEquals( + 'dolor', + Arr::last(['ipsum', 'dolor', 'lorem'], fn($value) => $value === 'dolor', 'default') + ); + $this->assertEquals( + 'default', + Arr::last(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'foo', 'default') + ); + $this->assertEquals( + 'default', + Arr::last(['ipsum', 'lorem', 'dolor'], fn($value) => str_starts_with($value,'p'), 'default') + ); + $this->assertEquals( + 'loller', + Arr::last(['ipsum', 'lorem', 'dolor', 'loller'], fn($value) => str_starts_with($value,'l'), 'default') + ); } }