Completed
Pull Request — master (#254)
by
unknown
02:30
created

SluggableTrait   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 437
Duplicated Lines 3.66 %

Coupling/Cohesion

Components 2
Dependencies 2

Importance

Changes 42
Bugs 8 Features 7
Metric Value
wmc 55
c 42
b 8
f 7
lcom 2
cbo 2
dl 16
loc 437
rs 6.8

22 Methods

Rating   Name   Duplication   Size   Complexity  
A needsSlugging() 0 16 4
A getSlugSource() 0 13 2
B generateSource() 0 17 5
B generateSlug() 0 24 6
A getSlugEngine() 0 4 1
B validateSlug() 0 24 5
B makeSlugUnique() 0 30 6
A generateSuffix() 0 23 2
B getExistingSlugs() 0 30 4
A usesSoftDeleting() 0 4 1
A setSlug() 0 6 1
A getSlug() 0 7 1
A sluggify() 0 20 4
A resluggify() 0 4 1
A createSlug() 0 8 1
A scopeWhereSlug() 0 6 1
A findBySlug() 0 4 1
A findBySlugOrFail() 0 4 1
A getSluggableConfig() 0 9 2
A findBySlugOrIdOrFail() 8 8 2
A findBySlugOrId() 8 8 2
A findBySlugOrNew() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SluggableTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SluggableTrait, and based on these observations, apply Extract Interface, too.

1
<?php namespace Cviebrock\EloquentSluggable;
2
3
use Cocur\Slugify\Slugify;
4
use Illuminate\Database\Eloquent\Model;
5
use Illuminate\Support\Collection;
6
7
/**
8
 * Class SluggableTrait
9
 *
10
 * @package Cviebrock\EloquentSluggable
11
 */
12
trait SluggableTrait
13
{
14
    /**
15
     * Determines whether the model needs slugging.
16
     *
17
     * @return bool
18
     */
19
    protected function needsSlugging()
20
    {
21
        $config = $this->getSluggableConfig();
22
        $save_to = $config['save_to'];
23
        $on_update = $config['on_update'];
24
25
        if (empty($this->attributes[$save_to])) {
0 ignored issues
show
Bug introduced by
The property attributes does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
26
            return true;
27
        }
28
29
        if ($this->isDirty($save_to)) {
0 ignored issues
show
Bug introduced by
It seems like isDirty() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
30
            return false;
31
        }
32
33
        return (!$this->exists || $on_update);
0 ignored issues
show
Bug introduced by
The property exists does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
34
    }
35
36
    /**
37
     * Get the source string for the slug.
38
     *
39
     * @return string
40
     */
41
    protected function getSlugSource()
42
    {
43
        $config = $this->getSluggableConfig();
44
        $from = $config['build_from'];
45
46
        if (is_null($from)) {
47
            return $this->__toString();
0 ignored issues
show
Bug introduced by
It seems like __toString() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
48
        }
49
50
        $source = array_map([$this, 'generateSource'], (array)$from);
51
52
        return join($source, ' ');
53
    }
54
55
    /**
56
     * Get value for slug.
57
     *
58
     * @param string $key
59
     * @return string|null
60
     */
61
    protected function generateSource($key)
62
    {
63
        if (isset($this->{$key})) {
64
            return $this->{$key};
65
        }
66
67
        $object = $this;
68
        foreach (explode('.', $key) as $segment) {
69
            if (!is_object($object) || !$tmp = $object->{$segment}) {
70
                return null;
71
            }
72
73
            $object = $object->{$segment};
74
        }
75
76
        return $object;
77
    }
78
79
    /**
80
     * Generate a slug from the given source string.
81
     *
82
     * @param string $source
83
     * @return string
84
     * @throws \UnexpectedValueException
85
     */
86
    protected function generateSlug($source)
87
    {
88
        $config = $this->getSluggableConfig();
89
        $separator = $config['separator'];
90
        $method = $config['method'];
91
        $max_length = $config['max_length'];
92
93
        if (empty($source)) {
94
            $slug = null;
95
        } elseif ($method === null) {
96
            $slugEngine = $this->getSlugEngine();
97
            $slug = $slugEngine->slugify($source, $separator);
98
        } elseif (is_callable($method)) {
99
            $slug = call_user_func($method, $source, $separator);
100
        } else {
101
            throw new \UnexpectedValueException('Sluggable method is not callable or null.');
102
        }
103
104
        if (is_string($slug) && $max_length) {
105
            $slug = substr($slug, 0, $max_length);
106
        }
107
108
        return $slug;
109
    }
110
111
    /**
112
     * Return a class that has a `slugify()` method, used to convert
113
     * strings into slugs.
114
     *
115
     * @return Slugify
116
     */
117
    protected function getSlugEngine()
118
    {
119
        return new Slugify();
120
    }
121
122
    /**
123
     * Checks that the given slug is not a reserved word.
124
     *
125
     * @param string $slug
126
     * @return string
127
     * @throws \UnexpectedValueException
128
     */
129
    protected function validateSlug($slug)
130
    {
131
        $config = $this->getSluggableConfig();
132
        $reserved = $config['reserved'];
133
134
        if ($reserved === null) {
135
            return $slug;
136
        }
137
138
        // check for reserved names
139
        if ($reserved instanceof \Closure) {
140
            $reserved = $reserved($this);
141
        }
142
143
        if (is_array($reserved)) {
144
            if (in_array($slug, $reserved)) {
145
                return $slug . $config['separator'] . '1';
146
            }
147
148
            return $slug;
149
        }
150
151
        throw new \UnexpectedValueException('Sluggable reserved is not null, an array, or a closure that returns null/array.');
152
    }
153
154
    /**
155
     * Checks if the slug should be unique, and makes it so if needed.
156
     *
157
     * @param string $slug
158
     * @return string
159
     */
160
    protected function makeSlugUnique($slug)
161
    {
162
        $config = $this->getSluggableConfig();
163
        if (!$config['unique']) {
164
            return $slug;
165
        }
166
167
        $separator = $config['separator'];
168
169
        // find all models where the slug is like the current one
170
        $list = $this->getExistingSlugs($slug);
171
172
        // if ...
173
        // 	a) the list is empty
174
        // 	b) our slug isn't in the list
175
        // 	c) our slug is in the list and it's for our model
176
        // ... we are okay
177
        if (
178
          count($list) === 0 ||
179
          !in_array($slug, $list) ||
180
          (array_key_exists($this->getKey(),
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
181
              $list) && $list[$this->getKey()] === $slug)
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
182
        ) {
183
            return $slug;
184
        }
185
186
        $suffix = $this->generateSuffix($slug, $list);
187
188
        return $slug . $separator . $suffix;
189
    }
190
191
    /**
192
     * Generate a unique suffix for the given slug (and list of existing, "similar" slugs.
193
     *
194
     * @param string $slug
195
     * @param array $list
196
     *
197
     * @return string
198
     */
199
    protected function generateSuffix($slug, $list)
200
    {
201
        $config = $this->getSluggableConfig();
202
        $separator = $config['separator'];
203
        $len = strlen($slug . $separator);
204
205
        // If the slug already exists, but belongs to
206
        // our model, return the current suffix.
207
        if ($this->id === array_search($this->slug, $list)) {
0 ignored issues
show
Bug introduced by
The property id does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
208
            $suffix = explode($separator, $this->slug);
0 ignored issues
show
Bug introduced by
The property slug does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
209
210
            return end($suffix);
211
        }
212
213
        array_walk($list, function (&$value, $key) use ($len) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
214
            $value = intval(substr($value, $len));
215
        });
216
217
        // find the highest increment
218
        rsort($list);
219
220
        return reset($list) + 1;
221
    }
222
223
    /**
224
     * Get all existing slugs that are similar to the given slug.
225
     *
226
     * @param string $slug
227
     * @return array
228
     */
229
    protected function getExistingSlugs($slug)
230
    {
231
        $config = $this->getSluggableConfig();
232
        $save_to = $config['save_to'];
233
        $include_trashed = $config['include_trashed'];
234
235
        $instance = new static;
236
237
        //check for direct match or something that has a separator followed by a suffix
238
        $query = $instance->where(function ($query) use (
0 ignored issues
show
Bug introduced by
It seems like where() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
239
          $save_to,
240
          $config,
241
          $slug
242
        ) {
243
            $query->where($save_to, $slug);
244
            $query->orWhere($save_to, 'LIKE',
245
              $slug . $config['separator'] . '%');
246
        });
247
248
        // include trashed models if required
249
        if ($include_trashed && $this->usesSoftDeleting()) {
250
            $query = $query->withTrashed();
251
        }
252
253
        // get a list of all matching slugs
254
        $list = $query->lists($save_to, $this->getKeyName());
0 ignored issues
show
Bug introduced by
It seems like getKeyName() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
255
256
        // Laravel 5.0/5.1 check
257
        return $list instanceof Collection ? $list->all() : $list;
258
    }
259
260
    /**
261
     * Does this model use softDeleting?
262
     *
263
     * @return bool
264
     */
265
    protected function usesSoftDeleting()
266
    {
267
        return method_exists($this, 'BootSoftDeletes');
268
    }
269
270
    /**
271
     * Set the slug manually.
272
     *
273
     * @param string $slug
274
     */
275
    protected function setSlug($slug)
276
    {
277
        $config = $this->getSluggableConfig();
278
        $save_to = $config['save_to'];
279
        $this->setAttribute($save_to, $slug);
0 ignored issues
show
Bug introduced by
It seems like setAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
280
    }
281
282
    /**
283
     * Get the current slug.
284
     *
285
     * @return mixed
286
     */
287
    public function getSlug()
288
    {
289
        $config = $this->getSluggableConfig();
290
        $save_to = $config['save_to'];
291
292
        return $this->getAttribute($save_to);
0 ignored issues
show
Bug introduced by
It seems like getAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
293
    }
294
295
    /**
296
     * Manually slug the current model.
297
     *
298
     * @param bool $force
299
     * @return $this
300
     */
301
    public function sluggify($force = false)
302
    {
303
        if ($this->fireModelEvent('slugging') === false) {
0 ignored issues
show
Bug introduced by
It seems like fireModelEvent() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
304
            return $this;
305
        }
306
307
        if ($force || $this->needsSlugging()) {
308
            $source = $this->getSlugSource();
309
            $slug = $this->generateSlug($source);
310
311
            $slug = $this->validateSlug($slug);
312
            $slug = $this->makeSlugUnique($slug);
313
314
            $this->setSlug($slug);
315
316
            $this->fireModelEvent('slugged');
0 ignored issues
show
Bug introduced by
It seems like fireModelEvent() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
317
        }
318
319
        return $this;
320
    }
321
322
    /**
323
     * Force slugging of current model.
324
     *
325
     * @return SluggableTrait
0 ignored issues
show
Comprehensibility Bug introduced by
The return type SluggableTrait is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?

In PHP traits cannot be used for type-hinting as they do not define a well-defined structure. This is because any class that uses a trait can rename that trait’s methods.

If you would like to return an object that has a guaranteed set of methods, you could create a companion interface that lists these methods explicitly.

Loading history...
326
     */
327
    public function resluggify()
328
    {
329
        return $this->sluggify(true);
330
    }
331
332
    /**
333
     * Generate a unique slug for a given string.
334
     *
335
     * @param  string $fromString
336
     * @return string
337
     */
338
    public static function createSlug($fromString)
339
    {
340
        $model = new self();
341
        $slug = $model->generateSlug($fromString);
342
        $slug = $model->validateSlug($slug);
343
344
        return $model->makeSlugUnique($slug);
345
    }
346
347
    /**
348
     * Query scope for finding a model by its slug.
349
     *
350
     * @param $scope
351
     * @param $slug
352
     * @return mixed
353
     */
354
    public function scopeWhereSlug($scope, $slug)
355
    {
356
        $config = $this->getSluggableConfig();
357
358
        return $scope->where($config['save_to'], $slug);
359
    }
360
361
    /**
362
     * Find a model by slug.
363
     *
364
     * @param $slug
365
     * @param array  $columns
366
     * @return \Illuminate\Database\Eloquent\Model|null
367
     */
368
    public static function findBySlug($slug, array $columns = ['*'])
369
    {
370
        return self::whereSlug($slug)->first($columns);
0 ignored issues
show
Bug introduced by
The method whereSlug() does not exist on Cviebrock\EloquentSluggable\SluggableTrait. Did you maybe mean scopeWhereSlug()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
371
    }
372
373
    /**
374
     * Find a model by slug or fail.
375
     *
376
     * @param $slug
377
     * @param array  $columns
378
     * @return \Illuminate\Database\Eloquent\Model
379
     */
380
    public static function findBySlugOrFail($slug, array $columns = ['*'])
381
    {
382
        return self::whereSlug($slug)->firstOrFail($columns);
0 ignored issues
show
Bug introduced by
The method whereSlug() does not exist on Cviebrock\EloquentSluggable\SluggableTrait. Did you maybe mean scopeWhereSlug()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
383
    }
384
385
    /**
386
     * Get the default configuration and merge in any model-specific overrides.
387
     *
388
     * @return array
389
     */
390
    protected function getSluggableConfig()
391
    {
392
        $defaults = app('config')->get('sluggable');
393
        if (property_exists($this, 'sluggable')) {
394
            return array_merge($defaults, $this->sluggable);
0 ignored issues
show
Bug introduced by
The property sluggable does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
395
        }
396
397
        return $defaults;
398
    }
399
400
    /**
401
     * Simple find by Id if it's numeric or slug if not. Fail if not found.
402
     *
403
     * @param $slug
404
     * @param array  $columns
405
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Support\Collection
406
     */
407 View Code Duplication
    public static function findBySlugOrIdOrFail($slug, array $columns = ['*'])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
408
    {
409
        if (!$result = self::findBySlug($slug, $columns)) {
410
            return self::findOrFail((int)$slug, $columns);
411
        }
412
413
        return $result;
414
    }
415
416
    /**
417
     * Simple find by Id if it's numeric or slug if not.
418
     *
419
     * @param $slug
420
     * @param array  $columns
421
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Support\Collection|null
422
     */
423 View Code Duplication
    public static function findBySlugOrId($slug, array $columns = ['*'])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
424
    {
425
        if (!$result = self::findBySlug($slug, $columns)) {
426
            return self::find($slug, $columns);
427
        }
428
429
        return $result;
430
    }
431
432
    /**
433
     * Find a model by slug or create new instance of model.
434
     *
435
     * @param $slug
436
     * @param array  $columns
437
     * @return \Illuminate\Database\Eloquent\Model
438
     */
439
    public static function findBySlugOrNew($slug, array $columns = ['*'])
440
    {
441
        $model =  self::findBySlug($slug,$columns);
442
443
        if(is_null($model)){
444
            return (new self);
445
        }
446
        return $model;
447
    }
448
}
449