Browse Source

Merge pull request #7951 from cakephp/3.2-entity-invalid

Allow invalid field data to be kept along with the error messages in the Entity
José Lorenzo Rodríguez 10 years ago
parent
commit
93bdcc6ca0

+ 48 - 1
src/Datasource/EntityTrait.php

@@ -96,6 +96,13 @@ trait EntityTrait
     protected $_errors = [];
 
     /**
+     * List of invalid fields and their data for errors upon validation/patching
+     *
+     * @var array
+     */
+    protected $_invalid = [];
+
+    /**
      * Map of properties in this entity that can be safely assigned, each
      * property name points to a boolean indicating its status. An empty array
      * means no properties are accessible
@@ -600,7 +607,7 @@ trait EntityTrait
         }
 
         $this->_dirty[$property] = true;
-        unset($this->_errors[$property]);
+        unset($this->_errors[$property], $this->_invalid[$property]);
         return true;
     }
 
@@ -615,6 +622,7 @@ trait EntityTrait
     {
         $this->_dirty = [];
         $this->_errors = [];
+        $this->_invalid = [];
     }
 
     /**
@@ -780,6 +788,44 @@ trait EntityTrait
     }
 
     /**
+     * Sets a field as invalid and not patchable into the entity.
+     *
+     * This is useful for batch operations when one needs to get the original value for an error message after patching.
+     * This value could not be patched into the entity and is simply copied into the _invalid property for debugging purposes
+     * or to be able to log it away.
+     *
+     * @param string|array|null $field The field to get invalid value for, or the value to set.
+     * @param mixed|null $value The invalid value to be set for $field.
+     * @param bool $overwrite Whether or not to overwrite pre-existing values for $field.
+     * @return $this|mixed
+     */
+    public function invalid($field = null, $value = null, $overwrite = false)
+    {
+        if ($field === null) {
+            return $this->_invalid;
+        }
+
+        if (is_string($field) && $value === null) {
+            $value = isset($this->_invalid[$field]) ? $this->_invalid[$field] : null;
+            return $value;
+        }
+
+        if (!is_array($field)) {
+            $field = [$field => $value];
+        }
+
+        foreach ($field as $f => $value) {
+            if ($overwrite) {
+                $this->_invalid[$f] = $value;
+                continue;
+            }
+            $this->_invalid += [$f => $value];
+        }
+
+        return $this;
+    }
+
+    /**
      * Stores whether or not a property value can be changed or set in this entity.
      * The special property `*` can also be marked as accessible or protected, meaning
      * that any other property specified before will take its value. For example
@@ -881,6 +927,7 @@ trait EntityTrait
             '[original]' => $this->_original,
             '[virtual]' => $this->_virtual,
             '[errors]' => $this->_errors,
+            '[invalid]' => $this->_invalid,
             '[repository]' => $this->_registryAlias
         ];
     }

+ 36 - 0
src/Datasource/InvalidPropertyInterface.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.2.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Datasource;
+
+/**
+ * Describes the methods that any class representing a data storage should
+ * comply with.
+ */
+interface InvalidPropertyInterface
+{
+    /**
+     * Sets a field as invalid and not patchable into the entity.
+     *
+     * This is useful for batch operations when one needs to get the original value for an error message after patching.
+     * This value could not be patched into the entity and is simply copied into the _invalid property for debugging purposes
+     * or to be able to log it away.
+     *
+     * @param string|array|null $field The field to get invalid value for, or the value to set.
+     * @param mixed|null $value The invalid value to be set for $field.
+     * @param bool $overwrite Whether or not to overwrite pre-existing values for $field.
+     * @return $this|mixed
+     */
+    public function invalid($field = null, $value = null, $overwrite = false);
+}

+ 7 - 0
src/Datasource/RulesChecker.php

@@ -14,6 +14,7 @@
  */
 namespace Cake\Datasource;
 
+use Cake\Datasource\InvalidPropertyInterface;
 use InvalidArgumentException;
 
 /**
@@ -331,6 +332,12 @@ class RulesChecker
                 $message = [$message];
             }
             $entity->errors($options['errorField'], $message);
+
+            if ($entity instanceof InvalidPropertyInterface && isset($entity->{$options['errorField']})) {
+                $invalidValue = $entity->{$options['errorField']};
+                $entity->invalid($options['errorField'], $invalidValue);
+            }
+
             return $pass === true;
         };
     }

+ 2 - 2
src/ORM/Entity.php

@@ -16,14 +16,14 @@ namespace Cake\ORM;
 
 use Cake\Datasource\EntityInterface;
 use Cake\Datasource\EntityTrait;
+use Cake\Datasource\InvalidPropertyInterface;
 
 /**
  * An entity represents a single result row from a repository. It exposes the
  * methods for retrieving and storing properties associated in this row.
  */
-class Entity implements EntityInterface
+class Entity implements EntityInterface, InvalidPropertyInterface
 {
-
     use EntityTrait;
 
     /**

+ 7 - 0
src/ORM/Marshaller.php

@@ -19,6 +19,7 @@ use Cake\Collection\Collection;
 use Cake\Database\Expression\TupleComparison;
 use Cake\Database\Type;
 use Cake\Datasource\EntityInterface;
+use Cake\Datasource\InvalidPropertyInterface;
 use RuntimeException;
 
 /**
@@ -128,6 +129,9 @@ class Marshaller
         $properties = [];
         foreach ($data as $key => $value) {
             if (!empty($errors[$key])) {
+                if ($entity instanceof InvalidPropertyInterface) {
+                    $entity->invalid($key, $value);
+                }
                 continue;
             }
             $columnType = $schema->columnType($key);
@@ -463,6 +467,9 @@ class Marshaller
         $properties = $marshalledAssocs = [];
         foreach ($data as $key => $value) {
             if (!empty($errors[$key])) {
+                if ($entity instanceof InvalidPropertyInterface) {
+                    $entity->invalid($key, $value);
+                }
                 continue;
             }
 

+ 2 - 0
tests/TestCase/ORM/EntityTest.php

@@ -1205,6 +1205,7 @@ class EntityTest extends TestCase
         $entity->virtualProperties(['baz']);
         $entity->dirty('foo', true);
         $entity->errors('foo', ['An error']);
+        $entity->invalid('foo', 'a value');
         $entity->source('foos');
         $result = $entity->__debugInfo();
         $expected = [
@@ -1216,6 +1217,7 @@ class EntityTest extends TestCase
             '[original]' => [],
             '[virtual]' => ['baz'],
             '[errors]' => ['foo' => ['An error']],
+            '[invalid]' => ['foo' => 'a value'],
             '[repository]' => 'foos'
         ];
         $this->assertSame($expected, $result);

+ 23 - 0
tests/TestCase/ORM/MarshallerTest.php

@@ -2524,6 +2524,25 @@ class MarshallerTest extends TestCase
     }
 
     /**
+     * Tests that invalid property is being filled when data cannot be patched into an entity.
+     *
+     * @return void
+     */
+    public function testValidationWithInvalidFilled()
+    {
+        $data = [
+            'title' => 'foo',
+            'number' => 'bar',
+        ];
+        $validator = (new Validator)->add('number', 'numeric', ['rule' => 'numeric']);
+        $marshall = new Marshaller($this->articles);
+        $entity = $marshall->one($data, ['validate' => $validator]);
+        $this->assertNotEmpty($entity->errors('number'));
+        $this->assertNull($entity->number);
+        $this->assertSame(['number' => 'bar'], $entity->invalid());
+    }
+
+    /**
      * Test merge with validation error
      *
      * @return void
@@ -2541,6 +2560,8 @@ class MarshallerTest extends TestCase
             'body' => 'My Content',
             'author_id' => 1
         ]);
+        $this->assertEmpty($entity->invalid());
+
         $entity->accessible('*', true);
         $entity->isNew(false);
         $entity->clean();
@@ -2561,7 +2582,9 @@ class MarshallerTest extends TestCase
 
         $this->articles->validator()->requirePresence('thing', 'create');
         $result = $marshall->merge($entity, $data, []);
+
         $this->assertEmpty($result->errors('thing'));
+        $this->assertSame(['author_id' => 'foo'], $result->invalid());
     }
 
     /**

+ 1 - 0
tests/TestCase/ORM/RulesCheckerIntegrationTest.php

@@ -116,6 +116,7 @@ class RulesCheckerIntegrationTest extends TestCase
         $this->assertNull($entity->article->get('author_id'));
         $this->assertFalse($entity->article->dirty('author_id'));
         $this->assertNotEmpty($entity->article->errors('title'));
+        $this->assertSame('A Title', $entity->article->invalid('title'));
     }
 
     /**