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 | |||||||
8 | namespace yii\validators; |
||||||
9 | |||||||
10 | use Yii; |
||||||
11 | use yii\base\Model; |
||||||
12 | use yii\db\ActiveQuery; |
||||||
13 | use yii\db\ActiveQueryInterface; |
||||||
14 | use yii\db\ActiveRecord; |
||||||
15 | use yii\db\ActiveRecordInterface; |
||||||
16 | use yii\helpers\Inflector; |
||||||
17 | |||||||
18 | /** |
||||||
19 | * UniqueValidator validates that the attribute value is unique in the specified database table. |
||||||
20 | * |
||||||
21 | * UniqueValidator checks if the value being validated is unique in the table column specified by |
||||||
22 | * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]]. |
||||||
23 | * |
||||||
24 | * The following are examples of validation rules using this validator: |
||||||
25 | * |
||||||
26 | * ```php |
||||||
27 | * // a1 needs to be unique |
||||||
28 | * ['a1', 'unique'] |
||||||
29 | * // a1 needs to be unique, but column a2 will be used to check the uniqueness of the a1 value |
||||||
30 | * ['a1', 'unique', 'targetAttribute' => 'a2'] |
||||||
31 | * // a1 and a2 need to be unique together, and they both will receive error message |
||||||
32 | * [['a1', 'a2'], 'unique', 'targetAttribute' => ['a1', 'a2']] |
||||||
33 | * // a1 and a2 need to be unique together, only a1 will receive error message |
||||||
34 | * ['a1', 'unique', 'targetAttribute' => ['a1', 'a2']] |
||||||
35 | * // a1 needs to be unique by checking the uniqueness of both a2 and a3 (using a1 value) |
||||||
36 | * ['a1', 'unique', 'targetAttribute' => ['a2', 'a1' => 'a3']] |
||||||
37 | * ``` |
||||||
38 | * |
||||||
39 | * @author Qiang Xue <[email protected]> |
||||||
40 | * @since 2.0 |
||||||
41 | */ |
||||||
42 | class UniqueValidator extends Validator |
||||||
43 | { |
||||||
44 | /** |
||||||
45 | * @var string the name of the ActiveRecord class that should be used to validate the uniqueness |
||||||
46 | * of the current attribute value. If not set, it will use the ActiveRecord class of the attribute being validated. |
||||||
47 | * @see targetAttribute |
||||||
48 | */ |
||||||
49 | public $targetClass; |
||||||
50 | /** |
||||||
51 | * @var string|array the name of the [[\yii\db\ActiveRecord|ActiveRecord]] attribute that should be used to |
||||||
52 | * validate the uniqueness of the current attribute value. If not set, it will use the name |
||||||
53 | * of the attribute currently being validated. You may use an array to validate the uniqueness |
||||||
54 | * of multiple columns at the same time. The array values are the attributes that will be |
||||||
55 | * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated. |
||||||
56 | */ |
||||||
57 | public $targetAttribute; |
||||||
58 | /** |
||||||
59 | * @var string|array|\Closure additional filter to be applied to the DB query used to check the uniqueness of the attribute value. |
||||||
60 | * This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]] |
||||||
61 | * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query` |
||||||
62 | * is the [[\yii\db\Query|Query]] object that you can modify in the function. |
||||||
63 | */ |
||||||
64 | public $filter; |
||||||
65 | /** |
||||||
66 | * @var string the user-defined error message. |
||||||
67 | * |
||||||
68 | * When validating single attribute, it may contain |
||||||
69 | * the following placeholders which will be replaced accordingly by the validator: |
||||||
70 | * |
||||||
71 | * - `{attribute}`: the label of the attribute being validated |
||||||
72 | * - `{value}`: the value of the attribute being validated |
||||||
73 | * |
||||||
74 | * When validating mutliple attributes, it may contain the following placeholders: |
||||||
75 | * |
||||||
76 | * - `{attributes}`: the labels of the attributes being validated. |
||||||
77 | * - `{values}`: the values of the attributes being validated. |
||||||
78 | */ |
||||||
79 | public $message; |
||||||
80 | /** |
||||||
81 | * @var string |
||||||
82 | * @since 2.0.9 |
||||||
83 | * @deprecated since version 2.0.10, to be removed in 2.1. Use [[message]] property |
||||||
84 | * to setup custom message for multiple target attributes. |
||||||
85 | */ |
||||||
86 | public $comboNotUnique; |
||||||
87 | /** |
||||||
88 | * @var string and|or define how target attributes are related |
||||||
89 | * @since 2.0.11 |
||||||
90 | */ |
||||||
91 | public $targetAttributeJunction = 'and'; |
||||||
92 | /** |
||||||
93 | * @var bool whether this validator is forced to always use master DB |
||||||
94 | * @since 2.0.14 |
||||||
95 | */ |
||||||
96 | public $forceMasterDb = true; |
||||||
97 | |||||||
98 | |||||||
99 | /** |
||||||
100 | * {@inheritdoc} |
||||||
101 | */ |
||||||
102 | 114 | public function init() |
|||||
103 | { |
||||||
104 | 114 | parent::init(); |
|||||
105 | 114 | if ($this->message !== null) { |
|||||
106 | 5 | return; |
|||||
107 | } |
||||||
108 | 114 | if (is_array($this->targetAttribute) && count($this->targetAttribute) > 1) { |
|||||
109 | // fallback for deprecated `comboNotUnique` property - use it as message if is set |
||||||
110 | 25 | if ($this->comboNotUnique === null) { |
|||||
111 | 20 | $this->message = Yii::t('yii', 'The combination {values} of {attributes} has already been taken.'); |
|||||
112 | } else { |
||||||
113 | 25 | $this->message = $this->comboNotUnique; |
|||||
114 | } |
||||||
115 | } else { |
||||||
116 | 94 | $this->message = Yii::t('yii', '{attribute} "{value}" has already been taken.'); |
|||||
117 | } |
||||||
118 | 114 | } |
|||||
119 | |||||||
120 | /** |
||||||
121 | * {@inheritdoc} |
||||||
122 | */ |
||||||
123 | 89 | public function validateAttribute($model, $attribute) |
|||||
124 | { |
||||||
125 | /* @var $targetClass ActiveRecordInterface */ |
||||||
126 | 89 | $targetClass = $this->getTargetClass($model); |
|||||
127 | 89 | $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute; |
|||||
128 | 89 | $rawConditions = $this->prepareConditions($targetAttribute, $model, $attribute); |
|||||
129 | 89 | $conditions = [$this->targetAttributeJunction === 'or' ? 'or' : 'and']; |
|||||
130 | |||||||
131 | 89 | foreach ($rawConditions as $key => $value) { |
|||||
132 | 89 | if (is_array($value)) { |
|||||
133 | 10 | $this->addError($model, $attribute, Yii::t('yii', '{attribute} is invalid.')); |
|||||
134 | 10 | return; |
|||||
135 | } |
||||||
136 | 84 | $conditions[] = [$key => $value]; |
|||||
137 | } |
||||||
138 | |||||||
139 | 84 | $db = $targetClass::getDb(); |
|||||
140 | |||||||
141 | 84 | $modelExists = false; |
|||||
142 | |||||||
143 | 84 | if ($this->forceMasterDb && method_exists($db, 'useMaster')) { |
|||||
144 | 84 | $db->useMaster(function () use ($targetClass, $conditions, $model, &$modelExists) { |
|||||
145 | 84 | $modelExists = $this->modelExists($targetClass, $conditions, $model); |
|||||
146 | 84 | }); |
|||||
147 | } else { |
||||||
148 | 5 | $modelExists = $this->modelExists($targetClass, $conditions, $model); |
|||||
149 | } |
||||||
150 | |||||||
151 | 79 | if ($modelExists) { |
|||||
152 | 47 | if (is_array($targetAttribute) && count($targetAttribute) > 1) { |
|||||
153 | 10 | $this->addComboNotUniqueError($model, $attribute); |
|||||
154 | } else { |
||||||
155 | 47 | $this->addError($model, $attribute, $this->message); |
|||||
156 | } |
||||||
157 | } |
||||||
158 | 79 | } |
|||||
159 | |||||||
160 | /** |
||||||
161 | * @param Model $model the data model to be validated |
||||||
162 | * @return string Target class name |
||||||
163 | */ |
||||||
164 | 104 | private function getTargetClass($model) |
|||||
165 | { |
||||||
166 | 104 | return $this->targetClass === null ? get_class($model) : $this->targetClass; |
|||||
167 | } |
||||||
168 | |||||||
169 | /** |
||||||
170 | * Checks whether the $model exists in the database. |
||||||
171 | * |
||||||
172 | * @param string $targetClass the name of the ActiveRecord class that should be used to validate the uniqueness |
||||||
173 | * of the current attribute value. |
||||||
174 | * @param array $conditions conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format. |
||||||
175 | * @param Model $model the data model to be validated |
||||||
176 | * |
||||||
177 | * @return bool whether the model already exists |
||||||
178 | */ |
||||||
179 | 84 | private function modelExists($targetClass, $conditions, $model) |
|||||
180 | { |
||||||
181 | /** @var ActiveRecordInterface|\yii\base\BaseObject $targetClass $query */ |
||||||
182 | 84 | $query = $this->prepareQuery($targetClass, $conditions); |
|||||
183 | |||||||
184 | 84 | if (!$model instanceof ActiveRecordInterface || $model->getIsNewRecord() || $model->className() !== $targetClass::className()) { |
|||||
0 ignored issues
–
show
The method
getIsNewRecord() does not exist on yii\base\Model . Since you implemented __call , consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
The method
className() does not exist on yii\db\ActiveRecordInterface . Since it exists in all sub-types, consider adding an abstract or default implementation to yii\db\ActiveRecordInterface .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
185 | // if current $model isn't in the database yet then it's OK just to call exists() |
||||||
186 | // also there's no need to run check based on primary keys, when $targetClass is not the same as $model's class |
||||||
187 | 54 | $exists = $query->exists(); |
|||||
188 | } else { |
||||||
189 | // if current $model is in the database already we can't use exists() |
||||||
190 | 47 | if ($query instanceof \yii\db\ActiveQuery) { |
|||||
191 | // only select primary key to optimize query |
||||||
192 | 47 | $columnsCondition = array_flip($targetClass::primaryKey()); |
|||||
193 | 47 | $query->select(array_flip($this->applyTableAlias($query, $columnsCondition))); |
|||||
194 | |||||||
195 | // any with relation can't be loaded because related fields are not selected |
||||||
196 | 47 | $query->with = null; |
|||||
197 | |||||||
198 | 47 | if (is_array($query->joinWith)) { |
|||||
199 | // any joinWiths need to have eagerLoading turned off to prevent related fields being loaded |
||||||
200 | 10 | foreach ($query->joinWith as &$joinWith) { |
|||||
201 | // \yii\db\ActiveQuery::joinWith adds eagerLoading at key 1 |
||||||
202 | 10 | $joinWith[1] = false; |
|||||
203 | } |
||||||
204 | 10 | unset($joinWith); |
|||||
205 | } |
||||||
206 | } |
||||||
207 | 47 | $models = $query->limit(2)->asArray()->all(); |
|||||
208 | 47 | $n = count($models); |
|||||
209 | 47 | if ($n === 1) { |
|||||
210 | // if there is one record, check if it is the currently validated model |
||||||
211 | 44 | $dbModel = reset($models); |
|||||
212 | 44 | $pks = $targetClass::primaryKey(); |
|||||
213 | 44 | $pk = []; |
|||||
214 | 44 | foreach ($pks as $pkAttribute) { |
|||||
215 | 44 | $pk[$pkAttribute] = $dbModel[$pkAttribute]; |
|||||
216 | } |
||||||
217 | 44 | $exists = ($pk != $model->getOldPrimaryKey(true)); |
|||||
218 | } else { |
||||||
219 | // if there is more than one record, the value is not unique |
||||||
220 | 8 | $exists = $n > 1; |
|||||
221 | } |
||||||
222 | } |
||||||
223 | |||||||
224 | 79 | return $exists; |
|||||
225 | } |
||||||
226 | |||||||
227 | /** |
||||||
228 | * Prepares a query by applying filtering conditions defined in $conditions method property |
||||||
229 | * and [[filter]] class property. |
||||||
230 | * |
||||||
231 | * @param ActiveRecordInterface $targetClass the name of the ActiveRecord class that should be used to validate |
||||||
232 | * the uniqueness of the current attribute value. |
||||||
233 | * @param array $conditions conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format |
||||||
234 | * |
||||||
235 | * @return ActiveQueryInterface|ActiveQuery |
||||||
236 | */ |
||||||
237 | 89 | private function prepareQuery($targetClass, $conditions) |
|||||
238 | { |
||||||
239 | 89 | $query = $targetClass::find(); |
|||||
240 | 89 | $query->andWhere($conditions); |
|||||
241 | 89 | if ($this->filter instanceof \Closure) { |
|||||
242 | 10 | call_user_func($this->filter, $query); |
|||||
243 | 84 | } elseif ($this->filter !== null) { |
|||||
244 | 5 | $query->andWhere($this->filter); |
|||||
245 | } |
||||||
246 | |||||||
247 | 89 | return $query; |
|||||
248 | } |
||||||
249 | |||||||
250 | /** |
||||||
251 | * Processes attributes' relations described in $targetAttribute parameter into conditions, compatible with |
||||||
252 | * [[\yii\db\Query::where()|Query::where()]] key-value format. |
||||||
253 | * |
||||||
254 | * @param string|array $targetAttribute the name of the [[\yii\db\ActiveRecord|ActiveRecord]] attribute that |
||||||
255 | * should be used to validate the uniqueness of the current attribute value. You may use an array to validate |
||||||
256 | * the uniqueness of multiple columns at the same time. The array values are the attributes that will be |
||||||
257 | * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated. |
||||||
258 | * If the key and the value are the same, you can just specify the value. |
||||||
259 | * @param Model $model the data model to be validated |
||||||
260 | * @param string $attribute the name of the attribute to be validated in the $model |
||||||
261 | * |
||||||
262 | * @return array conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format. |
||||||
263 | */ |
||||||
264 | 94 | private function prepareConditions($targetAttribute, $model, $attribute) |
|||||
265 | { |
||||||
266 | 94 | if (is_array($targetAttribute)) { |
|||||
267 | 45 | $conditions = []; |
|||||
268 | 45 | foreach ($targetAttribute as $k => $v) { |
|||||
269 | 45 | $conditions[$v] = is_int($k) ? $model->$v : $model->$k; |
|||||
270 | } |
||||||
271 | } else { |
||||||
272 | 64 | $conditions = [$targetAttribute => $model->$attribute]; |
|||||
273 | } |
||||||
274 | |||||||
275 | 94 | $targetModelClass = $this->getTargetClass($model); |
|||||
276 | 94 | if (!is_subclass_of($targetModelClass, 'yii\db\ActiveRecord')) { |
|||||
277 | 10 | return $conditions; |
|||||
278 | } |
||||||
279 | |||||||
280 | /** @var ActiveRecord $targetModelClass */ |
||||||
281 | 94 | return $this->applyTableAlias($targetModelClass::find(), $conditions); |
|||||
282 | } |
||||||
283 | |||||||
284 | /** |
||||||
285 | * Builds and adds [[comboNotUnique]] error message to the specified model attribute. |
||||||
286 | * @param \yii\base\Model $model the data model. |
||||||
287 | * @param string $attribute the name of the attribute. |
||||||
288 | */ |
||||||
289 | 10 | private function addComboNotUniqueError($model, $attribute) |
|||||
290 | { |
||||||
291 | 10 | $attributeCombo = []; |
|||||
292 | 10 | $valueCombo = []; |
|||||
293 | 10 | foreach ($this->targetAttribute as $key => $value) { |
|||||
294 | 10 | if (is_int($key)) { |
|||||
295 | 10 | $attributeCombo[] = $model->getAttributeLabel($value); |
|||||
296 | 10 | $valueCombo[] = '"' . $model->$value . '"'; |
|||||
297 | } else { |
||||||
298 | $attributeCombo[] = $model->getAttributeLabel($key); |
||||||
299 | 10 | $valueCombo[] = '"' . $model->$key . '"'; |
|||||
300 | } |
||||||
301 | } |
||||||
302 | 10 | $this->addError($model, $attribute, $this->message, [ |
|||||
303 | 10 | 'attributes' => Inflector::sentence($attributeCombo), |
|||||
304 | 10 | 'values' => implode('-', $valueCombo), |
|||||
305 | ]); |
||||||
306 | 10 | } |
|||||
307 | |||||||
308 | /** |
||||||
309 | * Returns conditions with alias. |
||||||
310 | * @param ActiveQuery $query |
||||||
311 | * @param array $conditions array of condition, keys to be modified |
||||||
312 | * @param null|string $alias set empty string for no apply alias. Set null for apply primary table alias |
||||||
313 | * @return array |
||||||
314 | */ |
||||||
315 | 94 | private function applyTableAlias($query, $conditions, $alias = null) |
|||||
316 | { |
||||||
317 | 94 | if ($alias === null) { |
|||||
318 | 94 | $alias = array_keys($query->getTablesUsedInFrom())[0]; |
|||||
319 | } |
||||||
320 | 94 | $prefixedConditions = []; |
|||||
321 | 94 | foreach ($conditions as $columnName => $columnValue) { |
|||||
322 | 94 | if (strpos($columnName, '(') === false) { |
|||||
323 | 94 | $columnName = preg_replace('/^' . preg_quote($alias) . '\.(.*)$/', '$1', $columnName); |
|||||
324 | 94 | if (strpos($columnName, '[[') === 0) { |
|||||
325 | 1 | $prefixedColumn = "{$alias}.{$columnName}"; |
|||||
326 | } else { |
||||||
327 | 94 | $prefixedColumn = "{$alias}.[[{$columnName}]]"; |
|||||
328 | } |
||||||
329 | } else { |
||||||
330 | // there is an expression, can't prefix it reliably |
||||||
331 | 5 | $prefixedColumn = $columnName; |
|||||
332 | } |
||||||
333 | |||||||
334 | 94 | $prefixedConditions[$prefixedColumn] = $columnValue; |
|||||
335 | } |
||||||
336 | |||||||
337 | 94 | return $prefixedConditions; |
|||||
338 | } |
||||||
339 | } |
||||||
340 |
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.