From 4454703d35ee51e1fa4b3a6e2ba64fab48bf2c2e Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 25 Mar 2024 14:01:23 -0400 Subject: [PATCH 01/26] Add handling for null/true/false in custom class based rule strings in builder. --- src/Fields/ClassRuleParser.php | 6 ++++++ tests/Fields/ClassRuleParserTest.php | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Fields/ClassRuleParser.php b/src/Fields/ClassRuleParser.php index b952369fdd1..5c128f81fd4 100644 --- a/src/Fields/ClassRuleParser.php +++ b/src/Fields/ClassRuleParser.php @@ -34,6 +34,12 @@ public function parse(string $rule): array return [$key => (string) Str::of($arg)->trim('"')->replace('\\"', '"')]; } elseif (Str::startsWith($arg, "'") && Str::endsWith($arg, "'")) { return [$key => (string) Str::of($arg)->trim("'")->replace("\\'", "'")]; + } elseif ($arg === 'null') { + return [$key => null]; + } elseif ($arg === 'true') { + return [$key => true]; + } elseif ($arg === 'false') { + return [$key => false]; } return [$key => $arg]; diff --git a/tests/Fields/ClassRuleParserTest.php b/tests/Fields/ClassRuleParserTest.php index 18d97578cc2..410ae25a0ce 100644 --- a/tests/Fields/ClassRuleParserTest.php +++ b/tests/Fields/ClassRuleParserTest.php @@ -66,6 +66,18 @@ public static function classRuleProvider() 'new App\MyRule(a: "foo:bar")', ['App\MyRule', ['a' => 'foo:bar']], ], + 'null argument' => [ + 'new App\MyRule(null)', + ['App\MyRule', [null]], + ], + 'true boolean argument' => [ + 'new App\MyRule(true)', + ['App\MyRule', [true]], + ], + 'false boolean argument' => [ + 'new App\MyRule(false)', + ['App\MyRule', [false]], + ], ]; } } From 418e291b5fde14ae0e601aba991a768e43fc1161 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 25 Mar 2024 14:03:18 -0400 Subject: [PATCH 02/26] Improve handling for class based rule string replacements. --- src/Fields/Validator.php | 42 ++++++++++++++++++++++++++-------- tests/Fields/ValidatorTest.php | 42 +++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/Fields/Validator.php b/src/Fields/Validator.php index 91ce5157dfe..2e1a46b459d 100644 --- a/src/Fields/Validator.php +++ b/src/Fields/Validator.php @@ -125,19 +125,43 @@ public function attributes() private function parse($rule) { if (is_string($rule) && Str::startsWith($rule, 'new ')) { - [$class, $arguments] = (new ClassRuleParser)->parse($rule); - - return new $class(...$arguments); + return $this->parseClassBasedRule($rule); } - if (! is_string($rule) || - ! Str::contains($rule, '{') || - Str::startsWith($rule, 'regex:') || - Str::startsWith($rule, 'not_regex:') - ) { - return $rule; + if (is_string($rule) && Str::contains($rule, '{') && ! Str::startsWith($rule, ['regex:', 'not_regex:'])) { + return $this->parseStringBasedRule($rule); } + return $rule; + } + + private function parseClassBasedRule($rule) + { + $rule = preg_replace_callback('/{\s*([a-zA-Z0-9_\-]+)\s*}/', function ($match) { + $value = Arr::get($this->replacements, $match[1]); + + if ($value === null) { + return 'null'; + } + + if ($value === true) { + return 'true'; + } + + if ($value === false) { + return 'false'; + } + + return is_string($value) ? "'{$value}'" : $value; + }, $rule); + + [$class, $arguments] = (new ClassRuleParser)->parse($rule); + + return new $class(...$arguments); + } + + private function parseStringBasedRule($rule) + { $rule = str_replace('{this}.', $this->context['prefix'] ?? '', $rule); return preg_replace_callback('/{\s*([a-zA-Z0-9_\-]+)\s*}/', function ($match) { diff --git a/tests/Fields/ValidatorTest.php b/tests/Fields/ValidatorTest.php index 3eb0684a15d..a261269679c 100644 --- a/tests/Fields/ValidatorTest.php +++ b/tests/Fields/ValidatorTest.php @@ -151,7 +151,7 @@ public function it_compiles_field_attributes() } /** @test */ - public function it_makes_replacements() + public function it_makes_string_based_replacements() { $field = Mockery::mock(Field::class); $field->shouldReceive('setValidationContext')->with([])->andReturnSelf(); @@ -179,6 +179,38 @@ public function it_makes_replacements() ], $validation->rules()); } + /** @test */ + public function it_makes_class_based_replacements() + { + $field = Mockery::mock(Field::class); + $field->shouldReceive('setValidationContext')->with([])->andReturnSelf(); + $field->shouldReceive('rules')->andReturn([ + 'one' => ['new Tests\\Fields\\FakeRule({string}, {zero}, {num}, {true}, {false}, {null})'], + ]); + + $fields = Mockery::mock(Fields::class); + $fields->shouldReceive('all')->andReturn(collect([$field])); + $fields->shouldReceive('preProcessValidatables')->andReturnSelf(); + + $validation = (new Validator)->fields($fields)->withReplacements([ + 'string' => 'FOO', + 'zero' => 0, + 'num' => 7, + 'true' => true, + 'false' => false, + 'null' => null, + ]); + + $rule = $validation->rules()['one'][0]; + + $this->assertSame('FOO', $rule->string); + $this->assertSame(0, $rule->zero); + $this->assertSame(7, $rule->num); + $this->assertSame(true, $rule->true); + $this->assertSame(false, $rule->false); + $this->assertSame(null, $rule->null); + } + /** @test */ public function it_does_not_make_replacements_in_regex_rules() { @@ -459,3 +491,11 @@ public function it_discards_this_at_top_level() ], $rules); } } + +class FakeRule +{ + public function __construct(public $string, public $zero, public $num, public $true, public $false, public $null) + { + // + } +} From c4f61870db3c29a24725dca9f8bcb08a57ae1119 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 25 Mar 2024 15:36:22 -0400 Subject: [PATCH 03/26] Move and update `UniqueEntryValue` class to `Statamic\Rules` w/ Laravel conventions. --- src/Rules/UniqueEntryValue.php | 45 +++++++++++++++++++++++++++++ src/Validation/UniqueEntryValue.php | 33 --------------------- 2 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 src/Rules/UniqueEntryValue.php delete mode 100644 src/Validation/UniqueEntryValue.php diff --git a/src/Rules/UniqueEntryValue.php b/src/Rules/UniqueEntryValue.php new file mode 100644 index 00000000000..b3f7dfd387a --- /dev/null +++ b/src/Rules/UniqueEntryValue.php @@ -0,0 +1,45 @@ +collection) { + $query->where('collection', $this->collection); + } + + if ($this->site) { + $query->where('site', $this->site); + } + + $existing = $query + ->where($attribute, $value) + ->first(); + + if (! $existing) { + return; + } + + if ($this->except == $existing->id()) { + return; + } + + $fail('statamic::validation.unique_entry_value')->translate(); + } +} diff --git a/src/Validation/UniqueEntryValue.php b/src/Validation/UniqueEntryValue.php deleted file mode 100644 index ae347d599b2..00000000000 --- a/src/Validation/UniqueEntryValue.php +++ /dev/null @@ -1,33 +0,0 @@ -where('collection', $collection); - } - - if ($site) { - $query->where('site', $site); - } - - $existing = $query - ->where($attribute, $value) - ->first(); - - if (! $existing) { - return true; - } - - return $except == $existing->id(); - } -} From 786866138bb2afc5ade125abf0124c584deda662 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 25 Mar 2024 15:36:49 -0400 Subject: [PATCH 04/26] Remove `unique_entry_value` string based implementation. --- src/Providers/ValidationServiceProvider.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Providers/ValidationServiceProvider.php b/src/Providers/ValidationServiceProvider.php index 6f7d9fb4303..edb4f699805 100644 --- a/src/Providers/ValidationServiceProvider.php +++ b/src/Providers/ValidationServiceProvider.php @@ -5,7 +5,6 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\ServiceProvider; use Statamic\Validation\CodeFieldtypeRulers; -use Statamic\Validation\UniqueEntryValue; use Statamic\Validation\UniqueFormHandle; use Statamic\Validation\UniqueTermValue; use Statamic\Validation\UniqueUserValue; @@ -13,7 +12,6 @@ class ValidationServiceProvider extends ServiceProvider { protected $rules = [ - 'unique_entry_value' => UniqueEntryValue::class, 'unique_term_value' => UniqueTermValue::class, 'unique_user_value' => UniqueUserValue::class, 'unique_form_handle' => UniqueFormHandle::class, From 98c3d57b33e290be1aef5aceb5bea36353aea7dc Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 25 Mar 2024 15:37:27 -0400 Subject: [PATCH 05/26] Ensure conditions builder uses new class based implementation. --- resources/js/components/field-validation/Rules.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/field-validation/Rules.js b/resources/js/components/field-validation/Rules.js index 65274d8eb38..8a5d68058d2 100644 --- a/resources/js/components/field-validation/Rules.js +++ b/resources/js/components/field-validation/Rules.js @@ -313,7 +313,7 @@ export default [ // }, { label: 'Unique Entry Value', - value: 'unique_entry_value:{collection},{id},{site}', + value: 'new \\Statamic\\Rules\\UniqueEntryValue({collection}, {id}, {site})', }, { label: 'URL', From 71e474d71079ec839dd0a3bd4cf03eff3a48b4b6 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 25 Mar 2024 15:49:11 -0400 Subject: [PATCH 06/26] =?UTF-8?q?In=20case=20they=E2=80=99re=20updating=20?= =?UTF-8?q?from=20older=20version,=20update=20script=20still=20runs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/UpdateScripts/AddUniqueSlugValidation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UpdateScripts/AddUniqueSlugValidation.php b/src/UpdateScripts/AddUniqueSlugValidation.php index c8e32092c99..b501bd3900e 100644 --- a/src/UpdateScripts/AddUniqueSlugValidation.php +++ b/src/UpdateScripts/AddUniqueSlugValidation.php @@ -38,7 +38,7 @@ private function updateBlueprint($collection, $blueprint) // be configured as a pipe delimited string. $rules = Validator::explodeRules($rules); - $rules[] = 'unique_entry_value:{collection},{id},{site}'; + $rules[] = 'new \\Statamic\\Rules\\UniqueEntryValue({collection}, {id}, {site})'; $rules = array_unique($rules); $blueprint->ensureFieldHasConfig('slug', ['validate' => $rules]); From 3c7cd9b22c1dc87fc3bec2a29294d8f0e7a8bce6 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 25 Mar 2024 16:09:24 -0400 Subject: [PATCH 07/26] Update tests. --- tests/Validation/UniqueEntryValueTest.php | 17 +++++++++-------- tests/__fixtures__/blueprints/article.yaml | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/Validation/UniqueEntryValueTest.php b/tests/Validation/UniqueEntryValueTest.php index 755b91ad911..5bb6540eb42 100644 --- a/tests/Validation/UniqueEntryValueTest.php +++ b/tests/Validation/UniqueEntryValueTest.php @@ -2,6 +2,7 @@ use Facades\Tests\Factories\EntryFactory; use Illuminate\Support\Facades\Validator; +use Statamic\Rules\UniqueEntryValue; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -17,12 +18,12 @@ public function it_fails_when_theres_a_duplicate_entry_entry_value_in_across_all $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_entry_value'] + ['slug' => new UniqueEntryValue] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'baz'], - ['slug' => 'unique_entry_value'] + ['slug' => new UniqueEntryValue] )->passes()); } @@ -34,12 +35,12 @@ public function it_fails_when_theres_a_duplicate_entry_entry_value_in_a_specific $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_entry_value:collection-one'] + ['slug' => new UniqueEntryValue('collection-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'bar'], - ['slug' => 'unique_entry_value:collection-one'] + ['slug' => new UniqueEntryValue('collection-one')] )->passes()); } @@ -50,12 +51,12 @@ public function it_passes_duplicate_slug_validation_when_updating_in_a_single_co $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_entry_value:collection-one,123'] + ['slug' => new UniqueEntryValue('collection-one', 123)] )->passes()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_entry_value:collection-one,456'] + ['slug' => new UniqueEntryValue('collection-one', 456)] )->fails()); } @@ -71,12 +72,12 @@ public function it_passes_when_theres_a_duplicate_entry_value_in_a_different_sit $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_entry_value:collection-one,null,site-one'] + ['slug' => new UniqueEntryValue('collection-one', null, 'site-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_entry_value:collection-one,null,site-two'] + ['slug' => new UniqueEntryValue('collection-one', null, 'site-two')] )->passes()); } } diff --git a/tests/__fixtures__/blueprints/article.yaml b/tests/__fixtures__/blueprints/article.yaml index aa959c57dcb..db149a65b03 100644 --- a/tests/__fixtures__/blueprints/article.yaml +++ b/tests/__fixtures__/blueprints/article.yaml @@ -888,7 +888,7 @@ tabs: localizable: true validate: - required - - 'unique_entry_value:{collection},{id},{site}' + - 'new \Statamic\Rules\UniqueEntryValue({collection}, {id}, {site})' - handle: date field: From b86212cb810696e18f7e48a23cde786c0ba696c0 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 26 Mar 2024 14:47:45 -0400 Subject: [PATCH 08/26] Move and update `AllowedFile` class to `Statamic\Rules` w/ Laravel conventions. --- .../CP/Assets/AssetsController.php | 2 +- .../Fieldtypes/FilesFieldtypeController.php | 2 +- src/Http/Requests/FrontendFormRequest.php | 2 +- src/{Validation => Rules}/AllowedFile.php | 19 ++++++++++--------- 4 files changed, 13 insertions(+), 12 deletions(-) rename src/{Validation => Rules}/AllowedFile.php (75%) diff --git a/src/Http/Controllers/CP/Assets/AssetsController.php b/src/Http/Controllers/CP/Assets/AssetsController.php index 733a59c8744..1a76894d299 100644 --- a/src/Http/Controllers/CP/Assets/AssetsController.php +++ b/src/Http/Controllers/CP/Assets/AssetsController.php @@ -11,7 +11,7 @@ use Statamic\Facades\User; use Statamic\Http\Controllers\CP\CpController; use Statamic\Http\Resources\CP\Assets\Asset as AssetResource; -use Statamic\Validation\AllowedFile; +use Statamic\Rules\AllowedFile; class AssetsController extends CpController { diff --git a/src/Http/Controllers/CP/Fieldtypes/FilesFieldtypeController.php b/src/Http/Controllers/CP/Fieldtypes/FilesFieldtypeController.php index e34e9cbc1bf..4b768348f77 100644 --- a/src/Http/Controllers/CP/Fieldtypes/FilesFieldtypeController.php +++ b/src/Http/Controllers/CP/Fieldtypes/FilesFieldtypeController.php @@ -5,7 +5,7 @@ use Illuminate\Http\Request; use Statamic\Assets\FileUploader as Uploader; use Statamic\Http\Controllers\CP\CpController; -use Statamic\Validation\AllowedFile; +use Statamic\Rules\AllowedFile; class FilesFieldtypeController extends CpController { diff --git a/src/Http/Requests/FrontendFormRequest.php b/src/Http/Requests/FrontendFormRequest.php index 728b398780d..a9f2b21558b 100644 --- a/src/Http/Requests/FrontendFormRequest.php +++ b/src/Http/Requests/FrontendFormRequest.php @@ -8,8 +8,8 @@ use Illuminate\Support\Traits\Localizable; use Illuminate\Validation\ValidationException; use Statamic\Facades\Site; +use Statamic\Rules\AllowedFile; use Statamic\Support\Arr; -use Statamic\Validation\AllowedFile; class FrontendFormRequest extends FormRequest { diff --git a/src/Validation/AllowedFile.php b/src/Rules/AllowedFile.php similarity index 75% rename from src/Validation/AllowedFile.php rename to src/Rules/AllowedFile.php index 5621466bfaf..e968eeece45 100644 --- a/src/Validation/AllowedFile.php +++ b/src/Rules/AllowedFile.php @@ -1,13 +1,14 @@ isAllowed($value)) { - $fail(__('validation.uploaded')); + if (! $this->isAllowedExtension($value)) { + $fail('statamic::validation.uploaded')->translate(); } } - private function isAllowed(UploadedFile $file): bool + private function isAllowedExtension(UploadedFile $file): bool { - $extensions = array_merge($this->extensions, config('statamic.assets.additional_uploadable_extensions', [])); + $extensions = array_merge(static::EXTENSIONS, config('statamic.assets.additional_uploadable_extensions', [])); return in_array(trim(strtolower($file->getClientOriginalExtension())), $extensions); } From 2f5889ad80c14110693493c38d5c3436177d04d1 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 26 Mar 2024 15:46:34 -0400 Subject: [PATCH 09/26] Move and update `DateFieldtype` rule class to `Statamic\Rules` w/ Laravel conventions. --- src/Fieldtypes/Date.php | 13 ++-- src/{Validation => Rules}/DateFieldtype.php | 73 ++++++++++++++------- 2 files changed, 56 insertions(+), 30 deletions(-) rename src/{Validation => Rules}/DateFieldtype.php (57%) diff --git a/src/Fieldtypes/Date.php b/src/Fieldtypes/Date.php index dda18bf26fc..c362a4bb589 100644 --- a/src/Fieldtypes/Date.php +++ b/src/Fieldtypes/Date.php @@ -4,16 +4,16 @@ use Carbon\Exceptions\InvalidFormatException; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Validator; use InvalidArgumentException; -use Statamic\Exceptions\ValidationException; use Statamic\Facades\GraphQL; use Statamic\Fields\Fieldtype; use Statamic\GraphQL\Fields\DateField; use Statamic\GraphQL\Types\DateRangeType; use Statamic\Query\Scopes\Filters\Fields\Date as DateFilter; +use Statamic\Rules\DateFieldtype as ValidationRule; use Statamic\Statamic; use Statamic\Support\DateFormat; -use Statamic\Validation\DateFieldtype as ValidationRule; class Date extends Fieldtype { @@ -366,11 +366,10 @@ public function secondsEnabled() public function preProcessValidatable($value) { - if ($error = (new ValidationRule($this))($value)) { - throw ValidationException::withMessages([ - $this->field->handle() => $error, - ]); - } + Validator::make( + [$this->field->handle() => $value], + [$this->field->handle() => [new ValidationRule($this)]], + )->validate(); if ($value === null) { return null; diff --git a/src/Validation/DateFieldtype.php b/src/Rules/DateFieldtype.php similarity index 57% rename from src/Validation/DateFieldtype.php rename to src/Rules/DateFieldtype.php index 9ef52b6f115..99217b94861 100644 --- a/src/Validation/DateFieldtype.php +++ b/src/Rules/DateFieldtype.php @@ -1,12 +1,14 @@ fieldtype = $fieldtype; } - public function __invoke($value) + public function validate(string $attribute, mixed $value, Closure $fail): void { if (is_null($value) || $value instanceof Carbon) { return; } if (! is_array($value)) { - return __('statamic::validation.array'); + $fail('statamic::validation.array')->translate(); + + return; } if ($this->fieldtype->config('mode') === 'single') { if (! Arr::has($value, 'date')) { - return $this->fail('date_required'); + $fail('statamic::validation.date_fieldtype_date_required')->translate(); + + return; } $date = $value['date']; if ($this->fieldtype->isRequired() && ! $date) { - return $this->fail('date_required'); + $fail('statamic::validation.date_fieldtype_date_required')->translate(); + + return; } if ($date && ! $this->validDateFormat($date)) { - return __('statamic::validation.date'); + $fail('statamic::validation.date')->translate(); + + return; } } @@ -50,7 +60,9 @@ public function __invoke($value) $date = $value['date']; if (! $date && $this->fieldtype->isRequired()) { - return $this->fail('date_required'); + $fail('statamic::validation.date_fieldtype_date_required')->translate(); + + return; } if (! $date) { @@ -58,30 +70,44 @@ public function __invoke($value) } if (! Arr::has($date, 'start')) { - return $this->fail('start_date_required'); + $fail('statamic::validation.date_fieldtype_start_date_required')->translate(); + + return; } if (! Arr::has($date, 'end')) { - return $this->fail('end_date_required'); + $fail('statamic::validation.date_fieldtype_end_date_required')->translate(); + + return; } if ($this->fieldtype->isRequired() && ! $date['start'] && ! $date['end']) { - return $this->fail('date_required'); + $fail('statamic::validation.date_fieldtype_date_required')->translate(); + + return; } if (! $date['start'] && $date['end']) { - return $this->fail('start_date_required'); + $fail('statamic::validation.date_fieldtype_start_date_required')->translate(); + + return; } if (! $date['end'] && $date['start']) { - return $this->fail('end_date_required'); + $fail('statamic::validation.date_fieldtype_end_date_required')->translate(); + + return; } if ($date['start'] && ! $this->validDateFormat($date['start'])) { - return $this->fail('start_date_invalid'); + $fail('statamic::validation.date_fieldtype_start_date_invalid')->translate(); + + return; } if ($date['end'] && ! $this->validDateFormat($date['end'])) { - return $this->fail('end_date_invalid'); + $fail('statamic::validation.date_fieldtype_end_date_invalid')->translate(); + + return; } } @@ -90,17 +116,23 @@ public function __invoke($value) } if (! Arr::has($value, 'time')) { - return $this->fail('time_required'); + $fail('statamic::validation.date_fieldtype_time_required')->translate(); + + return; } $time = $value['time']; if ($this->fieldtype->isRequired() && ! $time) { - return $this->fail('time_required'); + $fail('statamic::validation.date_fieldtype_time_required')->translate(); + + return; } if ($time && ! $this->validTimeFormat($time)) { - return __('statamic::validation.time'); + $fail('statamic::validation.time')->translate(); + + return; } } @@ -131,9 +163,4 @@ private function timeEnabled() { return $this->fieldtype->config('time_enabled'); } - - private function fail($message) - { - return __('statamic::validation.date_fieldtype_'.$message); - } } From 760cc431eb1b15a43dff391edcc3d77c815c51c4 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 26 Mar 2024 16:01:23 -0400 Subject: [PATCH 10/26] Move and update `TimeFieldtype` rule class to `Statamic\Rules` w/ Laravel conventions. --- src/Fieldtypes/Time.php | 2 +- src/{Validation => Rules}/TimeFieldtype.php | 11 +++++---- tests/Fieldtypes/TimeTest.php | 26 ++++++++++++--------- 3 files changed, 22 insertions(+), 17 deletions(-) rename src/{Validation => Rules}/TimeFieldtype.php (66%) diff --git a/src/Fieldtypes/Time.php b/src/Fieldtypes/Time.php index 0a6535bde3a..aec5bbbb5c2 100644 --- a/src/Fieldtypes/Time.php +++ b/src/Fieldtypes/Time.php @@ -3,7 +3,7 @@ namespace Statamic\Fieldtypes; use Statamic\Fields\Fieldtype; -use Statamic\Validation\TimeFieldtype as ValidationRule; +use Statamic\Rules\TimeFieldtype as ValidationRule; class Time extends Fieldtype { diff --git a/src/Validation/TimeFieldtype.php b/src/Rules/TimeFieldtype.php similarity index 66% rename from src/Validation/TimeFieldtype.php rename to src/Rules/TimeFieldtype.php index a749d082bdd..37918d69e5c 100644 --- a/src/Validation/TimeFieldtype.php +++ b/src/Rules/TimeFieldtype.php @@ -1,11 +1,12 @@ fieldtype = $fieldtype; } - public function __invoke($attribute, $value, $fail) + public function validate(string $attribute, mixed $value, Closure $fail): void { $format = $this->fieldtype->config('seconds_enabled') ? 'H:i:s' : 'H:i'; if (! $this->matchesFormat($value, $format)) { - return $fail('statamic::validation.time')->translate(); + $fail('statamic::validation.time')->translate(); } } diff --git a/tests/Fieldtypes/TimeTest.php b/tests/Fieldtypes/TimeTest.php index 6da0d4067c1..1aefb0bc90a 100644 --- a/tests/Fieldtypes/TimeTest.php +++ b/tests/Fieldtypes/TimeTest.php @@ -62,18 +62,22 @@ public static function processProvider() * * @dataProvider validationProvider */ - public function it_validates($config, $input, $expected) + public function it_validates($config, $input, $passes) { $field = $this->fieldtype($config)->field(); - $messages = []; + $messages = collect(); try { Validator::validate(['test' => $input], $field->rules(), [], $field->validationAttributes()); } catch (ValidationException $e) { - $messages = $e->validator->errors()->all(); + $messages = $e->validator->errors(); } - $this->assertEquals($expected, $messages); + if ($passes) { + $this->assertCount(0, $messages); + } else { + $this->assertEquals(__('statamic::validation.time'), $messages->first()); + } } public static function validationProvider() @@ -82,37 +86,37 @@ public static function validationProvider() 'valid time' => [ [], '14:00', - [], + true, ], 'valid time with seconds' => [ ['seconds_enabled' => true], '14:00:00', - [], + true, ], 'invalid time format' => [ [], 'not formatted like a time', - ['Not a valid time.'], + false, ], '12 hour time' => [ [], '1:00', - ['Not a valid time.'], + false, ], 'invalid hour' => [ [], '25:00', - ['Not a valid time.'], + false, ], 'invalid minute' => [ [], '14:65', - ['Not a valid time.'], + false, ], 'invalid second' => [ ['seconds_enabled' => true], '13:00:60', - ['Not a valid time.'], + false, ], ]; } From 46a2aba6a9e1a737e88d579c640b755edaa67a58 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 26 Mar 2024 16:11:29 -0400 Subject: [PATCH 11/26] Move and update `CodeFieldtypeRulers` rule class to `Statamic\Rules` w/ Laravel conventions. --- src/Fieldtypes/Code.php | 3 ++- src/Providers/ValidationServiceProvider.php | 2 -- src/Rules/CodeFieldtypeRulers.php | 18 ++++++++++++++++++ src/Validation/CodeFieldtypeRulers.php | 17 ----------------- 4 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 src/Rules/CodeFieldtypeRulers.php delete mode 100644 src/Validation/CodeFieldtypeRulers.php diff --git a/src/Fieldtypes/Code.php b/src/Fieldtypes/Code.php index f0ad3b3aca7..4b916685e11 100644 --- a/src/Fieldtypes/Code.php +++ b/src/Fieldtypes/Code.php @@ -6,6 +6,7 @@ use Statamic\Fields\ArrayableString; use Statamic\Fields\Fieldtype; use Statamic\GraphQL\Types\CodeType; +use Statamic\Rules\CodeFieldtypeRulers; class Code extends Fieldtype { @@ -110,7 +111,7 @@ protected function configFieldItems(): array 'key_header' => __('Columns'), 'value_header' => __('Line Style (dashed or solid)'), 'add_button' => __('Add Ruler'), - 'validate' => 'code_fieldtype_rulers', + 'validate' => [new CodeFieldtypeRulers], ], ], ], diff --git a/src/Providers/ValidationServiceProvider.php b/src/Providers/ValidationServiceProvider.php index edb4f699805..5232b31f1f3 100644 --- a/src/Providers/ValidationServiceProvider.php +++ b/src/Providers/ValidationServiceProvider.php @@ -4,7 +4,6 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\ServiceProvider; -use Statamic\Validation\CodeFieldtypeRulers; use Statamic\Validation\UniqueFormHandle; use Statamic\Validation\UniqueTermValue; use Statamic\Validation\UniqueUserValue; @@ -15,7 +14,6 @@ class ValidationServiceProvider extends ServiceProvider 'unique_term_value' => UniqueTermValue::class, 'unique_user_value' => UniqueUserValue::class, 'unique_form_handle' => UniqueFormHandle::class, - 'code_fieldtype_rulers' => CodeFieldtypeRulers::class, ]; public function boot() diff --git a/src/Rules/CodeFieldtypeRulers.php b/src/Rules/CodeFieldtypeRulers.php new file mode 100644 index 00000000000..6d0979110cc --- /dev/null +++ b/src/Rules/CodeFieldtypeRulers.php @@ -0,0 +1,18 @@ + $val) { + if (! is_int($key) || ! in_array($val, ['dashed', 'solid'])) { + $fail('statamic::validation.code_fieldtype_rulers')->translate(); + } + } + } +} diff --git a/src/Validation/CodeFieldtypeRulers.php b/src/Validation/CodeFieldtypeRulers.php deleted file mode 100644 index ab95947c2eb..00000000000 --- a/src/Validation/CodeFieldtypeRulers.php +++ /dev/null @@ -1,17 +0,0 @@ - $val) { - if (! is_int($key) || ! in_array($val, ['dashed', 'solid'])) { - return false; - } - } - - return true; - } -} From 26986516b61232f3533ef7cbb199b1212f14f2ce Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 26 Mar 2024 16:14:57 -0400 Subject: [PATCH 12/26] =?UTF-8?q?Revert=20"In=20case=20they=E2=80=99re=20u?= =?UTF-8?q?pdating=20from=20older=20version,=20update=20script=20still=20r?= =?UTF-8?q?uns."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 71e474d71079ec839dd0a3bd4cf03eff3a48b4b6. --- src/UpdateScripts/AddUniqueSlugValidation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UpdateScripts/AddUniqueSlugValidation.php b/src/UpdateScripts/AddUniqueSlugValidation.php index b501bd3900e..c8e32092c99 100644 --- a/src/UpdateScripts/AddUniqueSlugValidation.php +++ b/src/UpdateScripts/AddUniqueSlugValidation.php @@ -38,7 +38,7 @@ private function updateBlueprint($collection, $blueprint) // be configured as a pipe delimited string. $rules = Validator::explodeRules($rules); - $rules[] = 'new \\Statamic\\Rules\\UniqueEntryValue({collection}, {id}, {site})'; + $rules[] = 'unique_entry_value:{collection},{id},{site}'; $rules = array_unique($rules); $blueprint->ensureFieldHasConfig('slug', ['validate' => $rules]); From 612171624d908c7d4ef315052622cef55ca65165 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 26 Mar 2024 16:37:19 -0400 Subject: [PATCH 13/26] =?UTF-8?q?We=20want=20a=20(constructor=20property)?= =?UTF-8?q?=20promotion=20=E2=9C=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Rules/DateFieldtype.php | 6 ++---- src/Rules/TimeFieldtype.php | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Rules/DateFieldtype.php b/src/Rules/DateFieldtype.php index 99217b94861..caf35cf0c2a 100644 --- a/src/Rules/DateFieldtype.php +++ b/src/Rules/DateFieldtype.php @@ -10,11 +10,9 @@ class DateFieldtype implements ValidationRule { - private $fieldtype; - - public function __construct($fieldtype) + public function __construct(private $fieldtype) { - $this->fieldtype = $fieldtype; + // } public function validate(string $attribute, mixed $value, Closure $fail): void diff --git a/src/Rules/TimeFieldtype.php b/src/Rules/TimeFieldtype.php index 37918d69e5c..131baea1905 100644 --- a/src/Rules/TimeFieldtype.php +++ b/src/Rules/TimeFieldtype.php @@ -8,11 +8,9 @@ class TimeFieldtype implements ValidationRule { - protected $fieldtype; - - public function __construct($fieldtype) + public function __construct(private $fieldtype) { - $this->fieldtype = $fieldtype; + // } public function validate(string $attribute, mixed $value, Closure $fail): void From 92c1ac99a5ebaa1f8c432e9fd045232c900f0434 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 26 Mar 2024 17:27:45 -0400 Subject: [PATCH 14/26] Move and update `UniqueTermValue` rule class to `Statamic\Rules` w/ Laravel conventions. --- .../CP/Taxonomies/TermsController.php | 5 ++- src/Rules/UniqueTermValue.php | 45 +++++++++++++++++++ src/Validation/UniqueTermValue.php | 33 -------------- tests/Validation/UniqueTermValueTest.php | 17 +++---- 4 files changed, 57 insertions(+), 43 deletions(-) create mode 100644 src/Rules/UniqueTermValue.php delete mode 100644 src/Validation/UniqueTermValue.php diff --git a/src/Http/Controllers/CP/Taxonomies/TermsController.php b/src/Http/Controllers/CP/Taxonomies/TermsController.php index f842e1fbb93..9fb0ae3849b 100644 --- a/src/Http/Controllers/CP/Taxonomies/TermsController.php +++ b/src/Http/Controllers/CP/Taxonomies/TermsController.php @@ -15,6 +15,7 @@ use Statamic\Http\Resources\CP\Taxonomies\Terms; use Statamic\Query\Scopes\Filters\Concerns\QueriesFilters; use Statamic\Rules\Slug; +use Statamic\Rules\UniqueTermValue; class TermsController extends CpController { @@ -170,7 +171,7 @@ public function update(Request $request, $taxonomy, $term, $site) 'slug' => [ 'required', new Slug, - 'unique_term_value:'.$taxonomy->handle().','.$term->id().','.$site->handle(), + new UniqueTermValue($taxonomy->handle(), $term->id(), $site->handle()), ], ]); @@ -270,7 +271,7 @@ public function store(Request $request, $taxonomy, $site) $fields->validate([ 'title' => 'required', - 'slug' => 'required|unique_term_value:'.$taxonomy->handle().',null,'.$site->handle(), + 'slug' => ['required', new UniqueTermValue($taxonomy->handle(), null, $site->handle())], ]); $values = $fields->process()->values()->except(['slug', 'blueprint']); diff --git a/src/Rules/UniqueTermValue.php b/src/Rules/UniqueTermValue.php new file mode 100644 index 00000000000..8470a2b9769 --- /dev/null +++ b/src/Rules/UniqueTermValue.php @@ -0,0 +1,45 @@ +taxonomy) { + $query->where('taxonomy', $this->taxonomy); + } + + if ($this->site) { + $query->where('site', $this->site); + } + + $existing = $query + ->where($attribute, $value) + ->first(); + + if (! $existing) { + return; + } + + if ($this->except == $existing->id()) { + return; + } + + $fail('statamic::validation.unique_term_value')->translate(); + } +} diff --git a/src/Validation/UniqueTermValue.php b/src/Validation/UniqueTermValue.php deleted file mode 100644 index 8c36385254a..00000000000 --- a/src/Validation/UniqueTermValue.php +++ /dev/null @@ -1,33 +0,0 @@ -where('taxonomy', $taxonomy); - } - - if ($site) { - $query->where('site', $site); - } - - $existing = $query - ->where($attribute, $value) - ->first(); - - if (! $existing) { - return true; - } - - return $except == $existing->id(); - } -} diff --git a/tests/Validation/UniqueTermValueTest.php b/tests/Validation/UniqueTermValueTest.php index 7e97fa3cead..de82737ce48 100644 --- a/tests/Validation/UniqueTermValueTest.php +++ b/tests/Validation/UniqueTermValueTest.php @@ -3,6 +3,7 @@ use Illuminate\Support\Facades\Validator; use Statamic\Facades\Taxonomy; use Statamic\Facades\Term; +use Statamic\Rules\UniqueTermValue; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -21,12 +22,12 @@ public function it_fails_when_theres_a_duplicate_term_entry_value_in_across_all_ $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_term_value'] + ['slug' => new UniqueTermValue] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'baz'], - ['slug' => 'unique_term_value'] + ['slug' => new UniqueTermValue] )->passes()); } @@ -41,12 +42,12 @@ public function it_fails_when_theres_a_duplicate_term_entry_value_in_a_specific_ $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_term_value:taxonomy-one'] + ['slug' => new UniqueTermValue('taxonomy-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'bar'], - ['slug' => 'unique_term_value:taxonomy-one'] + ['slug' => new UniqueTermValue('taxonomy-one')] )->passes()); } @@ -60,12 +61,12 @@ public function it_passes_duplicate_slug_validation_when_updating_in_a_single_ta $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_term_value:taxonomy-one,'.$term->id()] + ['slug' => new UniqueTermValue('taxonomy-one', $term->id())] )->passes()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_term_value:taxonomy-one,456'] + ['slug' => new UniqueTermValue('taxonomy-one', 456)] )->fails()); } @@ -84,12 +85,12 @@ public function it_passes_when_theres_a_duplicate_term_value_in_a_different_site $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_term_value:taxonomy-one,null,site-one'] + ['slug' => new UniqueTermValue('taxonomy-one', null, 'site-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_term_value:taxonomy-one,null,site-two'] + ['slug' => new UniqueTermValue('taxonomy-one', null, 'site-two')] )->passes()); } } From 797760c8b82f53c32c02c2062ea2514e30ea68cd Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 26 Mar 2024 17:28:01 -0400 Subject: [PATCH 15/26] Remove `unique_term_value` string based implementation. --- src/Providers/ValidationServiceProvider.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Providers/ValidationServiceProvider.php b/src/Providers/ValidationServiceProvider.php index 5232b31f1f3..966be8588f9 100644 --- a/src/Providers/ValidationServiceProvider.php +++ b/src/Providers/ValidationServiceProvider.php @@ -5,13 +5,11 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\ServiceProvider; use Statamic\Validation\UniqueFormHandle; -use Statamic\Validation\UniqueTermValue; use Statamic\Validation\UniqueUserValue; class ValidationServiceProvider extends ServiceProvider { protected $rules = [ - 'unique_term_value' => UniqueTermValue::class, 'unique_user_value' => UniqueUserValue::class, 'unique_form_handle' => UniqueFormHandle::class, ]; From 4445a61725b97a6181f2891b90c61762d4e99f76 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Mar 2024 14:20:48 -0400 Subject: [PATCH 16/26] Move and update `UniqueUserValue` rule class to `Statamic\Rules` w/ Laravel conventions. --- .../Controllers/CP/Users/UsersController.php | 5 +-- src/Http/Controllers/UserController.php | 11 +++--- src/Rules/UniqueUserValue.php | 36 +++++++++++++++++++ src/Validation/UniqueUserValue.php | 25 ------------- tests/Validation/UniqueUserValueTest.php | 9 ++--- 5 files changed, 49 insertions(+), 37 deletions(-) create mode 100644 src/Rules/UniqueUserValue.php delete mode 100644 src/Validation/UniqueUserValue.php diff --git a/src/Http/Controllers/CP/Users/UsersController.php b/src/Http/Controllers/CP/Users/UsersController.php index f8819ba66c2..35a195e7930 100644 --- a/src/Http/Controllers/CP/Users/UsersController.php +++ b/src/Http/Controllers/CP/Users/UsersController.php @@ -16,6 +16,7 @@ use Statamic\Http\Resources\CP\Users\Users; use Statamic\Notifications\ActivateAccount; use Statamic\Query\Scopes\Filters\Concerns\QueriesFilters; +use Statamic\Rules\UniqueUserValue; use Statamic\Search\Result; use Symfony\Component\Mailer\Exception\TransportException; @@ -165,7 +166,7 @@ public function store(Request $request) $fields = $blueprint->fields()->except(['roles', 'groups'])->addValues($request->all()); - $fields->validate(['email' => 'required|email|unique_user_value']); + $fields->validate(['email' => ['required', 'email', new UniqueUserValue]]); if ($request->input('_validate_only')) { return []; @@ -277,7 +278,7 @@ public function update(Request $request, $user) $fields ->validator() - ->withRules(['email' => 'required|unique_user_value:{id}']) + ->withRules(['email' => ['required', 'email', new UniqueUserValue(except: $user->id())]]) ->withReplacements(['id' => $user->id()]) ->validate(); diff --git a/src/Http/Controllers/UserController.php b/src/Http/Controllers/UserController.php index d5c9e310472..92c6a5d421f 100644 --- a/src/Http/Controllers/UserController.php +++ b/src/Http/Controllers/UserController.php @@ -14,6 +14,7 @@ use Statamic\Exceptions\SilentFormFailureException; use Statamic\Exceptions\UnauthorizedHttpException; use Statamic\Facades\User; +use Statamic\Rules\UniqueUserValue; class UserController extends Controller { @@ -63,7 +64,7 @@ public function register(Request $request) $fields = $fields->addValues($values); $fieldRules = $fields->validator()->withRules([ - 'email' => ['required', 'email', 'unique_user_value'], + 'email' => ['required', 'email', new UniqueUserValue], 'password' => ['required', 'confirmed', Password::default()], ])->rules(); @@ -124,11 +125,9 @@ public function profile(Request $request) try { $fields ->validator() - ->withRules([ - 'email' => ['required', 'email', 'unique_user_value:{id}'], - ])->withReplacements([ - 'id' => $user->id(), - ])->validate(); + ->withRules(['email' => ['required', 'email', new UniqueUserValue(except: $user->id())]]) + ->withReplacements(['id' => $user->id()]) + ->validate(); } catch (ValidationException $e) { return $this->userProfileFailure($e->validator->errors()); } diff --git a/src/Rules/UniqueUserValue.php b/src/Rules/UniqueUserValue.php new file mode 100644 index 00000000000..a69121cee75 --- /dev/null +++ b/src/Rules/UniqueUserValue.php @@ -0,0 +1,36 @@ +column ??= $attribute; + + $existing = User::query() + ->where($this->column, $value) + ->first(); + + if (! $existing) { + return; + } + + if ($this->except == $existing->id()) { + return; + } + + $fail('statamic::validation.unique_user_value')->translate(); + } +} diff --git a/src/Validation/UniqueUserValue.php b/src/Validation/UniqueUserValue.php deleted file mode 100644 index 9584fd1625b..00000000000 --- a/src/Validation/UniqueUserValue.php +++ /dev/null @@ -1,25 +0,0 @@ -where($column, $value) - ->first(); - - if (! $existing) { - return true; - } - - return $except == $existing->id(); - } -} diff --git a/tests/Validation/UniqueUserValueTest.php b/tests/Validation/UniqueUserValueTest.php index ed4340b5971..157b5cbf2e9 100644 --- a/tests/Validation/UniqueUserValueTest.php +++ b/tests/Validation/UniqueUserValueTest.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\Validator; use Statamic\Facades\User; +use Statamic\Rules\UniqueUserValue; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -16,12 +17,12 @@ public function it_fails_when_theres_a_duplicate_user_value() $this->assertTrue(Validator::make( ['email' => 'foo@bar.com'], - ['email' => 'unique_user_value'] + ['email' => new UniqueUserValue] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'bar@bar.com'], - ['slug' => 'unique_user_value'] + ['slug' => new UniqueUserValue] )->passes()); } @@ -32,7 +33,7 @@ public function it_passes_when_updating() $this->assertTrue(Validator::make( ['email' => 'foo@bar.com'], - ['email' => 'unique_user_value:123'] + ['email' => new UniqueUserValue(except: 123)] )->passes()); } @@ -43,7 +44,7 @@ public function it_supports_overwriting_the_column() $this->assertTrue(Validator::make( ['baz' => 'foo@bar.com'], - ['baz' => 'unique_user_value:null,email'] + ['baz' => new UniqueUserValue(column: 'email')] )->fails()); } } From f753300bc88dac897f086ff201a8c29f371bc466 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Mar 2024 14:21:02 -0400 Subject: [PATCH 17/26] Remove `unique_user_value` string based implementation. --- src/Providers/ValidationServiceProvider.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Providers/ValidationServiceProvider.php b/src/Providers/ValidationServiceProvider.php index 966be8588f9..3ee63e73e99 100644 --- a/src/Providers/ValidationServiceProvider.php +++ b/src/Providers/ValidationServiceProvider.php @@ -5,12 +5,10 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\ServiceProvider; use Statamic\Validation\UniqueFormHandle; -use Statamic\Validation\UniqueUserValue; class ValidationServiceProvider extends ServiceProvider { protected $rules = [ - 'unique_user_value' => UniqueUserValue::class, 'unique_form_handle' => UniqueFormHandle::class, ]; From c3d792a837dc7d6347ef0ed3c93d4c8fbbb289ba Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Mar 2024 14:29:16 -0400 Subject: [PATCH 18/26] Use named args instead of passing `null`s. --- .../Controllers/CP/Taxonomies/TermsController.php | 4 ++-- tests/Validation/UniqueEntryValueTest.php | 12 ++++++------ tests/Validation/UniqueTermValueTest.php | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Http/Controllers/CP/Taxonomies/TermsController.php b/src/Http/Controllers/CP/Taxonomies/TermsController.php index 9fb0ae3849b..f1a8516db27 100644 --- a/src/Http/Controllers/CP/Taxonomies/TermsController.php +++ b/src/Http/Controllers/CP/Taxonomies/TermsController.php @@ -171,7 +171,7 @@ public function update(Request $request, $taxonomy, $term, $site) 'slug' => [ 'required', new Slug, - new UniqueTermValue($taxonomy->handle(), $term->id(), $site->handle()), + new UniqueTermValue(taxonomy: $taxonomy->handle(), except: $term->id(), site: $site->handle()), ], ]); @@ -271,7 +271,7 @@ public function store(Request $request, $taxonomy, $site) $fields->validate([ 'title' => 'required', - 'slug' => ['required', new UniqueTermValue($taxonomy->handle(), null, $site->handle())], + 'slug' => ['required', new UniqueTermValue(taxonomy: $taxonomy->handle(), site: $site->handle())], ]); $values = $fields->process()->values()->except(['slug', 'blueprint']); diff --git a/tests/Validation/UniqueEntryValueTest.php b/tests/Validation/UniqueEntryValueTest.php index 5bb6540eb42..2c1c4200a4c 100644 --- a/tests/Validation/UniqueEntryValueTest.php +++ b/tests/Validation/UniqueEntryValueTest.php @@ -35,12 +35,12 @@ public function it_fails_when_theres_a_duplicate_entry_entry_value_in_a_specific $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueEntryValue('collection-one')] + ['slug' => new UniqueEntryValue(collection: 'collection-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'bar'], - ['slug' => new UniqueEntryValue('collection-one')] + ['slug' => new UniqueEntryValue(collection: 'collection-one')] )->passes()); } @@ -51,12 +51,12 @@ public function it_passes_duplicate_slug_validation_when_updating_in_a_single_co $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueEntryValue('collection-one', 123)] + ['slug' => new UniqueEntryValue(collection: 'collection-one', except: 123)] )->passes()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueEntryValue('collection-one', 456)] + ['slug' => new UniqueEntryValue(collection: 'collection-one', except: 456)] )->fails()); } @@ -72,12 +72,12 @@ public function it_passes_when_theres_a_duplicate_entry_value_in_a_different_sit $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueEntryValue('collection-one', null, 'site-one')] + ['slug' => new UniqueEntryValue(collection: 'collection-one', site: 'site-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueEntryValue('collection-one', null, 'site-two')] + ['slug' => new UniqueEntryValue(collection: 'collection-one', site: 'site-two')] )->passes()); } } diff --git a/tests/Validation/UniqueTermValueTest.php b/tests/Validation/UniqueTermValueTest.php index de82737ce48..777d577696f 100644 --- a/tests/Validation/UniqueTermValueTest.php +++ b/tests/Validation/UniqueTermValueTest.php @@ -42,12 +42,12 @@ public function it_fails_when_theres_a_duplicate_term_entry_value_in_a_specific_ $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueTermValue('taxonomy-one')] + ['slug' => new UniqueTermValue(taxonomy: 'taxonomy-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'bar'], - ['slug' => new UniqueTermValue('taxonomy-one')] + ['slug' => new UniqueTermValue(taxonomy: 'taxonomy-one')] )->passes()); } @@ -61,12 +61,12 @@ public function it_passes_duplicate_slug_validation_when_updating_in_a_single_ta $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueTermValue('taxonomy-one', $term->id())] + ['slug' => new UniqueTermValue(taxonomy: 'taxonomy-one', except: $term->id())] )->passes()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueTermValue('taxonomy-one', 456)] + ['slug' => new UniqueTermValue(taxonomy: 'taxonomy-one', except: 456)] )->fails()); } @@ -85,12 +85,12 @@ public function it_passes_when_theres_a_duplicate_term_value_in_a_different_site $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueTermValue('taxonomy-one', null, 'site-one')] + ['slug' => new UniqueTermValue(taxonomy: 'taxonomy-one', site: 'site-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => new UniqueTermValue('taxonomy-one', null, 'site-two')] + ['slug' => new UniqueTermValue(taxonomy: 'taxonomy-one', site: 'site-two')] )->passes()); } } From 48234aa06fb876a559c15ec1dacc1efe0e851626 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Mar 2024 14:35:35 -0400 Subject: [PATCH 19/26] Move and update `UniqueFormHandle` rule class to `Statamic\Rules` w/ Laravel conventions. --- src/Actions/DuplicateForm.php | 3 ++- src/Rules/UniqueFormHandle.php | 17 +++++++++++++++++ src/Validation/UniqueFormHandle.php | 13 ------------- 3 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 src/Rules/UniqueFormHandle.php delete mode 100644 src/Validation/UniqueFormHandle.php diff --git a/src/Actions/DuplicateForm.php b/src/Actions/DuplicateForm.php index e54f50f2f4b..f2d5a8869b5 100644 --- a/src/Actions/DuplicateForm.php +++ b/src/Actions/DuplicateForm.php @@ -5,6 +5,7 @@ use Statamic\Contracts\Forms\Form; use Statamic\Facades\Form as Forms; use Statamic\Rules\Handle; +use Statamic\Rules\UniqueFormHandle; use Statamic\Statamic; class DuplicateForm extends Action @@ -31,7 +32,7 @@ protected function fieldItems() 'type' => 'slug', 'instructions' => __('statamic::messages.form_configure_handle_instructions'), 'separator' => '_', - 'validate' => ['required', new Handle, 'unique_form_handle'], + 'validate' => ['required', new Handle, new UniqueFormHandle], ], ]; } diff --git a/src/Rules/UniqueFormHandle.php b/src/Rules/UniqueFormHandle.php new file mode 100644 index 00000000000..3ade4be698c --- /dev/null +++ b/src/Rules/UniqueFormHandle.php @@ -0,0 +1,17 @@ +translate(); + } + } +} diff --git a/src/Validation/UniqueFormHandle.php b/src/Validation/UniqueFormHandle.php deleted file mode 100644 index 864b359fd53..00000000000 --- a/src/Validation/UniqueFormHandle.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Wed, 27 Mar 2024 14:37:03 -0400 Subject: [PATCH 20/26] Remove `unique_form_handle` string based implementation. --- src/Providers/ValidationServiceProvider.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Providers/ValidationServiceProvider.php b/src/Providers/ValidationServiceProvider.php index 3ee63e73e99..876c9b41c91 100644 --- a/src/Providers/ValidationServiceProvider.php +++ b/src/Providers/ValidationServiceProvider.php @@ -4,12 +4,11 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\ServiceProvider; -use Statamic\Validation\UniqueFormHandle; class ValidationServiceProvider extends ServiceProvider { protected $rules = [ - 'unique_form_handle' => UniqueFormHandle::class, + // ]; public function boot() From 023294b3cb3151b24405db5954d0a4b22f6bea2d Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Mar 2024 14:37:23 -0400 Subject: [PATCH 21/26] =?UTF-8?q?Don=E2=80=99t=20need=20this=20provider=20?= =?UTF-8?q?anymore=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Providers/StatamicServiceProvider.php | 1 - src/Providers/ValidationServiceProvider.php | 20 -------------------- 2 files changed, 21 deletions(-) delete mode 100644 src/Providers/ValidationServiceProvider.php diff --git a/src/Providers/StatamicServiceProvider.php b/src/Providers/StatamicServiceProvider.php index baee2638458..49c0aa63fcc 100644 --- a/src/Providers/StatamicServiceProvider.php +++ b/src/Providers/StatamicServiceProvider.php @@ -29,7 +29,6 @@ class StatamicServiceProvider extends AggregateServiceProvider \Statamic\StaticCaching\ServiceProvider::class, \Statamic\Revisions\ServiceProvider::class, CpServiceProvider::class, - ValidationServiceProvider::class, RouteServiceProvider::class, BroadcastServiceProvider::class, \Statamic\API\ServiceProvider::class, diff --git a/src/Providers/ValidationServiceProvider.php b/src/Providers/ValidationServiceProvider.php deleted file mode 100644 index 876c9b41c91..00000000000 --- a/src/Providers/ValidationServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -rules as $rule => $class) { - Validator::extend($rule, $class); - } - } -} From 2c4ef4356d1b3f2e08b2f26b2ca5417e7706acb0 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Mar 2024 16:01:57 -0400 Subject: [PATCH 22/26] Add these rules to validation builder as well, with replacements. --- resources/js/components/field-validation/Rules.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/js/components/field-validation/Rules.js b/resources/js/components/field-validation/Rules.js index 8a5d68058d2..fd3f248c206 100644 --- a/resources/js/components/field-validation/Rules.js +++ b/resources/js/components/field-validation/Rules.js @@ -315,6 +315,14 @@ export default [ label: 'Unique Entry Value', value: 'new \\Statamic\\Rules\\UniqueEntryValue({collection}, {id}, {site})', }, + { + label: 'Unique Term Value', + value: 'new \\Statamic\\Rules\\UniqueTermValue({taxonomy}, {id}, {site})', + }, + { + label: 'Unique User Value', + value: 'new \\Statamic\\Rules\\UniqueUserValue({id})', + }, { label: 'URL', value: 'url' From c0388e4d631b5a5c2308b1fc27e5cb2c9d57d8b8 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Mar 2024 18:28:46 -0400 Subject: [PATCH 23/26] Update script. --- src/Providers/ExtensionServiceProvider.php | 1 + .../UseClassBasedStatamicUniqueRules.php | 54 ++++++++++ .../UseClassBasedStatamicUniqueRulesTest.php | 102 ++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 src/UpdateScripts/UseClassBasedStatamicUniqueRules.php create mode 100644 tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php diff --git a/src/Providers/ExtensionServiceProvider.php b/src/Providers/ExtensionServiceProvider.php index 15636477c4b..0b1daf96232 100644 --- a/src/Providers/ExtensionServiceProvider.php +++ b/src/Providers/ExtensionServiceProvider.php @@ -230,6 +230,7 @@ class ExtensionServiceProvider extends ServiceProvider Updates\AddDefaultPreferencesToGitConfig::class, Updates\AddConfigureFormFieldsPermission::class, Updates\AddSitePermissions::class, + Updates\UseClassBasedStatamicUniqueRules::class, ]; public function register() diff --git a/src/UpdateScripts/UseClassBasedStatamicUniqueRules.php b/src/UpdateScripts/UseClassBasedStatamicUniqueRules.php new file mode 100644 index 00000000000..d8976f2dfa3 --- /dev/null +++ b/src/UpdateScripts/UseClassBasedStatamicUniqueRules.php @@ -0,0 +1,54 @@ +isUpdatingTo('5.0'); + } + + public function update() + { + collect() + ->merge(File::getFilesRecursively(resource_path('blueprints'))) + ->merge(File::getFilesRecursively(resource_path('fieldsets'))) + ->filter(fn ($path) => in_array(pathinfo($path)['extension'], ['yaml', 'yml'])) + ->each(fn ($path) => $this->updateStringBasedRules($path)); + } + + private function updateStringBasedRules($path) + { + $contents = File::get($path); + + $contents = str_replace( + "'unique_entry_value:{collection},{id},{site}'", + "'new \\Statamic\\Rules\\UniqueEntryValue({collection}, {id}, {site})'", + $contents + ); + + $contents = str_replace( + "'unique_term_value:{taxonomy},{id},{site}'", + "'new \\Statamic\\Rules\\UniqueTermValue({taxonomy}, {id}, {site})'", + $contents + ); + + $contents = str_replace( + "'unique_user_value:{id}'", + "'new \\Statamic\\Rules\\UniqueUserValue({id})'", + $contents + ); + + // If they were using the documented (but optional) `column` parameter on the `unique_user_value` rule + $contents = preg_replace( + "/'unique_user_value:\{id\}(,)([^']+)'/", + "'new \\Statamic\\Rules\\UniqueUserValue({id}, \"$2\")'", + $contents + ); + + File::put($path, $contents); + } +} diff --git a/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php b/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php new file mode 100644 index 00000000000..e47807d587a --- /dev/null +++ b/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php @@ -0,0 +1,102 @@ +assertUpdateScriptRegistered(UseClassBasedStatamicUniqueRules::class); + } + + public static function examplePaths() + { + return [ + 'example blueprint' => ['resources/blueprints/collections/articles/test.yaml'], + 'example fieldset' => ['resources/fieldsets/test.yaml'], + ]; + } + + /** + * @test + * + * @dataProvider examplePaths + */ + public function it_can_update_old_string_based_rules_in_file_based_blueprints($path) + { + File::put($path, <<<'BLUEPRINT' +field: + validate_array: + - 'unique_entry_value:{collection},{id},{site}' + - 'unique_term_value:{taxonomy},{id},{site}' + - 'unique_user_value:{id}' + - 'unique_user_value:{id},some_column' + validate_string_entry: 'unique_entry_value:{collection},{id},{site}' + validate_string_term: 'unique_term_value:{taxonomy},{id},{site}' + validate_string_user: 'unique_user_value:{id}' + validate_string_user_with_column: 'unique_user_value:{id},another_column' +BLUEPRINT + ); + + $this->runUpdateScript(UseClassBasedStatamicUniqueRules::class); + + $expected = <<<'BLUEPRINT' +field: + validate_array: + - 'new \Statamic\Rules\UniqueEntryValue({collection}, {id}, {site})' + - 'new \Statamic\Rules\UniqueTermValue({taxonomy}, {id}, {site})' + - 'new \Statamic\Rules\UniqueUserValue({id})' + - 'new \Statamic\Rules\UniqueUserValue({id}, "some_column")' + validate_string_entry: 'new \Statamic\Rules\UniqueEntryValue({collection}, {id}, {site})' + validate_string_term: 'new \Statamic\Rules\UniqueTermValue({taxonomy}, {id}, {site})' + validate_string_user: 'new \Statamic\Rules\UniqueUserValue({id})' + validate_string_user_with_column: 'new \Statamic\Rules\UniqueUserValue({id}, "another_column")' +BLUEPRINT; + + $this->assertEquals($expected, File::get($path)); + } + + /** + * @test + * + * @dataProvider examplePaths + */ + public function it_doesnt_overzealously_try_to_replace_complicated_pipe_delimed_validate_rules($path) + { + File::put($path, <<<'BLUEPRINT' +validate: 'unique_entry_value:{collection},{id},{site}' +validate: 'unique_term_value:{taxonomy},{id},{site}' +validate: 'unique_user_value:{id}' +validate: 'unique_user_value:{id},some_column' +validate_piped_entry: 'required|unique_entry_value:{collection},{id},{site}' +validate_piped_term: 'required|unique_term_value:{taxonomy},{id},{site}' +validate_piped_user: 'required|unique_user_value:{id}' +validate_piped_user_with_column: 'required|unique_user_value:{id},another_column' +BLUEPRINT + ); + + $this->runUpdateScript(UseClassBasedStatamicUniqueRules::class); + + $expected = <<<'BLUEPRINT' +validate: 'new \Statamic\Rules\UniqueEntryValue({collection}, {id}, {site})' +validate: 'new \Statamic\Rules\UniqueTermValue({taxonomy}, {id}, {site})' +validate: 'new \Statamic\Rules\UniqueUserValue({id})' +validate: 'new \Statamic\Rules\UniqueUserValue({id}, "some_column")' +validate_piped_entry: 'required|unique_entry_value:{collection},{id},{site}' +validate_piped_term: 'required|unique_term_value:{taxonomy},{id},{site}' +validate_piped_user: 'required|unique_user_value:{id}' +validate_piped_user_with_column: 'required|unique_user_value:{id},another_column' +BLUEPRINT; + + $this->assertEquals($expected, File::get($path)); + } +} From 17d78e4e8d334e4c7ae3960c6072127ea276eb87 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Wed, 27 Mar 2024 18:42:51 -0400 Subject: [PATCH 24/26] Delete example blueprints/fieldsets on tear down. --- .../UseClassBasedStatamicUniqueRulesTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php b/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php index e47807d587a..15fa99f6452 100644 --- a/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php +++ b/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php @@ -12,6 +12,15 @@ class UseClassBasedStatamicUniqueRulesTest extends TestCase { use PreventSavingStacheItemsToDisk, RunsUpdateScripts; + public function tearDown(): void + { + foreach (static::examplePaths() as $path) { + File::delete($path[0]); + } + + parent::tearDown(); + } + /** @test */ public function it_is_registered() { From 540fa896b4480caaf4a9562c1386cf25da608e4d Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 28 Mar 2024 09:59:04 -0400 Subject: [PATCH 25/26] Nitpick. --- tests/Fields/ValidatorTest.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/Fields/ValidatorTest.php b/tests/Fields/ValidatorTest.php index a261269679c..45bc120f789 100644 --- a/tests/Fields/ValidatorTest.php +++ b/tests/Fields/ValidatorTest.php @@ -185,7 +185,7 @@ public function it_makes_class_based_replacements() $field = Mockery::mock(Field::class); $field->shouldReceive('setValidationContext')->with([])->andReturnSelf(); $field->shouldReceive('rules')->andReturn([ - 'one' => ['new Tests\\Fields\\FakeRule({string}, {zero}, {num}, {true}, {false}, {null})'], + 'one' => ['new \\Tests\\Fields\\FakeRule({string}, {zero}, {num}, {true}, {false}, {null})'], ]); $fields = Mockery::mock(Fields::class); @@ -494,8 +494,14 @@ public function it_discards_this_at_top_level() class FakeRule { - public function __construct(public $string, public $zero, public $num, public $true, public $false, public $null) - { + public function __construct( + public $string, + public $zero, + public $num, + public $true, + public $false, + public $null + ) { // } } From 1f6191886ec14e3648f3af0e0843cd6b02bd387b Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 28 Mar 2024 14:19:19 -0400 Subject: [PATCH 26/26] keep it private --- src/Rules/AllowedFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/AllowedFile.php b/src/Rules/AllowedFile.php index e968eeece45..62c71f4c401 100644 --- a/src/Rules/AllowedFile.php +++ b/src/Rules/AllowedFile.php @@ -8,7 +8,7 @@ class AllowedFile implements ValidationRule { - const EXTENSIONS = [ + private const EXTENSIONS = [ '7z', 'aiff', 'asc',