Completed
Pull Request — master (#476)
by
unknown
01:41
created

SlugService   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 429
Duplicated Lines 5.13 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 57
lcom 1
cbo 3
dl 22
loc 429
rs 5.04
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A slug() 0 24 4
A getConfiguration() 0 6 1
A generateSuffix() 0 19 2
A getExistingSlugs() 0 25 4
A usesSoftDeleting() 0 4 1
A createSlug() 0 26 5
A setModel() 0 6 1
A buildSlug() 0 20 5
A needsSlugging() 0 15 4
A getSlugSource() 0 17 3
B generateSlug() 8 29 8
A getSlugEngine() 0 17 3
B validateSlug() 7 33 7
B makeSlugUnique() 7 48 9

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 SlugService 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 SlugService, and based on these observations, apply Extract Interface, too.

1
<?php namespace Cviebrock\EloquentSluggable\Services;
2
3
use Cocur\Slugify\Slugify;
4
use Illuminate\Database\Eloquent\Model;
5
use Illuminate\Support\Collection;
6
7
/**
8
 * Class SlugService
9
 *
10
 * @package Cviebrock\EloquentSluggable\Services
11
 */
12
class SlugService
13
{
14
15
    /**
16
     * @var \Illuminate\Database\Eloquent\Model;
17
     */
18
    protected $model;
19
20
    /**
21
     * Slug the current model.
22
     *
23
     * @param \Illuminate\Database\Eloquent\Model $model
24
     * @param bool $force
25
     *
26
     * @return bool
27
     */
28
    public function slug(Model $model, bool $force = false): bool
29
    {
30
        $this->setModel($model);
31
32
        $attributes = [];
33
34
        foreach ($this->model->sluggable() as $attribute => $config) {
35
            if (is_numeric($attribute)) {
36
                $attribute = $config;
37
                $config = $this->getConfiguration();
38
            } else {
39
                $config = $this->getConfiguration($config);
40
            }
41
42
            $slug = $this->buildSlug($attribute, $config, $force);
43
44
            if ($slug !== null) {
45
                $this->model->setAttribute($attribute, $slug);
46
                $attributes[] = $attribute;
47
            }
48
        }
49
50
        return $this->model->isDirty($attributes);
51
    }
52
53
    /**
54
     * Get the sluggable configuration for the current model,
55
     * including default values where not specified.
56
     *
57
     * @param array $overrides
58
     *
59
     * @return array
60
     */
61
    public function getConfiguration(array $overrides = []): array
62
    {
63
        $defaultConfig = config('sluggable', []);
64
65
        return array_merge($defaultConfig, $overrides);
66
    }
67
68
    /**
69
     * Build the slug for the given attribute of the current model.
70
     *
71
     * @param string $attribute
72
     * @param array $config
73
     * @param bool $force
74
     *
75
     * @return null|string
76
     */
77
    public function buildSlug(string $attribute, array $config, bool $force = null)
78
    {
79
        $slug = $this->model->getAttribute($attribute);
80
81
82
        if ($force || $this->needsSlugging($attribute, $config)) {
83
            $source = $this->getSlugSource($config['source']);
84
85
            if ($source || is_numeric($source)) {
86
                $tr = ["ö", "Ö", "ü", "Ü"];
87
                $rp = ["o", "O", "u", "U"];
88
                $source = str_replace($tr, $rp, $source);
89
                $slug = $this->generateSlug($source, $config, $attribute);
90
                $slug = $this->validateSlug($slug, $config, $attribute);
91
                $slug = $this->makeSlugUnique($slug, $config, $attribute);
92
            }
93
        }
94
95
        return $slug;
96
    }
97
98
    /**
99
     * Determines whether the model needs slugging.
100
     *
101
     * @param string $attribute
102
     * @param array $config
103
     *
104
     * @return bool
105
     */
106
    protected function needsSlugging(string $attribute, array $config): bool
107
    {
108
        if (
109
            $config['onUpdate'] === true ||
110
            empty($this->model->getAttributeValue($attribute))
111
        ) {
112
            return true;
113
        }
114
115
        if ($this->model->isDirty($attribute)) {
116
            return false;
117
        }
118
119
        return (!$this->model->exists);
120
    }
121
122
    /**
123
     * Get the source string for the slug.
124
     *
125
     * @param mixed $from
126
     *
127
     * @return string
128
     */
129
    protected function getSlugSource($from): string
130
    {
131
        if (is_null($from)) {
132
            return $this->model->__toString();
133
        }
134
135
        $sourceStrings = array_map(function($key) {
136
            $value = data_get($this->model, $key);
137
            if (is_bool($value)) {
138
                $value = (int) $value;
139
            }
140
141
            return $value;
142
        }, (array) $from);
143
144
        return implode($sourceStrings, ' ');
145
    }
146
147
    /**
148
     * Generate a slug from the given source string.
149
     *
150
     * @param string $source
151
     * @param array $config
152
     * @param string $attribute
153
     *
154
     * @return string
155
     * @throws \UnexpectedValueException
156
     */
157
    protected function generateSlug(string $source, array $config, string $attribute): string
158
    {
159
        $separator = $config['separator'];
160
        $method = $config['method'];
161
        $maxLength = $config['maxLength'];
162
        $maxLengthKeepWords = $config['maxLengthKeepWords'];
163
164 View Code Duplication
        if ($method === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
165
            $slugEngine = $this->getSlugEngine($attribute);
166
            $slug = $slugEngine->slugify($source, $separator);
167
        } elseif (is_callable($method)) {
168
            $slug = call_user_func($method, $source, $separator);
169
        } else {
170
            throw new \UnexpectedValueException('Sluggable "method" for ' . get_class($this->model) . ':' . $attribute . ' is not callable nor null.');
171
        }
172
173
        $len = mb_strlen($slug);
174
        if (is_string($slug) && $maxLength && $len > $maxLength) {
175
            $reverseOffset = $maxLength - $len;
176
            $lastSeparatorPos = mb_strrpos($slug, $separator, $reverseOffset);
177
            if ($maxLengthKeepWords && $lastSeparatorPos !== false) {
178
                $slug = mb_substr($slug, 0, $lastSeparatorPos);
179
            } else {
180
                $slug = trim(mb_substr($slug, 0, $maxLength), $separator);
181
            }
182
        }
183
184
        return $slug;
185
    }
186
187
    /**
188
     * Return a class that has a `slugify()` method, used to convert
189
     * strings into slugs.
190
     *
191
     * @param string $attribute
192
     *
193
     * @return \Cocur\Slugify\Slugify
194
     */
195
    protected function getSlugEngine(string $attribute): Slugify
196
    {
197
        static $slugEngines = [];
198
199
        $key = get_class($this->model) . '.' . $attribute;
200
201
        if (!array_key_exists($key, $slugEngines)) {
202
            $engine = new Slugify();
203
            if (method_exists($this->model, 'customizeSlugEngine')) {
204
                $engine = $this->model->customizeSlugEngine($engine, $attribute);
205
            }
206
207
            $slugEngines[$key] = $engine;
208
        }
209
210
        return $slugEngines[$key];
211
    }
212
213
    /**
214
     * Checks that the given slug is not a reserved word.
215
     *
216
     * @param string $slug
217
     * @param array $config
218
     * @param string $attribute
219
     *
220
     * @return string
221
     * @throws \UnexpectedValueException
222
     */
223
    protected function validateSlug(string $slug, array $config, string $attribute): string
224
    {
225
        $separator = $config['separator'];
226
        $reserved = $config['reserved'];
227
228
        if ($reserved === null) {
229
            return $slug;
230
        }
231
232
        // check for reserved names
233
        if ($reserved instanceof \Closure) {
234
            $reserved = $reserved($this->model);
235
        }
236
237
        if (is_array($reserved)) {
238
            if (in_array($slug, $reserved)) {
239
                $method = $config['uniqueSuffix'];
240 View Code Duplication
                if ($method === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
241
                    $suffix = $this->generateSuffix($slug, $separator, collect($reserved));
242
                } elseif (is_callable($method)) {
243
                    $suffix = $method($slug, $separator, collect($reserved));
244
                } else {
245
                    throw new \UnexpectedValueException('Sluggable "uniqueSuffix" for ' . get_class($this->model) . ':' . $attribute . ' is not null, or a closure.');
246
                }
247
248
                return $slug . $separator . $suffix;
249
            }
250
251
            return $slug;
252
        }
253
254
        throw new \UnexpectedValueException('Sluggable "reserved" for ' . get_class($this->model) . ':' . $attribute . ' is not null, an array, or a closure that returns null/array.');
255
    }
256
257
    /**
258
     * Checks if the slug should be unique, and makes it so if needed.
259
     *
260
     * @param string $slug
261
     * @param array $config
262
     * @param string $attribute
263
     *
264
     * @return string
265
     * @throws \UnexpectedValueException
266
     */
267
    protected function makeSlugUnique(string $slug, array $config, string $attribute): string
268
    {
269
        if (!$config['unique']) {
270
            return $slug;
271
        }
272
273
        $separator = $config['separator'];
274
275
        // find all models where the slug is like the current one
276
        $list = $this->getExistingSlugs($slug, $attribute, $config);
277
278
        // if ...
279
        // 	a) the list is empty, or
280
        // 	b) our slug isn't in the list
281
        // ... we are okay
282
        if (
283
            $list->count() === 0 ||
284
            $list->contains($slug) === false
285
        ) {
286
            return $slug;
287
        }
288
289
        // if our slug is in the list, but
290
        // 	a) it's for our model, or
291
        //  b) it looks like a suffixed version of our slug
292
        // ... we are also okay (use the current slug)
293
        if ($list->has($this->model->getKey())) {
294
            $currentSlug = $list->get($this->model->getKey());
295
296
            if (
297
                $currentSlug === $slug ||
298
                strpos($currentSlug, $slug) === 0
299
            ) {
300
                return $currentSlug;
301
            }
302
        }
303
304
        $method = $config['uniqueSuffix'];
305 View Code Duplication
        if ($method === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
306
            $suffix = $this->generateSuffix($slug, $separator, $list);
307
        } elseif (is_callable($method)) {
308
            $suffix = $method($slug, $separator, $list);
309
        } else {
310
            throw new \UnexpectedValueException('Sluggable "uniqueSuffix" for ' . get_class($this->model) . ':' . $attribute . ' is not null, or a closure.');
311
        }
312
313
        return $slug . $separator . $suffix;
314
    }
315
316
    /**
317
     * Generate a unique suffix for the given slug (and list of existing, "similar" slugs.
318
     *
319
     * @param string $slug
320
     * @param string $separator
321
     * @param \Illuminate\Support\Collection $list
322
     *
323
     * @return string
324
     */
325
    protected function generateSuffix(string $slug, string $separator, Collection $list): string
326
    {
327
        $len = strlen($slug . $separator);
328
329
        // If the slug already exists, but belongs to
330
        // our model, return the current suffix.
331
        if ($list->search($slug) === $this->model->getKey()) {
332
            $suffix = explode($separator, $slug);
333
334
            return end($suffix);
335
        }
336
337
        $list->transform(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...
338
            return (int) substr($value, $len);
339
        });
340
341
        // find the highest value and return one greater.
342
        return $list->max() + 1;
343
    }
344
345
    /**
346
     * Get all existing slugs that are similar to the given slug.
347
     *
348
     * @param string $slug
349
     * @param string $attribute
350
     * @param array $config
351
     *
352
     * @return \Illuminate\Support\Collection
353
     */
354
    protected function getExistingSlugs(string $slug, string $attribute, array $config): Collection
355
    {
356
        $includeTrashed = $config['includeTrashed'];
357
358
        $query = $this->model->newQuery()
359
            ->findSimilarSlugs($attribute, $config, $slug);
360
361
        // use the model scope to find similar slugs
362
        if (method_exists($this->model, 'scopeWithUniqueSlugConstraints')) {
363
            $query->withUniqueSlugConstraints($this->model, $attribute, $config, $slug);
364
        }
365
366
        // include trashed models if required
367
        if ($includeTrashed && $this->usesSoftDeleting()) {
368
            $query->withTrashed();
369
        }
370
371
        // get the list of all matching slugs
372
        $results = $query->select([$attribute, $this->model->getQualifiedKeyName()])
373
            ->get()
374
            ->toBase();
375
376
        // key the results and return
377
        return $results->pluck($attribute, $this->model->getKeyName());
378
    }
379
380
    /**
381
     * Does this model use softDeleting?
382
     *
383
     * @return bool
384
     */
385
    protected function usesSoftDeleting(): bool
386
    {
387
        return method_exists($this->model, 'bootSoftDeletes');
388
    }
389
390
    /**
391
     * Generate a unique slug for a given string.
392
     *
393
     * @param \Illuminate\Database\Eloquent\Model|string $model
394
     * @param string $attribute
395
     * @param string $fromString
396
     * @param array|null $config
397
     *
398
     * @return string
399
     * @throws \InvalidArgumentException
400
     * @throws \UnexpectedValueException
401
     */
402
    public static function createSlug($model, string $attribute, string $fromString, array $config = null): string
403
    {
404
        if (is_string($model)) {
405
            $model = new $model;
406
        }
407
        /** @var static $instance */
408
        $instance = (new static())->setModel($model);
409
410
        if ($config === null) {
411
            $config = array_get($model->sluggable(), $attribute);
0 ignored issues
show
Deprecated Code introduced by
The function array_get() has been deprecated with message: Arr::get() should be used directly instead. Will be removed in Laravel 5.9.

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

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

Loading history...
412
            if ($config === null) {
413
                $modelClass = get_class($model);
414
                throw new \InvalidArgumentException("Argument 2 passed to SlugService::createSlug ['{$attribute}'] is not a valid slug attribute for model {$modelClass}.");
415
            }
416
        } elseif (!is_array($config)) {
417
            throw new \UnexpectedValueException('SlugService::createSlug expects an array or null as the fourth argument; ' . gettype($config) . ' given.');
418
        }
419
420
        $config = $instance->getConfiguration($config);
421
422
        $slug = $instance->generateSlug($fromString, $config, $attribute);
423
        $slug = $instance->validateSlug($slug, $config, $attribute);
424
        $slug = $instance->makeSlugUnique($slug, $config, $attribute);
425
426
        return $slug;
427
    }
428
429
    /**
430
     * @param \Illuminate\Database\Eloquent\Model $model
431
     *
432
     * @return $this
433
     */
434
    public function setModel(Model $model)
435
    {
436
        $this->model = $model;
437
438
        return $this;
439
    }
440
}
441