Passed
Push — master ( 9a3093...472600 )
by Alexander
09:50
created

ExistValidator::setForceMasterDb()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

323
            $query->andWhere(/** @scrutinizer ignore-type */ $this->filter);
Loading history...
324
        }
325
326 16
        return $query;
327
    }
328
329
    /**
330
     * Returns conditions with alias.
331
     * @param ActiveQuery $query
332
     * @param array $conditions array of condition, keys to be modified
333
     * @param null|string $alias set empty string for no apply alias. Set null for apply primary table alias
334
     * @return array
335
     */
336 13
    private function applyTableAlias($query, $conditions, $alias = null)
337
    {
338 13
        if ($alias === null) {
339 13
            $alias = array_keys($query->getTablesUsedInFrom())[0];
340
        }
341 13
        $prefixedConditions = [];
342 13
        foreach ($conditions as $columnName => $columnValue) {
343 13
            if (strpos($columnName, '(') === false) {
344 10
                $prefixedColumn = "{$alias}.[[" . preg_replace(
345 10
                    '/^' . preg_quote($alias) . '\.(.*)$/',
346 10
                    '$1',
347 10
                    $columnName) . ']]';
348
            } else {
349
                // there is an expression, can't prefix it reliably
350 3
                $prefixedColumn = $columnName;
351
            }
352
353 13
            $prefixedConditions[$prefixedColumn] = $columnValue;
354
        }
355
356 13
        return $prefixedConditions;
357
    }
358
}
359