Browse Source

Merge pull request #3572 from cakephp/3.0-table-is-unique

3.0 table isUnique
José Lorenzo Rodríguez 12 years ago
parent
commit
b4c49db982

+ 59 - 0
src/ORM/Table.php

@@ -1754,6 +1754,65 @@ class Table implements RepositoryInterface, EventListener {
 	}
 
 /**
+ * Validator method used to check the uniqueness of a value for a column.
+ * This is meant to be used with the validation API and not to be called
+ * directly.
+ *
+ * ### Example:
+ *
+ * {{{
+ * $validator->add('email', [
+ *	'unique' => ['rule' => 'validateUnique', 'provider' => 'table']
+ * ])
+ * }}}
+ *
+ * Unique validation can be scoped to the value of another column:
+ *
+ * {{{
+ * $validator->add('email', [
+ *	'unique' => [
+ *		'rule' => ['validateUnique', ['scope' => 'site_id']],
+ *		'provider' => 'table'
+ *	]
+ * ]);
+ * }}}
+ *
+ * In the above example, the email uniqueness will be scoped to only rows having
+ * the same site_id. Scoping will only be used if the scoping field is present in
+ * the data to be validated.
+ *
+ * @param mixed $value The value of column to be checked for uniqueness
+ * @param array $options The options array, optionally containing the 'scope' key
+ * @param array $context The validation context as provided by the validation routine
+ * @return boolean true if the value is unique
+ */
+	public function validateUnique($value, array $options, array $context = []) {
+		if (empty($context)) {
+			$context = $options;
+		}
+
+		$conditions = [$context['field'] => $value];
+		if (!empty($options['scope']) && isset($context['data'][$options['scope']])) {
+			$scope = $options['scope'];
+			$scopedValue = $context['data'][$scope];
+			$conditions[$scope] = $scopedValue;
+		}
+
+		if (!$context['newRecord']) {
+			$keys = (array)$this->primaryKey();
+			$not = [];
+			foreach ($keys as $key) {
+				if (isset($context['data'][$key])) {
+					$not[$key] = $context['data'][$key];
+				}
+			}
+			$conditions['NOT'] = $not;
+		}
+
+		return !$this->exists($conditions);
+	}
+
+/**
  * Get the Model callbacks this table is interested in.
  *
  * By implementing the conventional methods a table class is assumed

+ 1 - 0
src/Validation/ValidationRule.php

@@ -101,6 +101,7 @@ class ValidationRule {
  * - newRecord: (boolean) whether or not the data to be validated belongs to a
  *   new record
  * - data: The full data that was passed to the validation process
+ * - field: The name of the field that is being processed
  * @return bool|string
  * @throws \InvalidArgumentException when the supplied rule is not a valid
  * callable for the configured scope

+ 5 - 4
src/Validation/Validator.php

@@ -100,7 +100,7 @@ class Validator implements \ArrayAccess, \IteratorAggregate, \Countable {
 				continue;
 			}
 
-			$result = $this->_processRules($field, $data[$name], $data, $newRecord);
+			$result = $this->_processRules($name, $field, $data, $newRecord);
 			if ($result) {
 				$errors[$name] = $result;
 			}
@@ -434,18 +434,19 @@ class Validator implements \ArrayAccess, \IteratorAggregate, \Countable {
  * Iterates over each rule in the validation set and collects the errors resulting
  * from executing them
  *
+ * @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 value to be checked
  * @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(ValidationSet $rules, $value, $data, $newRecord) {
+	protected function _processRules($field, ValidationSet $rules, $data, $newRecord) {
+		$value = $data[$field];
 		$errors = [];
 		// Loading default provider in case there is none
 		$this->provider('default');
 		foreach ($rules as $name => $rule) {
-			$result = $rule->process($value, $this->_providers, compact('newRecord', 'data'));
+			$result = $rule->process($value, $this->_providers, compact('newRecord', 'data', 'field'));
 			if ($result === true) {
 				continue;
 			}

+ 104 - 0
tests/TestCase/ORM/ValidationIntegrationTest.php

@@ -0,0 +1,104 @@
+<?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.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\ORM;
+
+use Cake\ORM\Entity;
+use Cake\ORM\TableRegistry;
+use Cake\TestSuite\TestCase;
+
+/**
+ * Tests the integration between the ORM and the validator
+ */
+class ValidationIntegrationTest extends TestCase {
+
+/**
+ * Fixtures to be loaded
+ *
+ * @var array
+ */
+	public $fixtures = ['core.article'];
+
+/**
+ * Tear down
+ *
+ * @return void
+ */
+	public function tearDown() {
+		parent::tearDown();
+		TableRegistry::clear();
+	}
+
+/**
+ * Tests that Table::validateUnique handle validation params correctly
+ *
+ * @return void
+ */
+	public function testValidateUnique() {
+		$articles = TableRegistry::get('articles');
+		$articles->validator()->add('title', [
+			'unique' => [
+				'rule' => 'validateUnique',
+				'provider' => 'table',
+				'message' => 'Y U NO WRITE UNIQUE?'
+			]
+		]);
+		$entity = new Entity(['title' => 'First Article', 'body' => 'Foo']);
+		$this->assertFalse($articles->validate($entity));
+		$this->assertEquals('Y U NO WRITE UNIQUE?', $entity->errors()['title']['unique']);
+
+		$entity = new Entity(['title' => 'New Article', 'body' => 'Foo']);
+		$this->assertTrue($articles->validate($entity));
+	}
+
+/**
+ * Tests that validateUnique can be scoped to another field in the provided data
+ *
+ * @return void
+ */
+	public function testValidateUniqueWithScope() {
+		$articles = TableRegistry::get('articles');
+		$articles->validator()->add('title', [
+			'unique' => [
+				'rule' => ['validateUnique', ['scope' => 'published']],
+				'provider' => 'table'
+			]
+		]);
+		$entity = new Entity(['title' => 'First Article', 'published' => 'N']);
+		$this->assertTrue($articles->validate($entity));
+
+		$entity->published = 'Y';
+		$this->assertFalse($articles->validate($entity));
+	}
+
+/**
+ * Tests that uniqueness validation excludes the same record when it exists
+ *
+ * @return void
+ */
+	public function testValidateUniqueUpdate() {
+		$articles = TableRegistry::get('articles');
+		$articles->validator()->add('title', [
+			'unique' => [
+				'rule' => 'validateUnique',
+				'provider' => 'table'
+			]
+		]);
+		$entity = new Entity(['id' => 1, 'title' => 'First Article'], ['markNew' => false]);
+		$this->assertTrue($articles->validate($entity));
+
+		$entity = new Entity(['id' => 2, 'title' => 'First Article'], ['markNew' => false]);
+		$this->assertFalse($articles->validate($entity));
+	}
+}

+ 4 - 2
tests/TestCase/Validation/ValidatorTest.php

@@ -406,7 +406,8 @@ class ValidatorTest extends \Cake\TestSuite\TestCase {
 					'data' => [
 						'email' => '!',
 						'title' => 'bar'
-					]
+					],
+					'field' => 'title'
 				];
 				$this->assertEquals($expected, $context);
 				return "That ain't cool, yo";
@@ -449,7 +450,8 @@ class ValidatorTest extends \Cake\TestSuite\TestCase {
 					'data' => [
 						'email' => '!',
 						'title' => 'bar'
-					]
+					],
+					'field' => 'title'
 				];
 				$this->assertEquals($expected, $context);
 				return "That ain't cool, yo";