Completed
Push — master ( 17ecb7...725fdf )
by Alexander
15:14
created

Exist::checkTargetAttributeExistence()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
c 0
b 0
f 0
nc 10
nop 2
dl 0
loc 24
cc 6
rs 9.2222
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 Yiisoft\Validator\Rule;
9
10
use yii\exceptions\InvalidConfigException;
0 ignored issues
show
Bug introduced by
The type yii\exceptions\InvalidConfigException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use yii\base\Model;
0 ignored issues
show
Bug introduced by
The type yii\base\Model was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use Yiisoft\ActiveRecord\ActiveQuery;
0 ignored issues
show
Bug introduced by
The type Yiisoft\ActiveRecord\ActiveQuery was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use Yiisoft\ActiveRecord\ActiveRecord;
0 ignored issues
show
Bug introduced by
The type Yiisoft\ActiveRecord\ActiveRecord was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use Yiisoft\Db\QueryInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\QueryInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use Yiisoft\Validator\Result;
16
use Yiisoft\Validator\Rule;
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
 * TODO: can we abstract it from storrage?
48
 */
49
class Exist extends Rule
50
{
51
    /**
52
     * @var string the name of the ActiveRecord class that should be used to validateValue the existence
53
     * of the current attribute value. If not set, it will use the ActiveRecord class of the attribute being validated.
54
     * @see targetAttribute
55
     */
56
    public $targetClass;
57
    /**
58
     * @var string|array the name of the ActiveRecord attribute that should be used to
59
     * validateValue the existence of the current attribute value. If not set, it will use the name
60
     * of the attribute currently being validated. You may use an array to validateValue the existence
61
     * of multiple columns at the same time. The array key is the name of the attribute with the value to validateValue,
62
     * the array value is the name of the database field to search.
63
     */
64
    public $targetAttribute;
65
    /**
66
     * @var string the name of the relation that should be used to validateValue the existence of the current attribute value
67
     * This param overwrites $targetClass and $targetAttribute
68
     */
69
    public $targetRelation;
70
    /**
71
     * @var string|array|\Closure additional filter to be applied to the DB query used to check the existence of the attribute value.
72
     * This can be a string or an array representing the additional query condition (refer to [[\Yiisoft\Db\Query::where()]]
73
     * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query`
74
     * is the [[\Yiisoft\Db\Query|Query]] object that you can modify in the function.
75
     */
76
    public $filter;
77
    /**
78
     * @var bool whether to allow array type attribute.
79
     */
80
    public $allowArray = false;
81
    /**
82
     * @var string and|or define how target attributes are related
83
     */
84
    public $targetAttributeJunction = 'and';
85
    /**
86
     * @var bool whether this validator is forced to always use master DB
87
     */
88
    public $forceMasterDb = true;
89
90
91
    private $message;
92
93
    public function init(): void
94
    {
95
        parent::init();
0 ignored issues
show
Bug introduced by
The method init() does not exist on Yiisoft\Validator\Rule. It seems like you code against a sub-type of Yiisoft\Validator\Rule such as Yiisoft\Validator\Rule\Unique or Yiisoft\Validator\Rule\Exist or Yiisoft\Validator\Rule\Image. ( Ignorable by Annotation )

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

95
        parent::/** @scrutinizer ignore-call */ 
96
                init();
Loading history...
96
        if ($this->message === null) {
97
            $this->message = $this->formatMessage( '{attribute} is invalid.');
98
        }
99
    }
100
101
    /**
102
     * Validates existence of the current attribute based on relation name
103
     * @param \yii\activerecord\ActiveRecord $model the data model to be validated
0 ignored issues
show
Bug introduced by
The type yii\activerecord\ActiveRecord was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
104
     * @param string $attribute the name of the attribute to be validated.
105
     */
106
    private function checkTargetRelationExistence($model, $attribute)
107
    {
108
        $exists = false;
109
        /** @var ActiveQuery $relationQuery */
110
        $relationQuery = $model->{'get' . ucfirst($this->targetRelation)}();
111
112
        if ($this->filter instanceof \Closure) {
113
            call_user_func($this->filter, $relationQuery);
114
        } elseif ($this->filter !== null) {
115
            $relationQuery->andWhere($this->filter);
116
        }
117
118
        if ($this->forceMasterDb && method_exists($model::getDb(), 'useMaster')) {
119
            $model::getDb()->useMaster(function () use ($relationQuery, &$exists) {
120
                $exists = $relationQuery->exists();
121
            });
122
        } else {
123
            $exists = $relationQuery->exists();
124
        }
125
126
127
        if (!$exists) {
128
            $this->addError($model, $attribute, $this->message);
0 ignored issues
show
Bug introduced by
The method addError() does not exist on Yiisoft\Validator\Rule\Exist. ( Ignorable by Annotation )

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

128
            $this->/** @scrutinizer ignore-call */ 
129
                   addError($model, $attribute, $this->message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
129
        }
130
    }
131
132
    /**
133
     * Validates existence of the current attribute based on targetAttribute
134
     * @param \yii\base\Model $model the data model to be validated
135
     * @param string $attribute the name of the attribute to be validated.
136
     */
137
    private function checkTargetAttributeExistence($model, $attribute)
138
    {
139
        $targetAttribute = $this->targetAttribute ?? $attribute;
140
        $params = $this->prepareConditions($targetAttribute, $model, $attribute);
141
        $conditions = [$this->targetAttributeJunction == 'or' ? 'or' : 'and'];
142
143
        if (!$this->allowArray) {
144
            foreach ($params as $key => $value) {
145
                if (is_array($value)) {
146
                    $this->addError($model, $attribute, Yii::t('yii', '{attribute} is invalid.'));
0 ignored issues
show
Bug introduced by
The type Yiisoft\Validator\Rule\Yii was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
147
148
                    return;
149
                }
150
                $conditions[] = [$key => $value];
151
            }
152
        } else {
153
            $conditions[] = $params;
154
        }
155
156
        $targetClass = $this->targetClass ?? get_class($model);
157
        $query = $this->createQuery($targetClass, $conditions);
158
159
        if (!$this->valueExists($targetClass, $query, $model->$attribute)) {
160
            $this->addError($model, $attribute, $this->message);
161
        }
162
    }
163
164
    /**
165
     * Processes attributes' relations described in $targetAttribute parameter into conditions, compatible with
166
     * [[\yii\db\Query::where()|Query::where()]] key-value format.
167
     *
168
     * @param $targetAttribute array|string $attribute the name of the ActiveRecord attribute that should be used to
169
     * validateValue the existence of the current attribute value. If not set, it will use the name
170
     * of the attribute currently being validated. You may use an array to validateValue the existence
171
     * of multiple columns at the same time. The array key is the name of the attribute with the value to validateValue,
172
     * the array value is the name of the database field to search.
173
     * If the key and the value are the same, you can just specify the value.
174
     * @param \yii\base\Model $model the data model to be validated
175
     * @param string $attribute the name of the attribute to be validated in the $model
176
     * @return array conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format.
177
     * @throws InvalidConfigException
178
     */
179
    private function prepareConditions($targetAttribute, $model, $attribute)
180
    {
181
        if (is_array($targetAttribute)) {
182
            if ($this->allowArray) {
183
                throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.');
184
            }
185
            $conditions = [];
186
            foreach ($targetAttribute as $k => $v) {
187
                $conditions[$v] = is_int($k) ? $model->$v : $model->$k;
188
            }
189
        } else {
190
            $conditions = [$targetAttribute => $model->$attribute];
191
        }
192
193
        $targetModelClass = $this->getTargetClass($model);
194
        if (!is_subclass_of($targetModelClass, 'yii\activerecord\ActiveRecord')) {
195
            return $conditions;
196
        }
197
198
        /** @var ActiveRecord $targetModelClass */
199
        return $this->applyTableAlias($targetModelClass::find(), $conditions);
200
    }
201
202
    /**
203
     * @param Model $model the data model to be validated
204
     * @return string Target class name
205
     */
206
    private function getTargetClass($model)
207
    {
208
        return $this->targetClass ?? get_class($model);
209
    }
210
211
    protected function validateValue($value): Result
212
    {
213
        if ($this->targetClass === null) {
214
            throw new InvalidConfigException('The "targetClass" property must be set.');
215
        }
216
        if (!is_string($this->targetAttribute)) {
217
            throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.');
218
        }
219
220
        if (is_array($value) && !$this->allowArray) {
221
            return [$this->message, []];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($this->message, array()) returns the type array<integer,array|mixed> which is incompatible with the type-hinted return Yiisoft\Validator\Result.
Loading history...
222
        }
223
224
        $query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]);
225
226
        return $this->valueExists($this->targetClass, $query, $value) ? null : [$this->message, []];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->valueExist...this->message, array()) returns the type array<integer,array|mixed>|null which is incompatible with the type-hinted return Yiisoft\Validator\Result.
Loading history...
227
    }
228
229
    /**
230
     * Check whether value exists in target table
231
     *
232
     * @param string $targetClass
233
     * @param QueryInterface $query
234
     * @param mixed $value the value want to be checked
235
     * @return bool
236
     */
237
    private function valueExists($targetClass, $query, $value)
238
    {
239
        $db = $targetClass::getDb();
240
        $exists = false;
241
242
        if ($this->forceMasterDb && method_exists($db, 'useMaster')) {
243
            $db->useMaster(function ($db) use ($query, $value, &$exists) {
244
                $exists = $this->queryValueExists($query, $value);
245
            });
246
        } else {
247
            $exists = $this->queryValueExists($query, $value);
248
        }
249
250
        return $exists;
251
    }
252
253
254
    /**
255
     * Run query to check if value exists
256
     *
257
     * @param QueryInterface $query
258
     * @param mixed $value the value to be checked
259
     * @return bool
260
     */
261
    private function queryValueExists($query, $value)
262
    {
263
        if (is_array($value)) {
264
            return $query->count("DISTINCT [[$this->targetAttribute]]") == count($value) ;
265
        }
266
        return $query->exists();
267
    }
268
269
    /**
270
     * Creates a query instance with the given condition.
271
     * @param string $targetClass the target AR class
272
     * @param mixed $condition query condition
273
     * @return \yii\activerecord\ActiveQueryInterface the query instance
0 ignored issues
show
Bug introduced by
The type yii\activerecord\ActiveQueryInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
274
     */
275
    protected function createQuery($targetClass, $condition)
276
    {
277
        /* @var $targetClass \yii\activerecord\ActiveRecordInterface */
278
        $query = $targetClass::find()->andWhere($condition);
279
        if ($this->filter instanceof \Closure) {
280
            call_user_func($this->filter, $query);
281
        } elseif ($this->filter !== null) {
282
            $query->andWhere($this->filter);
283
        }
284
285
        return $query;
286
    }
287
288
    /**
289
     * Returns conditions with alias.
290
     * @param ActiveQuery $query
291
     * @param array $conditions array of condition, keys to be modified
292
     * @param null|string $alias set empty string for no apply alias. Set null for apply primary table alias
293
     * @return array
294
     */
295
    private function applyTableAlias($query, $conditions, $alias = null)
296
    {
297
        if ($alias === null) {
298
            $alias = array_keys($query->getTablesUsedInFrom())[0];
299
        }
300
        $prefixedConditions = [];
301
        foreach ($conditions as $columnName => $columnValue) {
302
            if (strpos($columnName, '(') === false) {
303
                $prefixedColumn = "{$alias}.[[" . preg_replace(
304
                    '/^' . preg_quote($alias) . '\.(.*)$/',
305
                    '$1',
306
                    $columnName) . ']]';
307
            } else {
308
                // there is an expression, can't prefix it reliably
309
                $prefixedColumn = $columnName;
310
            }
311
312
            $prefixedConditions[$prefixedColumn] = $columnValue;
313
        }
314
315
        return $prefixedConditions;
316
    }
317
}
318