diff --git a/resources/js/components/field-validation/Rules.js b/resources/js/components/field-validation/Rules.js index 65274d8eb38..fd3f248c206 100644 --- a/resources/js/components/field-validation/Rules.js +++ b/resources/js/components/field-validation/Rules.js @@ -313,7 +313,15 @@ export default [ // }, { label: 'Unique Entry Value', - value: 'unique_entry_value:{collection},{id},{site}', + 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', 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/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/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/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/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/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/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/Controllers/CP/Taxonomies/TermsController.php b/src/Http/Controllers/CP/Taxonomies/TermsController.php index f842e1fbb93..f1a8516db27 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: $taxonomy->handle(), except: $term->id(), site: $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: $taxonomy->handle(), site: $site->handle())], ]); $values = $fields->process()->values()->except(['slug', 'blueprint']); 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/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/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/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 6f7d9fb4303..00000000000 --- a/src/Providers/ValidationServiceProvider.php +++ /dev/null @@ -1,29 +0,0 @@ - UniqueEntryValue::class, - 'unique_term_value' => UniqueTermValue::class, - 'unique_user_value' => UniqueUserValue::class, - 'unique_form_handle' => UniqueFormHandle::class, - 'code_fieldtype_rulers' => CodeFieldtypeRulers::class, - ]; - - public function boot() - { - foreach ($this->rules as $rule => $class) { - Validator::extend($rule, $class); - } - } -} 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..62c71f4c401 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); } 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/DateFieldtype.php b/src/Rules/DateFieldtype.php similarity index 55% rename from src/Validation/DateFieldtype.php rename to src/Rules/DateFieldtype.php index 9ef52b6f115..caf35cf0c2a 100644 --- a/src/Validation/DateFieldtype.php +++ b/src/Rules/DateFieldtype.php @@ -1,43 +1,51 @@ 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 +58,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 +68,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 +114,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 +161,4 @@ private function timeEnabled() { return $this->fieldtype->config('time_enabled'); } - - private function fail($message) - { - return __('statamic::validation.date_fieldtype_'.$message); - } } diff --git a/src/Validation/TimeFieldtype.php b/src/Rules/TimeFieldtype.php similarity index 55% rename from src/Validation/TimeFieldtype.php rename to src/Rules/TimeFieldtype.php index a749d082bdd..131baea1905 100644 --- a/src/Validation/TimeFieldtype.php +++ b/src/Rules/TimeFieldtype.php @@ -1,25 +1,24 @@ 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/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/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/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/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/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/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; - } -} 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(); - } -} 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 @@ -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/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/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]], + ], ]; } } diff --git a/tests/Fields/ValidatorTest.php b/tests/Fields/ValidatorTest.php index 3eb0684a15d..45bc120f789 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,17 @@ 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 + ) { + // + } +} 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, ], ]; } diff --git a/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php b/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php new file mode 100644 index 00000000000..15fa99f6452 --- /dev/null +++ b/tests/UpdateScripts/UseClassBasedStatamicUniqueRulesTest.php @@ -0,0 +1,111 @@ +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)); + } +} diff --git a/tests/Validation/UniqueEntryValueTest.php b/tests/Validation/UniqueEntryValueTest.php index 755b91ad911..2c1c4200a4c 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: 'collection-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'bar'], - ['slug' => 'unique_entry_value:collection-one'] + ['slug' => new UniqueEntryValue(collection: '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: 'collection-one', except: 123)] )->passes()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_entry_value:collection-one,456'] + ['slug' => new UniqueEntryValue(collection: 'collection-one', except: 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: 'collection-one', site: 'site-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_entry_value: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 7e97fa3cead..777d577696f 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: 'taxonomy-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'bar'], - ['slug' => 'unique_term_value:taxonomy-one'] + ['slug' => new UniqueTermValue(taxonomy: '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: 'taxonomy-one', except: $term->id())] )->passes()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_term_value:taxonomy-one,456'] + ['slug' => new UniqueTermValue(taxonomy: 'taxonomy-one', except: 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: 'taxonomy-one', site: 'site-one')] )->fails()); $this->assertTrue(Validator::make( ['slug' => 'foo'], - ['slug' => 'unique_term_value:taxonomy-one,null,site-two'] + ['slug' => new UniqueTermValue(taxonomy: 'taxonomy-one', site: 'site-two')] )->passes()); } } 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()); } } 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: