Skip to content

Commit

Permalink
Add allowPartialNulls flag to existsIn that matches SQLs behavior of …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
Jonas committed May 30, 2016
1 parent b5e77bf commit 2decc4e
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 5 deletions.
39 changes: 38 additions & 1 deletion src/ORM/Rule/ExistsIn.php
Expand Up @@ -39,15 +39,29 @@ 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;
}
Expand Down Expand Up @@ -96,6 +110,11 @@ public function __invoke(EntityInterface $entity, array $options)
return true;
}

if ($this->_options['allowPartialNulls'] === true
&& $this->_checkPartialSchemaNulls($entity, $source) === true
) {
return true;
}
if ($this->_fieldsAreNull($entity, $source)) {
return true;
}
Expand Down Expand Up @@ -129,4 +148,22 @@ protected function _fieldsAreNull($entity, $source)
}
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;
}
}
18 changes: 15 additions & 3 deletions src/ORM/RulesChecker.php
Expand Up @@ -71,14 +71,26 @@ public function isUnique(array $fields, $message = null)
* $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');
Expand All @@ -88,7 +100,7 @@ public function existsIn($field, $table, $message = null)
}

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

/**
Expand Down
56 changes: 55 additions & 1 deletion tests/TestCase/ORM/RulesCheckerIntegrationTest.php
Expand Up @@ -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'
];

/**
Expand Down Expand Up @@ -827,6 +827,60 @@ public function testExistsInErrorWithArrayField()
$this->assertEquals(['_existsIn' => 'This value does not exist'], $entity->errors('author_id'));
}

/**
* 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
*
Expand Down

0 comments on commit 2decc4e

Please sign in to comment.