Completed
Push — master ( 60f133...b1ddad )
by Colin
08:08 queued 05:16
created

SlugService::makeSlugUnique()   C

Complexity

Conditions 7
Paths 4

Size

Total Lines 34
Code Lines 17

Duplication

Lines 7
Ratio 20.59 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 7
loc 34
rs 6.7272
cc 7
eloc 17
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
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
86
                $slug = $this->validateSlug($slug, $config, $attribute);
87
88
                if ($config['unique']) {
89
                    $slug = $this->makeSlugUnique($slug, $config, $attribute);
90
                }
91
            }
92
        }
93
94
        return $slug;
95
    }
96
97
    /**
98
     * Determines whether the model needs slugging.
99
     *
100
     * @param string $attribute
101
     * @param array $config
102
     * @return bool
103
     */
104
    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...
105
    {
106
        if (empty($this->model->getAttributeValue($attribute))) {
107
            return true;
108
        }
109
110
        if ($this->model->isDirty($attribute)) {
111
            return false;
112
        }
113
114
        return (!$this->model->exists);
115
    }
116
117
    /**
118
     * Get the source string for the slug.
119
     *
120
     * @param mixed $from
121
     * @return string
122
     */
123
    protected function getSlugSource($from)
124
    {
125
        if (is_null($from)) {
126
            return $this->model->__toString();
127
        }
128
129
        $sourceStrings = array_map(function ($key) {
130
            return array_get($this->model, $key);
131
        }, (array)$from);
132
133
        return join($sourceStrings, ' ');
134
    }
135
136
    /**
137
     * Generate a slug from the given source string.
138
     *
139
     * @param string $source
140
     * @param array $config
141
     * @param string $attribute
142
     * @return string
143
     */
144
    protected function generateSlug($source, array $config, $attribute)
145
    {
146
        $separator = $config['separator'];
147
        $method = $config['method'];
148
        $maxLength = $config['maxLength'];
149
150 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...
151
            $slugEngine = $this->getSlugEngine($attribute);
152
            $slug = $slugEngine->slugify($source, $separator);
153
        } elseif (is_callable($method)) {
154
            $slug = call_user_func($method, $source, $separator);
155
        } else {
156
            throw new \UnexpectedValueException('Sluggable "method" for ' . get_class($this->model) . ':' . $attribute . ' is not callable nor null.');
157
        }
158
159
        if (is_string($slug) && $maxLength) {
160
            $slug = mb_substr($slug, 0, $maxLength);
161
        }
162
163
        return $slug;
164
    }
165
166
    /**
167
     * Return a class that has a `slugify()` method, used to convert
168
     * strings into slugs.
169
     *
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
        $separator = $config['separator'];
234
235
        // find all models where the slug is like the current one
236
        $list = $this->getExistingSlugs($slug, $attribute, $config);
237
238
        // if ...
239
        // 	a) the list is empty
240
        // 	b) our slug isn't in the list
241
        // 	c) our slug is in the list and it's for our model
242
        // ... we are okay
243
        if (
244
            $list->count() === 0 ||
245
            $list->contains($slug) === false ||
246
            (
247
                $list->has($this->model->getKey()) &&
248
                $list->get($this->model->getKey()) === $slug
249
            )
250
        ) {
251
            return $slug;
252
        }
253
254
        $method = $config['uniqueSuffix'];
255 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...
256
            $suffix = $this->generateSuffix($slug, $separator, $list);
257
        } else if (is_callable($method)) {
258
            $suffix = call_user_func($method, $slug, $separator, $list);
259
        } else {
260
            throw new \UnexpectedValueException('Sluggable "reserved" for ' . get_class($this->model) . ':' . $attribute . ' is not null, an array, or a closure that returns null/array.');
261
        }
262
263
        return $slug . $separator . $suffix;
264
    }
265
266
    /**
267
     * Generate a unique suffix for the given slug (and list of existing, "similar" slugs.
268
     *
269
     * @param string $slug
270
     * @param string $separator
271
     * @param \Illuminate\Support\Collection $list
272
     * @return string
273
     */
274
    protected function generateSuffix($slug, $separator, Collection $list)
275
    {
276
        $len = strlen($slug . $separator);
277
278
        // If the slug already exists, but belongs to
279
        // our model, return the current suffix.
280
        if ($list->search($slug) === $this->model->getKey()) {
281
            $suffix = explode($separator, $slug);
282
283
            return end($suffix);
284
        }
285
286
        $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...
287
            return intval(substr($value, $len));
288
        });
289
290
        // find the highest value and return one greater.
291
        return $list->max() + 1;
292
    }
293
294
    /**
295
     * Get all existing slugs that are similar to the given slug.
296
     *
297
     * @param string $slug
298
     * @param string $attribute
299
     * @param array $config
300
     * @return \Illuminate\Support\Collection
301
     */
302
    protected function getExistingSlugs($slug, $attribute, array $config)
303
    {
304
        $includeTrashed = $config['includeTrashed'];
305
306
        $query = $this->model->newQuery()
307
            ->findSimilarSlugs($this->model, $attribute, $config, $slug);
308
309
        // use the model scope to find similar slugs
310
        if (method_exists($this->model, 'scopeWithUniqueSlugConstraints')) {
311
            $query->withUniqueSlugConstraints($this->model, $attribute, $config, $slug);
312
        }
313
314
        // include trashed models if required
315
        if ($includeTrashed && $this->usesSoftDeleting()) {
316
            $query->withTrashed();
317
        }
318
319
        // get the list of all matching slugs
320
        return $query->pluck($attribute, $this->model->getKeyName());
321
    }
322
323
    /**
324
     * Does this model use softDeleting?
325
     *
326
     * @return bool
327
     */
328
    protected function usesSoftDeleting()
329
    {
330
        return method_exists($this->model, 'bootSoftDeletes');
331
    }
332
333
    /**
334
     * Generate a unique slug for a given string.
335
     *
336
     * @param \Illuminate\Database\Eloquent\Model|string $model
337
     * @param string $attribute
338
     * @param string $fromString
339
     * @return string
340
     */
341
    public static function createSlug($model, $attribute, $fromString)
342
    {
343
        if (is_string($model)) {
344
            $model = new $model;
345
        }
346
        $instance = (new self())->setModel($model);
347
348
        $config = array_get($model->sluggable(), $attribute);
349
        $config = $instance->getConfiguration($config);
350
351
        $slug = $instance->generateSlug($fromString, $config, $attribute);
352
        $slug = $instance->validateSlug($slug, $config, $attribute);
353
        if ($config['unique']) {
354
            $slug = $instance->makeSlugUnique($slug, $config, $attribute);
355
        }
356
357
        return $slug;
358
    }
359
360
    /**
361
     * @param \Illuminate\Database\Eloquent\Model $model
362
     * @return $this
363
     */
364
    public function setModel(Model $model)
365
    {
366
        $this->model = $model;
367
368
        return $this;
369
    }
370
}
371