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:
rossoneri
2018-02-15 07:12:54 +08:00
committed by Alexander Makarov
parent 41cf14e515
commit 63ffae028e
6 changed files with 164 additions and 18 deletions

View File

@ -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)

View File

@ -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();
}
/**

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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 {