Completed
Push — master ( 37308f...bacbc8 )
by Colin
10s
created

SlugService::generateSlug()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 21
rs 8.7624
cc 5
eloc 14
nc 5
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)
105
    {
106
        if (
107
            empty($this->model->getAttributeValue($attribute)) ||
108
            $config['onUpdate'] === true
109
        ) {
110
            return true;
111
        }
112
113
        if ($this->model->isDirty($attribute)) {
114
            return false;
115
        }
116
117
        return (!$this->model->exists);
118
    }
119
120
    /**
121
     * Get the source string for the slug.
122
     *
123
     * @param mixed $from
124
     * @return string
125
     */
126
    protected function getSlugSource($from)
127
    {
128
        if (is_null($from)) {
129
            return $this->model->__toString();
130
        }
131
132
        $sourceStrings = array_map(function ($key) {
133
            return data_get($this->model, $key);
134
        }, (array)$from);
135
136
        return join($sourceStrings, ' ');
137
    }
138
139
    /**
140
     * Generate a slug from the given source string.
141
     *
142
     * @param string $source
143
     * @param array $config
144
     * @param string $attribute
145
     * @return string
146
     */
147
    protected function generateSlug($source, array $config, $attribute)
148
    {
149
        $separator = $config['separator'];
150
        $method = $config['method'];
151
        $maxLength = $config['maxLength'];
152
153
        if ($method === null) {
154
            $slugEngine = $this->getSlugEngine($attribute);
155
            $slug = $slugEngine->slugify($source, $separator);
156
        } elseif (is_callable($method)) {
157
            $slug = call_user_func($method, $source, $separator);
158
        } else {
159
            throw new \UnexpectedValueException('Sluggable "method" for ' . get_class($this->model) . ':' . $attribute . ' is not callable nor null.');
160
        }
161
162
        if (is_string($slug) && $maxLength) {
163
            $slug = mb_substr($slug, 0, $maxLength);
164
        }
165
166
        return $slug;
167
    }
168
169
    /**
170
     * Return a class that has a `slugify()` method, used to convert
171
     * strings into slugs.
172
     *
173
     * @param string $attribute
174
     * @return Slugify
175
     */
176
    protected function getSlugEngine($attribute)
177
    {
178
        static $slugEngines = [];
179
180
        $key = get_class($this->model) . '.' . $attribute;
181
182
        if (!array_key_exists($key, $slugEngines)) {
183
            $engine = new Slugify();
184
            if (method_exists($this->model, 'customizeSlugEngine')) {
185
                $engine = $this->model->customizeSlugEngine($engine, $attribute);
186
            }
187
188
            $slugEngines[$key] = $engine;
189
        }
190
191
        return $slugEngines[$key];
192
    }
193
194
    /**
195
     * Checks that the given slug is not a reserved word.
196
     *
197
     * @param string $slug
198
     * @param array $config
199
     * @param string $attribute
200
     * @return string
201
     */
202
    protected function validateSlug($slug, array $config, $attribute)
203
    {
204
        $separator = $config['separator'];
205
        $reserved = $config['reserved'];
206
207
        if ($reserved === null) {
208
            return $slug;
209
        }
210
211
        // check for reserved names
212
        if ($reserved instanceof \Closure) {
213
            $reserved = $reserved($this->model);
214
        }
215
216
        if (is_array($reserved)) {
217
            if (in_array($slug, $reserved)) {
218
                return $slug . $separator . '1';
219
            }
220
221
            return $slug;
222
        }
223
224
        throw new \UnexpectedValueException('Sluggable "reserved" for ' . get_class($this->model) . ':' . $attribute . ' is not null, an array, or a closure that returns null/array.');
225
    }
226
227
    /**
228
     * Checks if the slug should be unique, and makes it so if needed.
229
     *
230
     * @param string $slug
231
     * @param array $config
232
     * @param string $attribute
233
     * @return string
234
     */
235
    protected function makeSlugUnique($slug, array $config, $attribute)
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
        if ($method === null) {
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
        if ($config['unique']) {
369
            $slug = $instance->makeSlugUnique($slug, $config, $attribute);
370
        }
371
372
        return $slug;
373
    }
374
375
    /**
376
     * @param \Illuminate\Database\Eloquent\Model $model
377
     * @return $this
378
     */
379
    public function setModel(Model $model)
380
    {
381
        $this->model = $model;
382
383
        return $this;
384
    }
385
}
386