ExistValidator.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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. /**
  14. * ExistValidator validates that the attribute value exists in a table.
  15. *
  16. * ExistValidator checks if the value being validated can be found in the table column specified by
  17. * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]].
  18. *
  19. * This validator is often used to verify that a foreign key contains a value
  20. * that can be found in the foreign table.
  21. *
  22. * The following are examples of validation rules using this validator:
  23. *
  24. * ```php
  25. * // a1 needs to exist
  26. * ['a1', 'exist']
  27. * // a1 needs to exist, but its value will use a2 to check for the existence
  28. * ['a1', 'exist', 'targetAttribute' => 'a2']
  29. * // a1 and a2 need to exist together, and they both will receive error message
  30. * [['a1', 'a2'], 'exist', 'targetAttribute' => ['a1', 'a2']]
  31. * // a1 and a2 need to exist together, only a1 will receive error message
  32. * ['a1', 'exist', 'targetAttribute' => ['a1', 'a2']]
  33. * // a1 needs to exist by checking the existence of both a2 and a3 (using a1 value)
  34. * ['a1', 'exist', 'targetAttribute' => ['a2', 'a1' => 'a3']]
  35. * ```
  36. *
  37. * @author Qiang Xue <qiang.xue@gmail.com>
  38. * @since 2.0
  39. */
  40. class ExistValidator extends Validator
  41. {
  42. /**
  43. * @var string the name of the ActiveRecord class that should be used to validate the existence
  44. * of the current attribute value. If not set, it will use the ActiveRecord class of the attribute being validated.
  45. * @see targetAttribute
  46. */
  47. public $targetClass;
  48. /**
  49. * @var string|array the name of the ActiveRecord attribute that should be used to
  50. * validate the existence of the current attribute value. If not set, it will use the name
  51. * of the attribute currently being validated. You may use an array to validate the existence
  52. * of multiple columns at the same time. The array key is the name of the attribute with the value to validate,
  53. * the array value is the name of the database field to search.
  54. */
  55. public $targetAttribute;
  56. /**
  57. * @var string|array|\Closure additional filter to be applied to the DB query used to check the existence of the attribute value.
  58. * This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]]
  59. * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query`
  60. * is the [[\yii\db\Query|Query]] object that you can modify in the function.
  61. */
  62. public $filter;
  63. /**
  64. * @var bool whether to allow array type attribute.
  65. */
  66. public $allowArray = false;
  67. /**
  68. * @var string and|or define how target attributes are related
  69. * @since 2.0.11
  70. */
  71. public $targetAttributeJunction = 'and';
  72. /**
  73. * @inheritdoc
  74. */
  75. public function init()
  76. {
  77. parent::init();
  78. if ($this->message === null) {
  79. $this->message = Yii::t('yii', '{attribute} is invalid.');
  80. }
  81. }
  82. /**
  83. * @inheritdoc
  84. */
  85. public function validateAttribute($model, $attribute)
  86. {
  87. $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
  88. $params = $this->prepareConditions($targetAttribute, $model, $attribute);
  89. $conditions[] = $this->targetAttributeJunction == 'or' ? 'or' : 'and';
  90. if (!$this->allowArray) {
  91. foreach ($params as $key => $value) {
  92. if (is_array($value)) {
  93. $this->addError($model, $attribute, Yii::t('yii', '{attribute} is invalid.'));
  94. return;
  95. }
  96. $conditions[] = [$key => $value];
  97. }
  98. } else {
  99. $conditions[] = $params;
  100. }
  101. $targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass;
  102. $query = $this->createQuery($targetClass, $conditions);
  103. if (is_array($model->$attribute)) {
  104. if ($query->count("DISTINCT [[$targetAttribute]]") != count($model->$attribute)) {
  105. $this->addError($model, $attribute, $this->message);
  106. }
  107. } elseif (!$query->exists()) {
  108. $this->addError($model, $attribute, $this->message);
  109. }
  110. }
  111. /**
  112. * Processes attributes' relations described in $targetAttribute parameter into conditions, compatible with
  113. * [[\yii\db\Query::where()|Query::where()]] key-value format.
  114. *
  115. * @param $targetAttribute array|string $attribute the name of the ActiveRecord attribute that should be used to
  116. * validate the existence of the current attribute value. If not set, it will use the name
  117. * of the attribute currently being validated. You may use an array to validate the existence
  118. * of multiple columns at the same time. The array key is the name of the attribute with the value to validate,
  119. * the array value is the name of the database field to search.
  120. * If the key and the value are the same, you can just specify the value.
  121. * @param \yii\base\Model $model the data model to be validated
  122. * @param string $attribute the name of the attribute to be validated in the $model
  123. * @return array conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format.
  124. * @throws InvalidConfigException
  125. */
  126. private function prepareConditions($targetAttribute, $model, $attribute)
  127. {
  128. if (is_array($targetAttribute)) {
  129. if ($this->allowArray) {
  130. throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.');
  131. }
  132. $conditions = [];
  133. foreach ($targetAttribute as $k => $v) {
  134. $conditions[$v] = is_int($k) ? $model->$v : $model->$k;
  135. }
  136. } else {
  137. $conditions = [$targetAttribute => $model->$attribute];
  138. }
  139. if (!$model instanceof ActiveRecord) {
  140. return $conditions;
  141. }
  142. return $this->prefixConditions($model, $conditions);
  143. }
  144. /**
  145. * @param Model $model the data model to be validated
  146. * @return string Target class name
  147. */
  148. private function getTargetClass($model)
  149. {
  150. return $this->targetClass === null ? get_class($model) : $this->targetClass;
  151. }
  152. /**
  153. * @inheritdoc
  154. */
  155. protected function validateValue($value)
  156. {
  157. if ($this->targetClass === null) {
  158. throw new InvalidConfigException('The "targetClass" property must be set.');
  159. }
  160. if (!is_string($this->targetAttribute)) {
  161. throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.');
  162. }
  163. $query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]);
  164. if (is_array($value)) {
  165. if (!$this->allowArray) {
  166. return [$this->message, []];
  167. }
  168. return $query->count("DISTINCT [[$this->targetAttribute]]") == count($value) ? null : [$this->message, []];
  169. } else {
  170. return $query->exists() ? null : [$this->message, []];
  171. }
  172. }
  173. /**
  174. * Creates a query instance with the given condition.
  175. * @param string $targetClass the target AR class
  176. * @param mixed $condition query condition
  177. * @return \yii\db\ActiveQueryInterface the query instance
  178. */
  179. protected function createQuery($targetClass, $condition)
  180. {
  181. /* @var $targetClass \yii\db\ActiveRecordInterface */
  182. $query = $targetClass::find()->andWhere($condition);
  183. if ($this->filter instanceof \Closure) {
  184. call_user_func($this->filter, $query);
  185. } elseif ($this->filter !== null) {
  186. $query->andWhere($this->filter);
  187. }
  188. return $query;
  189. }
  190. /**
  191. * Returns conditions with alias
  192. * @param ActiveQuery $query
  193. * @param array $conditions array of condition, keys to be modified
  194. * @param null|string $alias set empty string for no apply alias. Set null for apply primary table alias
  195. * @return array
  196. */
  197. private function applyTableAlias($query, $conditions, $alias = null)
  198. {
  199. if ($alias === null) {
  200. $alias = array_keys($query->getTablesUsedInFrom())[0];
  201. }
  202. $prefixedConditions = [];
  203. foreach ($conditions as $columnName => $columnValue) {
  204. $prefixedColumn = "{$alias}.[[" . preg_replace(
  205. '/^' . preg_quote($alias) . '\.(.*)$/',
  206. "$1",
  207. $columnName) . "]]";
  208. $prefixedConditions[$prefixedColumn] = $columnValue;
  209. }
  210. return $prefixedConditions;
  211. }
  212. /**
  213. * Prefix conditions with aliases
  214. *
  215. * @param ActiveRecord $model
  216. * @param array $conditions
  217. * @return array
  218. */
  219. private function prefixConditions($model, $conditions)
  220. {
  221. $targetModelClass = $this->getTargetClass($model);
  222. /** @var ActiveRecord $targetModelClass */
  223. return $this->applyTableAlias($targetModelClass::find(), $conditions);
  224. }
  225. }