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 ...@@ -16,6 +16,7 @@ Yii Framework 2 Change Log
- Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark)
- Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe) - 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) - 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 #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 #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue)
- Enh #1437: Added ListView::viewParams (qiangxue) - Enh #1437: Added ListView::viewParams (qiangxue)
......
...@@ -30,10 +30,25 @@ class ExistValidator extends Validator ...@@ -30,10 +30,25 @@ class ExistValidator extends Validator
*/ */
public $className; 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, * 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
* @see className * 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; public $attributeName;
...@@ -64,9 +79,7 @@ class ExistValidator extends Validator ...@@ -64,9 +79,7 @@ class ExistValidator extends Validator
/** @var \yii\db\ActiveRecordInterface $className */ /** @var \yii\db\ActiveRecordInterface $className */
$className = $this->className === null ? get_class($object) : $this->className; $className = $this->className === null ? get_class($object) : $this->className;
$attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
$query = $className::find(); if (!$this->exists($className, $attributeName, $object, $value)) {
$query->where([$attributeName => $value]);
if (!$query->exists()) {
$this->addError($object, $attribute, $this->message); $this->addError($object, $attribute, $this->message);
} }
} }
...@@ -85,10 +98,33 @@ class ExistValidator extends Validator ...@@ -85,10 +98,33 @@ class ExistValidator extends Validator
if ($this->attributeName === null) { if ($this->attributeName === null) {
throw new InvalidConfigException('The "attributeName" property must be set.'); 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 */ /** @var \yii\db\ActiveRecordInterface $className */
$className = $this->className;
$query = $className::find(); $query = $className::find();
$query->where([$this->attributeName => $value]); if (is_array($attributeName)) {
return $query->exists() ? null : [$this->message, []]; $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 ...@@ -26,9 +26,25 @@ class UniqueValidator extends Validator
*/ */
public $className; 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, * 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; public $attributeName;
...@@ -60,7 +76,20 @@ class UniqueValidator extends Validator ...@@ -60,7 +76,20 @@ class UniqueValidator extends Validator
$attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
$query = $className::find(); $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 (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) {
// if current $object isn't in the database yet then it's OK just to call exists() // 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 ...@@ -71,7 +100,11 @@ class UniqueValidator extends Validator
$objects = $query->limit(2)->all(); $objects = $query->limit(2)->all();
$n = count($objects); $n = count($objects);
if ($n === 1) { 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 // primary key is modified and not unique
$exists = $object->getOldPrimaryKey() != $object->getPrimaryKey(); $exists = $object->getOldPrimaryKey() != $object->getPrimaryKey();
} else { } else {
......
...@@ -7,6 +7,8 @@ use Yii; ...@@ -7,6 +7,8 @@ use Yii;
use yii\base\Exception; use yii\base\Exception;
use yii\validators\ExistValidator; use yii\validators\ExistValidator;
use yiiunit\data\ar\ActiveRecord; 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\ValidatorTestMainModel;
use yiiunit\data\validators\models\ValidatorTestRefModel; use yiiunit\data\validators\models\ValidatorTestRefModel;
use yiiunit\framework\db\DatabaseTestCase; use yiiunit\framework\db\DatabaseTestCase;
...@@ -92,4 +94,44 @@ class ExistValidatorTest extends DatabaseTestCase ...@@ -92,4 +94,44 @@ class ExistValidatorTest extends DatabaseTestCase
$val->validateAttribute($m, 'test_val'); $val->validateAttribute($m, 'test_val');
$this->assertTrue($m->hasErrors('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; ...@@ -6,6 +6,8 @@ namespace yiiunit\framework\validators;
use yii\validators\UniqueValidator; use yii\validators\UniqueValidator;
use Yii; use Yii;
use yiiunit\data\ar\ActiveRecord; 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\FakedValidationModel;
use yiiunit\data\validators\models\ValidatorTestMainModel; use yiiunit\data\validators\models\ValidatorTestMainModel;
use yiiunit\data\validators\models\ValidatorTestRefModel; use yiiunit\data\validators\models\ValidatorTestRefModel;
...@@ -85,4 +87,49 @@ class UniqueValidatorTest extends DatabaseTestCase ...@@ -85,4 +87,49 @@ class UniqueValidatorTest extends DatabaseTestCase
$m = new ValidatorTestMainModel(); $m = new ValidatorTestMainModel();
$val->validateAttribute($m, 'testMainVal'); $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