Completed
Pull Request — master (#369)
by
unknown
02:21
created

SlugService::getSlugSource()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 12
nc 3
nop 1
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
        if ($this->getConfiguration()['class_name_when_null'] && $sourceStrings[0] == null){
138
            $sourceStrings[0] = class_basename($this->model);
139
        }
140
141
        return join($sourceStrings, ' ');
142
    }
143
144
    /**
145
     * Generate a slug from the given source string.
146
     *
147
     * @param string $source
148
     * @param array $config
149
     * @param string $attribute
150
     * @return string
151
     */
152
    protected function generateSlug($source, array $config, $attribute)
153
    {
154
        $separator = $config['separator'];
155
        $method = $config['method'];
156
        $maxLength = $config['maxLength'];
157
158 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...
159
            $slugEngine = $this->getSlugEngine($attribute);
160
            $slug = $slugEngine->slugify($source, $separator);
161
        } elseif (is_callable($method)) {
162
            $slug = call_user_func($method, $source, $separator);
163
        } else {
164
            throw new \UnexpectedValueException('Sluggable "method" for ' . get_class($this->model) . ':' . $attribute . ' is not callable nor null.');
165
        }
166
167
        if (is_string($slug) && $maxLength) {
168
            $slug = mb_substr($slug, 0, $maxLength);
169
        }
170
171
        return $slug;
172
    }
173
174
    /**
175
     * Return a class that has a `slugify()` method, used to convert
176
     * strings into slugs.
177
     *
178
     * @param string $attribute
179
     * @return Slugify
180
     */
181
    protected function getSlugEngine($attribute)
182
    {
183
        static $slugEngines = [];
184
185
        $key = get_class($this->model) . '.' . $attribute;
186
187
        if (!array_key_exists($key, $slugEngines)) {
188
            $engine = new Slugify();
189
            if (method_exists($this->model, 'customizeSlugEngine')) {
190
                $engine = $this->model->customizeSlugEngine($engine, $attribute);
191
            }
192
193
            $slugEngines[$key] = $engine;
194
        }
195
196
        return $slugEngines[$key];
197
    }
198
199
    /**
200
     * Checks that the given slug is not a reserved word.
201
     *
202
     * @param string $slug
203
     * @param array $config
204
     * @param string $attribute
205
     * @return string
206
     */
207
    protected function validateSlug($slug, array $config, $attribute)
208
    {
209
210
        $separator = $config['separator'];
211
        $reserved = $config['reserved'];
212
213
        if ($reserved === null) {
214
            return $slug;
215
        }
216
217
        // check for reserved names
218
        if ($reserved instanceof \Closure) {
219
            $reserved = $reserved($this->model);
220
        }
221
222
        if (is_array($reserved)) {
223
            if (in_array($slug, $reserved)) {
224
225
                $method = $config['uniqueSuffix'];
226 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...
227
                    $suffix = $this->generateSuffix($slug, $separator, collect($reserved));
228
                } elseif (is_callable($method)) {
229
                    $suffix = call_user_func($method, $slug, $separator, collect($reserved));
230
                } else {
231
                    throw new \UnexpectedValueException('Sluggable "uniqueSuffix" for ' . get_class($this->model) . ':' . $attribute . ' is not null, or a closure.');
232
                }
233
234
                return $slug . $separator . $suffix;
235
236
            }
237
238
            return $slug;
239
        }
240
241
        throw new \UnexpectedValueException('Sluggable "reserved" for ' . get_class($this->model) . ':' . $attribute . ' is not null, an array, or a closure that returns null/array.');
242
243
    }
244
245
    /**
246
     * Checks if the slug should be unique, and makes it so if needed.
247
     *
248
     * @param string $slug
249
     * @param array $config
250
     * @param string $attribute
251
     * @return string
252
     */
253
    protected function makeSlugUnique($slug, array $config, $attribute)
254
    {
255
        if (!$config['unique']) {
256
            return $slug;
257
        }
258
259
        $separator = $config['separator'];
260
261
        // find all models where the slug is like the current one
262
        $list = $this->getExistingSlugs($slug, $attribute, $config);
263
264
        // if ...
265
        // 	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...
266
        // 	b) our slug isn't in the list
267
        // ... we are okay
268
        if (
269
            $list->count() === 0 ||
270
            $list->contains($slug) === false
271
        ) {
272
            return $slug;
273
        }
274
275
        // if our slug is in the list, but
276
        // 	a) it's for our model, or
277
        //  b) it looks like a suffixed version of our slug
278
        // ... we are also okay (use the current slug)
279
        if ($list->has($this->model->getKey())) {
280
            $currentSlug = $list->get($this->model->getKey());
281
282
            if (
283
                $currentSlug === $slug ||
284
                strpos($currentSlug, $slug) === 0
285
            ) {
286
                return $currentSlug;
287
            }
288
        }
289
290
        $method = $config['uniqueSuffix'];
291 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...
292
            $suffix = $this->generateSuffix($slug, $separator, $list);
293
        } elseif (is_callable($method)) {
294
            $suffix = call_user_func($method, $slug, $separator, $list);
295
        } else {
296
            throw new \UnexpectedValueException('Sluggable "uniqueSuffix" for ' . get_class($this->model) . ':' . $attribute . ' is not null, or a closure.');
297
        }
298
299
        return $slug . $separator . $suffix;
300
    }
301
302
    /**
303
     * Generate a unique suffix for the given slug (and list of existing, "similar" slugs.
304
     *
305
     * @param string $slug
306
     * @param string $separator
307
     * @param \Illuminate\Support\Collection $list
308
     * @return string
309
     */
310
    protected function generateSuffix($slug, $separator, Collection $list)
311
    {
312
        $len = strlen($slug . $separator);
313
314
        // If the slug already exists, but belongs to
315
        // our model, return the current suffix.
316
        if ($list->search($slug) === $this->model->getKey()) {
317
            $suffix = explode($separator, $slug);
318
319
            return end($suffix);
320
        }
321
322
        $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...
323
            return intval(substr($value, $len));
324
        });
325
326
        // find the highest value and return one greater.
327
        return $list->max() + 1;
328
    }
329
330
    /**
331
     * Get all existing slugs that are similar to the given slug.
332
     *
333
     * @param string $slug
334
     * @param string $attribute
335
     * @param array $config
336
     * @return \Illuminate\Support\Collection
337
     */
338
    protected function getExistingSlugs($slug, $attribute, array $config)
339
    {
340
        $includeTrashed = $config['includeTrashed'];
341
342
        $query = $this->model->newQuery()
343
            ->findSimilarSlugs($this->model, $attribute, $config, $slug);
344
345
        // use the model scope to find similar slugs
346
        if (method_exists($this->model, 'scopeWithUniqueSlugConstraints')) {
347
            $query->withUniqueSlugConstraints($this->model, $attribute, $config, $slug);
348
        }
349
350
        // include trashed models if required
351
        if ($includeTrashed && $this->usesSoftDeleting()) {
352
            $query->withTrashed();
353
        }
354
355
        // get the list of all matching slugs
356
        $results = $query->select([$attribute, $this->model->getTable() . '.' . $this->model->getKeyName()])
357
            ->get()
358
            ->toBase();
359
360
        // key the results and return
361
        return $results->pluck($attribute, $this->model->getKeyName());
362
    }
363
364
    /**
365
     * Does this model use softDeleting?
366
     *
367
     * @return bool
368
     */
369
    protected function usesSoftDeleting()
370
    {
371
        return method_exists($this->model, 'bootSoftDeletes');
372
    }
373
374
    /**
375
     * Generate a unique slug for a given string.
376
     *
377
     * @param \Illuminate\Database\Eloquent\Model|string $model
378
     * @param string $attribute
379
     * @param string $fromString
380
     * @param array $config
381
     * @return string
382
     */
383
    public static function createSlug($model, $attribute, $fromString, array $config = null)
384
    {
385
        if (is_string($model)) {
386
            $model = new $model;
387
        }
388
        $instance = (new static())->setModel($model);
389
390
        if ($config === null) {
391
            $config = array_get($model->sluggable(), $attribute);
392
        } elseif (!is_array($config)) {
393
            throw new \UnexpectedValueException('SlugService::createSlug expects an array or null as the fourth argument; ' . gettype($config) . ' given.');
394
        }
395
396
        $config = $instance->getConfiguration($config);
397
398
        $slug = $instance->generateSlug($fromString, $config, $attribute);
399
        $slug = $instance->validateSlug($slug, $config, $attribute);
400
        $slug = $instance->makeSlugUnique($slug, $config, $attribute);
401
402
        return $slug;
403
    }
404
405
    /**
406
     * @param \Illuminate\Database\Eloquent\Model $model
407
     * @return $this
408
     */
409
    public function setModel(Model $model)
410
    {
411
        $this->model = $model;
412
413
        return $this;
414
    }
415
}
416