mirror of
				https://github.com/yiisoft/yii2.git
				synced 2025-11-04 14:46:19 +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
 | 
					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 #15272: Removed type attribute from script tag (aleksbelic)
 | 
				
			||||||
- Enh #15120: Refactored dynamic caching introducing `DynamicContentAwareInterface` and `DynamicContentAwareTrait` (sergeymakinen)
 | 
					- Enh #15120: Refactored dynamic caching introducing `DynamicContentAwareInterface` and `DynamicContentAwareTrait` (sergeymakinen)
 | 
				
			||||||
- Bug #8983: Only truncate the original log file for rotation (matthewyang, developeruz)
 | 
					- 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 #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 #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 #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 #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 #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)
 | 
					- 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\base\Model;
 | 
				
			||||||
use yii\db\ActiveQuery;
 | 
					use yii\db\ActiveQuery;
 | 
				
			||||||
use yii\db\ActiveRecord;
 | 
					use yii\db\ActiveRecord;
 | 
				
			||||||
 | 
					use yii\db\QueryInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * ExistValidator validates that the attribute value exists in a table.
 | 
					 * ExistValidator validates that the attribute value exists in a table.
 | 
				
			||||||
@ -79,6 +80,12 @@ class ExistValidator extends Validator
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    public $targetAttributeJunction = 'and';
 | 
					    public $targetAttributeJunction = 'and';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var bool whether this validator is forced to always use master DB
 | 
				
			||||||
 | 
					     * @since 2.0.14
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public $forceMasterDb = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * {@inheritdoc}
 | 
					     * {@inheritdoc}
 | 
				
			||||||
@ -105,12 +112,25 @@ class ExistValidator extends Validator
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Validates existence of the current attribute based on relation name
 | 
					     * 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.
 | 
					     * @param string $attribute the name of the attribute to be validated.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private function checkTargetRelationExistence($model, $attribute)
 | 
					    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);
 | 
					            $this->addError($model, $attribute, $this->message);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -142,11 +162,7 @@ class ExistValidator extends Validator
 | 
				
			|||||||
        $targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass;
 | 
					        $targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass;
 | 
				
			||||||
        $query = $this->createQuery($targetClass, $conditions);
 | 
					        $query = $this->createQuery($targetClass, $conditions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (is_array($model->$attribute)) {
 | 
					        if (!$this->valueExists($targetClass, $query, $model->$attribute)) {
 | 
				
			||||||
            if ($query->count("DISTINCT [[$targetAttribute]]") != count($model->$attribute)) {
 | 
					 | 
				
			||||||
                $this->addError($model, $attribute, $this->message);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } elseif (!$query->exists()) {
 | 
					 | 
				
			||||||
            $this->addError($model, $attribute, $this->message);
 | 
					            $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.');
 | 
					            throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]);
 | 
					        if (is_array($value) && !$this->allowArray) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (is_array($value)) {
 | 
					 | 
				
			||||||
            if (!$this->allowArray) {
 | 
					 | 
				
			||||||
            return [$this->message, []];
 | 
					            return [$this->message, []];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return $query->count("DISTINCT [[$this->targetAttribute]]") == count($value) ? null : [$this->message, []];
 | 
					        $query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $this->valueExists($this->targetClass, $query, $value) ? null : [$this->message, []];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $query->exists() ? 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';
 | 
					    public $targetAttributeJunction = 'and';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var bool whether this validator is forced to always use master DB
 | 
				
			||||||
 | 
					     * @since 2.0.14
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public $forceMasterDb =  true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * {@inheritdoc}
 | 
					     * {@inheritdoc}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@ -131,7 +138,19 @@ class UniqueValidator extends Validator
 | 
				
			|||||||
            $conditions[] = [$key => $value];
 | 
					            $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) {
 | 
					            if (is_array($targetAttribute) && count($targetAttribute) > 1) {
 | 
				
			||||||
                $this->addComboNotUniqueError($model, $attribute);
 | 
					                $this->addComboNotUniqueError($model, $attribute);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace yiiunit\framework\db;
 | 
					namespace yiiunit\framework\db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use yii\caching\DummyCache;
 | 
				
			||||||
use yii\db\Connection;
 | 
					use yii\db\Connection;
 | 
				
			||||||
use yiiunit\TestCase as TestCase;
 | 
					use yiiunit\TestCase as TestCase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -129,4 +130,26 @@ abstract class DatabaseTestCase extends TestCase
 | 
				
			|||||||
                return $sql;
 | 
					                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');
 | 
					        $val->validateAttribute($m, 'id');
 | 
				
			||||||
        $this->assertTrue($m->hasErrors('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 {
 | 
					class WithCustomer extends Customer {
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user