Browse Source

Implement nested field validators.

This adds support for nested field validators. To better support
model-less forms and the elasticsearch ODM, we will need to validate
nested fields. This set of changes is the smallest change I could make
that implements the basic requirements.

Refs #6496
Mark Story 11 years ago
parent
commit
a86b0c7dba
2 changed files with 61 additions and 6 deletions
  1. 9 6
      src/Validation/Validator.php
  2. 52 0
      tests/TestCase/Validation/ValidatorTest.php

+ 9 - 6
src/Validation/Validator.php

@@ -17,6 +17,7 @@ namespace Cake\Validation;
 use ArrayAccess;
 use Cake\Validation\RulesProvider;
 use Cake\Validation\ValidationSet;
+use Cake\Utility\Hash;
 use Countable;
 use IteratorAggregate;
 
@@ -97,9 +98,11 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
             $requiredMessage = __d('cake', 'This field is required');
             $emptyMessage = __d('cake', 'This field cannot be left empty');
         }
+        $flat = Hash::flatten($data);
 
         foreach ($this->_fields as $name => $field) {
-            $keyPresent = array_key_exists($name, $data);
+            $isPath = strpos($name, '.') !== false;
+            $keyPresent = array_key_exists($name, $isPath ? $flat : $data);
 
             if (!$keyPresent && !$this->_checkPresence($field, $newRecord)) {
                 $errors[$name]['_required'] = isset($this->_presenceMessages[$name])
@@ -107,15 +110,15 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
                     : $requiredMessage;
                 continue;
             }
-
             if (!$keyPresent) {
                 continue;
             }
+            $value = $isPath ? $flat[$name] : $data[$name];
 
             $providers = $this->_providers;
             $context = compact('data', 'newRecord', 'field', 'providers');
             $canBeEmpty = $this->_canBeEmpty($field, $context);
-            $isEmpty = $this->_fieldIsEmpty($data[$name]);
+            $isEmpty = $this->_fieldIsEmpty($value);
 
             if (!$canBeEmpty && $isEmpty) {
                 $errors[$name]['_empty'] = isset($this->_allowEmptyMessages[$name])
@@ -128,7 +131,7 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
                 continue;
             }
 
-            $result = $this->_processRules($name, $field, $data, $newRecord);
+            $result = $this->_processRules($name, $value, $field, $data, $newRecord);
             if ($result) {
                 $errors[$name] = $result;
             }
@@ -547,13 +550,13 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
      *
      * @param string $field The name of the field that is being processed
      * @param ValidationSet $rules the list of rules for a field
+     * @param mixed $value The field value.
      * @param array $data the full data passed to the validator
      * @param bool $newRecord whether is it a new record or an existing one
      * @return array
      */
-    protected function _processRules($field, ValidationSet $rules, $data, $newRecord)
+    protected function _processRules($field, $value, ValidationSet $rules, $data, $newRecord)
     {
-        $value = $data[$field];
         $errors = [];
         // Loading default provider in case there is none
         $this->provider('default');

+ 52 - 0
tests/TestCase/Validation/ValidatorTest.php

@@ -47,6 +47,31 @@ class ValidatorTest extends TestCase
     }
 
     /**
+     * Testing you can add nested field rules
+     *
+     * @return void
+     */
+    public function testAddingNestedRulesToField()
+    {
+        $validator = new Validator;
+        $validator->add('user.username', 'not-blank', ['rule' => 'notBlank']);
+        $this->assertCount(0, $validator->field('user'));
+
+        $set = $validator->field('user.username');
+        $this->assertInstanceOf('Cake\Validation\ValidationSet', $set);
+        $this->assertCount(1, $set);
+
+        $validator->add('user.username', 'letters', ['rule' => 'alphanumeric']);
+        $this->assertCount(2, $set);
+
+        $validator->remove('user.username', 'letters');
+        $this->assertCount(1, $set);
+
+        $validator->requirePresence('user.twitter');
+        $this->assertTrue($validator->field('user.twitter')->isPresenceRequired());
+    }
+
+    /**
      * Tests that calling field will create a default validation set for it
      *
      * @return void
@@ -164,6 +189,33 @@ class ValidatorTest extends TestCase
     }
 
     /**
+     * Test that errors() can work with nested data.
+     *
+     * @return void
+     */
+    public function testErrorsWithNestedFields()
+    {
+        $validator = new Validator;
+        $validator->add('user.username', 'letter', ['rule' => 'alphanumeric']);
+        $validator->add('comments.0.comment', 'letter', ['rule' => 'alphanumeric']);
+
+        $data = [
+            'user' => [
+                'username' => 'is wrong'
+            ],
+            'comments' => [
+                ['comment' => 'is wrong']
+            ]
+        ];
+        $errors = $validator->errors($data);
+        $expected = [
+            'user.username' => ['letter' => 'The provided value is invalid'],
+            'comments.0.comment' => ['letter' => 'The provided value is invalid']
+        ];
+        $this->assertEquals($expected, $errors);
+    }
+
+    /**
      * Tests custom error messages generated when a field presence is required
      *
      * @return void