Completed
Push — master ( bdc372...c54ead )
by Dmitry
19:44 queued 16:30
created

SluggableBehavior::validateSlug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
ccs 9
cts 9
cp 1
rs 9.4285
cc 1
eloc 9
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\behaviors;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\db\BaseActiveRecord;
13
use yii\helpers\ArrayHelper;
14
use yii\helpers\Inflector;
15
use yii\validators\UniqueValidator;
16
17
/**
18
 * SluggableBehavior automatically fills the specified attribute with a value that can be used a slug in a URL.
19
 *
20
 * To use SluggableBehavior, insert the following code to your ActiveRecord class:
21
 *
22
 * ```php
23
 * use yii\behaviors\SluggableBehavior;
24
 *
25
 * public function behaviors()
26
 * {
27
 *     return [
28
 *         [
29
 *             'class' => SluggableBehavior::className(),
30
 *             'attribute' => 'title',
31
 *             // 'slugAttribute' => 'slug',
32
 *         ],
33
 *     ];
34
 * }
35
 * ```
36
 *
37
 * By default, SluggableBehavior will fill the `slug` attribute with a value that can be used a slug in a URL
38
 * when the associated AR object is being validated.
39
 *
40
 * Because attribute values will be set automatically by this behavior, they are usually not user input and should therefore
41
 * not be validated, i.e. the `slug` attribute should not appear in the [[\yii\base\Model::rules()|rules()]] method of the model.
42
 *
43
 * If your attribute name is different, you may configure the [[slugAttribute]] property like the following:
44
 *
45
 * ```php
46
 * public function behaviors()
47
 * {
48
 *     return [
49
 *         [
50
 *             'class' => SluggableBehavior::className(),
51
 *             'slugAttribute' => 'alias',
52
 *         ],
53
 *     ];
54
 * }
55
 * ```
56
 *
57
 * @author Alexander Kochetov <[email protected]>
58
 * @author Paul Klimov <[email protected]>
59
 * @since 2.0
60
 */
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()
130
    {
131 9
        parent::init();
132
133 9
        if (empty($this->attributes)) {
134 9
            $this->attributes = [BaseActiveRecord::EVENT_BEFORE_VALIDATE => $this->slugAttribute];
135
        }
136
137 9
        if ($this->attribute === null && $this->value === null) {
138
            throw new InvalidConfigException('Either "attribute" or "value" property must be specified.');
139
        }
140 9
    }
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);
0 ignored issues
show
Bug introduced by
It seems like $this->owner can be null; however, getValue() does not accept null, maybe add an additional type check?

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:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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)) {
0 ignored issues
show
Documentation Bug introduced by
The method isAttributeChanged does not exist on object<yii\base\Component>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
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(),
0 ignored issues
show
Deprecated Code introduced by
The method yii\base\BaseObject::className() has been deprecated with message: since 2.0.14. On PHP >=5.5, use `::class` instead.

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

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

Loading history...
244
            ],
245 4
            $this->uniqueValidator
246
        ));
247
248 4
        $model = clone $this->owner;
249 4
        $model->clearErrors();
0 ignored issues
show
Documentation Bug introduced by
The method clearErrors does not exist on object<yii\base\Component>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
250 4
        $model->{$this->slugAttribute} = $slug;
251
252 4
        $validator->validateAttribute($model, $this->slugAttribute);
253 4
        return !$model->hasErrors();
0 ignored issues
show
Documentation Bug introduced by
The method hasErrors does not exist on object<yii\base\Component>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
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)
280
    {
281 1
        return $slugPart === null || $slugPart === '';
282
    }
283
}
284