Completed
Push — arrayhelper-error ( e963b2 )
by Carsten
16:45
created

SluggableBehavior   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 194
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 94.74%

Importance

Changes 0
Metric Value
wmc 20
lcom 1
cbo 6
dl 0
loc 194
ccs 54
cts 57
cp 0.9474
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 12 4
B getValue() 0 19 5
B isNewSlugNeeded() 0 18 5
A generateSlug() 0 4 1
A validateSlug() 0 18 1
A generateUniqueSlug() 0 7 2
A makeUnique() 0 10 2
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\ArrayHelper;
13
use yii\helpers\Inflector;
14
use yii\validators\UniqueValidator;
15
use Yii;
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 the attribute or list of attributes whose value will be converted into a slug
69
     */
70
    public $attribute;
71
    /**
72
     * @var string|callable the value that will be used as a slug. This can be an anonymous function
73
     * or an arbitrary value. If the former, the return value of the function will be used as a slug.
74
     * The signature of the function should be as follows,
75
     *
76
     * ```php
77
     * function ($event)
78
     * {
79
     *     // return slug
80
     * }
81
     * ```
82
     */
83
    public $value;
84
    /**
85
     * @var bool whether to generate a new slug if it has already been generated before.
86
     * If true, the behavior will not generate a new slug even if [[attribute]] is changed.
87
     * @since 2.0.2
88
     */
89
    public $immutable = false;
90
    /**
91
     * @var bool whether to ensure generated slug value to be unique among owner class records.
92
     * If enabled behavior will validate slug uniqueness automatically. If validation fails it will attempt
93
     * generating unique slug value from based one until success.
94
     */
95
    public $ensureUnique = false;
96
    /**
97
     * @var array configuration for slug uniqueness validator. Parameter 'class' may be omitted - by default
98
     * [[UniqueValidator]] will be used.
99
     * @see UniqueValidator
100
     */
101
    public $uniqueValidator = [];
102
    /**
103
     * @var callable slug unique value generator. It is used in case [[ensureUnique]] enabled and generated
104
     * slug is not unique. This should be a PHP callable with following signature:
105
     *
106
     * ```php
107
     * function ($baseSlug, $iteration, $model)
108
     * {
109
     *     // return uniqueSlug
110
     * }
111
     * ```
112
     *
113
     * If not set unique slug will be generated adding incrementing suffix to the base slug.
114
     */
115
    public $uniqueSlugGenerator;
116
117
118
    /**
119
     * @inheritdoc
120
     */
121 6
    public function init()
122
    {
123 6
        parent::init();
124
125 6
        if (empty($this->attributes)) {
126 6
            $this->attributes = [BaseActiveRecord::EVENT_BEFORE_VALIDATE => $this->slugAttribute];
127 6
        }
128
129 6
        if ($this->attribute === null && $this->value === null) {
130
            throw new InvalidConfigException('Either "attribute" or "value" property must be specified.');
131
        }
132 6
    }
133
134
    /**
135
     * @inheritdoc
136
     */
137 6
    protected function getValue($event)
138
    {
139 6
        if ($this->attribute !== null) {
140 6
            if ($this->isNewSlugNeeded()) {
141 6
                $slugParts = [];
142 6
                foreach ((array) $this->attribute as $attribute) {
143 6
                    $slugParts[] = ArrayHelper::getValue($this->owner, $attribute);
144 6
                }
145
146 6
                $slug = $this->generateSlug($slugParts);
147 6
            } else {
148 1
                return $this->owner->{$this->slugAttribute};
149
            }
150 6
        } else {
151
            $slug = parent::getValue($event);
152
        }
153
154 6
        return $this->ensureUnique ? $this->makeUnique($slug) : $slug;
155
    }
156
157
    /**
158
     * Checks whether the new slug generation is needed
159
     * This method is called by [[getValue]] to check whether the new slug generation is needed.
160
     * You may override it to customize checking.
161
     * @return bool
162
     * @since 2.0.7
163
     */
164 6
    protected function isNewSlugNeeded()
165
    {
166 6
        if (empty($this->owner->{$this->slugAttribute})) {
167 6
            return true;
168
        }
169
170 1
        if ($this->immutable) {
171
            return false;
172
        }
173
174 1
        foreach ((array)$this->attribute as $attribute) {
175 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...
176 1
                return true;
177
            }
178 1
        }
179
180 1
        return false;
181
    }
182
183
    /**
184
     * This method is called by [[getValue]] to generate the slug.
185
     * You may override it to customize slug generation.
186
     * The default implementation calls [[\yii\helpers\Inflector::slug()]] on the input strings
187
     * concatenated by dashes (`-`).
188
     * @param array $slugParts an array of strings that should be concatenated and converted to generate the slug value.
189
     * @return string the conversion result.
190
     */
191 6
    protected function generateSlug($slugParts)
192
    {
193 6
        return Inflector::slug(implode('-', $slugParts));
194
    }
195
196
    /**
197
     * This method is called by [[getValue]] when [[ensureUnique]] is true to generate the unique slug.
198
     * Calls [[generateUniqueSlug]] until generated slug is unique and returns it.
199
     * @param string $slug basic slug value
200
     * @return string unique slug
201
     * @see getValue
202
     * @see generateUniqueSlug
203
     * @since 2.0.7
204
     */
205 6
    protected function makeUnique($slug)
206
    {
207 3
        $uniqueSlug = $slug;
208 3
        $iteration = 0;
209 3
        while (!$this->validateSlug($uniqueSlug)) {
210 2
            $iteration++;
211 2
            $uniqueSlug = $this->generateUniqueSlug($slug, $iteration);
212 2
        }
213 3
        return $uniqueSlug;
214 6
    }
215
216
    /**
217
     * Checks if given slug value is unique.
218
     * @param string $slug slug value
219
     * @return bool whether slug is unique.
220
     */
221 3
    protected function validateSlug($slug)
222
    {
223
        /* @var $validator UniqueValidator */
224
        /* @var $model BaseActiveRecord */
225 3
        $validator = Yii::createObject(array_merge(
226
            [
227 3
                'class' => UniqueValidator::className(),
228 3
            ],
229 3
            $this->uniqueValidator
230 3
        ));
231
232 3
        $model = clone $this->owner;
233 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...
234 3
        $model->{$this->slugAttribute} = $slug;
235
236 3
        $validator->validateAttribute($model, $this->slugAttribute);
237 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...
238
    }
239
240
    /**
241
     * Generates slug using configured callback or increment of iteration.
242
     * @param string $baseSlug base slug value
243
     * @param int $iteration iteration number
244
     * @return string new slug value
245
     * @throws \yii\base\InvalidConfigException
246
     */
247 2
    protected function generateUniqueSlug($baseSlug, $iteration)
248
    {
249 2
        if (is_callable($this->uniqueSlugGenerator)) {
250 1
            return call_user_func($this->uniqueSlugGenerator, $baseSlug, $iteration, $this->owner);
251
        }
252 1
        return $baseSlug . '-' . ($iteration + 1);
253
    }
254
}
255