Completed
Pull Request — master (#489)
by
unknown
01:18
created

SlugService::slug()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 24

Duplication

Lines 24
Ratio 100 %

Importance

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