1 | <?php |
||
61 | class SluggableBehavior extends AttributeBehavior |
||
62 | { |
||
63 | /** |
||
64 | * @var string the attribute that will receive the slug value |
||
65 | */ |
||
66 | public $slugAttribute = 'slug'; |
||
67 | /** |
||
68 | * @var string|array|null the attribute or list of attributes whose value will be converted into a slug |
||
69 | * or `null` meaning that the `$value` property will be used to generate a slug. |
||
70 | */ |
||
71 | public $attribute; |
||
72 | /** |
||
73 | * @var callable|string|null the value that will be used as a slug. This can be an anonymous function |
||
74 | * or an arbitrary value or null. If the former, the return value of the function will be used as a slug. |
||
75 | * If `null` then the `$attribute` property will be used to generate a slug. |
||
76 | * The signature of the function should be as follows, |
||
77 | * |
||
78 | * ```php |
||
79 | * function ($event) |
||
80 | * { |
||
81 | * // return slug |
||
82 | * } |
||
83 | * ``` |
||
84 | */ |
||
85 | public $value; |
||
86 | /** |
||
87 | * @var bool whether to generate a new slug if it has already been generated before. |
||
88 | * If true, the behavior will not generate a new slug even if [[attribute]] is changed. |
||
89 | * @since 2.0.2 |
||
90 | */ |
||
91 | public $immutable = false; |
||
92 | /** |
||
93 | * @var bool whether to ensure generated slug value to be unique among owner class records. |
||
94 | * If enabled behavior will validate slug uniqueness automatically. If validation fails it will attempt |
||
95 | * generating unique slug value from based one until success. |
||
96 | */ |
||
97 | public $ensureUnique = false; |
||
98 | /** |
||
99 | * @var bool whether to skip slug generation if [[attribute]] is null or an empty string. |
||
100 | * If true, the behaviour will not generate a new slug if [[attribute]] is null or an empty string. |
||
101 | * @since 2.0.13 |
||
102 | */ |
||
103 | public $skipOnEmpty = false; |
||
104 | /** |
||
105 | * @var array configuration for slug uniqueness validator. Parameter 'class' may be omitted - by default |
||
106 | * [[UniqueValidator]] will be used. |
||
107 | * @see UniqueValidator |
||
108 | */ |
||
109 | public $uniqueValidator = []; |
||
110 | /** |
||
111 | * @var callable slug unique value generator. It is used in case [[ensureUnique]] enabled and generated |
||
112 | * slug is not unique. This should be a PHP callable with following signature: |
||
113 | * |
||
114 | * ```php |
||
115 | * function ($baseSlug, $iteration, $model) |
||
116 | * { |
||
117 | * // return uniqueSlug |
||
118 | * } |
||
119 | * ``` |
||
120 | * |
||
121 | * If not set unique slug will be generated adding incrementing suffix to the base slug. |
||
122 | */ |
||
123 | public $uniqueSlugGenerator; |
||
124 | |||
125 | |||
126 | /** |
||
127 | * {@inheritdoc} |
||
128 | */ |
||
129 | 9 | public function init() |
|
141 | |||
142 | /** |
||
143 | * {@inheritdoc} |
||
144 | */ |
||
145 | 9 | protected function getValue($event) |
|
146 | { |
||
147 | 9 | if (!$this->isNewSlugNeeded()) { |
|
148 | 3 | return $this->owner->{$this->slugAttribute}; |
|
149 | } |
||
150 | |||
151 | 9 | if ($this->attribute !== null) { |
|
152 | 8 | $slugParts = []; |
|
153 | 8 | foreach ((array) $this->attribute as $attribute) { |
|
154 | 8 | $part = ArrayHelper::getValue($this->owner, $attribute); |
|
|
|||
155 | 8 | if ($this->skipOnEmpty && $this->isEmpty($part)) { |
|
156 | 1 | return $this->owner->{$this->slugAttribute}; |
|
157 | } |
||
158 | 8 | $slugParts[] = $part; |
|
159 | } |
||
160 | 8 | $slug = $this->generateSlug($slugParts); |
|
161 | } else { |
||
162 | 1 | $slug = parent::getValue($event); |
|
163 | } |
||
164 | |||
165 | 9 | return $this->ensureUnique ? $this->makeUnique($slug) : $slug; |
|
166 | } |
||
167 | |||
168 | /** |
||
169 | * Checks whether the new slug generation is needed |
||
170 | * This method is called by [[getValue]] to check whether the new slug generation is needed. |
||
171 | * You may override it to customize checking. |
||
172 | * @return bool |
||
173 | * @since 2.0.7 |
||
174 | */ |
||
175 | 9 | protected function isNewSlugNeeded() |
|
176 | { |
||
177 | 9 | if (empty($this->owner->{$this->slugAttribute})) { |
|
178 | 9 | return true; |
|
179 | } |
||
180 | |||
181 | 4 | if ($this->immutable) { |
|
182 | 2 | return false; |
|
183 | } |
||
184 | |||
185 | 2 | if ($this->attribute === null) { |
|
186 | return true; |
||
187 | } |
||
188 | |||
189 | 2 | foreach ((array) $this->attribute as $attribute) { |
|
190 | 2 | if ($this->owner->isAttributeChanged($attribute)) { |
|
191 | 2 | return true; |
|
192 | } |
||
193 | } |
||
194 | |||
195 | 1 | return false; |
|
196 | } |
||
197 | |||
198 | /** |
||
199 | * This method is called by [[getValue]] to generate the slug. |
||
200 | * You may override it to customize slug generation. |
||
201 | * The default implementation calls [[\yii\helpers\Inflector::slug()]] on the input strings |
||
202 | * concatenated by dashes (`-`). |
||
203 | * @param array $slugParts an array of strings that should be concatenated and converted to generate the slug value. |
||
204 | * @return string the conversion result. |
||
205 | */ |
||
206 | 8 | protected function generateSlug($slugParts) |
|
207 | { |
||
208 | 8 | return Inflector::slug(implode('-', $slugParts)); |
|
209 | } |
||
210 | |||
211 | /** |
||
212 | * This method is called by [[getValue]] when [[ensureUnique]] is true to generate the unique slug. |
||
213 | * Calls [[generateUniqueSlug]] until generated slug is unique and returns it. |
||
214 | * @param string $slug basic slug value |
||
215 | * @return string unique slug |
||
216 | * @see getValue |
||
217 | * @see generateUniqueSlug |
||
218 | * @since 2.0.7 |
||
219 | */ |
||
220 | 4 | protected function makeUnique($slug) |
|
221 | { |
||
222 | 4 | $uniqueSlug = $slug; |
|
223 | 4 | $iteration = 0; |
|
224 | 4 | while (!$this->validateSlug($uniqueSlug)) { |
|
225 | 2 | $iteration++; |
|
226 | 2 | $uniqueSlug = $this->generateUniqueSlug($slug, $iteration); |
|
227 | } |
||
228 | |||
229 | 4 | return $uniqueSlug; |
|
230 | } |
||
231 | |||
232 | /** |
||
233 | * Checks if given slug value is unique. |
||
234 | * @param string $slug slug value |
||
235 | * @return bool whether slug is unique. |
||
236 | */ |
||
237 | 4 | protected function validateSlug($slug) |
|
238 | { |
||
239 | /* @var $validator UniqueValidator */ |
||
240 | /* @var $model BaseActiveRecord */ |
||
241 | 4 | $validator = Yii::createObject(array_merge( |
|
242 | [ |
||
243 | 4 | 'class' => UniqueValidator::className(), |
|
244 | ], |
||
245 | 4 | $this->uniqueValidator |
|
246 | )); |
||
247 | |||
248 | 4 | $model = clone $this->owner; |
|
249 | 4 | $model->clearErrors(); |
|
250 | 4 | $model->{$this->slugAttribute} = $slug; |
|
251 | |||
252 | 4 | $validator->validateAttribute($model, $this->slugAttribute); |
|
253 | 4 | return !$model->hasErrors(); |
|
254 | } |
||
255 | |||
256 | /** |
||
257 | * Generates slug using configured callback or increment of iteration. |
||
258 | * @param string $baseSlug base slug value |
||
259 | * @param int $iteration iteration number |
||
260 | * @return string new slug value |
||
261 | * @throws \yii\base\InvalidConfigException |
||
262 | */ |
||
263 | 2 | protected function generateUniqueSlug($baseSlug, $iteration) |
|
264 | { |
||
265 | 2 | if (is_callable($this->uniqueSlugGenerator)) { |
|
266 | 1 | return call_user_func($this->uniqueSlugGenerator, $baseSlug, $iteration, $this->owner); |
|
267 | } |
||
268 | |||
269 | 1 | return $baseSlug . '-' . ($iteration + 1); |
|
270 | } |
||
271 | |||
272 | /** |
||
273 | * Checks if $slugPart is empty string or null. |
||
274 | * |
||
275 | * @param string $slugPart One of attributes that is used for slug generation. |
||
276 | * @return bool whether $slugPart empty or not. |
||
277 | * @since 2.0.13 |
||
278 | */ |
||
279 | 1 | protected function isEmpty($slugPart) |
|
283 | } |
||
284 |
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: