UniqueValidator.php 5.23 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
    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        if ($this->message === null) {
            $this->message = Yii::t('yii', '{attribute} "{value}" has already been taken.');
        }
    }
Qiang Xue committed
72

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

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

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

95 96 97
                return;
            }
        }
Alexander Makarov committed
98

99 100
        $query = $targetClass::find();
        $query->where($params);
101

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

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

        if ($exists) {
            $this->addError($object, $attribute, $this->message);
        }
    }
Zander Baldwin committed
137
}