Completed
Pull Request — master (#260)
by Colin
06:42
created

SlugService::getConfiguration()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 2
eloc 5
nc 2
nop 1
1
<?php namespace Cviebrock\EloquentSluggable\Services;
2
3
use Cocur\Slugify\Slugify;
4
use Illuminate\Database\Eloquent\Builder;
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Collection;
7
8
9
/**
10
 * Class SlugService
11
 *
12
 * @package Cviebrock\EloquentSluggable\Services
13
 */
14
class SlugService
15
{
16
17
    /**
18
     * @var \Illuminate\Database\Eloquent\Model;
19
     */
20
    protected $model;
21
22
    /**
23
     * Slug the current model.
24
     *
25
     * @param \Illuminate\Database\Eloquent\Model $model
26
     * @param bool $force
27
     * @return bool
28
     */
29
    public function slug(Model $model, $force = false)
30
    {
31
        $this->setModel($model);
32
33
        $attributes = [];
34
35
        foreach ($this->model->sluggable() as $attribute => $config) {
36
            if (is_numeric($attribute)) {
37
                $attribute = $config;
38
                $config = $this->getConfiguration();
39
            } else {
40
                $config = $this->getConfiguration($config);
41
            }
42
43
            $slug = $this->buildSlug($attribute, $config, $force);
44
45
            $this->model->setAttribute($attribute, $slug);
46
47
            $attributes[] = $attribute;
48
        }
49
50
        return $this->model->isDirty($attributes);
51
    }
52
53
    /**
54
     * Get the sluggable configuration for the current model,
55
     * including default values where not specified.
56
     *
57
     * @param array $overrides
58
     * @return array
59
     */
60
    public function getConfiguration(array $overrides = [])
61
    {
62
        static $defaultConfig = null;
63
        if ($defaultConfig === null) {
64
            $defaultConfig = app('config')->get('sluggable');
65
        }
66
67
        return array_merge($defaultConfig, $overrides);
68
    }
69
70
    /**
71
     * Build the slug for the given attribute of the current model.
72
     *
73
     * @param string $attribute
74
     * @param array $config
75
     * @param bool $force
76
     * @return null|string
77
     */
78
    public function buildSlug($attribute, array $config, $force = null)
79
    {
80
        $slug = $this->model->getAttribute($attribute);
81
82
        if ($force || $this->needsSlugging($attribute, $config)) {
83
            $source = $this->getSlugSource($config['source']);
84
85
            if ($source) {
86
                $slug = $this->generateSlug($source, $config, $attribute);
87
88
                $slug = $this->validateSlug($slug, $config, $attribute);
89
90
                if ($config['unique']) {
91
                    $slug = $this->makeSlugUnique($slug, $config, $attribute);
92
                }
93
            }
94
        }
95
96
        return $slug;
97
    }
98
99
    /**
100
     * Determines whether the model needs slugging.
101
     *
102
     * @param string $attribute
103
     * @param array $config
104
     * @return bool
105
     */
106
    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...
107
    {
108
        if (empty($this->model->getAttributeValue($attribute))) {
109
            return true;
110
        }
111
112
        if ($this->model->isDirty($attribute)) {
113
            return false;
114
        }
115
116
        return (!$this->model->exists);
117
    }
118
119
    /**
120
     * Get the source string for the slug.
121
     *
122
     * @param mixed $from
123
     * @return string
124
     */
125
    protected function getSlugSource($from)
126
    {
127
        if (is_null($from)) {
128
            return $this->model->__toString();
129
        }
130
131
        $sourceStrings = array_map(function ($key) {
132
            return array_get($this->model, $key);
133
        }, (array)$from);
134
135
        return join($sourceStrings, ' ');
136
    }
137
138
    /**
139
     * Generate a slug from the given source string.
140
     *
141
     * @param string $source
142
     * @param array $config
143
     * @param string $attribute
144
     * @return string
145
     */
146
    protected function generateSlug($source, array $config, $attribute)
147
    {
148
        $separator = $config['separator'];
149
        $method = $config['method'];
150
        $maxLength = $config['maxLength'];
151
152 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...
153
            $slugEngine = $this->getSlugEngine($attribute);
154
            $slug = $slugEngine->slugify($source, $separator);
155
        } elseif (is_callable($method)) {
156
            $slug = call_user_func($method, $source, $separator);
157
        } else {
158
            throw new \UnexpectedValueException('Sluggable "method" for ' . get_class($this->model) . ':' . $attribute . ' is not callable nor null.');
159
        }
160
161
        if (is_string($slug) && $maxLength) {
162
            $slug = mb_substr($slug, 0, $maxLength);
163
        }
164
165
        return $slug;
166
    }
167
168
    /**
169
     * Return a class that has a `slugify()` method, used to convert
170
     * strings into slugs.
171
     *
172
     * @return Slugify
173
     */
174
    protected function getSlugEngine($attribute)
175
    {
176
        static $slugEngines = [];
177
178
        $key = get_class($this->model) . '.' . $attribute;
179
180
        if (!array_key_exists($key, $slugEngines)) {
181
            $engine = new Slugify();
182
            if (method_exists($this->model, 'customizeSlugEngine')) {
183
                $engine = $this->model->customizeSlugEngine($engine, $attribute);
184
            }
185
186
            $slugEngines[$key] = $engine;
187
        }
188
189
        return $slugEngines[$key];
190
    }
191
192
    /**
193
     * Checks that the given slug is not a reserved word.
194
     *
195
     * @param string $slug
196
     * @param array $config
197
     * @param string $attribute
198
     * @return string
199
     */
200
    protected function validateSlug($slug, array $config, $attribute)
201
    {
202
        $separator = $config['separator'];
203
        $reserved = $config['reserved'];
204
205
        if ($reserved === null) {
206
            return $slug;
207
        }
208
209
        // check for reserved names
210
        if ($reserved instanceof \Closure) {
211
            $reserved = $reserved($this->model);
212
        }
213
214
        if (is_array($reserved)) {
215
            if (in_array($slug, $reserved)) {
216
                return $slug . $separator . '1';
217
            }
218
219
            return $slug;
220
        }
221
222
        throw new \UnexpectedValueException('Sluggable "reserved" for ' . get_class($this->model) . ':' . $attribute . ' is not null, an array, or a closure that returns null/array.');
223
    }
224
225
    /**
226
     * Checks if the slug should be unique, and makes it so if needed.
227
     *
228
     * @param string $slug
229
     * @param array $config
230
     * @param string $attribute
231
     * @return string
232
     */
233
    protected function makeSlugUnique($slug, array $config, $attribute)
234
    {
235
        $separator = $config['separator'];
236
237
        // find all models where the slug is like the current one
238
        $list = $this->getExistingSlugs($slug, $attribute, $config);
239
240
        // if ...
241
        // 	a) the list is empty
242
        // 	b) our slug isn't in the list
243
        // 	c) our slug is in the list and it's for our model
244
        // ... we are okay
245
        if (
246
            $list->count() === 0 ||
247
            $list->contains($slug) === false ||
248
            (
249
                $list->has($this->model->getKey()) &&
250
                $list->get($this->model->getKey()) === $slug
251
            )
252
        ) {
253
            return $slug;
254
        }
255
256
        $method = $config['uniqueSuffix'];
257 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...
258
            $suffix = $this->generateSuffix($slug, $separator, $list);
259
        } else if (is_callable($method)) {
260
            $suffix = call_user_func($method, $slug, $separator, $list);
261
        } else {
262
            throw new \UnexpectedValueException('Sluggable "reserved" for ' . get_class($this->model) . ':' . $attribute . ' is not null, an array, or a closure that returns null/array.');
263
        }
264
265
        return $slug . $separator . $suffix;
266
    }
267
268
    /**
269
     * Generate a unique suffix for the given slug (and list of existing, "similar" slugs.
270
     *
271
     * @param string $slug
272
     * @param string $separator
273
     * @param \Illuminate\Support\Collection $list
274
     * @return string
275
     */
276
    protected function generateSuffix($slug, $separator, Collection $list)
277
    {
278
        $len = strlen($slug . $separator);
279
280
        // If the slug already exists, but belongs to
281
        // our model, return the current suffix.
282
        if ($list->search($slug) === $this->model->getKey()) {
283
            $suffix = explode($separator, $slug);
284
285
            return end($suffix);
286
        }
287
288
        $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...
289
            return intval(substr($value, $len));
290
        });
291
292
        // find the highest value and return one greater.
293
        return $list->max() + 1;
294
    }
295
296
    /**
297
     * Get all existing slugs that are similar to the given slug.
298
     *
299
     * @param string $slug
300
     * @param string $attribute
301
     * @param array $config
302
     * @return \Illuminate\Support\Collection
303
     */
304
    protected function getExistingSlugs($slug, $attribute, array $config)
305
    {
306
        $includeTrashed = $config['includeTrashed'];
307
308
        $query = $this->model->newQuery()
309
            ->findSimilarSlugs($this->model, $attribute, $config, $slug);
310
311
        // use the model scope to find similar slugs
312
        if (method_exists($this->model, 'scopeWithUniqueSlugConstraints')) {
313
            $query->withUniqueSlugConstraints($this->model, $attribute, $config, $slug);
314
        }
315
316
        // include trashed models if required
317
        if ($includeTrashed && $this->usesSoftDeleting()) {
318
            $query->withTrashed();
319
        }
320
321
        // get the list of all matching slugs
322
        return $query->pluck($attribute, $this->model->getKeyName());
323
    }
324
325
    /**
326
     * Does this model use softDeleting?
327
     *
328
     * @return bool
329
     */
330
    protected function usesSoftDeleting()
331
    {
332
        return method_exists($this->model, 'bootSoftDeletes');
333
    }
334
335
    /**
336
     * Generate a unique slug for a given string.
337
     *
338
     * @param \Illuminate\Database\Eloquent\Model|string $model
339
     * @param string $attribute
340
     * @param string $fromString
341
     * @return string
342
     */
343
    public static function createSlug($model, $attribute, $fromString)
344
    {
345
        if (is_string($model)) {
346
            $model = new $model;
347
        }
348
        $instance = (new self())->setModel($model);
349
350
        $config = array_get($model->sluggable(), $attribute);
351
        $config = $instance->getConfiguration($config);
352
353
        $slug = $instance->generateSlug($fromString, $config, $attribute);
354
        $slug = $instance->validateSlug($slug, $config, $attribute);
355
        if ($config['unique']) {
356
            $slug = $instance->makeSlugUnique($slug, $config, $attribute);
357
        }
358
359
        return $slug;
360
    }
361
362
    /**
363
     * @param \Illuminate\Database\Eloquent\Model $model
364
     * @return $this
365
     */
366
    public function setModel(Model $model)
367
    {
368
        $this->model = $model;
369
370
        return $this;
371
    }
372
}
373