Completed
Push — master ( 278833...786648 )
by Colin
03:06
created

SlugService::buildSlug()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 20
rs 8.8571
cc 5
eloc 10
nc 4
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
use Illuminate\Support\Str;
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
     * @return bool
27
     */
28
    public function slug(Model $model, $force = false)
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
            $this->model->setAttribute($attribute, $slug);
45
46
            $attributes[] = $attribute;
47
        }
48
49
        return $this->model->isDirty($attributes);
50
    }
51
52
    /**
53
     * Get the sluggable configuration for the current model,
54
     * including default values where not specified.
55
     *
56
     * @param array $overrides
57
     * @return array
58
     */
59
    public function getConfiguration(array $overrides = [])
60
    {
61
        static $defaultConfig = null;
62
        if ($defaultConfig === null) {
63
            $defaultConfig = app('config')->get('sluggable');
64
        }
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
     * @return null|string
76
     */
77
    public function buildSlug($attribute, array $config, $force = null)
78
    {
79
        $slug = $this->model->getAttribute($attribute);
80
81
        if ($force || $this->needsSlugging($attribute, $config)) {
82
            $source = $this->getSlugSource($config['source']);
83
84
            if ($source) {
85
                $slug = $this->generateSlug($source, $config, $attribute);
86
87
                $slug = $this->validateSlug($slug, $config, $attribute);
88
89
                if ($config['unique']) {
90
                    $slug = $this->makeSlugUnique($slug, $config, $attribute);
91
                }
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
     * @return bool
104
     */
105
    protected function needsSlugging($attribute, array $config)
0 ignored issues
show
Unused Code introduced by
The parameter $config 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...
106
    {
107
        if (empty($this->model->getAttributeValue($attribute))) {
108
            return true;
109
        }
110
111
        if ($this->model->isDirty($attribute)) {
112
            return false;
113
        }
114
115
        return (!$this->model->exists);
116
    }
117
118
    /**
119
     * Get the source string for the slug.
120
     *
121
     * @param mixed $from
122
     * @return string
123
     */
124
    protected function getSlugSource($from)
125
    {
126
        if (is_null($from)) {
127
            return $this->model->__toString();
128
        }
129
130
        $sourceStrings = array_map(function ($key) {
131
            return array_get($this->model, $key);
132
        }, (array)$from);
133
134
        return join($sourceStrings, ' ');
135
    }
136
137
    /**
138
     * Generate a slug from the given source string.
139
     *
140
     * @param string $source
141
     * @param array $config
142
     * @param string $attribute
143
     * @return string
144
     */
145
    protected function generateSlug($source, array $config, $attribute)
146
    {
147
        $separator = $config['separator'];
148
        $method = $config['method'];
149
        $maxLength = $config['maxLength'];
150
151 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...
152
            $slugEngine = $this->getSlugEngine($attribute);
153
            $slug = $slugEngine->slugify($source, $separator);
154
        } elseif (is_callable($method)) {
155
            $slug = call_user_func($method, $source, $separator);
156
        } else {
157
            throw new \UnexpectedValueException('Sluggable "method" for ' . get_class($this->model) . ':' . $attribute . ' is not callable nor null.');
158
        }
159
160
        if (is_string($slug) && $maxLength) {
161
            $slug = mb_substr($slug, 0, $maxLength);
162
        }
163
164
        return $slug;
165
    }
166
167
    /**
168
     * Return a class that has a `slugify()` method, used to convert
169
     * strings into slugs.
170
     *
171
     * @return Slugify
172
     */
173
    protected function getSlugEngine($attribute)
174
    {
175
        static $slugEngines = [];
176
177
        $key = get_class($this->model) . '.' . $attribute;
178
179
        if (!array_key_exists($key, $slugEngines)) {
180
            $engine = new Slugify();
181
            if (method_exists($this->model, 'customizeSlugEngine')) {
182
                $engine = $this->model->customizeSlugEngine($engine, $attribute);
183
            }
184
185
            $slugEngines[$key] = $engine;
186
        }
187
188
        return $slugEngines[$key];
189
    }
190
191
    /**
192
     * Checks that the given slug is not a reserved word.
193
     *
194
     * @param string $slug
195
     * @param array $config
196
     * @param string $attribute
197
     * @return string
198
     */
199
    protected function validateSlug($slug, array $config, $attribute)
200
    {
201
        $separator = $config['separator'];
202
        $reserved = $config['reserved'];
203
204
        if ($reserved === null) {
205
            return $slug;
206
        }
207
208
        // check for reserved names
209
        if ($reserved instanceof \Closure) {
210
            $reserved = $reserved($this->model);
211
        }
212
213
        if (is_array($reserved)) {
214
            if (in_array($slug, $reserved)) {
215
                return $slug . $separator . '1';
216
            }
217
218
            return $slug;
219
        }
220
221
        throw new \UnexpectedValueException('Sluggable "reserved" for ' . get_class($this->model) . ':' . $attribute . ' is not null, an array, or a closure that returns null/array.');
222
    }
223
224
    /**
225
     * Checks if the slug should be unique, and makes it so if needed.
226
     *
227
     * @param string $slug
228
     * @param array $config
229
     * @param string $attribute
230
     * @return string
231
     */
232
    protected function makeSlugUnique($slug, array $config, $attribute)
233
    {
234
        $separator = $config['separator'];
235
236
        // find all models where the slug is like the current one
237
        $list = $this->getExistingSlugs($slug, $attribute, $config);
238
239
        // if ...
240
        // 	a) the list is empty
241
        // 	b) our slug isn't in the list
242
        // 	c) our slug is in the list and it's for our model
243
        // ... we are okay
244
        if (
245
            $list->count() === 0 ||
246
            $list->contains($slug) === false ||
247
            (
248
                $list->has($this->model->getKey()) &&
249
                $list->get($this->model->getKey()) === $slug
250
            )
251
        ) {
252
            return $slug;
253
        }
254
255
        $method = $config['uniqueSuffix'];
256 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...
257
            $suffix = $this->generateSuffix($slug, $separator, $list);
258
        } else if (is_callable($method)) {
259
            $suffix = call_user_func($method, $slug, $separator, $list);
260
        } else {
261
            throw new \UnexpectedValueException('Sluggable "reserved" for ' . get_class($this->model) . ':' . $attribute . ' is not null, an array, or a closure that returns null/array.');
262
        }
263
264
        return $slug . $separator . $suffix;
265
    }
266
267
    /**
268
     * Generate a unique suffix for the given slug (and list of existing, "similar" slugs.
269
     *
270
     * @param string $slug
271
     * @param string $separator
272
     * @param \Illuminate\Support\Collection $list
273
     * @return string
274
     */
275
    protected function generateSuffix($slug, $separator, Collection $list)
276
    {
277
        $len = strlen($slug . $separator);
278
279
        // If the slug already exists, but belongs to
280
        // our model, return the current suffix.
281
        if ($list->search($slug) === $this->model->getKey()) {
282
            $suffix = explode($separator, $slug);
283
284
            return end($suffix);
285
        }
286
287
        $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...
288
            return intval(substr($value, $len));
289
        });
290
291
        // find the highest value and return one greater.
292
        return $list->max() + 1;
293
    }
294
295
    /**
296
     * Get all existing slugs that are similar to the given slug.
297
     *
298
     * @param string $slug
299
     * @param string $attribute
300
     * @param array $config
301
     * @return \Illuminate\Support\Collection
302
     */
303
    protected function getExistingSlugs($slug, $attribute, array $config)
304
    {
305
        $includeTrashed = $config['includeTrashed'];
306
307
        $query = $this->model->newQuery()
308
            ->findSimilarSlugs($this->model, $attribute, $config, $slug);
309
310
        // use the model scope to find similar slugs
311
        if (method_exists($this->model, 'scopeWithUniqueSlugConstraints')) {
312
            $query->withUniqueSlugConstraints($this->model, $attribute, $config, $slug);
313
        }
314
315
        // include trashed models if required
316
        if ($includeTrashed && $this->usesSoftDeleting()) {
317
            $query->withTrashed();
318
        }
319
320
        // get the list of all matching slugs
321
        // (need to do this check because of changes in Query Builder between 5.1 and 5.2)
322
        // @todo refactor this to universally working code
323
        if (version_compare($this->getApplicationVersion(), '5.2', '>=')) {
324
            return $query->pluck($attribute, $this->model->getKeyName());
325
        } else {
326
            return $query->lists($attribute, $this->model->getKeyName());
327
        }
328
    }
329
330
    /**
331
     * Does this model use softDeleting?
332
     *
333
     * @return bool
334
     */
335
    protected function usesSoftDeleting()
336
    {
337
        return method_exists($this->model, 'bootSoftDeletes');
338
    }
339
340
    /**
341
     * Generate a unique slug for a given string.
342
     *
343
     * @param \Illuminate\Database\Eloquent\Model|string $model
344
     * @param string $attribute
345
     * @param string $fromString
346
     * @return string
347
     */
348
    public static function createSlug($model, $attribute, $fromString)
349
    {
350
        if (is_string($model)) {
351
            $model = new $model;
352
        }
353
        $instance = (new self())->setModel($model);
354
355
        $config = array_get($model->sluggable(), $attribute);
356
        $config = $instance->getConfiguration($config);
357
358
        $slug = $instance->generateSlug($fromString, $config, $attribute);
359
        $slug = $instance->validateSlug($slug, $config, $attribute);
360
        if ($config['unique']) {
361
            $slug = $instance->makeSlugUnique($slug, $config, $attribute);
362
        }
363
364
        return $slug;
365
    }
366
367
    /**
368
     * @param \Illuminate\Database\Eloquent\Model $model
369
     * @return $this
370
     */
371
    public function setModel(Model $model)
372
    {
373
        $this->model = $model;
374
375
        return $this;
376
    }
377
378
    /**
379
     * Determine the version of Laravel (or the Illuminate components) that we are running.
380
     *
381
     * @return mixed|string
382
     */
383
    protected function getApplicationVersion()
384
    {
385
        static $version;
386
387
        if (!$version) {
388
            $version = app()->version();
389
            // parse out Lumen version
390
            if (preg_match('/Lumen \((.*?)\)/i', $version, $matches)) {
391
                $version = $matches[1];
392
            }
393
        }
394
        return $version;
395
    }
396
}
397