/
UniqueValidator.php
137 lines (120 loc) · 4.56 KB
/
UniqueValidator.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/
namespace craft\validators;
use Craft;
use craft\helpers\StringHelper;
use yii\base\Model;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveRecord;
use yii\validators\UniqueValidator as YiiUniqueValidator;
/**
* Class UniqueValidator.
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 3.0.0
*/
class UniqueValidator extends YiiUniqueValidator
{
/**
* @var string|string[] If [[targetClass]] is set, this defines the model
* attributes that represent the record's primary key(s). Can be set to a
* string or array of strings of model attributes in the same respective
* order as the primary keys defined by the record's primaryKey() method, or
* can be set to an array of attribute/PK pairs, which explicitly maps model
* attributes to record primary keys. Defaults to whatever the record's
* primaryKey() method returns.
*/
public string|array $pk;
/**
* @var Model|null The model that is being validated
*/
protected ?Model $originalModel = null;
/**
* @var bool Whether a case-insensitive check should be performed.
*/
public bool $caseInsensitive = false;
/**
* @inheritdoc
*/
public function validateAttribute($model, $attribute): void
{
if ($targetClass = $this->targetClass) {
// Exclude this model's row using the filter
/** @var ActiveRecord|string $targetClass */
$pks = $targetClass::primaryKey();
if (isset($this->pk)) {
$pkMap = is_string($this->pk) ? StringHelper::split($this->pk) : $this->pk;
} else {
$pkMap = $pks;
}
$exists = false;
$pkFilter = ['and'];
$tableName = Craft::$app->getDb()->getSchema()->getRawTableName($targetClass::tableName());
foreach ($pkMap as $k => $v) {
if (is_int($k)) {
$pkAttribute = $v;
$pkColumn = $pks[$k];
} else {
$pkAttribute = $k;
$pkColumn = $v;
}
if ($model->$pkAttribute) {
$exists = true;
$pkFilter[] = ['not', ["$tableName.$pkColumn" => $model->$pkAttribute]];
}
}
if ($exists) {
if ($this->filter) {
if (is_callable($this->filter)) {
$currentFilter = $this->filter;
// Wrap the closure in another closure that will add the PK filter
$this->filter = function(ActiveQueryInterface $query) use ($currentFilter, $pkFilter) {
$currentFilter($query);
$query->andWhere($pkFilter);
};
} else {
// If it isn't a closure then `filter` will be an array or string
$this->filter = ['and', $this->filter, $pkFilter];
}
} else {
$this->filter = $pkFilter;
}
}
}
$originalAttributes = [];
$originalTargetAttribute = $this->targetAttribute;
if ($this->caseInsensitive && Craft::$app->getDb()->getIsPgsql()) {
// Convert targetAttribute to an array of ['attribute' => 'lower([[column]])'] conditions
// and set the model attributes to lowercase
$targetAttributes = (array)($this->targetAttribute ?? $attribute);
$newTargetAttributes = [];
foreach ($targetAttributes as $k => $v) {
$a = is_int($k) ? $v : $k;
$originalAttributes[$a] = $model->$a;
$model->$a = mb_strtolower($model->$a);
$newTargetAttributes[$a] = "lower([[$v]])";
}
$this->targetAttribute = $newTargetAttributes;
}
parent::validateAttribute($model, $attribute);
$this->targetAttribute = $originalTargetAttribute;
foreach ($originalAttributes as $k => $v) {
$model->$k = $v;
}
}
/**
* @inheritdoc
*/
public function addError($model, $attribute, $message, $params = []): void
{
// Use the original model if there is one
if (isset($this->originalModel)) {
$model = $this->originalModel;
}
parent::addError($model, $attribute, $message, $params);
}
}