Commit 252b6c9e by Qiang Xue

Fixes #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator`

parent 604d6671
......@@ -16,6 +16,7 @@ Yii Framework 2 Change Log
- Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark)
- Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe)
- Bug: Fixed issue with tabular input on ActiveField::radio() and ActiveField::checkbox() (jom)
- Enh #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` (qiangxue)
- Enh #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe)
- Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue)
- Enh #1437: Added ListView::viewParams (qiangxue)
......
......@@ -30,10 +30,25 @@ class ExistValidator extends Validator
*/
public $className;
/**
* @var string the yii\db\ActiveRecord class attribute name that should be
* @var string|array the ActiveRecord class attribute name that should be
* used to look for the attribute value being validated. Defaults to null,
* meaning using the name of the attribute being validated.
* @see className
* meaning using the name of the attribute being validated. Use a string
* to specify the attribute that is different from the attribute being validated
* (often used together with [[className]]). Use an array to validate the existence about
* multiple columns. For example,
*
* ```php
* // a1 needs to exist
* array('a1', 'exist')
* // a1 needs to exist, but its value will use a2 to check for the existence
* array('a1', 'exist', 'attributeName' => 'a2')
* // a1 and a2 need to exist together, and they both will receive error message
* array('a1, a2', 'exist', 'attributeName' => array('a1', 'a2'))
* // a1 and a2 need to exist together, only a1 will receive error message
* array('a1', 'exist', 'attributeName' => array('a1', 'a2'))
* // a1 and a2 need to exist together, a2 will take value 10, only a1 will receive error message
* array('a1', 'exist', 'attributeName' => array('a1', 'a2' => 10))
* ```
*/
public $attributeName;
......@@ -64,9 +79,7 @@ class ExistValidator extends Validator
/** @var \yii\db\ActiveRecordInterface $className */
$className = $this->className === null ? get_class($object) : $this->className;
$attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
$query = $className::find();
$query->where([$attributeName => $value]);
if (!$query->exists()) {
if (!$this->exists($className, $attributeName, $object, $value)) {
$this->addError($object, $attribute, $this->message);
}
}
......@@ -85,10 +98,33 @@ class ExistValidator extends Validator
if ($this->attributeName === null) {
throw new InvalidConfigException('The "attributeName" property must be set.');
}
return $this->exists($this->className, $this->attributeName, null, $value) ? null : [$this->message, []];
}
/**
* Performs existence check.
* @param string $className the AR class name to be checked against
* @param string|array $attributeName the attribute(s) to be checked
* @param \yii\db\ActiveRecordInterface $object the object whose value is being validated
* @param mixed $value the attribute value currently being validated
* @return boolean whether the data being validated exists in the database already
*/
protected function exists($className, $attributeName, $object, $value)
{
/** @var \yii\db\ActiveRecordInterface $className */
$className = $this->className;
$query = $className::find();
$query->where([$this->attributeName => $value]);
return $query->exists() ? null : [$this->message, []];
if (is_array($attributeName)) {
$params = [];
foreach ($attributeName as $k => $v) {
if (is_integer($k)) {
$params[$v] = $this->className === null && $object !== null ? $object->$v : $value;
} else {
$params[$k] = $v;
}
}
} else {
$params = [$attributeName => $value];
}
return $query->where($params)->exists();
}
}
......@@ -26,9 +26,25 @@ class UniqueValidator extends Validator
*/
public $className;
/**
* @var string the ActiveRecord class attribute name that should be
* @var string|array the ActiveRecord class attribute name that should be
* used to look for the attribute value being validated. Defaults to null,
* meaning using the name of the attribute being validated.
* meaning using the name of the attribute being validated. Use a string
* to specify the attribute that is different from the attribute being validated
* (often used together with [[className]]). Use an array to validate uniqueness about
* multiple columns. For example,
*
* ```php
* // a1 needs to be unique
* array('a1', 'unique')
* // a1 needs to be unique, but its value will use a2 to check for the uniqueness
* array('a1', 'unique', 'attributeName' => 'a2')
* // a1 and a2 need to unique together, and they both will receive error message
* array('a1, a2', 'unique', 'attributeName' => array('a1', 'a2'))
* // a1 and a2 need to unique together, only a1 will receive error message
* array('a1', 'unique', 'attributeName' => array('a1', 'a2'))
* // a1 and a2 need to unique together, a2 will take value 10, only a1 will receive error message
* array('a1', 'unique', 'attributeName' => array('a1', 'a2' => 10))
* ```
*/
public $attributeName;
......@@ -60,7 +76,20 @@ class UniqueValidator extends Validator
$attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
$query = $className::find();
$query->where([$attributeName => $value]);
if (is_array($attributeName)) {
$params = [];
foreach ($attributeName as $k => $v) {
if (is_integer($k)) {
$params[$v] = $this->className === null ? $object->$v : $value;
} else {
$params[$k] = $v;
}
}
} else {
$params = [$attributeName => $value];
}
$query->where($params);
if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) {
// if current $object isn't in the database yet then it's OK just to call exists()
......@@ -71,7 +100,11 @@ class UniqueValidator extends Validator
$objects = $query->limit(2)->all();
$n = count($objects);
if ($n === 1) {
if (in_array($attributeName, $className::primaryKey())) {
$keys = array_keys($params);
$pks = $className::primaryKey();
sort($keys);
sort($pks);
if ($keys === $pks) {
// primary key is modified and not unique
$exists = $object->getOldPrimaryKey() != $object->getPrimaryKey();
} else {
......
......@@ -7,6 +7,8 @@ use Yii;
use yii\base\Exception;
use yii\validators\ExistValidator;
use yiiunit\data\ar\ActiveRecord;
use yiiunit\data\ar\Order;
use yiiunit\data\ar\OrderItem;
use yiiunit\data\validators\models\ValidatorTestMainModel;
use yiiunit\data\validators\models\ValidatorTestRefModel;
use yiiunit\framework\db\DatabaseTestCase;
......@@ -92,4 +94,44 @@ class ExistValidatorTest extends DatabaseTestCase
$val->validateAttribute($m, 'test_val');
$this->assertTrue($m->hasErrors('test_val'));
}
public function testValidateCompositeKeys()
{
$val = new ExistValidator([
'className' => OrderItem::className(),
'attributeName' => ['order_id', 'item_id'],
]);
// validate old record
$m = OrderItem::find(['order_id' => 1, 'item_id' => 2]);
$val->validateAttribute($m, 'order_id');
$this->assertFalse($m->hasErrors('order_id'));
// validate new record
$m = new OrderItem(['order_id' => 1, 'item_id' => 2]);
$val->validateAttribute($m, 'order_id');
$this->assertFalse($m->hasErrors('order_id'));
$m = new OrderItem(['order_id' => 10, 'item_id' => 2]);
$val->validateAttribute($m, 'order_id');
$this->assertTrue($m->hasErrors('order_id'));
$val = new ExistValidator([
'className' => OrderItem::className(),
'attributeName' => ['order_id', 'item_id' => 2],
]);
// validate old record
$m = Order::find(1);
$val->validateAttribute($m, 'id');
$this->assertFalse($m->hasErrors('id'));
$m = Order::find(1);
$m->id = 10;
$val->validateAttribute($m, 'id');
$this->assertTrue($m->hasErrors('id'));
$m = new Order(['id' => 1]);
$val->validateAttribute($m, 'id');
$this->assertFalse($m->hasErrors('id'));
$m = new Order(['id' => 10]);
$val->validateAttribute($m, 'id');
$this->assertTrue($m->hasErrors('id'));
}
}
......@@ -6,6 +6,8 @@ namespace yiiunit\framework\validators;
use yii\validators\UniqueValidator;
use Yii;
use yiiunit\data\ar\ActiveRecord;
use yiiunit\data\ar\Order;
use yiiunit\data\ar\OrderItem;
use yiiunit\data\validators\models\FakedValidationModel;
use yiiunit\data\validators\models\ValidatorTestMainModel;
use yiiunit\data\validators\models\ValidatorTestRefModel;
......@@ -85,4 +87,49 @@ class UniqueValidatorTest extends DatabaseTestCase
$m = new ValidatorTestMainModel();
$val->validateAttribute($m, 'testMainVal');
}
public function testValidateCompositeKeys()
{
$val = new UniqueValidator([
'className' => OrderItem::className(),
'attributeName' => ['order_id', 'item_id'],
]);
// validate old record
$m = OrderItem::find(['order_id' => 1, 'item_id' => 2]);
$val->validateAttribute($m, 'order_id');
$this->assertFalse($m->hasErrors('order_id'));
$m->item_id = 1;
$val->validateAttribute($m, 'order_id');
$this->assertTrue($m->hasErrors('order_id'));
// validate new record
$m = new OrderItem(['order_id' => 1, 'item_id' => 2]);
$val->validateAttribute($m, 'order_id');
$this->assertTrue($m->hasErrors('order_id'));
$m = new OrderItem(['order_id' => 10, 'item_id' => 2]);
$val->validateAttribute($m, 'order_id');
$this->assertFalse($m->hasErrors('order_id'));
$val = new UniqueValidator([
'className' => OrderItem::className(),
'attributeName' => ['order_id', 'item_id' => 2],
]);
// validate old record
$m = Order::find(1);
$val->validateAttribute($m, 'id');
$this->assertFalse($m->hasErrors('id'));
$m->id = 2;
$val->validateAttribute($m, 'id');
$this->assertFalse($m->hasErrors('id'));
$m->id = 3;
$val->validateAttribute($m, 'id');
$this->assertTrue($m->hasErrors('id'));
$m = new Order(['id' => 1]);
$val->validateAttribute($m, 'id');
$this->assertTrue($m->hasErrors('id'));
$m = new Order(['id' => 10]);
$val->validateAttribute($m, 'id');
$this->assertFalse($m->hasErrors('id'));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment