Completed
Push — master ( bacbc8...506525 )
by Colin
03:03
created

SlugService::createSlug()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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