Issues (915)

framework/behaviors/SluggableBehavior.php (1 issue)

1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\behaviors;
10
11
use Yii;
12
use yii\base\InvalidConfigException;
13
use yii\db\BaseActiveRecord;
14
use yii\helpers\ArrayHelper;
15
use yii\helpers\Inflector;
16
use yii\validators\UniqueValidator;
17
18
/**
19
 * SluggableBehavior automatically fills the specified attribute with a value that can be used a slug in a URL.
20
 *
21
 * Note: This behavior relies on php-intl extension for transliteration. If it is not installed it
22
 * falls back to replacements defined in [[\yii\helpers\Inflector::$transliteration]].
23
 *
24
 * To use SluggableBehavior, insert the following code to your ActiveRecord class:
25
 *
26
 * ```php
27
 * use yii\behaviors\SluggableBehavior;
28
 *
29
 * public function behaviors()
30
 * {
31
 *     return [
32
 *         [
33
 *             'class' => SluggableBehavior::class,
34
 *             'attribute' => 'title',
35
 *             // 'slugAttribute' => 'slug',
36
 *         ],
37
 *     ];
38
 * }
39
 * ```
40
 *
41
 * By default, SluggableBehavior will fill the `slug` attribute with a value that can be used a slug in a URL
42
 * when the associated AR object is being validated.
43
 *
44
 * Because attribute values will be set automatically by this behavior, they are usually not user input and should therefore
45
 * not be validated, i.e. the `slug` attribute should not appear in the [[\yii\base\Model::rules()|rules()]] method of the model.
46
 *
47
 * If your attribute name is different, you may configure the [[slugAttribute]] property like the following:
48
 *
49
 * ```php
50
 * public function behaviors()
51
 * {
52
 *     return [
53
 *         [
54
 *             'class' => SluggableBehavior::class,
55
 *             'slugAttribute' => 'alias',
56
 *         ],
57
 *     ];
58
 * }
59
 * ```
60
 *
61
 * @author Alexander Kochetov <[email protected]>
62
 * @author Paul Klimov <[email protected]>
63
 * @since 2.0
64
 */
65
class SluggableBehavior extends AttributeBehavior
66
{
67
    /**
68
     * @var string the attribute that will receive the slug value
69
     */
70
    public $slugAttribute = 'slug';
71
    /**
72
     * @var string|array|null the attribute or list of attributes whose value will be converted into a slug
73
     * or `null` meaning that the `$value` property will be used to generate a slug.
74
     */
75
    public $attribute;
76
    /**
77
     * @var callable|string|null the value that will be used as a slug. This can be an anonymous function
78
     * or an arbitrary value or null. If the former, the return value of the function will be used as a slug.
79
     * If `null` then the `$attribute` property will be used to generate a slug.
80
     * The signature of the function should be as follows,
81
     *
82
     * ```php
83
     * function ($event)
84
     * {
85
     *     // return slug
86
     * }
87
     * ```
88
     */
89
    public $value;
90
    /**
91
     * @var bool whether to generate a new slug if it has already been generated before.
92
     * If true, the behavior will not generate a new slug even if [[attribute]] is changed.
93
     * @since 2.0.2
94
     */
95
    public $immutable = false;
96
    /**
97
     * @var bool whether to ensure generated slug value to be unique among owner class records.
98
     * If enabled behavior will validate slug uniqueness automatically. If validation fails it will attempt
99
     * generating unique slug value from based one until success.
100
     */
101
    public $ensureUnique = false;
102
    /**
103
     * @var bool whether to skip slug generation if [[attribute]] is null or an empty string.
104
     * If true, the behaviour will not generate a new slug if [[attribute]] is null or an empty string.
105
     * @since 2.0.13
106
     */
107
    public $skipOnEmpty = false;
108
    /**
109
     * @var array configuration for slug uniqueness validator. Parameter 'class' may be omitted - by default
110
     * [[UniqueValidator]] will be used.
111
     * @see UniqueValidator
112
     */
113
    public $uniqueValidator = [];
114
    /**
115
     * @var callable|null slug unique value generator. It is used in case [[ensureUnique]] enabled and generated
116
     * slug is not unique. This should be a PHP callable with following signature:
117
     *
118
     * ```php
119
     * function ($baseSlug, $iteration, $model)
120
     * {
121
     *     // return uniqueSlug
122
     * }
123
     * ```
124
     *
125
     * If not set unique slug will be generated adding incrementing suffix to the base slug.
126
     */
127
    public $uniqueSlugGenerator;
128
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 9
    public function init()
134
    {
135 9
        parent::init();
136
137 9
        if (empty($this->attributes)) {
138 9
            $this->attributes = [BaseActiveRecord::EVENT_BEFORE_VALIDATE => $this->slugAttribute];
139
        }
140
141 9
        if ($this->attribute === null && $this->value === null) {
142
            throw new InvalidConfigException('Either "attribute" or "value" property must be specified.');
143
        }
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 9
    protected function getValue($event)
150
    {
151 9
        if (!$this->isNewSlugNeeded()) {
152 3
            return $this->owner->{$this->slugAttribute};
153
        }
154
155 9
        if ($this->attribute !== null) {
156 8
            $slugParts = [];
157 8
            foreach ((array) $this->attribute as $attribute) {
158 8
                $part = ArrayHelper::getValue($this->owner, $attribute);
159 8
                if ($this->skipOnEmpty && $this->isEmpty($part)) {
160 1
                    return $this->owner->{$this->slugAttribute};
161
                }
162 8
                $slugParts[] = $part;
163
            }
164 8
            $slug = $this->generateSlug($slugParts);
165
        } else {
166 1
            $slug = parent::getValue($event);
167
        }
168
169 9
        return $this->ensureUnique ? $this->makeUnique($slug) : $slug;
170
    }
171
172
    /**
173
     * Checks whether the new slug generation is needed
174
     * This method is called by [[getValue]] to check whether the new slug generation is needed.
175
     * You may override it to customize checking.
176
     * @return bool
177
     * @since 2.0.7
178
     */
179 9
    protected function isNewSlugNeeded()
180
    {
181 9
        if (empty($this->owner->{$this->slugAttribute})) {
182 9
            return true;
183
        }
184
185 4
        if ($this->immutable) {
186 2
            return false;
187
        }
188
189 2
        if ($this->attribute === null) {
190
            return true;
191
        }
192
193 2
        foreach ((array) $this->attribute as $attribute) {
194 2
            if ($this->owner->isAttributeChanged($attribute)) {
195 2
                return true;
196
            }
197
        }
198
199 1
        return false;
200
    }
201
202
    /**
203
     * This method is called by [[getValue]] to generate the slug.
204
     * You may override it to customize slug generation.
205
     * The default implementation calls [[\yii\helpers\Inflector::slug()]] on the input strings
206
     * concatenated by dashes (`-`).
207
     * @param array $slugParts an array of strings that should be concatenated and converted to generate the slug value.
208
     * @return string the conversion result.
209
     */
210 8
    protected function generateSlug($slugParts)
211
    {
212 8
        return Inflector::slug(implode('-', $slugParts));
213
    }
214
215
    /**
216
     * This method is called by [[getValue]] when [[ensureUnique]] is true to generate the unique slug.
217
     * Calls [[generateUniqueSlug]] until generated slug is unique and returns it.
218
     * @param string $slug basic slug value
219
     * @return string unique slug
220
     * @see getValue
221
     * @see generateUniqueSlug
222
     * @since 2.0.7
223
     */
224 4
    protected function makeUnique($slug)
225
    {
226 4
        $uniqueSlug = $slug;
227 4
        $iteration = 0;
228 4
        while (!$this->validateSlug($uniqueSlug)) {
229 2
            $iteration++;
230 2
            $uniqueSlug = $this->generateUniqueSlug($slug, $iteration);
231
        }
232
233 4
        return $uniqueSlug;
234
    }
235
236
    /**
237
     * Checks if given slug value is unique.
238
     * @param string $slug slug value
239
     * @return bool whether slug is unique.
240
     */
241 4
    protected function validateSlug($slug)
242
    {
243
        /* @var $validator UniqueValidator */
244
        /* @var $model BaseActiveRecord */
245 4
        $validator = Yii::createObject(array_merge(
246 4
            [
247 4
                'class' => UniqueValidator::className(),
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

247
                'class' => /** @scrutinizer ignore-deprecated */ UniqueValidator::className(),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
248 4
            ],
249 4
            $this->uniqueValidator
250 4
        ));
251
252 4
        $model = clone $this->owner;
253 4
        $model->clearErrors();
254 4
        $model->{$this->slugAttribute} = $slug;
255
256 4
        $validator->validateAttribute($model, $this->slugAttribute);
257 4
        return !$model->hasErrors();
258
    }
259
260
    /**
261
     * Generates slug using configured callback or increment of iteration.
262
     * @param string $baseSlug base slug value
263
     * @param int $iteration iteration number
264
     * @return string new slug value
265
     * @throws \yii\base\InvalidConfigException
266
     */
267 2
    protected function generateUniqueSlug($baseSlug, $iteration)
268
    {
269 2
        if (is_callable($this->uniqueSlugGenerator)) {
270 1
            return call_user_func($this->uniqueSlugGenerator, $baseSlug, $iteration, $this->owner);
271
        }
272
273 1
        return $baseSlug . '-' . ($iteration + 1);
274
    }
275
276
    /**
277
     * Checks if $slugPart is empty string or null.
278
     *
279
     * @param string $slugPart One of attributes that is used for slug generation.
280
     * @return bool whether $slugPart empty or not.
281
     * @since 2.0.13
282
     */
283 1
    protected function isEmpty($slugPart)
284
    {
285 1
        return $slugPart === null || $slugPart === '';
286
    }
287
}
288