From 2f566bbfc3e043ea7fc1c489d2a1a644bbdd08ff Mon Sep 17 00:00:00 2001 From: Muhammad Syifa Date: Sat, 16 Jun 2018 19:00:53 +0700 Subject: [PATCH 1/7] Enhance error message for array attributes --- src/Attribute.php | 31 +++++++++++++++++++--- src/Helper.php | 17 ++++++++++++ src/Validation.php | 20 ++++++++++++-- tests/ValidatorTest.php | 59 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 6 deletions(-) diff --git a/src/Attribute.php b/src/Attribute.php index 2d28ef4..7a512e4 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -9,8 +9,6 @@ class Attribute protected $key; - protected $humanizedKey; - protected $alias; protected $validation; @@ -21,12 +19,13 @@ class Attribute protected $otherAttributes = []; + protected $keyIndexes = []; + public function __construct(Validation $validation, $key, $alias = null, array $rules = array()) { $this->validation = $validation; $this->alias = $alias; $this->key = $key; - $this->humanizedKey = ucfirst(str_replace('_', ' ', $key)); foreach($rules as $rule) { $this->addRule($rule); } @@ -37,6 +36,11 @@ public function setPrimaryAttribute(Attribute $primaryAttribute) $this->primaryAttribute = $primaryAttribute; } + public function setKeyIndexes(array $keyIndexes) + { + $this->keyIndexes = $keyIndexes; + } + public function getPrimaryAttribute() { return $this->primaryAttribute; @@ -97,9 +101,28 @@ public function getKey() return $this->key; } + public function getKeyIndexes() + { + return $this->keyIndexes; + } + public function getHumanizedKey() { - return $this->humanizedKey; + $primaryAttribute = $this->getPrimaryAttribute(); + $key = str_replace('_', ' ', $this->key); + + // Resolve key from array validation + if ($primaryAttribute) { + $split = explode('.', $key); + $key = implode(' ', array_map(function($word) { + if (is_numeric($word)) { + $word = $word + 1; + } + return Helper::snakeCase($word, ' '); + }, $split)); + } + + return ucfirst($key); } public function setAlias($alias) diff --git a/src/Helper.php b/src/Helper.php index caf4809..77aa788 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -158,4 +158,21 @@ public static function arraySet(&$target, $key, $value, $overwrite = true) return $target; } + /** + * Get snake_case format from given string + * + * @param string $value + * @param string $delimiter + * @return string + */ + public static function snakeCase($value, $delimiter = '_') + { + if (! ctype_lower($value)) { + $value = preg_replace('/\s+/u', '', ucwords($value)); + $value = strtolower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $value)); + } + + return $value; + } + } diff --git a/src/Validation.php b/src/Validation.php index 0399edb..214ff39 100644 --- a/src/Validation.php +++ b/src/Validation.php @@ -117,7 +117,7 @@ protected function parseArrayAttribute(Attribute $attribute) $attributeKey = $attribute->getKey(); $data = Helper::arrayDot($this->initializeAttributeOnData($attributeKey)); - $pattern = str_replace('\*', '[^\.]+', preg_quote($attributeKey)); + $pattern = str_replace('\*', '([^\.]+)', preg_quote($attributeKey)); $data = array_merge($data, $this->extractValuesForWildcards( $data, $attributeKey @@ -126,9 +126,10 @@ protected function parseArrayAttribute(Attribute $attribute) $attributes = []; foreach ($data as $key => $value) { - if ((bool) preg_match('/^'.$pattern.'\z/', $key)) { + if ((bool) preg_match('/^'.$pattern.'\z/', $key, $match)) { $attr = new Attribute($this, $key, null, $attribute->getRules()); $attr->setPrimaryAttribute($attribute); + $attr->setKeyIndexes(array_slice($match, 1)); $attributes[] = $attr; } } @@ -304,6 +305,7 @@ protected function resolveMessage(Attribute $attribute, $value, Rule $validator) } } + // Replace message params $vars = array_merge($params, [ 'attribute' => $alias, 'value' => $value, @@ -314,6 +316,20 @@ protected function resolveMessage(Attribute $attribute, $value, Rule $validator) $message = str_replace(':'.$key, $value, $message); } + // Replace key indexes + $keyIndexes = $attribute->getKeyIndexes(); + foreach ($keyIndexes as $pathIndex => $index) { + $replacers = [ + "[{$pathIndex}]" => $index, + ]; + + if (is_numeric($index)) { + $replacers["{{$pathIndex}}"] = $index + 1; + } + + $message = str_replace(array_keys($replacers), array_values($replacers), $message); + } + return $message; } diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 6cfedd9..656b4f8 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -749,4 +749,63 @@ public function testUsingDefaults() 'is_published' => 'invalid-value', ]); } + + public function testHumanizedKeyInArrayValidation() + { + $validation = $this->validator->validate([ + 'cart' => [ + [ + 'qty' => 'xyz', + ], + ] + ], [ + 'cart.*.itemName' => 'required', + 'cart.*.qty' => 'required|numeric' + ]); + + $errors = $validation->errors(); + + $this->assertEquals($errors->first('cart.*.qty'), 'The Cart 1 qty must be numeric'); + $this->assertEquals($errors->first('cart.*.itemName'), 'The Cart 1 item name is required'); + } + + public function testCustomMessageInArrayValidation() + { + $validation = $this->validator->make([ + 'cart' => [ + [ + 'qty' => 'xyz', + 'itemName' => 'Lorem ipsum' + ], + [ + 'qty' => 10, + 'attributes' => [ + [ + 'name' => 'color', + 'value' => null + ] + ] + ], + ] + ], [ + 'cart.*.itemName' => 'required', + 'cart.*.qty' => 'required|numeric', + 'cart.*.attributes.*.value' => 'required' + ]); + + $validation->setMessages([ + 'cart.*.itemName:required' => 'Item [0] name is required', + 'cart.*.qty:numeric' => 'Item {0} qty is not a number', + 'cart.*.attributes.*.value' => 'Item {0} attribute {1} value is required', + ]); + + $validation->validate(); + + $errors = $validation->errors(); + + $this->assertEquals($errors->first('cart.*.qty'), 'Item 1 qty is not a number'); + $this->assertEquals($errors->first('cart.*.itemName'), 'Item 1 name is required'); + $this->assertEquals($errors->first('cart.*.attributes.*.value'), 'Item 2 attribute 1 value is required'); + } + } From 982f2e9342f664d2dc4dc4a1633d8de08bbbac5c Mon Sep 17 00:00:00 2001 From: Muhammad Syifa Date: Sat, 16 Jun 2018 19:45:34 +0700 Subject: [PATCH 2/7] Fix callback rule not called when attribute is not required --- src/Validation.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Validation.php b/src/Validation.php index 214ff39..101fec0 100644 --- a/src/Validation.php +++ b/src/Validation.php @@ -73,6 +73,8 @@ protected function validateAttribute(Attribute $attribute) $attributeKey = $attribute->getKey(); $rules = $attribute->getRules(); + + $value = $this->getValue($attributeKey); $isEmptyValue = $this->isEmptyValue($value); @@ -84,11 +86,11 @@ protected function validateAttribute(Attribute $attribute) continue; } + $valid = $ruleValidator->check($value); + if ($isEmptyValue AND $this->ruleIsOptional($attribute, $ruleValidator)) { continue; } - - $valid = $ruleValidator->check($value); if (!$valid) { $isValid = false; From 619a2b36235b1c279351b98dedb6518a490e8b55 Mon Sep 17 00:00:00 2001 From: Muhammad Syifa Date: Sat, 16 Jun 2018 21:01:14 +0700 Subject: [PATCH 3/7] Make sure rule using validated attribute --- src/Validation.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Validation.php b/src/Validation.php index 101fec0..5c12623 100644 --- a/src/Validation.php +++ b/src/Validation.php @@ -74,12 +74,13 @@ protected function validateAttribute(Attribute $attribute) $attributeKey = $attribute->getKey(); $rules = $attribute->getRules(); - $value = $this->getValue($attributeKey); $isEmptyValue = $this->isEmptyValue($value); $isValid = true; foreach($rules as $ruleValidator) { + $ruleValidator->setAttribute($attribute); + if ($isEmptyValue && $ruleValidator instanceof Defaults) { $value = $ruleValidator->check(null); $isEmptyValue = $this->isEmptyValue($value); From d05b52a3fb4b71d1eab0ddb525bc0fa13bb71385 Mon Sep 17 00:00:00 2001 From: Muhammad Syifa Date: Sat, 16 Jun 2018 21:01:53 +0700 Subject: [PATCH 4/7] Add some methods to get array attribute sibling key and value --- src/Attribute.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Attribute.php b/src/Attribute.php index 7a512e4..2faa0f4 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -106,6 +106,36 @@ public function getKeyIndexes() return $this->keyIndexes; } + public function getValue($key = null) + { + if ($key && $this->isArrayAttribute()) { + $key = $this->resolveSiblingKey($key); + } + + if (!$key) { + $key = $this->getKey(); + } + + return $this->validation->getValue($key); + } + + public function isArrayAttribute() + { + return count($this->getKeyIndexes()) > 0; + } + + public function resolveSiblingKey($key) + { + $indexes = $this->getKeyIndexes(); + $keys = explode("*", $key); + $countAsterisks = count($keys) - 1; + if (count($indexes) < $countAsterisks) { + $indexes = array_merge($indexes, array_fill(0, $countAsterisks - count($indexes), "*")); + } + $args = array_merge([str_replace("*", "%s", $key)], $indexes); + return call_user_func_array('sprintf', $args); + } + public function getHumanizedKey() { $primaryAttribute = $this->getPrimaryAttribute(); From f02d8d58a19468a2f03b28b1aa35692ac1a1bec6 Mon Sep 17 00:00:00 2001 From: Muhammad Syifa Date: Sat, 16 Jun 2018 21:02:48 +0700 Subject: [PATCH 5/7] Fix required_if on array attribute --- src/Rules/RequiredIf.php | 2 +- tests/ValidatorTest.php | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Rules/RequiredIf.php b/src/Rules/RequiredIf.php index 31289eb..586f9df 100644 --- a/src/Rules/RequiredIf.php +++ b/src/Rules/RequiredIf.php @@ -23,7 +23,7 @@ public function check($value) $anotherAttribute = $this->parameter('field'); $definedValues = $this->parameter('values'); - $anotherValue = $this->validation->getValue($anotherAttribute); + $anotherValue = $this->getAttribute()->getValue($anotherAttribute); $validator = $this->validation->getValidator(); $required_validator = $validator('required'); diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 656b4f8..b5afcc9 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -808,4 +808,38 @@ public function testCustomMessageInArrayValidation() $this->assertEquals($errors->first('cart.*.attributes.*.value'), 'Item 2 attribute 1 value is required'); } + public function testRequiredIfOnArrayAttribute() + { + $validation = $this->validator->validate([ + 'products' => [ + // invalid because has_notes is not empty + '10' => [ + 'quantity' => 8, + 'has_notes' => 1, + 'notes' => '' + ], + // valid because has_notes is null + '12' => [ + 'quantity' => 0, + 'has_notes' => null, + 'notes' => '' + ], + // valid because no has_notes + '14' => [ + 'quantity' => 0, + 'notes' => '' + ], + ] + ], [ + 'products.*.notes' => 'required_if:products.*.has_notes,1', + ]); + + $this->assertFalse($validation->passes()); + + $errors = $validation->errors(); + $this->assertNotNull($errors->first('products.10.notes')); + $this->assertNull($errors->first('products.12.notes')); + $this->assertNull($errors->first('products.14.notes')); + } + } From f091bf6b8adbd753067f84a4277dc360a2c87908 Mon Sep 17 00:00:00 2001 From: Muhammad Syifa Date: Sat, 16 Jun 2018 21:04:38 +0700 Subject: [PATCH 6/7] Fix required_unless on array attribute --- src/Rules/RequiredUnless.php | 2 +- tests/ValidatorTest.php | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Rules/RequiredUnless.php b/src/Rules/RequiredUnless.php index 7314500..ef4cbec 100644 --- a/src/Rules/RequiredUnless.php +++ b/src/Rules/RequiredUnless.php @@ -23,7 +23,7 @@ public function check($value) $anotherAttribute = $this->parameter('field'); $definedValues = $this->parameter('values'); - $anotherValue = $this->validation->getValue($anotherAttribute); + $anotherValue = $this->getAttribute()->getValue($anotherAttribute); $validator = $this->validation->getValidator(); $required_validator = $validator('required'); diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index b5afcc9..088edc1 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -842,4 +842,38 @@ public function testRequiredIfOnArrayAttribute() $this->assertNull($errors->first('products.14.notes')); } + public function testRequiredUnlessOnArrayAttribute() + { + $validation = $this->validator->validate([ + 'products' => [ + // valid because has_notes is 1 + '10' => [ + 'quantity' => 8, + 'has_notes' => 1, + 'notes' => '' + ], + // invalid because has_notes is not 1 + '12' => [ + 'quantity' => 0, + 'has_notes' => null, + 'notes' => '' + ], + // invalid because no has_notes + '14' => [ + 'quantity' => 0, + 'notes' => '' + ], + ] + ], [ + 'products.*.notes' => 'required_unless:products.*.has_notes,1', + ]); + + $this->assertFalse($validation->passes()); + + $errors = $validation->errors(); + $this->assertNull($errors->first('products.10.notes')); + $this->assertNotNull($errors->first('products.12.notes')); + $this->assertNotNull($errors->first('products.14.notes')); + } + } From 3289d7dc33164da8f2822068fec42e3dfea244d0 Mon Sep 17 00:00:00 2001 From: Muhammad Syifa Date: Sat, 16 Jun 2018 21:08:12 +0700 Subject: [PATCH 7/7] Fix rule same on array attribute --- src/Rules/Same.php | 2 +- tests/ValidatorTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Rules/Same.php b/src/Rules/Same.php index ac92fc7..508071b 100644 --- a/src/Rules/Same.php +++ b/src/Rules/Same.php @@ -16,7 +16,7 @@ public function check($value) $this->requireParameters($this->fillable_params); $field = $this->parameter('field'); - $anotherValue = $this->validation->getValue($field); + $anotherValue = $this->getAttribute()->getValue($field); return $value == $anotherValue; } diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 088edc1..0b8068b 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -876,4 +876,28 @@ public function testRequiredUnlessOnArrayAttribute() $this->assertNotNull($errors->first('products.14.notes')); } + public function testSameRuleOnArrayAttribute() + { + $validation = $this->validator->validate([ + 'users' => [ + [ + 'password' => 'foo', + 'password_confirmation' => 'foo' + ], + [ + 'password' => 'foo', + 'password_confirmation' => 'bar' + ], + ] + ], [ + 'users.*.password_confirmation' => 'required|same:users.*.password', + ]); + + $this->assertFalse($validation->passes()); + + $errors = $validation->errors(); + $this->assertNull($errors->first('users.0.password_confirmation:same')); + $this->assertNotNull($errors->first('users.1.password_confirmation:same')); + } + }