ExistValidator.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\validators;
  8. use Yii;
  9. use yii\base\InvalidConfigException;
  10. use yii\base\Model;
  11. use yii\db\ActiveQuery;
  12. use yii\db\ActiveRecord;
  13. use yii\db\QueryInterface;
  14. /**
  15. * ExistValidator validates that the attribute value exists in a table.
  16. *
  17. * ExistValidator checks if the value being validated can be found in the table column specified by
  18. * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]].
  19. *
  20. * This validator is often used to verify that a foreign key contains a value
  21. * that can be found in the foreign table.
  22. *
  23. * The following are examples of validation rules using this validator:
  24. *
  25. * ```php
  26. * // a1 needs to exist
  27. * ['a1', 'exist']
  28. * // a1 needs to exist, but its value will use a2 to check for the existence
  29. * ['a1', 'exist', 'targetAttribute' => 'a2']
  30. * // a1 and a2 need to exist together, and they both will receive error message
  31. * [['a1', 'a2'], 'exist', 'targetAttribute' => ['a1', 'a2']]
  32. * // a1 and a2 need to exist together, only a1 will receive error message
  33. * ['a1', 'exist', 'targetAttribute' => ['a1', 'a2']]
  34. * // a1 needs to exist by checking the existence of both a2 and a3 (using a1 value)
  35. * ['a1', 'exist', 'targetAttribute' => ['a2', 'a1' => 'a3']]
  36. * ```
  37. *
  38. * @author Qiang Xue <qiang.xue@gmail.com>
  39. * @since 2.0
  40. */
  41. class ExistValidator extends Validator
  42. {
  43. /**
  44. * @var string the name of the ActiveRecord class that should be used to validate the existence
  45. * of the current attribute value. If not set, it will use the ActiveRecord class of the attribute being validated.
  46. * @see targetAttribute
  47. */
  48. public $targetClass;
  49. /**
  50. * @var string|array the name of the ActiveRecord attribute that should be used to
  51. * validate the existence of the current attribute value. If not set, it will use the name
  52. * of the attribute currently being validated. You may use an array to validate the existence
  53. * of multiple columns at the same time. The array key is the name of the attribute with the value to validate,
  54. * the array value is the name of the database field to search.
  55. */
  56. public $targetAttribute;
  57. /**
  58. * @var string the name of the relation that should be used to validate the existence of the current attribute value
  59. * This param overwrites $targetClass and $targetAttribute
  60. * @since 2.0.14
  61. */
  62. public $targetRelation;
  63. /**
  64. * @var string|array|\Closure additional filter to be applied to the DB query used to check the existence of the attribute value.
  65. * This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]]
  66. * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query`
  67. * is the [[\yii\db\Query|Query]] object that you can modify in the function.
  68. */
  69. public $filter;
  70. /**
  71. * @var bool whether to allow array type attribute.
  72. */
  73. public $allowArray = false;
  74. /**
  75. * @var string and|or define how target attributes are related
  76. * @since 2.0.11
  77. */
  78. public $targetAttributeJunction = 'and';
  79. /**
  80. * @var bool whether this validator is forced to always use master DB
  81. * @since 2.0.14
  82. */
  83. public $forceMasterDb = true;
  84. /**
  85. * {@inheritdoc}
  86. */
  87. public function init()
  88. {
  89. parent::init();
  90. if ($this->message === null) {
  91. $this->message = Yii::t('yii', '{attribute} is invalid.');
  92. }
  93. }
  94. /**
  95. * {@inheritdoc}
  96. */
  97. public function validateAttribute($model, $attribute)
  98. {
  99. if (!empty($this->targetRelation)) {
  100. $this->checkTargetRelationExistence($model, $attribute);
  101. } else {
  102. $this->checkTargetAttributeExistence($model, $attribute);
  103. }
  104. }
  105. /**
  106. * Validates existence of the current attribute based on relation name
  107. * @param \yii\db\ActiveRecord $model the data model to be validated
  108. * @param string $attribute the name of the attribute to be validated.
  109. */
  110. private function checkTargetRelationExistence($model, $attribute)
  111. {
  112. $exists = false;
  113. /** @var ActiveQuery $relationQuery */
  114. $relationQuery = $model->{'get' . ucfirst($this->targetRelation)}();
  115. if ($this->filter instanceof \Closure) {
  116. call_user_func($this->filter, $relationQuery);
  117. } elseif ($this->filter !== null) {
  118. $relationQuery->andWhere($this->filter);
  119. }
  120. if ($this->forceMasterDb && method_exists($model::getDb(), 'useMaster')) {
  121. $model::getDb()->useMaster(function() use ($relationQuery, &$exists) {
  122. $exists = $relationQuery->exists();
  123. });
  124. } else {
  125. $exists = $relationQuery->exists();
  126. }
  127. if (!$exists) {
  128. $this->addError($model, $attribute, $this->message);
  129. }
  130. }
  131. /**
  132. * Validates existence of the current attribute based on targetAttribute
  133. * @param \yii\base\Model $model the data model to be validated
  134. * @param string $attribute the name of the attribute to be validated.
  135. */
  136. private function checkTargetAttributeExistence($model, $attribute)
  137. {
  138. $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
  139. $params = $this->prepareConditions($targetAttribute, $model, $attribute);
  140. $conditions = [$this->targetAttributeJunction == 'or' ? 'or' : 'and'];
  141. if (!$this->allowArray) {
  142. foreach ($params as $key => $value) {
  143. if (is_array($value)) {
  144. $this->addError($model, $attribute, Yii::t('yii', '{attribute} is invalid.'));
  145. return;
  146. }
  147. $conditions[] = [$key => $value];
  148. }
  149. } else {
  150. $conditions[] = $params;
  151. }
  152. $targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass;
  153. $query = $this->createQuery($targetClass, $conditions);
  154. if (!$this->valueExists($targetClass, $query, $model->$attribute)) {
  155. $this->addError($model, $attribute, $this->message);
  156. }
  157. }
  158. /**
  159. * Processes attributes' relations described in $targetAttribute parameter into conditions, compatible with
  160. * [[\yii\db\Query::where()|Query::where()]] key-value format.
  161. *
  162. * @param $targetAttribute array|string $attribute the name of the ActiveRecord attribute that should be used to
  163. * validate the existence of the current attribute value. If not set, it will use the name
  164. * of the attribute currently being validated. You may use an array to validate the existence
  165. * of multiple columns at the same time. The array key is the name of the attribute with the value to validate,
  166. * the array value is the name of the database field to search.
  167. * If the key and the value are the same, you can just specify the value.
  168. * @param \yii\base\Model $model the data model to be validated
  169. * @param string $attribute the name of the attribute to be validated in the $model
  170. * @return array conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format.
  171. * @throws InvalidConfigException
  172. */
  173. private function prepareConditions($targetAttribute, $model, $attribute)
  174. {
  175. if (is_array($targetAttribute)) {
  176. if ($this->allowArray) {
  177. throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.');
  178. }
  179. $conditions = [];
  180. foreach ($targetAttribute as $k => $v) {
  181. $conditions[$v] = is_int($k) ? $model->$v : $model->$k;
  182. }
  183. } else {
  184. $conditions = [$targetAttribute => $model->$attribute];
  185. }
  186. $targetModelClass = $this->getTargetClass($model);
  187. if (!is_subclass_of($targetModelClass, 'yii\db\ActiveRecord')) {
  188. return $conditions;
  189. }
  190. /** @var ActiveRecord $targetModelClass */
  191. return $this->applyTableAlias($targetModelClass::find(), $conditions);
  192. }
  193. /**
  194. * @param Model $model the data model to be validated
  195. * @return string Target class name
  196. */
  197. private function getTargetClass($model)
  198. {
  199. return $this->targetClass === null ? get_class($model) : $this->targetClass;
  200. }
  201. /**
  202. * {@inheritdoc}
  203. */
  204. protected function validateValue($value)
  205. {
  206. if ($this->targetClass === null) {
  207. throw new InvalidConfigException('The "targetClass" property must be set.');
  208. }
  209. if (!is_string($this->targetAttribute)) {
  210. throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.');
  211. }
  212. if (is_array($value) && !$this->allowArray) {
  213. return [$this->message, []];
  214. }
  215. $query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]);
  216. return $this->valueExists($this->targetClass, $query, $value) ? null : [$this->message, []];
  217. }
  218. /**
  219. * Check whether value exists in target table
  220. *
  221. * @param string $targetClass
  222. * @param QueryInterface $query
  223. * @param mixed $value the value want to be checked
  224. * @return bool
  225. */
  226. private function valueExists($targetClass, $query, $value)
  227. {
  228. $db = $targetClass::getDb();
  229. $exists = false;
  230. if ($this->forceMasterDb && method_exists($db, 'useMaster')) {
  231. $db->useMaster(function ($db) use ($query, $value, &$exists) {
  232. $exists = $this->queryValueExists($query, $value);
  233. });
  234. } else {
  235. $exists = $this->queryValueExists($query, $value);
  236. }
  237. return $exists;
  238. }
  239. /**
  240. * Run query to check if value exists
  241. *
  242. * @param QueryInterface $query
  243. * @param mixed $value the value to be checked
  244. * @return bool
  245. */
  246. private function queryValueExists($query, $value)
  247. {
  248. if (is_array($value)) {
  249. return $query->count("DISTINCT [[$this->targetAttribute]]") == count($value) ;
  250. }
  251. return $query->exists();
  252. }
  253. /**
  254. * Creates a query instance with the given condition.
  255. * @param string $targetClass the target AR class
  256. * @param mixed $condition query condition
  257. * @return \yii\db\ActiveQueryInterface the query instance
  258. */
  259. protected function createQuery($targetClass, $condition)
  260. {
  261. /* @var $targetClass \yii\db\ActiveRecordInterface */
  262. $query = $targetClass::find()->andWhere($condition);
  263. if ($this->filter instanceof \Closure) {
  264. call_user_func($this->filter, $query);
  265. } elseif ($this->filter !== null) {
  266. $query->andWhere($this->filter);
  267. }
  268. return $query;
  269. }
  270. /**
  271. * Returns conditions with alias.
  272. * @param ActiveQuery $query
  273. * @param array $conditions array of condition, keys to be modified
  274. * @param null|string $alias set empty string for no apply alias. Set null for apply primary table alias
  275. * @return array
  276. */
  277. private function applyTableAlias($query, $conditions, $alias = null)
  278. {
  279. if ($alias === null) {
  280. $alias = array_keys($query->getTablesUsedInFrom())[0];
  281. }
  282. $prefixedConditions = [];
  283. foreach ($conditions as $columnName => $columnValue) {
  284. if (strpos($columnName, '(') === false) {
  285. $prefixedColumn = "{$alias}.[[" . preg_replace(
  286. '/^' . preg_quote($alias) . '\.(.*)$/',
  287. '$1',
  288. $columnName) . ']]';
  289. } else {
  290. // there is an expression, can't prefix it reliably
  291. $prefixedColumn = $columnName;
  292. }
  293. $prefixedConditions[$prefixedColumn] = $columnValue;
  294. }
  295. return $prefixedConditions;
  296. }
  297. }