Completed
Pull Request — master (#356)
by
unknown
10:36
created

SlugService::validateSlug()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 37
Code Lines 19

Duplication

Lines 7
Ratio 18.92 %

Importance

Changes 0
Metric Value
dl 7
loc 37
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 19
nc 11
nop 3
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
     * @return bool
26
     */
27
    public function slug(Model $model, $force = false)
28
    {
29
        $this->setModel($model);
30
31
        $attributes = [];
32
33
        foreach ($this->model->sluggable() as $attribute => $config) {
34
            if (is_numeric($attribute)) {
35
                $attribute = $config;
36
                $config = $this->getConfiguration();
37
            } else {
38
                $config = $this->getConfiguration($config);
39
            }
40
41
            $slug = $this->buildSlug($attribute, $config, $force);
42
43
            $this->model->setAttribute($attribute, $slug);
44
45
            $attributes[] = $attribute;
46
        }
47
48
        return $this->model->isDirty($attributes);
49
    }
50
51
    /**
52
     * Get the sluggable configuration for the current model,
53
     * including default values where not specified.
54
     *
55
     * @param array $overrides
56
     * @return array
57
     */
58
    public function getConfiguration(array $overrides = [])
59
    {
60
        static $defaultConfig = null;
61
        if ($defaultConfig === null) {
62
            $defaultConfig = app('config')->get('sluggable');
63
        }
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
     * @return null|string
75
     */
76
    public function buildSlug($attribute, array $config, $force = null)
77
    {
78
        $slug = $this->model->getAttribute($attribute);
79
80
        if ($force || $this->needsSlugging($attribute, $config)) {
81
            $source = $this->getSlugSource($config['source']);
82
83
            if ($source || is_numeric($source)) {
84
                $slug = $this->generateSlug($source, $config, $attribute);
85
                $slug = $this->validateSlug($slug, $config, $attribute);
86
                $slug = $this->makeSlugUnique($slug, $config, $attribute);
87
            }
88
        }
89
90
        return $slug;
91
    }
92
93
    /**
94
     * Determines whether the model needs slugging.
95
     *
96
     * @param string $attribute
97
     * @param array $config
98
     * @return bool
99
     */
100
    protected function needsSlugging($attribute, array $config)
101
    {
102
        if (
103
            empty($this->model->getAttributeValue($attribute)) ||
104
            $config['onUpdate'] === true
105
        ) {
106
            return true;
107
        }
108
109
        if ($this->model->isDirty($attribute)) {
110
            return false;
111
        }
112
113
        return (!$this->model->exists);
114
    }
115
116
    /**
117
     * Get the source string for the slug.
118
     *
119
     * @param mixed $from
120
     * @return string
121
     */
122
    protected function getSlugSource($from)
123
    {
124
        if (is_null($from)) {
125
            return $this->model->__toString();
126
        }
127
128
        $sourceStrings = array_map(function ($key) {
129
            $value = data_get($this->model, $key);
130
            if (is_bool($value)) {
131
                $value = (int) $value;
132
            }
133
134
            return $value;
135
        }, (array)$from);
136
137
        return join($sourceStrings, ' ');
138
    }
139
140
    /**
141
     * Generate a slug from the given source string.
142
     *
143
     * @param string $source
144
     * @param array $config
145
     * @param string $attribute
146
     * @return string
147
     */
148
    protected function generateSlug($source, array $config, $attribute)
149
    {
150
        $separator = $config['separator'];
151
        $method = $config['method'];
152
        $maxLength = $config['maxLength'];
153
154 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...
155
            $slugEngine = $this->getSlugEngine($attribute);
156
            $slug = $slugEngine->slugify($source, $separator);
157
        } elseif (is_callable($method)) {
158
            $slug = call_user_func($method, $source, $separator);
159
        } else {
160
            throw new \UnexpectedValueException('Sluggable "method" for ' . get_class($this->model) . ':' . $attribute . ' is not callable nor null.');
161
        }
162
163
        if (is_string($slug) && $maxLength) {
164
            $slug = mb_substr($slug, 0, $maxLength);
165
        }
166
167
        return $slug;
168
    }
169
170
    /**
171
     * Return a class that has a `slugify()` method, used to convert
172
     * strings into slugs.
173
     *
174
     * @param string $attribute
175
     * @return Slugify
176
     */
177
    protected function getSlugEngine($attribute)
178
    {
179
        static $slugEngines = [];
180
181
        $key = get_class($this->model) . '.' . $attribute;
182
183
        if (!array_key_exists($key, $slugEngines)) {
184
            $engine = new Slugify();
185
            if (method_exists($this->model, 'customizeSlugEngine')) {
186
                $engine = $this->model->customizeSlugEngine($engine, $attribute);
187
            }
188
189
            $slugEngines[$key] = $engine;
190
        }
191
192
        return $slugEngines[$key];
193
    }
194
195
    /**
196
     * Checks that the given slug is not a reserved word.
197
     *
198
     * @param string $slug
199
     * @param array $config
200
     * @param string $attribute
201
     * @return string
202
     */
203
    protected function validateSlug($slug, array $config, $attribute)
204
    {
205
206
        $separator = $config['separator'];
207
        $reserved = $config['reserved'];
208
209
        if ($reserved === null) {
210
            return $slug;
211
        }
212
213
        // check for reserved names
214
        if ($reserved instanceof \Closure) {
215
            $reserved = $reserved($this->model);
216
        }
217
218
        if (is_array($reserved)) {
219
            if (in_array($slug, $reserved)) {
220
221
                $method = $config['uniqueSuffix'];
222 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...
223
                    $suffix = $this->generateSuffix($slug, $separator, collect($reserved));
224
                } elseif (is_callable($method)) {
225
                    $suffix = call_user_func($method, $slug, $separator, collect($reserved));
226
                } else {
227
                    throw new \UnexpectedValueException('Sluggable "uniqueSuffix" for ' . get_class($this->model) . ':' . $attribute . ' is not null, or a closure.');
228
                }
229
230
                return $slug . $separator . $suffix;
231
232
            }
233
234
            return $slug;
235
        }
236
237
        throw new \UnexpectedValueException('Sluggable "reserved" for ' . get_class($this->model) . ':' . $attribute . ' is not null, an array, or a closure that returns null/array.');
238
239
    }
240
241
    /**
242
     * Checks if the slug should be unique, and makes it so if needed.
243
     *
244
     * @param string $slug
245
     * @param array $config
246
     * @param string $attribute
247
     * @return string
248
     */
249
    protected function makeSlugUnique($slug, array $config, $attribute)
250
    {
251
        if (!$config['unique']) {
252
            return $slug;
253
        }
254
255
        $separator = $config['separator'];
256
257
        // find all models where the slug is like the current one
258
        $list = $this->getExistingSlugs($slug, $attribute, $config);
259
260
        // if ...
261
        // 	a) the list is empty, or
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
262
        // 	b) our slug isn't in the list
263
        // ... we are okay
264
        if (
265
            $list->count() === 0 ||
266
            $list->contains($slug) === false
267
        ) {
268
            return $slug;
269
        }
270
271
        // if our slug is in the list, but
272
        // 	a) it's for our model, or
273
        //  b) it looks like a suffixed version of our slug
274
        // ... we are also okay (use the current slug)
275
        if ($list->has($this->model->getKey())) {
276
            $currentSlug = $list->get($this->model->getKey());
277
278
            if (
279
                $currentSlug === $slug ||
280
                strpos($currentSlug, $slug) === 0
281
            ) {
282
                return $currentSlug;
283
            }
284
        }
285
286
        $method = $config['uniqueSuffix'];
287 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...
288
            $suffix = $this->generateSuffix($slug, $separator, $list);
289
        } elseif (is_callable($method)) {
290
            $suffix = call_user_func($method, $slug, $separator, $list);
291
        } else {
292
            throw new \UnexpectedValueException('Sluggable "unigueSuffix" for ' . get_class($this->model) . ':' . $attribute . ' is not null, or a closure.');
293
        }
294
295
        return $slug . $separator . $suffix;
296
    }
297
298
    /**
299
     * Generate a unique suffix for the given slug (and list of existing, "similar" slugs.
300
     *
301
     * @param string $slug
302
     * @param string $separator
303
     * @param \Illuminate\Support\Collection $list
304
     * @return string
305
     */
306
    protected function generateSuffix($slug, $separator, Collection $list)
307
    {
308
        $len = strlen($slug . $separator);
309
310
        // If the slug already exists, but belongs to
311
        // our model, return the current suffix.
312
        if ($list->search($slug) === $this->model->getKey()) {
313
            $suffix = explode($separator, $slug);
314
315
            return end($suffix);
316
        }
317
318
        $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...
319
            return intval(substr($value, $len));
320
        });
321
322
        // find the highest value and return one greater.
323
        return $list->max() + 1;
324
    }
325
326
    /**
327
     * Get all existing slugs that are similar to the given slug.
328
     *
329
     * @param string $slug
330
     * @param string $attribute
331
     * @param array $config
332
     * @return \Illuminate\Support\Collection
333
     */
334
    protected function getExistingSlugs($slug, $attribute, array $config)
335
    {
336
        $includeTrashed = $config['includeTrashed'];
337
338
        $query = $this->model->newQuery()
339
            ->findSimilarSlugs($this->model, $attribute, $config, $slug);
340
341
        // use the model scope to find similar slugs
342
        if (method_exists($this->model, 'scopeWithUniqueSlugConstraints')) {
343
            $query->withUniqueSlugConstraints($this->model, $attribute, $config, $slug);
344
        }
345
346
        // include trashed models if required
347
        if ($includeTrashed && $this->usesSoftDeleting()) {
348
            $query->withTrashed();
349
        }
350
351
        // get the list of all matching slugs
352
        $results = $query->select([$attribute, $this->model->getTable() . '.' . $this->model->getKeyName()])
353
            ->get()
354
            ->toBase();
355
356
        // key the results and return
357
        return $results->pluck($attribute, $this->model->getKeyName());
358
    }
359
360
    /**
361
     * Does this model use softDeleting?
362
     *
363
     * @return bool
364
     */
365
    protected function usesSoftDeleting()
366
    {
367
        return method_exists($this->model, 'bootSoftDeletes');
368
    }
369
370
    /**
371
     * Generate a unique slug for a given string.
372
     *
373
     * @param \Illuminate\Database\Eloquent\Model|string $model
374
     * @param string $attribute
375
     * @param string $fromString
376
     * @param array $config
377
     * @return string
378
     */
379
    public static function createSlug($model, $attribute, $fromString, array $config = null)
380
    {
381
        if (is_string($model)) {
382
            $model = new $model;
383
        }
384
        $instance = (new static())->setModel($model);
385
386
        if ($config === null) {
387
            $config = array_get($model->sluggable(), $attribute);
388
        } elseif (!is_array($config)) {
389
            throw new \UnexpectedValueException('SlugService::createSlug expects an array or null as the fourth argument; ' . gettype($config) . ' given.');
390
        }
391
392
        $config = $instance->getConfiguration($config);
393
394
        $slug = $instance->generateSlug($fromString, $config, $attribute);
395
        $slug = $instance->validateSlug($slug, $config, $attribute);
396
        $slug = $instance->makeSlugUnique($slug, $config, $attribute);
397
398
        return $slug;
399
    }
400
401
    /**
402
     * @param \Illuminate\Database\Eloquent\Model $model
403
     * @return $this
404
     */
405
    public function setModel(Model $model)
406
    {
407
        $this->model = $model;
408
409
        return $this;
410
    }
411
}
412