AttributeIndexValidator::validateAttribute()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.4285
cc 2
eloc 6
nc 2
nop 2
crap 2
1
<?php
2
/**
3
 * Yii2 Attribute Index Validator
4
 *
5
 * This file contains validator.
6
 *
7
 * @author  Martin Stolz <[email protected]>
8
 */
9
10
namespace herroffizier\yii2aiv;
11
12
use Closure;
13
use yii\validators\Validator;
14
use yii\db\ActiveQueryInterface;
15
use yii\db\ActiveRecordInterface;
16
17
class AttributeIndexValidator extends Validator
18
{
19
    /**
20
     * Separator between base value and index.
21
     *
22
     * @var string
23
     */
24
    public $separator = '-';
25
26
    /**
27
     * Start index value.
28
     *
29
     * @var integer
30
     */
31
    public $startIndex = 1;
32
33
    /**
34
     * Additional filter applied to query used to check uniqueness.
35
     *
36
     * @var string|array|Closure
37
     */
38
    public $filter = null;
39
40
    /**
41
     * Escaped separator.
42
     *
43
     * @var string
44
     */
45
    protected $escapedSeparator = null;
46
47
    /**
48
     * Escape string for regexp.
49
     *
50
     * @param  string $string
51
     * @return string
52
     */
53 32
    protected function escapeForRegexp($string)
54
    {
55 32
        return addcslashes($string, '[]().?-*^$/:<>');
56
    }
57
58
    /**
59
     * Get escaped separator for regexps.
60
     *
61
     * @return string
62
     */
63 32
    protected function getEscapedSeparator()
64
    {
65 32
        if ($this->escapedSeparator === null) {
66 32
            $this->escapedSeparator = $this->escapeForRegexp($this->separator);
67 24
        }
68
69 32
        return $this->escapedSeparator;
70
    }
71
72
    /**
73
     * Add filter to query (if any exists).
74
     *
75
     * @param ActiveQueryInterface $query
76
     */
77 32
    protected function addFilterToQuery(ActiveQueryInterface $query)
78
    {
79 32
        if (!$this->filter) {
80 28
            return;
81
        }
82
83 4
        if ($this->filter instanceof Closure) {
84 4
            call_user_func_array($this->filter, [$query]);
85 3
        } else {
86 4
            $query->andWhere($this->filter);
87
        }
88 4
    }
89
90
    /**
91
     * Get condition to exclude current model by it's primay key.
92
     *
93
     * If model is new, empty array will be returned.
94
     *
95
     * @param  ActiveRecordInterface $model
96
     * @return array
97
     */
98 32
    protected function getExcludeByPkCondition(ActiveRecordInterface $model)
99
    {
100 32
        if (array_filter($pk = $model->getPrimaryKey(true))) {
101 4
            $condition = ['not', $pk];
102 3
        } else {
103 28
            $condition = [];
104
        }
105
106 32
        return $condition;
107
    }
108
109
    /**
110
     * Whether there are an attribute value collision.
111
     *
112
     * @param  ActiveRecordInterface $model
113
     * @param  string                $attribute
114
     * @return boolean
115
     */
116 32
    protected function hasCollision(ActiveRecordInterface $model, $attribute)
117
    {
118
        $query =
119 32
            $model->find()->
120 32
                andWhere($this->getExcludeByPkCondition($model))->
121 32
                andWhere([$attribute => $model->$attribute]);
122 32
        $this->addFilterToQuery($query);
123
124 32
        return $query->exists();
125
    }
126
127
    /**
128
     * Get attribute value common part, e. g. part without index but with separator.
129
     *
130
     * For example, common part for 'test' and 'test-1' will be 'test-'.
131
     *
132
     * @param  ActiveRecordInterface $model
133
     * @param  string                $attribute
134
     * @return string
135
     */
136 32
    protected function getCommonPart(ActiveRecordInterface $model, $attribute)
137
    {
138 32
        $escapedSeparator = $this->getEscapedSeparator();
139
140 32
        return preg_replace('/'.$escapedSeparator.'\d+$/', '', $model->$attribute).$this->separator;
141
    }
142
143
    /**
144
     * Find max index stored in database.
145
     *
146
     * If no index found, startIndex - 1 will be returned.
147
     *
148
     * @param  ActiveRecordInterface $model
149
     * @param  string                $attribute
150
     * @param  string                $commonPart
151
     * @return integer
152
     */
153 32
    protected function findMaxIndex(ActiveRecordInterface $model, $attribute, $commonPart)
154
    {
155
        // Find all possible max values.
156 32
        $db = $model::getDb();
157 32
        $indexExpression = 'SUBSTRING('.$db->quoteColumnName($attribute).', :commonPartOffset)';
158
        $query =
159 32
            $model->find()->
160 32
                select(['_index' => $indexExpression])->
161 32
                andWhere($this->getExcludeByPkCondition($model))->
162 32
                andWhere(['like', $attribute, $commonPart])->
163 32
                andHaving(['not in', '_index', [0]])->
164 32
                orderBy(['CAST('.$db->quoteColumnName('_index').' AS UNSIGNED)' => SORT_DESC])->
165 32
                addParams(['commonPartOffset' => mb_strlen($commonPart) + 1])->
166 32
                asArray();
167 32
        $this->addFilterToQuery($query);
168 32
        foreach ($query->each() as $row) {
169 20
            $index = $row['_index'];
170 20
            if (!preg_match('/^\d+$/', $index)) {
171 4
                continue;
172
            }
173
174 20
            return $index;
175 21
        }
176
177 28
        return $this->startIndex - 1;
178
    }
179
180
    /**
181
     * @inheritdoc
182
     */
183 32
    public function validateAttribute($model, $attribute)
184
    {
185 32
        if (!$this->hasCollision($model, $attribute)) {
186 12
            return;
187
        }
188
189 32
        $commonPart = $this->getCommonPart($model, $attribute);
190 32
        $maxIndex = $this->findMaxIndex($model, $attribute, $commonPart);
191
192 32
        $model->$attribute = $commonPart.($maxIndex + 1);
193 32
    }
194
}
195