diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 4d2c7dc27f72..c37288c57e59 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -133,13 +133,11 @@ function array_deep_search($key, array $array) * Both arrays of objects and arrays of array can be sorted. * * Example: - * array_sort_by_multiple_keys($players, - * [ - * 'team.hierarchy' => SORT_ASC, - * 'position' => SORT_ASC, - * 'name' => SORT_STRING, - * ] - * ); + * array_sort_by_multiple_keys($players, [ + * 'team.hierarchy' => SORT_ASC, + * 'position' => SORT_ASC, + * 'name' => SORT_STRING, + * ]); * * The '.' dot operator in the column name indicates a deeper array or * object level. In principle, any number of sublevels could be used, @@ -180,7 +178,7 @@ function array_sort_by_multiple_keys(array &$array, array $sortColumns): bool { $carry[$index] = $object->$keySegment; } - + continue; } @@ -200,3 +198,35 @@ function array_sort_by_multiple_keys(array &$array, array $sortColumns): bool return array_multisort(...$tempArray); } } + +if (! function_exists('array_flatten_with_dots')) +{ + /** + * Flatten a multidimensional array using dots as separators. + * + * @param iterable $array The multi-dimensional array + * @param string $id Something to initially prepend to the flattened keys + * + * @return array The flattened array + */ + function array_flatten_with_dots(iterable $array, string $id = ''): array + { + $flattened = []; + + foreach ($array as $key => $value) + { + $newKey = $id . $key; + + if (is_array($value)) + { + $flattened = array_merge($flattened, array_flatten_with_dots($value, $newKey . '.')); + } + else + { + $flattened[$newKey] = $value; + } + } + + return $flattened; + } +} diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 558323c3499a..3b69e418474b 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -202,7 +202,7 @@ public function check($value, string $rule, array $errors = []): bool * @param string|null $label * @param string|array $value Value to be validated, can be a string or an array * @param array|null $rules - * @param array $data // All of the fields to check. + * @param array $data All of the fields to check. * * @return boolean */ @@ -213,15 +213,16 @@ protected function processRules(string $field, string $label = null, $value, $ru throw new InvalidArgumentException('You must supply the parameter: data.'); } - // If the if_exist rule is defined... if (in_array('if_exist', $rules, true)) { - // and the current field does not exists in the input data - // we can return true. Ignoring all other rules to this field. - if (! array_key_exists($field, $data)) + // If the if_exist rule is defined + // and the current field does not exist in the input data + // we can return true, ignoring all other rules to this field. + if (! array_key_exists($field, array_flatten_with_dots($data))) { return true; } + // Otherwise remove the if_exist rule and continue the process $rules = array_diff($rules, ['if_exist']); } diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index ebdd5f1c3f1f..18224c1f7a13 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -1,6 +1,10 @@ - $dataSet){ + foreach ($data as $index => $dataSet) + { $data[$index] = (object) $dataSet; } @@ -228,7 +233,7 @@ public function testArraySortByMultipleKeysFailsInconsistentArraySizes($data) { $this->expectException('ValueError'); } - + $this->expectExceptionMessage('Array sizes are inconsistent'); $sortColumns = [ @@ -236,11 +241,9 @@ public function testArraySortByMultipleKeysFailsInconsistentArraySizes($data) 'positions' => SORT_ASC, ]; - $success = array_sort_by_multiple_keys($data, $sortColumns); + array_sort_by_multiple_keys($data, $sortColumns); } - //-------------------------------------------------------------------- - public static function deepSearchProvider() { return [ @@ -319,4 +322,98 @@ public static function sortByMultipleKeysProvider() ], ]; } + + /** + * @dataProvider arrayFlattenProvider + * + * @param iterable $input + * @param iterable $expected + * + * @return void + */ + public function testArrayFlattening($input, $expected): void + { + $this->assertSame($expected, array_flatten_with_dots($input)); + } + + public function arrayFlattenProvider(): iterable + { + yield 'normal' => [ + [ + 'id' => '12', + 'user' => [ + 'first_name' => 'john', + 'last_name' => 'smith', + 'age' => '26 years', + ], + ], + [ + 'id' => '12', + 'user.first_name' => 'john', + 'user.last_name' => 'smith', + 'user.age' => '26 years', + ], + ]; + + yield 'many-levels' => [ + [ + 'foo' => 1, + 'bar' => [ + 'bax' => [ + 'baz' => 2, + 'biz' => 3, + ], + ], + 'baz' => [ + 'fizz' => 4, + ], + ], + [ + 'foo' => 1, + 'bar.bax.baz' => 2, + 'bar.bax.biz' => 3, + 'baz.fizz' => 4, + ], + ]; + + yield 'with-empty-arrays' => [ + [ + 'foo' => 'bar', + 'baz' => [], + 'bar' => [ + 'fizz' => 'buzz', + 'nope' => 'yeah', + 'why' => [], + ], + ], + [ + 'foo' => 'bar', + 'bar.fizz' => 'buzz', + 'bar.nope' => 'yeah', + ], + ]; + + yield 'with-mixed-empty' => [ + [ + 'foo' => 1, + '' => [ + 'bar' => 2, + 'baz' => 3, + ], + 0 => [ + 'fizz' => 4, + ], + 1 => [ + 'buzz' => 5, + ], + ], + [ + 'foo' => 1, + '.bar' => 2, + '.baz' => 3, + '0.fizz' => 4, + '1.buzz' => 5, + ], + ]; + } } diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index d937741716e4..a5909975321b 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -179,6 +179,17 @@ public function ifExistProvider() [], true, ], + // Testing for multi-dimensional data + [ + ['foo.bar' => 'if_exist|required'], + ['foo' => ['bar' => '']], + false, + ], + [ + ['foo.bar' => 'if_exist|required'], + ['foo' => []], + true, + ], ]; } @@ -1496,10 +1507,15 @@ public function inListProvider() public function testRequiredWith($field, $check, $expected = false) { $data = [ - 'foo' => 'bar', - 'bar' => 'something', - 'baz' => null, - 'array' => ['nonEmptyField1'=>'value1','nonEmptyField2'=>'value2', 'emptyField1'=>null, 'emptyField2'=>null], + 'foo' => 'bar', + 'bar' => 'something', + 'baz' => null, + 'array' => [ + 'nonEmptyField1' => 'value1', + 'nonEmptyField2' => 'value2', + 'emptyField1' => null, + 'emptyField2' => null, + ], ]; $this->validation->setRules([ @@ -1578,10 +1594,15 @@ public function requiredWithProvider() public function testRequiredWithout($field, $check, $expected = false) { $data = [ - 'foo' => 'bar', - 'bar' => 'something', - 'baz' => null, - 'array' => ['nonEmptyField1'=>'value1','nonEmptyField2'=>'value2', 'emptyField1'=>null, 'emptyField2'=>null], + 'foo' => 'bar', + 'bar' => 'something', + 'baz' => null, + 'array' => [ + 'nonEmptyField1' => 'value1', + 'nonEmptyField2' => 'value2', + 'emptyField1' => null, + 'emptyField2' => null, + ], ]; $this->validation->setRules([ @@ -1636,11 +1657,11 @@ public function requiredWithoutProvider() 'array.nonEmptyField2', true, ], - [ + [ 'array.nonEmptyField1', 'array.nonEmptyField2', true, - ], + ], ]; } diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 52d5bb91386a..a572feff3246 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -444,14 +444,11 @@ public function testRawInput() $config->baseURL = 'http://example.com/'; $request = new IncomingRequest($config, new URI(), $rawstring, new UserAgent()); - $request->setMethod('patch'); $rules = [ 'role' => 'required|min_length[5]', ]; - $this->validation->withRequest($request) - ->run($data); - + $this->validation->withRequest($request->withMethod('patch'))->run($data); $this->assertEquals([], $this->validation->getErrors()); } @@ -472,13 +469,12 @@ public function testJsonInput() $config->baseURL = 'http://example.com/'; $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); - $request->setMethod('patch'); $rules = [ 'role' => 'required|min_length[5]', ]; $validated = $this->validation - ->withRequest($request) + ->withRequest($request->withMethod('patch')) ->setRules($rules) ->run(); @@ -702,11 +698,9 @@ public function testRulesForArrayField($body, $rules, $results) $config->baseURL = 'http://example.com/'; $request = new IncomingRequest($config, new URI(), http_build_query($body), new UserAgent()); - $request->setMethod('post'); $this->validation->setRules($rules); - $this->validation->withRequest($request) - ->run($body); + $this->validation->withRequest($request->withMethod('post'))->run($body); $this->assertEquals($results, $this->validation->getErrors()); } @@ -787,15 +781,13 @@ public function testRulesForSingleRuleWithAsteriskWillReturnNoError() ]; $request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent()); - $request->setMethod('post'); $this->validation->setRules([ 'id_user.*' => 'numeric', 'name_user.*' => 'alpha_numeric', ]); - $this->validation->withRequest($request) - ->run(); + $this->validation->withRequest($request->withMethod('post'))->run(); $this->assertEquals([], $this->validation->getErrors()); } @@ -818,15 +810,13 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError() ]; $request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent()); - $request->setMethod('post'); $this->validation->setRules([ 'id_user.*' => 'numeric', 'name_user.*' => 'alpha', ]); - $this->validation->withRequest($request) - ->run(); + $this->validation->withRequest($request->withMethod('post'))->run(); $this->assertEquals([ 'id_user.*' => 'The id_user.* field must contain only numbers.', 'name_user.*' => 'The name_user.* field may only contain alphabetical characters.', @@ -845,14 +835,12 @@ public function testRulesForSingleRuleWithSingleValue() ]; $request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent()); - $request->setMethod('post'); $this->validation->setRules([ 'id_user' => 'numeric', ]); - $this->validation->withRequest($request) - ->run(); + $this->validation->withRequest($request->withMethod('post'))->run(); $this->assertEquals([ 'id_user' => 'The id_user field must contain only numbers.', ], $this->validation->getErrors()); diff --git a/user_guide_src/source/helpers/array_helper.rst b/user_guide_src/source/helpers/array_helper.rst index 5394ab650ab4..096501858723 100644 --- a/user_guide_src/source/helpers/array_helper.rst +++ b/user_guide_src/source/helpers/array_helper.rst @@ -72,7 +72,7 @@ The following functions are available: :param array $sortColumns: The array keys to sort after and the respective PHP sort flags as an associative array. :returns: Whether sorting was successful or not. - :rtype: boolean + :rtype: bool This method sorts the elements of a multidimensional array by the values of one or more keys in a hierarchical way. Take the following array, that might be returned @@ -111,12 +111,10 @@ The following functions are available: Now sort this array by two keys. Note that the method supports the dot-notation to access values in deeper array levels, but does not support wildcards:: - array_sort_by_multiple_keys($players, - [ - 'team.order' => SORT_ASC, - 'position' => SORT_ASC, - ] - ); + array_sort_by_multiple_keys($players, [ + 'team.order' => SORT_ASC, + 'position' => SORT_ASC, + ]); The ``$players`` array is now sorted by the 'order' value in each players' 'team' subarray. If this value is equal for several players, these players @@ -155,4 +153,56 @@ The following functions are available: In the same way, the method can also handle an array of objects. In the example above it is further possible that each 'player' is represented by an array, while the 'teams' are objects. The method will detect the type of elements in - each nesting level and handle it accordingly. \ No newline at end of file + each nesting level and handle it accordingly. + +.. php:function:: array_flatten_with_dots(iterable $array[, string $id = '']): array + + :param iterable $array: The multidimensional array to flatten + :param string $id: Optional ID to prepend to the outer keys. Used internally for flattening keys. + :rtype: array + :returns: The flattened array + + This function flattens a multidimensional array to a single key-value array by using dots + as separators for the keys. + + :: + + $arrayToFlatten = [ + 'personal' => [ + 'first_name' => 'john', + 'last_name' => 'smith', + 'age' => '26', + 'address' => 'US', + ], + 'other_details' => 'marines officer', + ]; + + $flattened = array_flatten_with_dots($arrayToFlatten); + + On inspection, ``$flattened`` is equal to:: + + [ + 'personal.first_name' => 'john', + 'personal.last_name' => 'smith', + 'personal.age' => '26', + 'personal.address' => 'US', + 'other_details' => 'marines officer', + ]; + + Users may use the ``$id`` parameter on their own, but are not required to do so. + The function uses this parameter internally to track the flattened keys. If users + will be supplying an initial ``$id``, it will be prepended to all keys. + + :: + + // using the same data from above + $flattened = array_flatten_with_dots($arrayToFlatten, 'foo_'); + + // $flattened is now: + [ + 'foo_personal.first_name' => 'john', + 'foo_personal.last_name' => 'smith', + 'foo_personal.age' => '26', + 'foo_personal.address' => 'US', + 'foo_other_details' => 'marines officer', + ];