Skip to content

Commit dd4df97

Browse files
committed
Adds validation rule
1 parent ec6ce25 commit dd4df97

7 files changed

Lines changed: 382 additions & 43 deletions

File tree

src/Illuminate/Database/Eloquent/Attributes/Sluggable.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public function __construct(
2222
public bool $unique = true,
2323
public int $maxAttempts = 100,
2424
public ?int $maxLength = null,
25+
public ?string $errorKey = null,
26+
public ?string $errorMessage = null,
2527
) {
2628
}
2729
}

src/Illuminate/Database/Eloquent/CouldNotGenerateSlugException.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,29 @@
22

33
namespace Illuminate\Database\Eloquent;
44

5+
use Illuminate\Validation\ValidationException;
56
use RuntimeException;
67

78
class CouldNotGenerateSlugException extends RuntimeException
89
{
9-
//
10+
/**
11+
* Create a new exception instance.
12+
*/
13+
public function __construct(
14+
string $message,
15+
protected string $errorKey,
16+
protected string $errorMessage,
17+
) {
18+
parent::__construct($message);
19+
}
20+
21+
/**
22+
* Get the exception's context for the handler.
23+
*/
24+
public function getInnerException(): ValidationException
25+
{
26+
return ValidationException::withMessages([
27+
$this->errorKey => $this->errorMessage,
28+
]);
29+
}
1030
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent;
4+
5+
use Illuminate\Contracts\Debug\ShouldntReport;
6+
7+
class EmptySlugException extends CouldNotGenerateSlugException implements ShouldntReport
8+
{
9+
//
10+
}

src/Illuminate/Database/Eloquent/SlugGenerator.php

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,45 @@ public function generate(): string
5656
$slug = $this->slugify($this->resolveSourceValue());
5757

5858
if ($slug === '') {
59-
$columns = implode(', ', Arr::wrap($this->options()->from));
60-
61-
throw new CouldNotGenerateSlugException(
62-
'Could not generate a slug for ['.get_class($this->model)."] using column(s) [{$columns}]."
63-
);
59+
$this->throwEmptySlugException();
6460
}
6561

6662
return $this->ensureUnique($slug);
6763
}
6864

65+
/**
66+
* Throw an exception when the slug source produces an empty slug.
67+
*
68+
* @throws EmptySlugException
69+
*/
70+
protected function throwEmptySlugException(): void
71+
{
72+
$options = $this->options();
73+
$from = Arr::wrap($options->from);
74+
$errorKey = $options->errorKey ?? $from[0];
75+
$columns = implode(', ', $from);
76+
77+
throw new EmptySlugException(
78+
"Could not generate a slug for [".get_class($this->model)."] using column(s) [{$columns}].",
79+
$errorKey,
80+
$this->resolveErrorMessage($errorKey, $options),
81+
);
82+
}
83+
84+
/**
85+
* Resolve the user-facing error message for a failed slug generation.
86+
*/
87+
protected function resolveErrorMessage(string $errorKey, Sluggable $options): string
88+
{
89+
$from = Arr::wrap($options->from);
90+
$attribute = count($from) === 1 ? $from[0] : implode(' and ', [implode(', ', array_slice($from, 0, -1)), end($from)]);
91+
$replacements = ['attribute' => $attribute, 'column' => $options->column];
92+
93+
return $options->errorMessage
94+
? __($options->errorMessage, $replacements)
95+
: __('validation.sluggable', $replacements);
96+
}
97+
6998
/**
7099
* Determine if the slug source columns have changed.
71100
*/
@@ -153,8 +182,12 @@ protected function ensureUnique(string $slug): string
153182
$count++;
154183

155184
if ($count > $options->maxAttempts) {
185+
$errorKey = $options->errorKey ?? $options->column;
186+
156187
throw new CouldNotGenerateSlugException(
157-
'Could not generate a unique slug for ['.get_class($this->model)."] with base [{$originalSlug}] after {$options->maxAttempts} attempts."
188+
'Could not generate a unique slug for ['.get_class($this->model)."] with base [{$originalSlug}] after {$options->maxAttempts} attempts.",
189+
$errorKey,
190+
$this->resolveErrorMessage($errorKey, $options),
158191
);
159192
}
160193

src/Illuminate/Translation/lang/en/validation.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
'numeric' => 'The :attribute field must be :size.',
158158
'string' => 'The :attribute field must be :size characters.',
159159
],
160+
'sluggable' => 'The :attribute must be able to generate a valid :column.',
160161
'starts_with' => 'The :attribute field must start with one of the following: :values.',
161162
'string' => 'The :attribute field must be a string.',
162163
'timezone' => 'The :attribute field must be a valid timezone.',

tests/Database/DatabaseEloquentSluggableTest.php

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use Illuminate\Database\Capsule\Manager as DB;
66
use Illuminate\Database\Eloquent\Attributes\Sluggable;
7-
use Illuminate\Database\Eloquent\CouldNotGenerateSlugException;
7+
88
use Illuminate\Database\Eloquent\Model;
99
use Illuminate\Database\Eloquent\SoftDeletes;
1010
use Illuminate\Events\Dispatcher;
@@ -305,16 +305,6 @@ public function test_it_includes_soft_deleted_records_in_uniqueness_check()
305305

306306
// maxAttempts
307307

308-
public function test_it_throws_after_maximum_attempts_exceeded()
309-
{
310-
$this->expectException(CouldNotGenerateSlugException::class);
311-
$this->expectExceptionMessage('Could not generate a unique slug for [Illuminate\Tests\Database\SluggableMaxAttemptsPost] with base [hello] after 2 attempts.');
312-
313-
SluggableMaxAttemptsPost::create(['name' => 'Hello']);
314-
SluggableMaxAttemptsPost::create(['name' => 'Hello']);
315-
SluggableMaxAttemptsPost::create(['name' => 'Hello']);
316-
}
317-
318308
// maxLength
319309

320310
public function test_it_respects_maximum_length()
@@ -528,31 +518,6 @@ public function test_slug_generation(string $input, string $expected)
528518
$this->assertSame($expected, $post->slug);
529519
}
530520

531-
// edge cases
532-
533-
public function test_it_throws_when_source_produces_empty_slug()
534-
{
535-
$this->expectException(CouldNotGenerateSlugException::class);
536-
$this->expectExceptionMessage('Could not generate a slug for [Illuminate\Tests\Database\SluggablePost] using column(s) [name].');
537-
538-
SluggablePost::create(['name' => '!!!']);
539-
}
540-
541-
public function test_it_throws_when_emoji_only_source_produces_empty_slug()
542-
{
543-
$this->expectException(CouldNotGenerateSlugException::class);
544-
$this->expectExceptionMessage('Could not generate a slug for [Illuminate\Tests\Database\SluggablePost] using column(s) [name].');
545-
546-
SluggablePost::create(['name' => '🚀🎯🔥']);
547-
}
548-
549-
public function test_it_throws_when_source_column_is_null()
550-
{
551-
$this->expectException(CouldNotGenerateSlugException::class);
552-
$this->expectExceptionMessage('Could not generate a slug for [Illuminate\Tests\Database\SluggablePost] using column(s) [name].');
553-
554-
SluggablePost::create([]);
555-
}
556521
}
557522

558523
#[Sluggable]
@@ -660,3 +625,11 @@ class SluggableCustomColumnsPost extends Model
660625

661626
protected $guarded = [];
662627
}
628+
629+
#[Sluggable(errorKey: 'custom_field', errorMessage: 'Please enter a valid name.')]
630+
class SluggableCustomErrorPost extends Model
631+
{
632+
protected $table = 'sluggable_posts';
633+
634+
protected $guarded = [];
635+
}

0 commit comments

Comments
 (0)