Completed
Push — 2.1 ( c952e8...98ed49 )
by Carsten
10:00
created

UniqueValidator   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 115
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 94.55%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 18
c 2
b 1
f 0
lcom 1
cbo 3
dl 0
loc 115
ccs 52
cts 55
cp 0.9455
rs 10

2 Methods

Rating   Name   Duplication   Size   Complexity  
F validateAttribute() 0 70 16
A init() 0 7 2
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\db\ActiveRecordInterface;
12
13
/**
14
 * UniqueValidator validates that the attribute value is unique in the specified database table.
15
 *
16
 * UniqueValidator checks if the value being validated is unique in the table column specified by
17
 * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]].
18
 *
19
 * The following are examples of validation rules using this validator:
20
 *
21
 * ```php
22
 * // a1 needs to be unique
23
 * ['a1', 'unique']
24
 * // a1 needs to be unique, but column a2 will be used to check the uniqueness of the a1 value
25
 * ['a1', 'unique', 'targetAttribute' => 'a2']
26
 * // a1 and a2 need to be unique together, and they both will receive error message
27
 * [['a1', 'a2'], 'unique', 'targetAttribute' => ['a1', 'a2']]
28
 * // a1 and a2 need to be unique together, only a1 will receive error message
29
 * ['a1', 'unique', 'targetAttribute' => ['a1', 'a2']]
30
 * // a1 needs to be unique by checking the uniqueness of both a2 and a3 (using a1 value)
31
 * ['a1', 'unique', 'targetAttribute' => ['a2', 'a1' => 'a3']]
32
 * ```
33
 *
34
 * @author Qiang Xue <[email protected]>
35
 * @since 2.0
36
 */
37
class UniqueValidator extends Validator
38
{
39
    /**
40
     * @var string the name of the ActiveRecord class that should be used to validate the uniqueness
41
     * of the current attribute value.
42
     * This must be a fully qualified class name.
43
     *
44
     * If not set, it will use the ActiveRecord class of the attribute being validated.
45
     *
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 uniqueness 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 uniqueness
53
     * of multiple columns at the same time. The array values are the attributes that will be
54
     * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated.
55
     * If the key and the value are the same, you can just specify the value.
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
67
    /**
68
     * @inheritdoc
69
     */
70 33
    public function init()
71
    {
72 33
        parent::init();
73 33
        if ($this->message === null) {
74 33
            $this->message = Yii::t('yii', '{attribute} "{value}" has already been taken.');
75 33
        }
76 33
    }
77
78
    /**
79
     * @inheritdoc
80
     */
81 30
    public function validateAttribute($model, $attribute)
82
    {
83
        /* @var $targetClass ActiveRecordInterface */
84 30
        $targetClass = $this->targetClass ?: get_class($model);
85 30
        $targetClass = ltrim($targetClass, '\\');
86 30
        $targetAttribute = $this->targetAttribute ?: $attribute;
87
88 30
        if (is_array($targetAttribute)) {
89 9
            $params = [];
90 9
            foreach ($targetAttribute as $k => $v) {
91 9
                $params[$v] = is_int($k) ? $model->$v : $model->$k;
92 9
            }
93 9
        } else {
94 24
            $params = [$targetAttribute => $model->$attribute];
95
        }
96
97 30
        foreach ($params as $value) {
98 30
            if (is_array($value)) {
99 6
                $this->addError(
100 6
                    $model,
101 6
                    $attribute,
102 6
                    Yii::t('yii', '{attribute} is invalid.')
103 6
                );
104
105 6
                return;
106
            }
107 27
        }
108
109 27
        $query = $targetClass::find();
110 27
        $query->andWhere($params);
111
112 27
        if ($this->filter instanceof \Closure) {
113
            call_user_func($this->filter, $query);
114 27
        } elseif ($this->filter !== null) {
115
            $query->andWhere($this->filter);
116
        }
117
118
        if (!$model instanceof ActiveRecordInterface
119 27
            || $model->getIsNewRecord()
120 24
            || get_class($model) !== $targetClass
121 27
        ) {
122
            // if current $model isn't in the database yet then it's OK just to call exists()
123
            // also there's no need to run check based on primary keys, when $targetClass is not the same as $model's class
124 24
            $exists = $query->exists();
125 21
        } else {
126
            // if current $model is in the database already we can't use exists()
127
            /* @var $models ActiveRecordInterface[] */
128 10
            $models = $query->limit(2)->all();
129 10
            $n = count($models);
130 10
            if ($n === 1) {
131 10
                $keys = array_keys($params);
132 10
                $pks = $targetClass::primaryKey();
133 10
                sort($keys);
134 10
                sort($pks);
135 10
                if ($keys === $pks) {
136
                    // primary key is modified and not unique
137 6
                    $exists = $model->getOldPrimaryKey() != $model->getPrimaryKey();
138 6
                } else {
139
                    // non-primary key, need to exclude the current record based on PK
140 7
                    $exists = $models[0]->getPrimaryKey() != $model->getOldPrimaryKey();
141
                }
142 10
            } else {
143 3
                $exists = $n > 1;
144
            }
145
        }
146
147 24
        if ($exists) {
148 20
            $this->addError($model, $attribute, $this->message);
149 20
        }
150 24
    }
151
}
152