Browse Source

Add allowPartialNulls flag to existsIn that matches SQLs behavior of composite foreign keys with nullable nulls - set 'allowPartialNulls' true to accept composite foreign keys where one or more nullable columns are null. Ths Retargets #8903 to 3.next - in a clean way

Jonas 9 years ago
parent
commit
2decc4eb16

+ 38 - 1
src/ORM/Rule/ExistsIn.php

@@ -40,14 +40,28 @@ class ExistsIn
     protected $_repository;
 
     /**
+     * Options for the constructor
+     *
+     * @var array
+     */
+    protected $_options = [];
+
+    /**
      * Constructor.
      *
+     * Available option for $options is 'allowPartialNulls' flag.
+     * Set to true to accept composite foreign keys where one or more nullable columns are null.'
+     *
      * @param string|array $fields The field or fields to check existence as primary key.
      * @param object|string $repository The repository where the field will be looked for,
      * or the association name for the repository.
+     * @param array $options The options that modify the rules behavior.
      */
-    public function __construct($fields, $repository)
+    public function __construct($fields, $repository, array $options = [])
     {
+        $options += ['allowPartialNulls' => false];
+        $this->_options = $options;
+
         $this->_fields = (array)$fields;
         $this->_repository = $repository;
     }
@@ -96,6 +110,11 @@ class ExistsIn
             return true;
         }
 
+        if ($this->_options['allowPartialNulls'] === true
+            && $this->_checkPartialSchemaNulls($entity, $source) === true
+        ) {
+            return true;
+        }
         if ($this->_fieldsAreNull($entity, $source)) {
             return true;
         }
@@ -129,4 +148,22 @@ class ExistsIn
         }
         return $nulls === count($this->_fields);
     }
+
+    /**
+     * Check whether there are nullable nulls in at least one part of the foreign key.
+     *
+     * @param \Cake\Datasource\EntityInterface $entity The entity to check.
+     * @param \Cake\ORM\Table $source The table to use schema from.
+     * @return bool
+     */
+    protected function _checkPartialSchemaNulls($entity, $source)
+    {
+        $schema = $source->schema();
+        foreach ($this->_fields as $field) {
+            if ($schema->isNullable($field) === true && $entity->get($field) === null) {
+                return true;
+            }
+        }
+        return false;
+    }
 }

+ 15 - 3
src/ORM/RulesChecker.php

@@ -71,14 +71,26 @@ class RulesChecker extends BaseRulesChecker
      * $rules->add($rules->existsIn('site_id', new SitesTable(), 'Invalid Site'));
      * ```
      *
+     * Available $options are error 'message' and 'allowPartialNulls' flag.
+     * 'message' sets a custom error message.
+     * Set 'allowPartialNulls' to true to accept composite foreign keys where one or more nullable columns are null.'
+     *
      * @param string|array $field The field or list of fields to check for existence by
      * primary key lookup in the other table.
      * @param object|string $table The table name where the fields existence will be checked.
-     * @param string|null $message The error message to show in case the rule does not pass.
+     * @param array|string|null $options List of options or error message string to show in case the rule does not pass.
      * @return callable
      */
-    public function existsIn($field, $table, $message = null)
+    public function existsIn($field, $table, $options = null)
     {
+        if (is_string($options)) {
+            $options = ['message' => $options];
+        }
+
+        $options = (array)$options + ['message' => null];
+        $message = $options['message'];
+        unset($options['message']);
+
         if (!$message) {
             if ($this->_useI18n) {
                 $message = __d('cake', 'This value does not exist');
@@ -88,7 +100,7 @@ class RulesChecker extends BaseRulesChecker
         }
 
         $errorField = is_string($field) ? $field : current($field);
-        return $this->_addError(new ExistsIn($field, $table), '_existsIn', compact('errorField', 'message'));
+        return $this->_addError(new ExistsIn($field, $table, $options), '_existsIn', compact('errorField', 'message'));
     }
 
     /**

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

@@ -32,7 +32,7 @@ class RulesCheckerIntegrationTest extends TestCase
      */
     public $fixtures = [
         'core.articles', 'core.articles_tags', 'core.authors', 'core.tags',
-        'core.special_tags', 'core.categories'
+        'core.special_tags', 'core.categories', 'core.site_articles', 'core.site_authors'    
     ];
 
     /**
@@ -828,6 +828,60 @@ class RulesCheckerIntegrationTest extends TestCase
     }
 
     /**
+     * Tests new allowPartialNulls flag with author id set to null
+     *
+     * @return
+     */
+    public function testExistsInAllowSqlNullsWithParentIdNull()
+    {
+        $entity = new Entity([
+            'id' => 10,
+            'author_id' => null,
+            'site_id' => 1,
+            'name' => 'New Site Article without Author',
+        ]);
+        $table = TableRegistry::get('SiteArticles');
+        $table->belongsTo('SiteAuthors');
+        $rules = $table->rulesChecker();
+
+        $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', ['allowPartialNulls' => true]));
+        $this->assertInstanceOf('Cake\ORM\Entity', $table->save(clone $entity));
+
+        $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', ['allowPartialNulls' => false]));
+        $this->assertFalse($table->save(clone $entity));
+
+        $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors'));
+        $this->assertFalse($table->save(clone $entity));
+    }
+
+    /**
+     * Tests new allowPartialNulls flag with author id set to 1
+     *
+     * @return
+     */
+    public function testExistsInAllowSqlNullsWithParentId1()
+    {
+        $entity = new Entity([
+            'id' => 10,
+            'author_id' => 1,
+            'site_id' => 1,
+            'name' => 'New Site Article with Author',
+        ]);
+        $table = TableRegistry::get('SiteArticles');
+        $table->belongsTo('SiteAuthors');
+        $rules = $table->rulesChecker();
+
+        $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', ['allowPartialNulls' => true]));
+        $this->assertInstanceOf('Cake\ORM\Entity', $table->save(clone $entity));
+
+        $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', ['allowPartialNulls' => false]));
+        $this->assertInstanceOf('Cake\ORM\Entity', $table->save(clone $entity));
+
+        $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors'));
+        $this->assertInstanceOf('Cake\ORM\Entity', $table->save(clone $entity));
+    }
+
+    /**
      * Tests using rules to prevent delete operations
      *
      * @group delete