Completed
Push — master ( b76d40...a40261 )
by Colin
03:17
created

SlugService::getApplicationVersion()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 13
rs 9.4285
cc 3
eloc 7
nc 3
nop 0
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
     * @param string $attribute
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
        $results = $query->addSelect([$attribute, $this->model->getKeyName()])
322
            ->get();
323
324
        // key the results and return
325
        return $results->pluck($attribute, $this->model->getKeyName());
326
    }
327
328
    /**
329
     * Does this model use softDeleting?
330
     *
331
     * @return bool
332
     */
333
    protected function usesSoftDeleting()
334
    {
335
        return method_exists($this->model, 'bootSoftDeletes');
336
    }
337
338
    /**
339
     * Generate a unique slug for a given string.
340
     *
341
     * @param \Illuminate\Database\Eloquent\Model|string $model
342
     * @param string $attribute
343
     * @param string $fromString
344
     * @return string
345
     */
346
    public static function createSlug($model, $attribute, $fromString)
347
    {
348
        if (is_string($model)) {
349
            $model = new $model;
350
        }
351
        $instance = (new self())->setModel($model);
352
353
        $config = array_get($model->sluggable(), $attribute);
354
        $config = $instance->getConfiguration($config);
355
356
        $slug = $instance->generateSlug($fromString, $config, $attribute);
357
        $slug = $instance->validateSlug($slug, $config, $attribute);
358
        if ($config['unique']) {
359
            $slug = $instance->makeSlugUnique($slug, $config, $attribute);
360
        }
361
362
        return $slug;
363
    }
364
365
    /**
366
     * @param \Illuminate\Database\Eloquent\Model $model
367
     * @return $this
368
     */
369
    public function setModel(Model $model)
370
    {
371
        $this->model = $model;
372
373
        return $this;
374
    }
375
}
376