1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Padosoft\Sluggable; |
4
|
|
|
|
5
|
|
|
use Illuminate\Database\Eloquent\Model; |
6
|
|
|
use Illuminate\Support\Str; |
7
|
|
|
|
8
|
|
|
trait HasSlug |
9
|
|
|
{ |
10
|
|
|
/** @var \Padosoft\Sluggable\SlugOptions */ |
11
|
|
|
protected $slugOptions; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Boot the trait. |
15
|
|
|
*/ |
16
|
|
|
protected static function bootHasSlug() |
17
|
|
|
{ |
18
|
|
|
static::creating(function (Model $model) { |
19
|
|
|
$model->addSlug(); |
20
|
|
|
}); |
21
|
|
|
|
22
|
|
|
static::updating(function (Model $model) { |
23
|
|
|
$model->addSlug(); |
24
|
|
|
}); |
25
|
|
|
} |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Add the slug to the model. |
29
|
|
|
*/ |
30
|
|
|
protected function addSlug() |
31
|
|
|
{ |
32
|
|
|
$this->slugOptions = $this->getSlugOptionsOrDefault(); |
33
|
|
|
|
34
|
|
|
$this->guardAgainstInvalidSlugOptions(); |
35
|
|
|
|
36
|
|
|
$slug = $this->generateNonUniqueSlug(); |
37
|
|
|
if ($this->slugOptions->generateUniqueSlugs) { |
38
|
|
|
$slug = $this->makeSlugUnique($slug); |
39
|
|
|
} |
40
|
|
|
$slugField = $this->slugOptions->slugField; |
41
|
|
|
|
42
|
|
|
$this->$slugField = $slug; |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Retrive a specifice SlugOptions for this model, or return default SlugOptions |
47
|
|
|
*/ |
48
|
|
|
protected function getSlugOptionsOrDefault() |
49
|
|
|
{ |
50
|
|
|
if (method_exists($this, 'getSlugOptions')) { |
51
|
|
|
return $this->getSlugOptions(); |
52
|
|
|
} else { |
53
|
|
|
return SlugOptions::create()->getSlugOptionsDefault(); |
54
|
|
|
} |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Generate a non unique slug for this record. |
59
|
|
|
* @return string |
60
|
|
|
* @throws InvalidOption |
61
|
|
|
*/ |
62
|
|
|
protected function generateNonUniqueSlug(): string |
63
|
|
|
{ |
64
|
|
|
if ($this->hasCustomSlugBeenUsed()) { |
65
|
|
|
$slugField = $this->slugOptions->slugCustomField; |
66
|
|
|
if (!$this->$slugField) { |
67
|
|
|
return ''; |
68
|
|
|
} |
69
|
|
|
return !$this->slugOptions->slugifyCustomSlug ? $this->$slugField : Str::slug($this->$slugField, $this->slugOptions->separator); |
70
|
|
|
} |
71
|
|
|
if ($this->hasSlugBeenUsed()) { |
72
|
|
|
$slugField = $this->slugOptions->slugField; |
73
|
|
|
return $this->$slugField ?? ''; |
74
|
|
|
} |
75
|
|
|
$generatedSlug = $this->getSlugSourceString(); |
76
|
|
|
if (!$this->slugOptions->slugifySlugSourceString) { |
77
|
|
|
return $generatedSlug; |
78
|
|
|
} |
79
|
|
|
return Str::slug($generatedSlug, $this->slugOptions->separator); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Determine if a custom slug has been saved. |
84
|
|
|
* @return bool |
85
|
|
|
*/ |
86
|
|
|
protected function hasCustomSlugBeenUsed(): bool |
87
|
|
|
{ |
88
|
|
|
$slugField = $this->slugOptions->slugCustomField; |
89
|
|
View Code Duplication |
if (!$slugField || trim($slugField) == '' || !$this->$slugField || trim($this->$slugField) == '') { |
|
|
|
|
90
|
|
|
return false; |
91
|
|
|
} |
92
|
|
|
return true; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Determine if a custom slug has been saved. |
97
|
|
|
* @return bool |
98
|
|
|
*/ |
99
|
|
|
protected function hasSlugBeenUsed(): bool |
100
|
|
|
{ |
101
|
|
|
$slugField = $this->slugOptions->slugField; |
102
|
|
|
|
103
|
|
View Code Duplication |
if (!$slugField || trim($slugField) == '' || !$this->$slugField || trim($this->$slugField) == '') { |
|
|
|
|
104
|
|
|
return false; |
105
|
|
|
} |
106
|
|
|
return $this->getOriginal($slugField) != $this->$slugField; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Get the string that should be used as base for the slug. |
111
|
|
|
* @return string |
112
|
|
|
* @throws InvalidOption |
113
|
|
|
*/ |
114
|
|
|
protected function getSlugSourceString(): string |
115
|
|
|
{ |
116
|
|
|
if (is_callable($this->slugOptions->generateSlugFrom)) { |
117
|
|
|
$slugSourceString = call_user_func($this->slugOptions->generateSlugFrom, $this); |
118
|
|
|
return substr($slugSourceString, 0, $this->slugOptions->maximumLength); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
$slugFrom = $this->getSlugFrom($this->slugOptions->generateSlugFrom); |
122
|
|
|
|
123
|
|
|
if (is_null($slugFrom) || (!is_array($slugFrom) && trim($slugFrom) == '')) { |
124
|
|
|
if (!$this->slugOptions->generateSlugIfAllSourceFieldsEmpty) { |
125
|
|
|
throw InvalidOption::missingFromField(); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
return Str::random($this->slugOptions->maximumLength > $this->slugOptions->randomUrlLen ? $this->slugOptions->randomUrlLen : $this->slugOptions->maximumLength); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
$slugSourceString = $this->getImplodeSourceString($slugFrom, $this->slugOptions->separator); |
132
|
|
|
|
133
|
|
|
return substr($slugSourceString, 0, $this->slugOptions->maximumLength); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Get the correct field(s) from to generate slug |
138
|
|
|
* @param string|array|callable $fieldName |
139
|
|
|
* @return string|array |
140
|
|
|
*/ |
141
|
|
|
protected function getSlugFrom($fieldName) |
142
|
|
|
{ |
143
|
|
|
if (!is_callable($fieldName) && !is_array($fieldName) && trim($fieldName) == '') { |
144
|
|
|
return ''; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
if (!is_callable($fieldName) && !is_array($fieldName) && (!data_get($this, $fieldName))) { |
148
|
|
|
return ''; |
149
|
|
|
} elseif (!is_array($fieldName)) { |
150
|
|
|
return $fieldName; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
$slugSourceString = ''; |
154
|
|
|
$countFieldName = count($fieldName); |
155
|
|
|
for ($i = 0; $i < $countFieldName; $i++) { |
156
|
|
|
|
157
|
|
|
$currFieldName = $fieldName[$i]; |
158
|
|
|
if (!is_array($currFieldName) && trim($currFieldName) == '') { |
159
|
|
|
continue; |
160
|
|
|
} |
161
|
|
|
if (!is_array($currFieldName) && (!data_get($this, $currFieldName))) { |
162
|
|
|
continue; |
163
|
|
|
} |
164
|
|
|
if (!is_array($currFieldName) && data_get($this, $currFieldName)) { |
165
|
|
|
$slugSourceString = $currFieldName; |
166
|
|
|
break; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$slugSourceString = $this->getImplodeSourceString($currFieldName, ''); |
170
|
|
|
|
171
|
|
|
if ($slugSourceString != '') { |
172
|
|
|
$slugSourceString = $currFieldName; |
173
|
|
|
break; |
174
|
|
|
} |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
return $slugSourceString; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Make the given slug unique. |
182
|
|
|
* @param string $slug |
183
|
|
|
* @return string |
184
|
|
|
*/ |
185
|
|
|
protected function makeSlugUnique(string $slug): string |
186
|
|
|
{ |
187
|
|
|
$originalSlug = $slug; |
188
|
|
|
$i = 1; |
189
|
|
|
|
190
|
|
|
while ($this->otherRecordExistsWithSlug($slug) || $slug === '') { |
191
|
|
|
$slug = $originalSlug . $this->slugOptions->separator . $i++; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
return $slug; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Determine if a record exists with the given slug. |
199
|
|
|
* @param string $slug |
200
|
|
|
* @return bool |
201
|
|
|
*/ |
202
|
|
|
protected function otherRecordExistsWithSlug(string $slug): bool |
203
|
|
|
{ |
204
|
|
|
return (bool)static::where($this->slugOptions->slugField, $slug) |
205
|
|
|
->where($this->getKeyName(), '!=', $this->getKey() ?? '0') |
206
|
|
|
->first(); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* This function will throw an exception when any of the options is missing or invalid. |
211
|
|
|
* @throws InvalidOption |
212
|
|
|
*/ |
213
|
|
|
protected function guardAgainstInvalidSlugOptions() |
214
|
|
|
{ |
215
|
|
|
if (is_array($this->slugOptions->generateSlugFrom) && count($this->slugOptions->generateSlugFrom) < 1) { |
216
|
|
|
throw InvalidOption::missingFromField(); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
if (!strlen($this->slugOptions->slugField)) { |
220
|
|
|
throw InvalidOption::missingSlugField(); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
if ($this->slugOptions->maximumLength <= 0) { |
224
|
|
|
throw InvalidOption::invalidMaximumLength(); |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* @param $slugFrom |
230
|
|
|
* @param string $separator |
231
|
|
|
* @return string |
232
|
|
|
*/ |
233
|
|
|
protected function getImplodeSourceString($slugFrom, string $separator): string |
234
|
|
|
{ |
235
|
|
|
$slugSourceString = collect($slugFrom) |
236
|
|
|
->map(function (string $fieldName): string { |
237
|
|
|
if ($fieldName == '') { |
238
|
|
|
return ''; |
239
|
|
|
} |
240
|
|
|
return data_get($this, $fieldName) ?? ''; |
241
|
|
|
}) |
242
|
|
|
->implode($separator); |
243
|
|
|
return $slugSourceString; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* |
248
|
|
|
* SCOPE HELPERS |
249
|
|
|
* |
250
|
|
|
*/ |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Query scope for finding a model by its slug field. |
254
|
|
|
* |
255
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $scope |
256
|
|
|
* @param string $slug |
257
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
258
|
|
|
*/ |
259
|
|
|
public function scopeWhereSlug(\Illuminate\Database\Eloquent\Builder $scope, $slug) |
260
|
|
|
{ |
261
|
|
|
$this->slugOptions = $this->getSlugOptionsOrDefault(); |
262
|
|
|
return $scope->where($this->slugOptions->slugField, $slug); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Find a model by its slug field. |
267
|
|
|
* |
268
|
|
|
* @param string $slug |
269
|
|
|
* @param array $columns |
270
|
|
|
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|static[]|static|null |
271
|
|
|
*/ |
272
|
|
|
public static function findBySlug($slug, array $columns = ['*']) |
273
|
|
|
{ |
274
|
|
|
return static::whereSlug($slug)->first($columns); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Find a model by its slug field or throw an exception. |
279
|
|
|
* |
280
|
|
|
* @param string $slug |
281
|
|
|
* @param array $columns |
282
|
|
|
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection |
283
|
|
|
* |
284
|
|
|
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException |
285
|
|
|
*/ |
286
|
|
|
public static function findBySlugOrFail($slug, array $columns = ['*']) |
287
|
|
|
{ |
288
|
|
|
return static::whereSlug($slug)->firstOrFail($columns); |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
|
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.