Completed
Push — group-order-expression ( 5a462d )
by Carsten
13:40
created

SluggableBehavior::init()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

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