Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions src/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ class Attribute

protected $key;

protected $humanizedKey;

protected $alias;

protected $validation;
Expand All @@ -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);
}
Expand All @@ -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;
Expand Down Expand Up @@ -97,9 +101,58 @@ public function getKey()
return $this->key;
}

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()
{
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)
Expand Down
17 changes: 17 additions & 0 deletions src/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
2 changes: 1 addition & 1 deletion src/Rules/RequiredIf.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/RequiredUnless.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Same.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
27 changes: 23 additions & 4 deletions src/Validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,25 @@ 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);
continue;
}

$valid = $ruleValidator->check($value);

if ($isEmptyValue AND $this->ruleIsOptional($attribute, $ruleValidator)) {
continue;
}

$valid = $ruleValidator->check($value);

if (!$valid) {
$isValid = false;
Expand Down Expand Up @@ -117,7 +120,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
Expand All @@ -126,9 +129,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;
}
}
Expand Down Expand Up @@ -304,6 +308,7 @@ protected function resolveMessage(Attribute $attribute, $value, Rule $validator)
}
}

// Replace message params
$vars = array_merge($params, [
'attribute' => $alias,
'value' => $value,
Expand All @@ -314,6 +319,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;
}

Expand Down
151 changes: 151 additions & 0 deletions tests/ValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -749,4 +749,155 @@ 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');
}

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'));
}

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'));
}

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'));
}

}