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

SlugService::getSlugSource()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
cc 3
nc 2
nop 1
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
0 ignored issues
show
Comprehensibility Best Practice introduced by
The type Cviebrock\EloquentSluggable\Services\SlugService has been defined more than once; this definition is ignored, only the first definition in src/ServiceProvider.php (L13-473) is considered.

This check looks for classes that have been defined more than once.

If you can, we would recommend to use standard object-oriented programming techniques. For example, to avoid multiple types, it might make sense to create a common interface, and then multiple, different implementations for that interface.

This also has the side-effect of providing you with better IDE auto-completion, static analysis and also better OPCode caching from PHP.

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