mirror of
https://github.com/yiisoft/yii2.git
synced 2025-08-26 14:26:54 +08:00
Fixes #14254: add an option to specify whether validator is forced to always use master DB for yii\validators\UniqueValidator
and yii\validators\ExistValidator
This commit is contained in:

committed by
Alexander Makarov

parent
41cf14e515
commit
63ffae028e
@ -4,6 +4,7 @@ Yii Framework 2 Change Log
|
||||
2.0.14 under development
|
||||
------------------------
|
||||
|
||||
- Enh #14254: add an option to specify whether validator is forced to always use master DB for `yii\validators\UniqueValidator` and `yii\validators\ExistValidator` (rossoneri, samdark)
|
||||
- Enh #15272: Removed type attribute from script tag (aleksbelic)
|
||||
- Enh #15120: Refactored dynamic caching introducing `DynamicContentAwareInterface` and `DynamicContentAwareTrait` (sergeymakinen)
|
||||
- Bug #8983: Only truncate the original log file for rotation (matthewyang, developeruz)
|
||||
@ -83,7 +84,6 @@ Yii Framework 2 Change Log
|
||||
- Enh #14638: Added `yii\db\SchemaBuilderTrait::tinyInteger()` (rob006)
|
||||
- Enh #14643: Added `yii\web\ErrorAction::$layout` property to conveniently set layout from error action config (swods, cebe, samdark)
|
||||
- Enh #14662: Added support for custom `Content-Type` specification to `yii\web\JsonResponseFormatter` (Kolyunya)
|
||||
- Enh #14538: Added `yii\behaviors\AttributeTypecastBehavior::typecastAfterFind` property (littlefuntik, silverfire)
|
||||
- Enh #14732, #11218, #14810, #10855: It is now possible to pass `yii\db\Query` anywhere, where `yii\db\Expression` was supported (silverfire)
|
||||
- Enh #14806: Added $placeFooterAfterBody option for GridView (terehru)
|
||||
- Enh #15024: `yii\web\Pjax` widget does not prevent CSS files from sending anymore because they are handled by client-side plugin correctly (onmotion)
|
||||
|
@ -12,6 +12,7 @@ use yii\base\InvalidConfigException;
|
||||
use yii\base\Model;
|
||||
use yii\db\ActiveQuery;
|
||||
use yii\db\ActiveRecord;
|
||||
use yii\db\QueryInterface;
|
||||
|
||||
/**
|
||||
* ExistValidator validates that the attribute value exists in a table.
|
||||
@ -79,6 +80,12 @@ class ExistValidator extends Validator
|
||||
*/
|
||||
public $targetAttributeJunction = 'and';
|
||||
|
||||
/**
|
||||
* @var bool whether this validator is forced to always use master DB
|
||||
* @since 2.0.14
|
||||
*/
|
||||
public $forceMasterDb = true;
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
@ -105,12 +112,25 @@ class ExistValidator extends Validator
|
||||
|
||||
/**
|
||||
* Validates existence of the current attribute based on relation name
|
||||
* @param \yii\base\Model $model the data model to be validated
|
||||
* @param \yii\db\ActiveRecord $model the data model to be validated
|
||||
* @param string $attribute the name of the attribute to be validated.
|
||||
*/
|
||||
private function checkTargetRelationExistence($model, $attribute)
|
||||
{
|
||||
if (!$model->{'get' . ucfirst($this->targetRelation)}()->exists()) {
|
||||
$exists = false;
|
||||
/** @var ActiveQuery $relationQuery */
|
||||
$relationQuery = $model->{'get' . ucfirst($this->targetRelation)}();
|
||||
|
||||
if ($this->forceMasterDb) {
|
||||
$model::getDb()->useMaster(function() use ($relationQuery, &$exists) {
|
||||
$exists = $relationQuery->exists();
|
||||
});
|
||||
} else {
|
||||
$relationQuery->exists();
|
||||
}
|
||||
|
||||
|
||||
if (!$exists) {
|
||||
$this->addError($model, $attribute, $this->message);
|
||||
}
|
||||
}
|
||||
@ -142,11 +162,7 @@ class ExistValidator extends Validator
|
||||
$targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass;
|
||||
$query = $this->createQuery($targetClass, $conditions);
|
||||
|
||||
if (is_array($model->$attribute)) {
|
||||
if ($query->count("DISTINCT [[$targetAttribute]]") != count($model->$attribute)) {
|
||||
$this->addError($model, $attribute, $this->message);
|
||||
}
|
||||
} elseif (!$query->exists()) {
|
||||
if (!$this->valueExists($targetClass, $query, $model->$attribute)) {
|
||||
$this->addError($model, $attribute, $this->message);
|
||||
}
|
||||
}
|
||||
@ -210,17 +226,53 @@ class ExistValidator extends Validator
|
||||
throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.');
|
||||
}
|
||||
|
||||
$query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]);
|
||||
|
||||
if (is_array($value)) {
|
||||
if (!$this->allowArray) {
|
||||
return [$this->message, []];
|
||||
}
|
||||
|
||||
return $query->count("DISTINCT [[$this->targetAttribute]]") == count($value) ? null : [$this->message, []];
|
||||
if (is_array($value) && !$this->allowArray) {
|
||||
return [$this->message, []];
|
||||
}
|
||||
|
||||
return $query->exists() ? null : [$this->message, []];
|
||||
$query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]);
|
||||
|
||||
return $this->valueExists($this->targetClass, $query, $value) ? null : [$this->message, []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether value exists in target table
|
||||
*
|
||||
* @param string $targetClass
|
||||
* @param QueryInterface $query
|
||||
* @param mixed $value the value want to be checked
|
||||
* @return boolean
|
||||
*/
|
||||
private function valueExists($targetClass, $query, $value)
|
||||
{
|
||||
$db = $targetClass::getDb();
|
||||
$exists = false;
|
||||
|
||||
if ($this->forceMasterDb) {
|
||||
$db->useMaster(function ($db) use ($query, $value, &$exists) {
|
||||
$exists = $this->queryValueExists($query, $value);
|
||||
});
|
||||
} else {
|
||||
$exists = $this->queryValueExists($query, $value);
|
||||
}
|
||||
|
||||
return $exists;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run query to check if value exists
|
||||
*
|
||||
* @param QueryInterface $query
|
||||
* @param mixed $value the value to be checked
|
||||
* @return bool
|
||||
*/
|
||||
private function queryValueExists($query, $value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $query->count("DISTINCT [[$this->targetAttribute]]") == count($value) ;
|
||||
}
|
||||
return $query->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,6 +91,13 @@ class UniqueValidator extends Validator
|
||||
public $targetAttributeJunction = 'and';
|
||||
|
||||
|
||||
/**
|
||||
* @var bool whether this validator is forced to always use master DB
|
||||
* @since 2.0.14
|
||||
*/
|
||||
public $forceMasterDb = true;
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@ -131,7 +138,19 @@ class UniqueValidator extends Validator
|
||||
$conditions[] = [$key => $value];
|
||||
}
|
||||
|
||||
if ($this->modelExists($targetClass, $conditions, $model)) {
|
||||
$db = $targetClass::getDb();
|
||||
|
||||
$modelExists = false;
|
||||
|
||||
if ($this->forceMasterDb) {
|
||||
$db->useMaster(function () use ($targetClass, $conditions, $model, &$modelExists) {
|
||||
$modelExists = $this->modelExists($targetClass, $conditions, $model);
|
||||
});
|
||||
} else {
|
||||
$modelExists = $this->modelExists($targetClass, $conditions, $model);
|
||||
}
|
||||
|
||||
if ($modelExists) {
|
||||
if (is_array($targetAttribute) && count($targetAttribute) > 1) {
|
||||
$this->addComboNotUniqueError($model, $attribute);
|
||||
} else {
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
namespace yiiunit\framework\db;
|
||||
|
||||
use yii\caching\DummyCache;
|
||||
use yii\db\Connection;
|
||||
use yiiunit\TestCase as TestCase;
|
||||
|
||||
@ -129,4 +130,26 @@ abstract class DatabaseTestCase extends TestCase
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \yii\db\Connection
|
||||
*/
|
||||
protected function getConnectionWithInvalidSlave()
|
||||
{
|
||||
$config = array_merge($this->database, [
|
||||
'serverStatusCache' => new DummyCache(),
|
||||
'slaves' => [
|
||||
[], // invalid config
|
||||
],
|
||||
]);
|
||||
|
||||
if (isset($config['fixture'])) {
|
||||
$fixture = $config['fixture'];
|
||||
unset($config['fixture']);
|
||||
} else {
|
||||
$fixture = null;
|
||||
}
|
||||
|
||||
return $this->prepareDatabase($config, $fixture, true);
|
||||
}
|
||||
}
|
||||
|
@ -218,4 +218,30 @@ abstract class ExistValidatorTest extends DatabaseTestCase
|
||||
$val->validateAttribute($m, 'id');
|
||||
$this->assertTrue($m->hasErrors('id'));
|
||||
}
|
||||
|
||||
public function testForceMaster()
|
||||
{
|
||||
$connection = $this->getConnectionWithInvalidSlave();
|
||||
ActiveRecord::$db = $connection;
|
||||
|
||||
$model = null;
|
||||
$connection->useMaster(function() use (&$model) {
|
||||
$model = ValidatorTestMainModel::findOne(2);
|
||||
});
|
||||
|
||||
$validator = new ExistValidator([
|
||||
'forceMasterDb' => true,
|
||||
'targetRelation' => 'references',
|
||||
]);
|
||||
$validator->validateAttribute($model, 'id');
|
||||
|
||||
$this->expectException('\yii\base\InvalidConfigException');
|
||||
$validator = new ExistValidator([
|
||||
'forceMasterDb' => false,
|
||||
'targetRelation' => 'references',
|
||||
]);
|
||||
$validator->validateAttribute($model, 'id');
|
||||
|
||||
ActiveRecord::$db = $this->getConnection();
|
||||
}
|
||||
}
|
||||
|
@ -453,6 +453,32 @@ abstract class UniqueValidatorTest extends DatabaseTestCase
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function testForceMaster()
|
||||
{
|
||||
$connection = $this->getConnectionWithInvalidSlave();
|
||||
ActiveRecord::$db = $connection;
|
||||
|
||||
$model = null;
|
||||
$connection->useMaster(function() use (&$model) {
|
||||
$model = WithCustomer::find()->one();
|
||||
});
|
||||
|
||||
$validator = new UniqueValidator([
|
||||
'forceMasterDb' => true,
|
||||
'targetAttribute' => ['status', 'profile_id']
|
||||
]);
|
||||
$validator->validateAttribute($model, 'email');
|
||||
|
||||
$this->expectException('\yii\base\InvalidConfigException');
|
||||
$validator = new UniqueValidator([
|
||||
'forceMasterDb' => false,
|
||||
'targetAttribute' => ['status', 'profile_id']
|
||||
]);
|
||||
$validator->validateAttribute($model, 'email');
|
||||
|
||||
ActiveRecord::$db = $this->getConnection();
|
||||
}
|
||||
}
|
||||
|
||||
class WithCustomer extends Customer {
|
||||
|
Reference in New Issue
Block a user