UniqueValidator.php 5.21 KB
Newer Older
w  
Qiang Xue committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
Qiang Xue committed
4
 * @copyright Copyright (c) 2008 Yii Software LLC
w  
Qiang Xue committed
5 6 7
 * @license http://www.yiiframework.com/license/
 */

w  
Qiang Xue committed
8
namespace yii\validators;
Qiang Xue committed
9 10

use Yii;
Carsten Brandt committed
11
use yii\db\ActiveRecordInterface;
w  
Qiang Xue committed
12

w  
Qiang Xue committed
13
/**
14 15 16 17 18 19 20 21 22 23 24 25
 * UniqueValidator validates that the attribute value is unique in the specified database table.
 *
 * UniqueValidator checks if the value being validated is unique in the table column specified by
 * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]].
 *
 * The followings are examples of validation rules using this validator:
 *
 * ```php
 * // a1 needs to be unique
 * ['a1', 'unique']
 * // a1 needs to be unique, but column a2 will be used to check the uniqueness of the a1 value
 * ['a1', 'unique', 'targetAttribute' => 'a2']
26
 * // a1 and a2 need to be unique together, and they both will receive error message
Qiang Xue committed
27
 * [['a1', 'a2'], 'unique', 'targetAttribute' => ['a1', 'a2']]
28
 * // a1 and a2 need to be unique together, only a1 will receive error message
29 30 31 32
 * ['a1', 'unique', 'targetAttribute' => ['a1', 'a2']]
 * // a1 needs to be unique by checking the uniqueness of both a2 and a3 (using a1 value)
 * ['a1', 'unique', 'targetAttribute' => ['a2', 'a1' => 'a3']]
 * ```
w  
Qiang Xue committed
33 34
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
Alexander Makarov committed
35
 * @since 2.0
w  
Qiang Xue committed
36
 */
Alexander Makarov committed
37
class UniqueValidator extends Validator
w  
Qiang Xue committed
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. If not set, it will use the ActiveRecord class of the attribute being validated.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
     * @see targetAttribute
     */
    public $targetClass;
    /**
     * @var string|array the name of the ActiveRecord attribute that should be used to
     * validate the uniqueness of the current attribute value. If not set, it will use the name
     * of the attribute currently being validated. You may use an array to validate the uniqueness
     * of multiple columns at the same time. The array values are the attributes that will be
     * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated.
     * If the key and the value are the same, you can just specify the value.
     */
    public $targetAttribute;
    /**
     * @var string|array|\Closure additional filter to be applied to the DB query used to check the uniqueness of the attribute value.
     * This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]]
     * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query`
     * is the [[\yii\db\Query|Query]] object that you can modify in the function.
     */
    public $filter;
w  
Qiang Xue committed
61

62

63 64 65 66 67 68 69 70 71 72
    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        if ($this->message === null) {
            $this->message = Yii::t('yii', '{attribute} "{value}" has already been taken.');
        }
    }
Qiang Xue committed
73

74 75 76
    /**
     * @inheritdoc
     */
Qiang Xue committed
77
    public function validateAttribute($model, $attribute)
78
    {
79
        /* @var $targetClass ActiveRecordInterface */
Qiang Xue committed
80
        $targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass;
81
        $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
82

83 84 85
        if (is_array($targetAttribute)) {
            $params = [];
            foreach ($targetAttribute as $k => $v) {
Qiang Xue committed
86
                $params[$v] = is_integer($k) ? $model->$v : $model->$k;
87 88
            }
        } else {
Qiang Xue committed
89
            $params = [$targetAttribute => $model->$attribute];
90
        }
91

92 93
        foreach ($params as $value) {
            if (is_array($value)) {
Qiang Xue committed
94
                $this->addError($model, $attribute, Yii::t('yii', '{attribute} is invalid.'));
95

96 97 98
                return;
            }
        }
Alexander Makarov committed
99

100
        $query = $targetClass::find();
101
        $query->andWhere($params);
102

103 104 105 106 107
        if ($this->filter instanceof \Closure) {
            call_user_func($this->filter, $query);
        } elseif ($this->filter !== null) {
            $query->andWhere($this->filter);
        }
w  
Qiang Xue committed
108

Qiang Xue committed
109 110
        if (!$model instanceof ActiveRecordInterface || $model->getIsNewRecord()) {
            // if current $model isn't in the database yet then it's OK just to call exists()
111 112
            $exists = $query->exists();
        } else {
Qiang Xue committed
113 114 115 116
            // if current $model is in the database already we can't use exists()
            /* @var $models ActiveRecordInterface[] */
            $models = $query->limit(2)->all();
            $n = count($models);
117 118 119 120 121 122 123
            if ($n === 1) {
                $keys = array_keys($params);
                $pks = $targetClass::primaryKey();
                sort($keys);
                sort($pks);
                if ($keys === $pks) {
                    // primary key is modified and not unique
Qiang Xue committed
124
                    $exists = $model->getOldPrimaryKey() != $model->getPrimaryKey();
125 126
                } else {
                    // non-primary key, need to exclude the current record based on PK
Qiang Xue committed
127
                    $exists = $models[0]->getPrimaryKey() != $model->getOldPrimaryKey();
128 129 130 131 132 133 134
                }
            } else {
                $exists = $n > 1;
            }
        }

        if ($exists) {
Qiang Xue committed
135
            $this->addError($model, $attribute, $this->message);
136 137
        }
    }
Zander Baldwin committed
138
}