├── .gitattributes ├── src ├── Contract │ ├── ImplicitRule.php │ ├── DataAwareRule.php │ ├── UncompromisedVerifier.php │ ├── ValidatorAwareRule.php │ ├── Rule.php │ ├── ValidatesWhenResolved.php │ ├── PresenceVerifierInterface.php │ └── ValidatorFactoryInterface.php ├── UnauthorizedException.php ├── Event │ └── ValidatorFactoryResolved.php ├── Annotation │ └── Scene.php ├── DatabasePresenceVerifierFactory.php ├── Rules │ ├── Exists.php │ ├── ImageFile.php │ ├── NotIn.php │ ├── RequiredIf.php │ ├── In.php │ ├── ArrayRule.php │ ├── ExcludeIf.php │ ├── ProhibitedIf.php │ ├── Unique.php │ ├── Dimensions.php │ ├── Enum.php │ ├── DatabaseRule.php │ ├── Password.php │ └── File.php ├── ValidationExceptionHandler.php ├── ValidatorFactoryFactory.php ├── ClosureValidationRule.php ├── ConditionalRules.php ├── ConfigProvider.php ├── ValidatesWhenResolvedTrait.php ├── NotPwnedVerifier.php ├── ValidationData.php ├── ValidationException.php ├── DatabasePresenceVerifier.php ├── Rule.php ├── Middleware │ └── ValidationMiddleware.php ├── Request │ └── FormRequest.php ├── ValidatorFactory.php ├── ValidationRuleParser.php ├── Concerns │ ├── FormatsMessages.php │ └── ReplacesAttributes.php └── Validator.php ├── LICENSE ├── composer.json ├── README.md └── publish ├── zh_CN └── validation.php └── en └── validation.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /.github export-ignore 3 | /types export-ignore 4 | -------------------------------------------------------------------------------- /src/Contract/ImplicitRule.php: -------------------------------------------------------------------------------- 1 | get(ConnectionResolverInterface::class); 25 | 26 | return make(DatabasePresenceVerifier::class, compact('db')); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Rules/Exists.php: -------------------------------------------------------------------------------- 1 | table, 29 | $this->column, 30 | $this->formatWheres() 31 | ), ','); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Rules/ImageFile.php: -------------------------------------------------------------------------------- 1 | rules('image'); 23 | } 24 | 25 | /** 26 | * The dimension constraints for the uploaded file. 27 | */ 28 | public function dimensions(Dimensions $dimensions): static 29 | { 30 | $this->rules($dimensions); 31 | 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Contract/PresenceVerifierInterface.php: -------------------------------------------------------------------------------- 1 | '"' . str_replace('"', '""', (string) $value) . '"', $this->values); 39 | 40 | return $this->rule . ':' . implode(',', $values); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Rules/RequiredIf.php: -------------------------------------------------------------------------------- 1 | condition = $condition; 32 | } 33 | 34 | /** 35 | * Convert the rule to a validation string. 36 | */ 37 | public function __toString(): string 38 | { 39 | if (is_callable($this->condition)) { 40 | return call_user_func($this->condition) ? 'required' : ''; 41 | } 42 | 43 | return $this->condition ? 'required' : ''; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | Copyright (c) Hyperf 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Rules/In.php: -------------------------------------------------------------------------------- 1 | '"' . str_replace('"', '""', (string) $value) . '"', $this->values); 42 | 43 | return $this->rule . ':' . implode(',', $values); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Contract/ValidatorFactoryInterface.php: -------------------------------------------------------------------------------- 1 | stopPropagation(); 25 | /** @var ValidationException $throwable */ 26 | $body = $throwable->validator->errors()->first(); 27 | if (! $response->hasHeader('content-type')) { 28 | $response = $response->addHeader('content-type', 'text/plain; charset=utf-8'); 29 | } 30 | return $response->setStatus($throwable->status)->setBody(new SwooleStream($body)); 31 | } 32 | 33 | public function isValid(Throwable $throwable): bool 34 | { 35 | return $throwable instanceof ValidationException; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Rules/ArrayRule.php: -------------------------------------------------------------------------------- 1 | toArray(); 29 | } 30 | $this->keys = is_array($keys) ? $keys : func_get_args(); 31 | } 32 | 33 | public function __toString(): string 34 | { 35 | if (empty($this->keys)) { 36 | return 'array'; 37 | } 38 | 39 | $keys = array_map( 40 | static fn ($key) => match (true) { 41 | $key instanceof BackedEnum => $key->value, 42 | $key instanceof UnitEnum => $key->name, 43 | default => $key, 44 | }, 45 | $this->keys, 46 | ); 47 | 48 | return 'array:' . implode(',', $keys); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Rules/ExcludeIf.php: -------------------------------------------------------------------------------- 1 | condition = $condition; 32 | } else { 33 | throw new InvalidArgumentException('The provided condition must be a callable or boolean.'); 34 | } 35 | } 36 | 37 | /** 38 | * Convert the rule to a validation string. 39 | */ 40 | public function __toString(): string 41 | { 42 | if (is_callable($this->condition)) { 43 | return call_user_func($this->condition) ? 'exclude' : ''; 44 | } 45 | 46 | return $this->condition ? 'exclude' : ''; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Rules/ProhibitedIf.php: -------------------------------------------------------------------------------- 1 | condition = $condition; 34 | } else { 35 | throw new InvalidArgumentException('The provided condition must be a callable or boolean.'); 36 | } 37 | } 38 | 39 | /** 40 | * Convert the rule to a validation string. 41 | */ 42 | public function __toString(): string 43 | { 44 | if (is_callable($this->condition)) { 45 | return call_user_func($this->condition) ? 'prohibited' : ''; 46 | } 47 | 48 | return $this->condition ? 'prohibited' : ''; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ValidatorFactoryFactory.php: -------------------------------------------------------------------------------- 1 | get(TranslatorInterface::class); 29 | 30 | /** @var ValidatorFactory $validatorFactory */ 31 | $validatorFactory = make(ValidatorFactory::class, compact('translator', 'container')); 32 | 33 | if ($container->has(ConnectionResolverInterface::class) && $container->has(PresenceVerifierInterface::class)) { 34 | $presenceVerifier = $container->get(PresenceVerifierInterface::class); 35 | $validatorFactory->setPresenceVerifier($presenceVerifier); 36 | } 37 | 38 | $eventDispatcher = $container->get(EventDispatcherInterface::class); 39 | $eventDispatcher->dispatch(new ValidatorFactoryResolved($validatorFactory)); 40 | 41 | return $validatorFactory; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ClosureValidationRule.php: -------------------------------------------------------------------------------- 1 | failed = false; 46 | 47 | $this->callback->__invoke($attribute, $value, function ($message) { 48 | $this->failed = true; 49 | 50 | $this->message = $message; 51 | }); 52 | 53 | return ! $this->failed; 54 | } 55 | 56 | /** 57 | * Get the validation error message. 58 | */ 59 | public function message(): string 60 | { 61 | return $this->message; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ConditionalRules.php: -------------------------------------------------------------------------------- 1 | condition) 36 | ? call_user_func($this->condition, new Fluent($data)) 37 | : $this->condition; 38 | } 39 | 40 | /** 41 | * Get the rules. 42 | */ 43 | public function rules(array $data = []) 44 | { 45 | return is_string($this->rules) 46 | ? explode('|', $this->rules) 47 | : value($this->rules, new Fluent($data)); 48 | } 49 | 50 | /** 51 | * Get the default rules. 52 | * 53 | * @return array 54 | */ 55 | public function defaultRules(array $data = []) 56 | { 57 | return is_string($this->defaultRules) 58 | ? explode('|', $this->defaultRules) 59 | : value($this->defaultRules, new Fluent($data)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/validation", 3 | "description": "hyperf validation", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "validation", 8 | "hyperf" 9 | ], 10 | "require": { 11 | "php": ">=8.1", 12 | "egulias/email-validator": "^3.0", 13 | "hyperf/collection": "~3.1.0", 14 | "hyperf/conditionable": "~3.1.0", 15 | "hyperf/context": "~3.1.0", 16 | "hyperf/contract": "~3.1.0", 17 | "hyperf/di": "~3.1.0", 18 | "hyperf/framework": "~3.1.0", 19 | "hyperf/macroable": "~3.1.0", 20 | "hyperf/stringable": "~3.1.0", 21 | "hyperf/support": "~3.1.0", 22 | "hyperf/tappable": "~3.1.0", 23 | "hyperf/translation": "~3.1.0", 24 | "nesbot/carbon": "^2.21", 25 | "psr/container": "^1.0 || ^2.0", 26 | "psr/event-dispatcher": "^1.0", 27 | "psr/http-message": "^1.0 || ^2.0" 28 | }, 29 | "suggest": { 30 | "hyperf/database": "Required if you want to use the database validation rule (~3.1.0).", 31 | "hyperf/http-server": "Required if you want to use the request validation rule (~3.1.0)." 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Hyperf\\Validation\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "HyperfTest\\Validation\\": "tests" 41 | } 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "branch-alias": { 48 | "dev-master": "3.1-dev" 49 | }, 50 | "hyperf": { 51 | "config": "Hyperf\\Validation\\ConfigProvider" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 32 | PresenceVerifierInterface::class => DatabasePresenceVerifierFactory::class, 33 | FactoryInterface::class => ValidatorFactoryFactory::class, 34 | UncompromisedVerifier::class => NotPwnedVerifier::class, 35 | ], 36 | 'publish' => [ 37 | [ 38 | 'id' => 'zh_CN', 39 | 'description' => 'The message bag for validation.', 40 | 'source' => __DIR__ . '/../publish/zh_CN/validation.php', 41 | 'destination' => $languagesPath . '/zh_CN/validation.php', 42 | ], 43 | [ 44 | 'id' => 'en', 45 | 'description' => 'The message bag for validation.', 46 | 'source' => __DIR__ . '/../publish/en/validation.php', 47 | 'destination' => $languagesPath . '/en/validation.php', 48 | ], 49 | ], 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Rules/Unique.php: -------------------------------------------------------------------------------- 1 | table, 44 | $this->column, 45 | $this->ignore ? '"' . addslashes((string) $this->ignore) . '"' : 'NULL', 46 | $this->idColumn, 47 | $this->formatWheres() 48 | ), ','); 49 | } 50 | 51 | /** 52 | * Ignore the given ID during the unique check. 53 | * 54 | * @param mixed $id 55 | * @return $this 56 | */ 57 | public function ignore($id, ?string $idColumn = null) 58 | { 59 | if ($id instanceof Model) { 60 | return $this->ignoreModel($id, $idColumn); 61 | } 62 | 63 | $this->ignore = $id; 64 | $this->idColumn = $idColumn ?? 'id'; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Ignore the given model during the unique check. 71 | * 72 | * @param Model $model 73 | * @return $this 74 | */ 75 | public function ignoreModel($model, ?string $idColumn = null) 76 | { 77 | $this->idColumn = $idColumn ?? $model->getKeyName(); 78 | $this->ignore = $model->{$this->idColumn}; 79 | 80 | return $this; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/ValidatesWhenResolvedTrait.php: -------------------------------------------------------------------------------- 1 | prepareForValidation(); 28 | 29 | if (! $this->passesAuthorization()) { 30 | $this->failedAuthorization(); 31 | } 32 | 33 | $instance = $this->getValidatorInstance(); 34 | 35 | if ($instance->fails()) { 36 | $this->failedValidation($instance); 37 | } 38 | } 39 | 40 | /** 41 | * Prepare the data for validation. 42 | */ 43 | protected function prepareForValidation() 44 | { 45 | // no default action 46 | } 47 | 48 | /** 49 | * Get the validator instance for the request. 50 | */ 51 | protected function getValidatorInstance(): ValidatorInterface 52 | { 53 | return $this->validator(); 54 | } 55 | 56 | /** 57 | * Handle a failed validation attempt. 58 | * 59 | * @throws ValidationException 60 | */ 61 | protected function failedValidation(ValidatorInterface $validator) 62 | { 63 | throw new ValidationException($validator); 64 | } 65 | 66 | /** 67 | * Determine if the request passes the authorization check. 68 | */ 69 | protected function passesAuthorization(): bool 70 | { 71 | if (method_exists($this, 'authorize')) { 72 | return $this->authorize(); 73 | } 74 | 75 | return true; 76 | } 77 | 78 | /** 79 | * Handle a failed authorization attempt. 80 | * 81 | * @throws UnauthorizedException 82 | */ 83 | protected function failedAuthorization() 84 | { 85 | throw new UnauthorizedException(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperf Validation 2 | 3 | ## About 4 | 5 | [hyperf/validation](https://github.com/hyperf/validation) 组件衍生于 `Laravel Validation` 组件的,我们对它进行了一些改造,大部分功能保持了相同。在这里感谢一下 Laravel 开发组,实现了如此强大好用的 Validation 组件。 6 | 7 | ## Installation 8 | 9 | ``` 10 | composer require hyperf/validation 11 | ``` 12 | 13 | ## Config 14 | 15 | ### Publish config file 16 | 17 | ``` 18 | # 发布国际化配置,已经发布过国际化配置可以省略 19 | php bin/hyperf.php vendor:publish hyperf/translation 20 | 21 | php bin/hyperf.php vendor:publish hyperf/validation 22 | ``` 23 | 24 | ### Configuration path 25 | 26 | ``` 27 | your/config/path/autoload/translation.php 28 | ``` 29 | 30 | ### Configuration 31 | 32 | ```php 33 | 'zh_CN', 36 | 'fallback_locale' => 'en', 37 | 'path' => BASE_PATH . '/storage/languages', 38 | ]; 39 | ``` 40 | 41 | ### Exception handler 42 | 43 | ```php 44 | [ 47 | 'http' => [ 48 | \Hyperf\Validation\ValidationExceptionHandler::class, 49 | ], 50 | ], 51 | ]; 52 | ``` 53 | 54 | ### Validation middleware 55 | 56 | ```php 57 | [ 60 | \Hyperf\Validation\Middleware\ValidationMiddleware::class, 61 | ], 62 | ]; 63 | ``` 64 | 65 | ## Usage 66 | 67 | ### Generate form request 68 | 69 | Command: 70 | ``` 71 | php bin/hyperf.php gen:request FooRequest 72 | ``` 73 | 74 | Usage: 75 | ```php 76 | class IndexController 77 | { 78 | public function foo(FooRequest $request) 79 | { 80 | $request->input('foo'); 81 | } 82 | 83 | public function bar(RequestInterface $request) 84 | { 85 | $factory = $this->container->get(\Hyperf\Validation\Contract\ValidatorFactoryInterface::class); 86 | 87 | $factory->extend('foo', function ($attribute, $value, $parameters, $validator) { 88 | return $value == 'foo'; 89 | }); 90 | 91 | $factory->replacer('foo', function ($message, $attribute, $rule, $parameters) { 92 | return str_replace(':foo', $attribute, $message); 93 | }); 94 | 95 | $validator = $factory->make( 96 | $request->all(), 97 | [ 98 | 'name' => 'required|foo', 99 | ], 100 | [ 101 | 'name.foo' => ':foo is not foo', 102 | ] 103 | ); 104 | 105 | if (!$validator->passes()) { 106 | $validator->errors(); 107 | } 108 | } 109 | } 110 | ``` 111 | -------------------------------------------------------------------------------- /src/Rules/Dimensions.php: -------------------------------------------------------------------------------- 1 | constraints as $key => $value) { 36 | $result .= "{$key}={$value},"; 37 | } 38 | 39 | return 'dimensions:' . substr($result, 0, -1); 40 | } 41 | 42 | /** 43 | * Set the "width" constraint. 44 | */ 45 | public function width(int $value): static 46 | { 47 | $this->constraints['width'] = $value; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Set the "height" constraint. 54 | */ 55 | public function height(int $value): static 56 | { 57 | $this->constraints['height'] = $value; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Set the "min width" constraint. 64 | */ 65 | public function minWidth(int $value): static 66 | { 67 | $this->constraints['min_width'] = $value; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Set the "min height" constraint. 74 | */ 75 | public function minHeight(int $value): static 76 | { 77 | $this->constraints['min_height'] = $value; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Set the "max width" constraint. 84 | */ 85 | public function maxWidth(int $value): static 86 | { 87 | $this->constraints['max_width'] = $value; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Set the "max height" constraint. 94 | */ 95 | public function maxHeight(int $value): static 96 | { 97 | $this->constraints['max_height'] = $value; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Set the "ratio" constraint. 104 | */ 105 | public function ratio(float $value): static 106 | { 107 | $this->constraints['ratio'] = $value; 108 | 109 | return $this; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/NotPwnedVerifier.php: -------------------------------------------------------------------------------- 1 | getHash($value); 43 | return ! $this->search($hashPrefix) 44 | ->contains(static function ($line) use ($hash, $hashPrefix, $threshold) { 45 | [$hashSuffix, $count] = explode(':', $line); 46 | 47 | return $hashPrefix . $hashSuffix === $hash && $count > $threshold; 48 | }); 49 | } 50 | 51 | /** 52 | * Get the hash and its first 5 chars. 53 | */ 54 | protected function getHash(string $value): array 55 | { 56 | $hash = strtoupper(sha1($value)); 57 | 58 | $hashPrefix = substr($hash, 0, 5); 59 | 60 | return [$hash, $hashPrefix]; 61 | } 62 | 63 | /** 64 | * Search by the given hash prefix and returns all occurrences of leaked passwords. 65 | */ 66 | protected function search(string $hashPrefix): Collection 67 | { 68 | $client = $this->factory->create([ 69 | 'timeout' => $this->timeout, 70 | ]); 71 | $response = $client->get( 72 | 'https://api.pwnedpasswords.com/range/' . $hashPrefix, 73 | [ 74 | 'headers' => [ 75 | 'Add-Padding' => true, 76 | ], 77 | ] 78 | ); 79 | 80 | $body = ($response->getStatusCode() === 200) 81 | ? $response->getBody()->getContents() 82 | : ''; 83 | 84 | return Str::of($body)->trim()->explode("\n")->filter(function ($line) { 85 | return str_contains($line, ':'); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Rules/Enum.php: -------------------------------------------------------------------------------- 1 | type) { 54 | return $this->isDesirable($value); 55 | } 56 | 57 | if (is_null($value) || ! enum_exists($this->type) || ! method_exists($this->type, 'tryFrom')) { 58 | return false; 59 | } 60 | 61 | try { 62 | $value = $this->type::tryFrom($value); 63 | return ! is_null($value) && $this->isDesirable($value); 64 | } catch (TypeError) { 65 | return false; 66 | } 67 | } 68 | 69 | /** 70 | * Specify the cases that should be considered valid. 71 | */ 72 | public function only(array|UnitEnum $values): static 73 | { 74 | $this->only = Arr::wrap($values); 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Specify the cases that should be considered invalid. 81 | */ 82 | public function except(array|UnitEnum $values): static 83 | { 84 | $this->except = Arr::wrap($values); 85 | 86 | return $this; 87 | } 88 | 89 | public function message(): array|string 90 | { 91 | $message = $this->validator->getTranslator()->get('validation.enum'); 92 | 93 | return $message === 'validation.enum' 94 | ? ['The selected :attribute is invalid.'] 95 | : $message; 96 | } 97 | 98 | public function setValidator(Validator $validator): static 99 | { 100 | $this->validator = $validator; 101 | return $this; 102 | } 103 | 104 | /** 105 | * Determine if the given case is a valid case based on the only / except values. 106 | */ 107 | protected function isDesirable(mixed $value): bool 108 | { 109 | return match (true) { 110 | ! empty($this->only) => in_array(needle: $value, haystack: $this->only, strict: true), 111 | ! empty($this->except) => ! in_array(needle: $value, haystack: $this->except, strict: true), 112 | default => true, 113 | }; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/ValidationData.php: -------------------------------------------------------------------------------- 1 | 'foo.bar' 60 | * 61 | * Allows us to not spin through all of the flattened data for some operations. 62 | * 63 | * @return string 64 | */ 65 | public static function getLeadingExplicitAttributePath(string $attribute) 66 | { 67 | return rtrim(explode('*', $attribute)[0], '.') ?: null; 68 | } 69 | 70 | /** 71 | * Gather a copy of the attribute data filled with any missing attributes. 72 | * 73 | * @return array 74 | */ 75 | protected static function initializeAttributeOnData(string $attribute, array $masterData) 76 | { 77 | $explicitPath = static::getLeadingExplicitAttributePath($attribute); 78 | 79 | $data = static::extractDataFromPath($explicitPath, $masterData); 80 | 81 | if (! Str::contains($attribute, '*') || Str::endsWith($attribute, '*')) { 82 | return $data; 83 | } 84 | 85 | return data_set($data, $attribute, null, true); 86 | } 87 | 88 | /** 89 | * Get all of the exact attribute values for a given wildcard attribute. 90 | */ 91 | protected static function extractValuesForWildcards(array $masterData, array $data, string $attribute): array 92 | { 93 | $keys = []; 94 | 95 | $pattern = str_replace('\*', '[^\.]+', preg_quote($attribute)); 96 | 97 | foreach ($data as $key => $value) { 98 | if ((bool) preg_match('/^' . $pattern . '/', $key, $matches)) { 99 | $keys[] = $matches[0]; 100 | } 101 | } 102 | 103 | $keys = array_unique($keys); 104 | 105 | $data = []; 106 | 107 | foreach ($keys as $key) { 108 | $data[$key] = Arr::get($masterData, $key); 109 | } 110 | 111 | return $data; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/ValidationException.php: -------------------------------------------------------------------------------- 1 | get(ValidatorFactoryInterface::class); 59 | 60 | return new static(tap($factory->make([], []), function ($validator) use ($messages) { 61 | foreach ($messages as $key => $value) { 62 | foreach (Arr::wrap($value) as $message) { 63 | $validator->errors()->add($key, $message); 64 | } 65 | } 66 | })); 67 | } 68 | 69 | /** 70 | * Get all of the validation error messages. 71 | */ 72 | public function errors(): array 73 | { 74 | return $this->validator->errors()->messages(); 75 | } 76 | 77 | /** 78 | * Set the HTTP status code to be used for the response. 79 | * 80 | * @return $this 81 | */ 82 | public function status(int $status) 83 | { 84 | $this->status = $status; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Set the error bag on the exception. 91 | * 92 | * @return $this 93 | */ 94 | public function errorBag(string $errorBag) 95 | { 96 | $this->errorBag = $errorBag; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Set the URL to redirect to on a validation error. 103 | * 104 | * @return $this 105 | */ 106 | public function redirectTo(string $url) 107 | { 108 | $this->redirectTo = $url; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Get the underlying response instance. 115 | * 116 | * @return ResponseInterface 117 | */ 118 | public function getResponse() 119 | { 120 | return $this->response; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/DatabasePresenceVerifier.php: -------------------------------------------------------------------------------- 1 | table($collection)->where($column, '=', $value); 49 | 50 | if (! is_null($excludeId) && $excludeId !== 'NULL') { 51 | $query->where($idColumn ?: 'id', '<>', $excludeId); 52 | } 53 | 54 | return $this->addConditions($query, $extra)->count(); 55 | } 56 | 57 | /** 58 | * Count the number of objects in a collection with the given values. 59 | */ 60 | public function getMultiCount(string $collection, string $column, array $values, array $extra = []): int 61 | { 62 | $query = $this->table($collection)->whereIn($column, $values); 63 | 64 | return $this->addConditions($query, $extra)->distinct()->count($column); 65 | } 66 | 67 | /** 68 | * Get a query builder for the given table. 69 | * 70 | * @return Builder 71 | */ 72 | public function table(string $table) 73 | { 74 | return $this->db->connection($this->connection)->table($table)->useWritePdo(); 75 | } 76 | 77 | /** 78 | * Set the connection to be used. 79 | */ 80 | public function setConnection(?string $connection) 81 | { 82 | $this->connection = $connection; 83 | } 84 | 85 | /** 86 | * Add the given conditions to the query. 87 | * 88 | * @param Builder $query 89 | * @return Builder 90 | */ 91 | protected function addConditions($query, array $conditions) 92 | { 93 | foreach ($conditions as $key => $value) { 94 | if ($value instanceof Closure) { 95 | $query->where(function ($query) use ($value) { 96 | $value($query); 97 | }); 98 | } else { 99 | $this->addWhere($query, $key, $value); 100 | } 101 | } 102 | 103 | return $query; 104 | } 105 | 106 | /** 107 | * Add a "where" clause to the given query. 108 | * 109 | * @param Builder $query 110 | * @param string $extraValue 111 | */ 112 | protected function addWhere($query, string $key, $extraValue) 113 | { 114 | if ($extraValue === 'NULL') { 115 | $query->whereNull($key); 116 | } elseif ($extraValue === 'NOT_NULL') { 117 | $query->whereNotNull($key); 118 | } elseif (Str::startsWith((string) $extraValue, '!')) { 119 | $query->where($key, '!=', mb_substr($extraValue, 1)); 120 | } else { 121 | $query->where($key, $extraValue); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Rules/DatabaseRule.php: -------------------------------------------------------------------------------- 1 | whereIn($column, $value); 50 | } 51 | 52 | if ($column instanceof Closure) { 53 | return $this->using($column); 54 | } 55 | 56 | $this->wheres[] = compact('column', 'value'); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Set a "where not" constraint on the query. 63 | * 64 | * @param array|string $value 65 | */ 66 | public function whereNot(string $column, mixed $value): static 67 | { 68 | if (is_array($value)) { 69 | return $this->whereNotIn($column, $value); 70 | } 71 | 72 | return $this->where($column, '!' . $value); 73 | } 74 | 75 | /** 76 | * Set a "where null" constraint on the query. 77 | * 78 | * @return $this 79 | */ 80 | public function whereNull(string $column): static 81 | { 82 | return $this->where($column, 'NULL'); 83 | } 84 | 85 | /** 86 | * Set a "where not null" constraint on the query. 87 | * 88 | * @return $this 89 | */ 90 | public function whereNotNull(string $column) 91 | { 92 | return $this->where($column, 'NOT_NULL'); 93 | } 94 | 95 | /** 96 | * Set a "where in" constraint on the query. 97 | * 98 | * @return $this 99 | */ 100 | public function whereIn(string $column, array $values) 101 | { 102 | return $this->where(function ($query) use ($column, $values) { 103 | $query->whereIn($column, $values); 104 | }); 105 | } 106 | 107 | /** 108 | * Set a "where not in" constraint on the query. 109 | * 110 | * @return $this 111 | */ 112 | public function whereNotIn(string $column, array $values) 113 | { 114 | return $this->where(function ($query) use ($column, $values) { 115 | $query->whereNotIn($column, $values); 116 | }); 117 | } 118 | 119 | /** 120 | * Register a custom query callback. 121 | * 122 | * @return $this 123 | */ 124 | public function using(Closure $callback) 125 | { 126 | $this->using[] = $callback; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Get the custom query callbacks for the rule. 133 | */ 134 | public function queryCallbacks(): array 135 | { 136 | return $this->using; 137 | } 138 | 139 | /** 140 | * Format the where clauses. 141 | */ 142 | protected function formatWheres(): string 143 | { 144 | return collect($this->wheres)->map(fn ($where) => $where['column'] . ',"' . str_replace('"', '""', (string) $where['value']) . '"')->implode(','); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Rule.php: -------------------------------------------------------------------------------- 1 | toArray(); 53 | } 54 | 55 | return new Rules\In(is_array($values) ? $values : func_get_args()); 56 | } 57 | 58 | /** 59 | * Get a not_in constraint builder instance. 60 | */ 61 | public static function notIn(mixed $values): Rules\NotIn 62 | { 63 | if ($values instanceof Arrayable) { 64 | $values = $values->toArray(); 65 | } 66 | 67 | return new Rules\NotIn(is_array($values) ? $values : func_get_args()); 68 | } 69 | 70 | /** 71 | * Get a required_if constraint builder instance. 72 | */ 73 | public static function requiredIf(bool|callable $callback): Rules\RequiredIf 74 | { 75 | return new Rules\RequiredIf($callback); 76 | } 77 | 78 | /** 79 | * Get a unique constraint builder instance. 80 | */ 81 | public static function unique(string $table, string $column = 'NULL'): Rules\Unique 82 | { 83 | return new Rules\Unique($table, $column); 84 | } 85 | 86 | public static function prohibitedIf($callback): ProhibitedIf 87 | { 88 | return new ProhibitedIf($callback); 89 | } 90 | 91 | public static function excludeIf($callback): ExcludeIf 92 | { 93 | return new ExcludeIf($callback); 94 | } 95 | 96 | /** 97 | * Apply the given rules if the given condition is truthy. 98 | */ 99 | public static function when( 100 | bool|Closure $condition, 101 | array|Closure|RuleContract|string $rules, 102 | array|Closure|RuleContract|string $defaultRules = [] 103 | ): ConditionalRules { 104 | return new ConditionalRules($condition, $rules, $defaultRules); 105 | } 106 | 107 | /** 108 | * Apply the given rules if the given condition is falsy. 109 | */ 110 | public static function unless( 111 | bool|Closure $condition, 112 | array|Closure|RuleContract|string $rules, 113 | array|Closure|RuleContract|string $defaultRules = [] 114 | ): ConditionalRules { 115 | return new ConditionalRules($condition, $defaultRules, $rules); 116 | } 117 | 118 | /** 119 | * Get an array rule builder instance. 120 | * @param null|mixed $keys 121 | */ 122 | public static function array($keys = null): ArrayRule 123 | { 124 | return new ArrayRule(...func_get_args()); 125 | } 126 | 127 | /** 128 | * Get an enum rule builder instance. 129 | */ 130 | public static function enum(string $type): Enum 131 | { 132 | return new Enum($type); 133 | } 134 | 135 | /** 136 | * Get a file rule builder instance. 137 | */ 138 | public static function file(): File 139 | { 140 | return new File(); 141 | } 142 | 143 | /** 144 | * Get an image file rule builder instance. 145 | */ 146 | public static function imageFile(): File 147 | { 148 | return new ImageFile(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Middleware/ValidationMiddleware.php: -------------------------------------------------------------------------------- 1 | getAttribute(Dispatched::class); 50 | 51 | if (! $dispatched instanceof Dispatched) { 52 | throw new ServerException(sprintf('The dispatched object is not a %s object.', Dispatched::class)); 53 | } 54 | 55 | Context::set(ServerRequestInterface::class, $request); 56 | 57 | if ($this->shouldHandle($dispatched)) { 58 | try { 59 | [$requestHandler, $method] = $this->prepareHandler($dispatched->handler->callback); 60 | if ($method) { 61 | $reflectionMethod = ReflectionManager::reflectMethod($requestHandler, $method); 62 | $parameters = $reflectionMethod->getParameters(); 63 | foreach ($parameters as $parameter) { 64 | if ($parameter->getType() === null) { 65 | continue; 66 | } 67 | $className = $parameter->getType()->getName(); 68 | if ($this->isImplementedValidatesWhenResolved($className)) { 69 | /** @var ValidatesWhenResolved $formRequest */ 70 | $formRequest = $this->container->get($className); 71 | if ($formRequest instanceof FormRequest) { 72 | $this->handleSceneAnnotation($formRequest, $requestHandler, $method, $parameter->getName()); 73 | } 74 | $formRequest->validateResolved(); 75 | } 76 | } 77 | } 78 | } catch (UnauthorizedException $exception) { 79 | return $this->handleUnauthorizedException($exception); 80 | } 81 | } 82 | 83 | return $handler->handle($request); 84 | } 85 | 86 | public function isImplementedValidatesWhenResolved(string $className): bool 87 | { 88 | if (! isset($this->implements[$className]) && class_exists($className)) { 89 | $implements = class_implements($className); 90 | $this->implements[$className] = in_array(ValidatesWhenResolved::class, $implements, true); 91 | } 92 | return $this->implements[$className] ?? false; 93 | } 94 | 95 | protected function handleSceneAnnotation(FormRequest $request, string $class, string $method, string $argument): void 96 | { 97 | /** @var null|MultipleAnnotation $scene */ 98 | $scene = AnnotationCollector::getClassMethodAnnotation($class, $method)[Scene::class] ?? null; 99 | if (! $scene) { 100 | return; 101 | } 102 | 103 | $annotations = $scene->toAnnotations(); 104 | if (empty($annotations)) { 105 | return; 106 | } 107 | 108 | /** @var Scene $annotation */ 109 | foreach ($annotations as $annotation) { 110 | if ($annotation->argument === null || $annotation->argument === $argument) { 111 | $request->scene($annotation->scene ?? $method); 112 | return; 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * @param UnauthorizedException $exception Keep this argument here even this argument is unused in the method, 119 | * maybe the user need the details of exception when rewrite this method 120 | */ 121 | protected function handleUnauthorizedException(UnauthorizedException $exception): ResponseInterface 122 | { 123 | return Context::override(ResponseInterface::class, fn (ResponseInterface $response) => $response->withStatus(403)); 124 | } 125 | 126 | protected function shouldHandle(Dispatched $dispatched): bool 127 | { 128 | return $dispatched->status === Dispatcher::FOUND && ! $dispatched->handler->callback instanceof Closure; 129 | } 130 | 131 | /** 132 | * @see \Hyperf\HttpServer\CoreMiddleware::prepareHandler() 133 | */ 134 | protected function prepareHandler(array|string $handler): array 135 | { 136 | if (is_string($handler)) { 137 | if (str_contains($handler, '@')) { 138 | return explode('@', $handler); 139 | } 140 | $array = explode('::', $handler); 141 | if (! isset($array[1]) && class_exists($handler) && method_exists($handler, '__invoke')) { 142 | $array[1] = '__invoke'; 143 | } 144 | return [$array[0], $array[1] ?? null]; 145 | } 146 | if (is_array($handler) && isset($handler[0], $handler[1])) { 147 | return $handler; 148 | } 149 | throw new RuntimeException('Handler not exist.'); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Request/FormRequest.php: -------------------------------------------------------------------------------- 1 | getContextValidatorKey('scene'), $scene); 55 | return $this; 56 | } 57 | 58 | public function getScene(): ?string 59 | { 60 | return Context::get($this->getContextValidatorKey('scene')); 61 | } 62 | 63 | /** 64 | * Get the proper failed validation response for the request. 65 | */ 66 | public function response(): ResponseInterface 67 | { 68 | return ResponseContext::get()->withStatus(422); 69 | } 70 | 71 | /** 72 | * Get the validated data from the request. 73 | */ 74 | public function validated(): array 75 | { 76 | return $this->getValidatorInstance()->validated(); 77 | } 78 | 79 | /** 80 | * Get custom messages for validator errors. 81 | */ 82 | public function messages(): array 83 | { 84 | return []; 85 | } 86 | 87 | /** 88 | * Get custom attributes for validator errors. 89 | */ 90 | public function attributes(): array 91 | { 92 | return []; 93 | } 94 | 95 | /** 96 | * Set the container implementation. 97 | */ 98 | public function setContainer(ContainerInterface $container): static 99 | { 100 | $this->container = $container; 101 | 102 | return $this; 103 | } 104 | 105 | public function rules(): array 106 | { 107 | return []; 108 | } 109 | 110 | /** 111 | * Get the validator instance for the request. 112 | */ 113 | protected function getValidatorInstance(): ValidatorInterface 114 | { 115 | return Context::getOrSet($this->getContextValidatorKey(ValidatorInterface::class), function () { 116 | $factory = $this->container->get(ValidationFactory::class); 117 | 118 | if (method_exists($this, 'validator')) { 119 | $validator = call_user_func_array([$this, 'validator'], compact('factory')); 120 | } else { 121 | $validator = $this->createDefaultValidator($factory); 122 | } 123 | 124 | if (method_exists($this, 'withValidator')) { 125 | $this->withValidator($validator); 126 | } 127 | 128 | return $validator; 129 | }); 130 | } 131 | 132 | /** 133 | * Create the default validator instance. 134 | */ 135 | protected function createDefaultValidator(ValidationFactory $factory): ValidatorInterface 136 | { 137 | return $factory->make( 138 | $this->validationData(), 139 | $this->getRules(), 140 | $this->messages(), 141 | $this->attributes() 142 | ); 143 | } 144 | 145 | /** 146 | * Get data to be validated from the request. 147 | */ 148 | protected function validationData(): array 149 | { 150 | return array_merge_recursive($this->all(), $this->getUploadedFiles()); 151 | } 152 | 153 | /** 154 | * Handle a failed validation attempt. 155 | * 156 | * @throws ValidationException 157 | */ 158 | protected function failedValidation(ValidatorInterface $validator) 159 | { 160 | throw new ValidationException($validator, $this->response()); 161 | } 162 | 163 | /** 164 | * Format the errors from the given Validator instance. 165 | */ 166 | protected function formatErrors(ValidatorInterface $validator): array 167 | { 168 | return $validator->getMessageBag()->toArray(); 169 | } 170 | 171 | /** 172 | * Determine if the request passes the authorization check. 173 | */ 174 | protected function passesAuthorization(): bool 175 | { 176 | if (method_exists($this, 'authorize')) { 177 | return call_user_func_array([$this, 'authorize'], []); 178 | } 179 | 180 | return false; 181 | } 182 | 183 | /** 184 | * Handle a failed authorization attempt. 185 | */ 186 | protected function failedAuthorization() 187 | { 188 | throw new UnauthorizedException('This action is unauthorized.'); 189 | } 190 | 191 | /** 192 | * Get context validator key. 193 | */ 194 | protected function getContextValidatorKey(string $key): string 195 | { 196 | return sprintf('%s:%s', spl_object_hash($this), $key); 197 | } 198 | 199 | /** 200 | * Get scene rules. 201 | */ 202 | protected function getRules(): array 203 | { 204 | $rules = $this->rules(); 205 | $scene = $this->getScene(); 206 | if ($scene && isset($this->scenes[$scene]) && is_array($this->scenes[$scene])) { 207 | $result = []; 208 | foreach ($this->scenes[$scene] as $key => $value) { 209 | if (is_string($key)) { 210 | $result[$key] = $value; 211 | } elseif (is_numeric($key) && is_string($value) && isset($rules[$value])) { 212 | $result[$value] = $rules[$value]; 213 | } 214 | } 215 | return $result; 216 | } 217 | return $rules; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/ValidatorFactory.php: -------------------------------------------------------------------------------- 1 | resolve( 89 | $data, 90 | $rules, 91 | $messages, 92 | $customAttributes 93 | ); 94 | 95 | // The presence verifier is responsible for checking the unique and exists data 96 | // for the validator. It is behind an interface so that multiple versions of 97 | // it may be written besides database. We'll inject it into the validator. 98 | if (! is_null($this->verifier)) { 99 | $validator->setPresenceVerifier($this->verifier); 100 | } 101 | 102 | // Next we'll set the IoC container instance of the validator, which is used to 103 | // resolve out class based validator extensions. If it is not set then these 104 | // types of extensions will not be possible on these validation instances. 105 | if (! is_null($this->container)) { 106 | $validator->setContainer($this->container); 107 | } 108 | 109 | $validator instanceof Validator && $this->addExtensions($validator); 110 | 111 | return $validator; 112 | } 113 | 114 | /** 115 | * Validate the given data against the provided rules. 116 | * 117 | * @throws ValidationException 118 | */ 119 | public function validate(array $data, array $rules, array $messages = [], array $customAttributes = []): array 120 | { 121 | return $this->make($data, $rules, $messages, $customAttributes)->validate(); 122 | } 123 | 124 | /** 125 | * Register a custom validator extension. 126 | */ 127 | public function extend(string $rule, Closure|string $extension, ?string $message = null) 128 | { 129 | $this->extensions[$rule] = $extension; 130 | 131 | if ($message) { 132 | $this->fallbackMessages[StrCache::snake($rule)] = $message; 133 | } 134 | } 135 | 136 | /** 137 | * Register a custom implicit validator extension. 138 | */ 139 | public function extendImplicit(string $rule, Closure|string $extension, ?string $message = null) 140 | { 141 | $this->implicitExtensions[$rule] = $extension; 142 | 143 | if ($message) { 144 | $this->fallbackMessages[StrCache::snake($rule)] = $message; 145 | } 146 | } 147 | 148 | /** 149 | * Register a custom dependent validator extension. 150 | */ 151 | public function extendDependent(string $rule, Closure|string $extension, ?string $message = null) 152 | { 153 | $this->dependentExtensions[$rule] = $extension; 154 | 155 | if ($message) { 156 | $this->fallbackMessages[StrCache::snake($rule)] = $message; 157 | } 158 | } 159 | 160 | /** 161 | * Register a custom validator message replacer. 162 | */ 163 | public function replacer(string $rule, Closure|string $replacer) 164 | { 165 | $this->replacers[$rule] = $replacer; 166 | } 167 | 168 | /** 169 | * Set the Validator instance resolver. 170 | */ 171 | public function resolver(Closure $resolver) 172 | { 173 | $this->resolver = $resolver; 174 | } 175 | 176 | /** 177 | * Get the Translator implementation. 178 | */ 179 | public function getTranslator(): TranslatorInterface 180 | { 181 | return $this->translator; 182 | } 183 | 184 | /** 185 | * Get the Presence Verifier implementation. 186 | */ 187 | public function getPresenceVerifier(): PresenceVerifierInterface 188 | { 189 | return $this->verifier; 190 | } 191 | 192 | /** 193 | * Set the Presence Verifier implementation. 194 | */ 195 | public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier) 196 | { 197 | $this->verifier = $presenceVerifier; 198 | } 199 | 200 | /** 201 | * Resolve a new Validator instance. 202 | */ 203 | protected function resolve(array $data, array $rules, array $messages, array $customAttributes): ValidatorInterface 204 | { 205 | if (is_null($this->resolver)) { 206 | return new Validator($this->translator, $data, $rules, $messages, $customAttributes); 207 | } 208 | 209 | return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes); 210 | } 211 | 212 | /** 213 | * Add the extensions to a validator instance. 214 | */ 215 | protected function addExtensions(Validator $validator): void 216 | { 217 | $validator->addExtensions($this->extensions); 218 | 219 | // Next, we will add the implicit extensions, which are similar to the required 220 | // and accepted rule in that they are run even if the attributes is not in a 221 | // array of data that is given to a validator instances via instantiation. 222 | $validator->addImplicitExtensions($this->implicitExtensions); 223 | 224 | $validator->addDependentExtensions($this->dependentExtensions); 225 | 226 | $validator->addReplacers($this->replacers); 227 | 228 | $validator->setFallbackMessages($this->fallbackMessages); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /publish/zh_CN/validation.php: -------------------------------------------------------------------------------- 1 | ':attribute 必须接受', 25 | 'active_url' => ':attribute 必须是一个合法的 URL', 26 | 'after' => ':attribute 必须是 :date 之后的一个日期', 27 | 'after_or_equal' => ':attribute 必须是 :date 之后或相同的一个日期', 28 | 'alpha' => ':attribute 只能包含字母', 29 | 'alpha_dash' => ':attribute 只能包含字母、数字、中划线或下划线', 30 | 'alpha_num' => ':attribute 只能包含字母和数字', 31 | 'array' => ':attribute 必须是一个数组', 32 | 'before' => ':attribute 必须是 :date 之前的一个日期', 33 | 'before_or_equal' => ':attribute 必须是 :date 之前或相同的一个日期', 34 | 'between' => [ 35 | 'numeric' => ':attribute 必须在 :min 到 :max 之间', 36 | 'file' => ':attribute 必须在 :min 到 :max kb 之间', 37 | 'string' => ':attribute 必须在 :min 到 :max 个字符之间', 38 | 'array' => ':attribute 必须在 :min 到 :max 项之间', 39 | ], 40 | 'boolean' => ':attribute 字符必须是 true 或 false, 1 或 0', 41 | 'confirmed' => ':attribute 二次确认不匹配', 42 | 'date' => ':attribute 必须是一个合法的日期', 43 | 'date_format' => ':attribute 与给定的格式 :format 不符合', 44 | 'decimal' => ':attribute 必须有 :decimal 位小数', 45 | 'different' => ':attribute 必须不同于 :other', 46 | 'digits' => ':attribute 必须是 :digits 位', 47 | 'digits_between' => ':attribute 必须在 :min 和 :max 位之间', 48 | 'dimensions' => ':attribute 具有无效的图片尺寸', 49 | 'distinct' => ':attribute 字段具有重复值', 50 | 'email' => ':attribute 必须是一个合法的电子邮件地址', 51 | 'exclude' => ':attribute 字段是被排除的', 52 | 'exclude_if' => '当 :other 为 :value 时,排除 :attribute 字段', 53 | 'exclude_unless' => '除非 :other 是在 :values 中,否则排除 :attribute 字段', 54 | 'exclude_with' => '当 :values 存在时,排除 :attribute 字段', 55 | 'exclude_without' => '当 :values 不存在时,排除 :attribute 字段', 56 | 'exists' => '选定的 :attribute 是无效的', 57 | 'file' => ':attribute 必须是一个文件', 58 | 'filled' => ':attribute 的字段是必填的', 59 | 'gt' => [ 60 | 'numeric' => ':attribute 必须大于 :value', 61 | 'file' => ':attribute 必须大于 :value kb', 62 | 'string' => ':attribute 必须大于 :value 个字符', 63 | 'array' => ':attribute 必须大于 :value 项', 64 | ], 65 | 'gte' => [ 66 | 'numeric' => ':attribute 必须大于等于 :value', 67 | 'file' => ':attribute 必须大于等于 :value kb', 68 | 'string' => ':attribute 必须大于等于 :value 个字符', 69 | 'array' => ':attribute 必须大于等于 :value 项', 70 | ], 71 | 'image' => ':attribute 必须是 jpg, jpeg, png, bmp 或者 gif 格式的图片', 72 | 'in' => '选定的 :attribute 是无效的', 73 | 'in_array' => ':attribute 字段不存在于 :other', 74 | 'integer' => ':attribute 必须是个整数', 75 | 'ip' => ':attribute 必须是一个合法的 IP 地址', 76 | 'ipv4' => ':attribute 必须是一个合法的 IPv4 地址', 77 | 'ipv6' => ':attribute 必须是一个合法的 IPv6 地址', 78 | 'json' => ':attribute 必须是一个合法的 JSON 字符串', 79 | 'list' => ':attribute 必须是一个数组列表', 80 | 'lt' => [ 81 | 'numeric' => ':attribute 必须小于 :value', 82 | 'file' => ':attribute 必须小于 :value kb', 83 | 'string' => ':attribute 必须小于 :value 个字符', 84 | 'array' => ':attribute 必须小于 :value 项', 85 | ], 86 | 'lte' => [ 87 | 'numeric' => ':attribute 必须小于等于 :value', 88 | 'file' => ':attribute 必须小于等于 :value kb', 89 | 'string' => ':attribute 必须小于等于 :value 个字符', 90 | 'array' => ':attribute 必须小于等于 :value 项', 91 | ], 92 | 'max' => [ 93 | 'numeric' => ':attribute 的最大值为 :max', 94 | 'file' => ':attribute 的最大为 :max kb', 95 | 'string' => ':attribute 的最大长度为 :max 字符', 96 | 'array' => ':attribute 至多有 :max 项', 97 | ], 98 | 'mimes' => ':attribute 的文件类型必须是 :values', 99 | 'mimetypes' => ':attribute 的文件MIME必须是 :values', 100 | 'min' => [ 101 | 'numeric' => ':attribute 的最小值为 :min', 102 | 'file' => ':attribute 大小至少为 :min kb', 103 | 'string' => ':attribute 的最小长度为 :min 字符', 104 | 'array' => ':attribute 至少有 :min 项', 105 | ], 106 | 'not_in' => '选定的 :attribute 是无效的', 107 | 'not_regex' => ':attribute 不能匹配给定的正则', 108 | 'numeric' => ':attribute 必须是数字', 109 | 'present' => ':attribute 字段必须存在', 110 | 'prohibits' => '必须提供 :attribute 字段', 111 | 'regex' => ':attribute 格式是无效的', 112 | 'required' => ':attribute 字段是必须的', 113 | 'required_if' => ':attribute 字段是必须的当 :other 是 :value', 114 | 'required_unless' => ':attribute 字段是必须的,除非 :other 是在 :values 中', 115 | 'required_with' => ':attribute 字段是必须的当 :values 是存在的', 116 | 'required_with_all' => ':attribute 字段是必须的当 :values 是存在的', 117 | 'required_without' => ':attribute 字段是必须的当 :values 是不存在的', 118 | 'required_without_all' => ':attribute 字段是必须的当 没有一个 :values 是存在的', 119 | 'same' => ':attribute 和 :other 必须匹配', 120 | 'size' => [ 121 | 'numeric' => ':attribute 必须是 :size', 122 | 'file' => ':attribute 必须是 :size kb', 123 | 'string' => ':attribute 必须是 :size 个字符', 124 | 'array' => ':attribute 必须包括 :size 项', 125 | ], 126 | 'starts_with' => ':attribute 必须以 :values 为开头', 127 | 'string' => ':attribute 必须是一个字符串', 128 | 'timezone' => ':attribute 必须是个有效的时区', 129 | 'unique' => ':attribute 已存在', 130 | 'uploaded' => ':attribute 上传失败', 131 | 'url' => ':attribute 无效的格式', 132 | 'uuid' => ':attribute 无效的UUID格式', 133 | 'max_if' => [ 134 | 'numeric' => '当 :other 为 :value 时 :attribute 不能大于 :max', 135 | 'file' => '当 :other 为 :value 时 :attribute 不能大于 :max kb', 136 | 'string' => '当 :other 为 :value 时 :attribute 不能大于 :max 个字符', 137 | 'array' => '当 :other 为 :value 时 :attribute 最多只有 :max 个单元', 138 | ], 139 | 'min_if' => [ 140 | 'numeric' => '当 :other 为 :value 时 :attribute 必须大于等于 :min', 141 | 'file' => '当 :other 为 :value 时 :attribute 大小不能小于 :min kb', 142 | 'string' => '当 :other 为 :value 时 :attribute 至少为 :min 个字符', 143 | 'array' => '当 :other 为 :value 时 :attribute 至少有 :min 个单元', 144 | ], 145 | 'between_if' => [ 146 | 'numeric' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max 之间', 147 | 'file' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max kb 之间', 148 | 'string' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max 个字符之间', 149 | 'array' => '当 :other 为 :value 时 :attribute 必须只有 :min - :max 个单元', 150 | ], 151 | 152 | /* 153 | |-------------------------------------------------------------------------- 154 | | Custom Validation Language Lines 155 | |-------------------------------------------------------------------------- 156 | | 157 | | Here you may specify custom validation messages for attributes using the 158 | | convention "attribute.rule" to name the lines. This makes it quick to 159 | | specify a specific custom language line for a given attribute rule. 160 | | 161 | */ 162 | 163 | 'custom' => [ 164 | 'attribute-name' => [ 165 | 'rule-name' => 'custom-message', 166 | ], 167 | ], 168 | 169 | /* 170 | |-------------------------------------------------------------------------- 171 | | Custom Validation Attributes 172 | |-------------------------------------------------------------------------- 173 | | 174 | | The following language lines are used to swap attribute place-holders 175 | | with something more reader friendly such as E-Mail Address instead 176 | | of "email". This simply helps us make messages a little cleaner. 177 | | 178 | */ 179 | 180 | 'attributes' => [], 181 | 'phone_number' => ':attribute 必须为一个有效的电话号码', 182 | 'telephone_number' => ':attribute 必须为一个有效的手机号码', 183 | 184 | 'chinese_word' => ':attribute 必须包含以下有效字符 (中文/英文,数字, 下划线)', 185 | 'sequential_array' => ':attribute 必须是一个有序数组', 186 | ]; 187 | -------------------------------------------------------------------------------- /src/ValidationRuleParser.php: -------------------------------------------------------------------------------- 1 | implicitAttributes = []; 52 | 53 | $rules = $this->explodeRules($rules); 54 | 55 | return (object) [ 56 | 'rules' => $rules, 57 | 'implicitAttributes' => $this->implicitAttributes, 58 | ]; 59 | } 60 | 61 | /** 62 | * Merge additional rules into a given attribute(s). 63 | * 64 | * @param array|string|Stringable $rules 65 | */ 66 | public function mergeRules(array $results, array|string $attribute, mixed $rules = []): array 67 | { 68 | if (is_array($attribute)) { 69 | foreach ($attribute as $innerAttribute => $innerRules) { 70 | $results = $this->mergeRulesForAttribute($results, $innerAttribute, $innerRules); 71 | } 72 | 73 | return $results; 74 | } 75 | 76 | return $this->mergeRulesForAttribute( 77 | $results, 78 | $attribute, 79 | $rules 80 | ); 81 | } 82 | 83 | /** 84 | * Extract the rule name and parameters from a rule. 85 | * 86 | * @param array|string $rules 87 | */ 88 | public static function parse(mixed $rules): array 89 | { 90 | if ($rules instanceof RuleContract) { 91 | return [$rules, []]; 92 | } 93 | 94 | if (is_array($rules)) { 95 | $rules = static::parseArrayRule($rules); 96 | } else { 97 | $rules = static::parseStringRule((string) $rules); 98 | } 99 | 100 | $rules[0] = static::normalizeRule($rules[0]); 101 | 102 | return $rules; 103 | } 104 | 105 | /** 106 | * Expand the conditional rules in the given array of rules. 107 | * @param mixed $rules 108 | */ 109 | public static function filterConditionalRules($rules, array $data = []): array 110 | { 111 | return collect($rules)->mapWithKeys(function ($attributeRules, $attribute) use ($data) { 112 | if (! is_array($attributeRules) 113 | && ! $attributeRules instanceof ConditionalRules) { 114 | return [$attribute => $attributeRules]; 115 | } 116 | 117 | if ($attributeRules instanceof ConditionalRules) { 118 | return [$attribute => $attributeRules->passes($data) 119 | ? array_filter($attributeRules->rules($data)) 120 | : array_filter($attributeRules->defaultRules($data)), ]; 121 | } 122 | 123 | return [$attribute => collect($attributeRules)->map(function ($rule) use ($data) { 124 | if (! $rule instanceof ConditionalRules) { 125 | return [$rule]; 126 | } 127 | 128 | return $rule->passes($data) ? $rule->rules($data) : $rule->defaultRules($data); 129 | })->filter()->flatten(1)->values()->all()]; 130 | })->all(); 131 | } 132 | 133 | /** 134 | * Explode the rules into an array of explicit rules. 135 | */ 136 | protected function explodeRules(array $rules): array 137 | { 138 | foreach ($rules as $key => $rule) { 139 | if (Str::contains((string) $key, '*')) { 140 | $rules = $this->explodeWildcardRules($rules, $key, [$rule]); 141 | 142 | unset($rules[$key]); 143 | } else { 144 | $rules[$key] = $this->explodeExplicitRule($rule); 145 | } 146 | } 147 | 148 | return $rules; 149 | } 150 | 151 | /** 152 | * Explode the explicit rule into an array if necessary. 153 | * 154 | * @param array|object|string $rule 155 | */ 156 | protected function explodeExplicitRule($rule): array 157 | { 158 | if (is_string($rule)) { 159 | return explode('|', $rule); 160 | } 161 | if (is_object($rule)) { 162 | return [$this->prepareRule($rule)]; 163 | } 164 | 165 | return array_map([$this, 'prepareRule'], $rule); 166 | } 167 | 168 | /** 169 | * Prepare the given rule for the Validator. 170 | * 171 | * @param mixed $rule 172 | * @return mixed 173 | */ 174 | protected function prepareRule($rule) 175 | { 176 | if ($rule instanceof Closure) { 177 | $rule = new ClosureValidationRule($rule); 178 | } 179 | 180 | if (! is_object($rule) 181 | || $rule instanceof RuleContract 182 | || ($rule instanceof Exists && $rule->queryCallbacks()) 183 | || ($rule instanceof Unique && $rule->queryCallbacks())) { 184 | return $rule; 185 | } 186 | 187 | return (string) $rule; 188 | } 189 | 190 | /** 191 | * Define a set of rules that apply to each element in an array attribute. 192 | * 193 | * @param array|string|Stringable $rules 194 | */ 195 | protected function explodeWildcardRules(array $results, string $attribute, mixed $rules): array 196 | { 197 | $pattern = str_replace('\*', '[^\.]*', preg_quote($attribute)); 198 | 199 | $data = ValidationData::initializeAndGatherData($attribute, $this->data); 200 | 201 | foreach ($data as $key => $value) { 202 | if (Str::startsWith($key, $attribute) || (bool) preg_match('/^' . $pattern . '\z/', $key)) { 203 | foreach ((array) $rules as $rule) { 204 | $this->implicitAttributes[$attribute][] = $key; 205 | 206 | $results = $this->mergeRules($results, $key, $rule); 207 | } 208 | } 209 | } 210 | 211 | return $results; 212 | } 213 | 214 | /** 215 | * Merge additional rules into a given attribute. 216 | * 217 | * @param array|string|Stringable $rules 218 | */ 219 | protected function mergeRulesForAttribute(array $results, string $attribute, mixed $rules): array 220 | { 221 | $merge = head($this->explodeRules([$rules])); 222 | 223 | $results[$attribute] = array_merge( 224 | isset($results[$attribute]) ? $this->explodeExplicitRule($results[$attribute]) : [], 225 | $merge 226 | ); 227 | 228 | return $results; 229 | } 230 | 231 | /** 232 | * Parse an array based rule. 233 | */ 234 | protected static function parseArrayRule(array $rules): array 235 | { 236 | return [StrCache::studly(trim((string) Arr::get($rules, 0))), array_slice($rules, 1)]; 237 | } 238 | 239 | /** 240 | * Parse a string based rule. 241 | */ 242 | protected static function parseStringRule(string $rules): array 243 | { 244 | $parameters = []; 245 | 246 | // The format for specifying validation rules and parameters follows an 247 | // easy {rule}:{parameters} formatting convention. For instance the 248 | // rule "Max:3" states that the value may only be three letters. 249 | if (str_contains($rules, ':')) { 250 | [$rules, $parameter] = explode(':', $rules, 2); 251 | 252 | $parameters = static::parseParameters($rules, $parameter); 253 | } 254 | 255 | return [StrCache::studly(trim($rules)), $parameters]; 256 | } 257 | 258 | /** 259 | * Parse a parameter list. 260 | */ 261 | protected static function parseParameters(string $rule, string $parameter): array 262 | { 263 | $rule = strtolower($rule); 264 | 265 | if (in_array($rule, ['regex', 'not_regex', 'notregex'], true)) { 266 | return [$parameter]; 267 | } 268 | 269 | return str_getcsv($parameter, escape: '\\'); 270 | } 271 | 272 | /** 273 | * Normalizes a rule so that we can accept short types. 274 | */ 275 | protected static function normalizeRule(string $rule): string 276 | { 277 | return match ($rule) { 278 | 'Int' => 'Integer', 279 | 'Bool' => 'Boolean', 280 | default => $rule, 281 | }; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/Rules/Password.php: -------------------------------------------------------------------------------- 1 | min = max((int) $min, 1); 106 | } 107 | 108 | /** 109 | * Set the default callback to be used for determining a password's default rules. 110 | * 111 | * If no arguments are passed, the default password rule configuration will be returned. 112 | */ 113 | public static function defaults(null|callable|self|string $callback = null): ?static 114 | { 115 | if (is_null($callback)) { 116 | return static::default(); 117 | } 118 | 119 | if (! is_callable($callback) && ! $callback instanceof static) { 120 | throw new InvalidArgumentException('The given callback should be callable or an instance of ' . static::class); 121 | } 122 | 123 | static::$defaultCallback = $callback; 124 | return null; 125 | } 126 | 127 | /** 128 | * Get the default configuration of the password rule. 129 | */ 130 | public static function default() 131 | { 132 | $password = is_callable(static::$defaultCallback) 133 | ? call_user_func(static::$defaultCallback) 134 | : static::$defaultCallback; 135 | 136 | return $password instanceof Rule ? $password : static::min(8); 137 | } 138 | 139 | /** 140 | * Get the default configuration of the password rule and mark the field as required. 141 | */ 142 | public static function required(): array 143 | { 144 | return ['required', static::default()]; 145 | } 146 | 147 | /** 148 | * Get the default configuration of the password rule and mark the field as sometimes being required. 149 | */ 150 | public static function sometimes(): array 151 | { 152 | return ['sometimes', static::default()]; 153 | } 154 | 155 | /** 156 | * Set the performing validator. 157 | */ 158 | public function setValidator(Validator $validator): static 159 | { 160 | $this->validator = $validator; 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * Set the data under validation. 167 | */ 168 | public function setData(array $data): static 169 | { 170 | $this->data = $data; 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * Set the minimum size of the password. 177 | * @param mixed $size 178 | */ 179 | public static function min($size): static 180 | { 181 | return new static($size); 182 | } 183 | 184 | /** 185 | * Set the maximum size of the password. 186 | * @param mixed $size 187 | */ 188 | public function max($size): static 189 | { 190 | $this->max = $size; 191 | 192 | return $this; 193 | } 194 | 195 | /** 196 | * Ensures the password has not been compromised in data leaks. 197 | */ 198 | public function uncompromised(int $threshold = 0): static 199 | { 200 | $this->uncompromised = true; 201 | 202 | $this->compromisedThreshold = $threshold; 203 | 204 | return $this; 205 | } 206 | 207 | /** 208 | * Makes the password require at least one uppercase and one lowercase letter. 209 | */ 210 | public function mixedCase(): static 211 | { 212 | $this->mixedCase = true; 213 | 214 | return $this; 215 | } 216 | 217 | /** 218 | * Makes the password require at least one letter. 219 | */ 220 | public function letters(): static 221 | { 222 | $this->letters = true; 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * Makes the password require at least one number. 229 | */ 230 | public function numbers(): static 231 | { 232 | $this->numbers = true; 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * Makes the password require at least one symbol. 239 | */ 240 | public function symbols(): static 241 | { 242 | $this->symbols = true; 243 | 244 | return $this; 245 | } 246 | 247 | /** 248 | * Specify additional validation rules that should be merged with the default rules during validation. 249 | */ 250 | public function rules(array|Closure|Rule|string $rules): static 251 | { 252 | $this->customRules = Arr::wrap($rules); 253 | 254 | return $this; 255 | } 256 | 257 | /** 258 | * Determine if the validation rule passes. 259 | */ 260 | public function passes(string $attribute, mixed $value): bool 261 | { 262 | $this->messages = []; 263 | 264 | $container = $this->getContainer(); 265 | 266 | $validator = $container->get(ValidatorFactory::class)->make( 267 | $this->data, 268 | [$attribute => [ 269 | 'string', 270 | 'min:' . $this->min, 271 | ...($this->max ? ['max:' . $this->max] : []), 272 | ...$this->customRules, 273 | ]], 274 | $this->validator->customMessages, 275 | $this->validator->customAttributes 276 | )->after(function ($validator) use ($attribute, $value) { 277 | if (! is_string($value)) { 278 | return; 279 | } 280 | 281 | if ($this->mixedCase && ! preg_match('/(\p{Ll}+.*\p{Lu})|(\p{Lu}+.*\p{Ll})/u', $value)) { 282 | $validator->addFailure($attribute, 'password.mixed'); 283 | } 284 | 285 | if ($this->letters && ! preg_match('/\pL/u', $value)) { 286 | $validator->addFailure($attribute, 'password.letters'); 287 | } 288 | 289 | if ($this->symbols && ! preg_match('/\p{Z}|\p{S}|\p{P}/u', $value)) { 290 | $validator->addFailure($attribute, 'password.symbols'); 291 | } 292 | 293 | if ($this->numbers && ! preg_match('/\pN/u', $value)) { 294 | $validator->addFailure($attribute, 'password.numbers'); 295 | } 296 | }); 297 | 298 | if ($validator->fails()) { 299 | return $this->fail($validator->messages()->all()); 300 | } 301 | 302 | if ($this->uncompromised && ! $container->get(UncompromisedVerifier::class)->verify([ 303 | 'value' => $value, 304 | 'threshold' => $this->compromisedThreshold, 305 | ])) { 306 | $validator->addFailure($attribute, 'password.uncompromised'); 307 | 308 | return $this->fail($validator->messages()->all()); 309 | } 310 | 311 | return true; 312 | } 313 | 314 | /** 315 | * Get the validation error message. 316 | */ 317 | public function message(): array|string 318 | { 319 | return $this->messages; 320 | } 321 | 322 | public function getContainer(): ContainerInterface 323 | { 324 | return $this->container ?? ApplicationContext::getContainer(); 325 | } 326 | 327 | public function setContainer(ContainerInterface $container): static 328 | { 329 | $this->container = $container; 330 | 331 | return $this; 332 | } 333 | 334 | /** 335 | * Adds the given failures, and return false. 336 | */ 337 | protected function fail(array|string $messages): bool 338 | { 339 | $this->messages = array_merge($this->messages, Arr::wrap($messages)); 340 | 341 | return false; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/Rules/File.php: -------------------------------------------------------------------------------- 1 | |string $mimetypes 128 | */ 129 | public static function types(array|string $mimetypes): static 130 | { 131 | return \Hyperf\Tappable\tap(new static(), fn ($file) => $file->allowedMimetypes = (array) $mimetypes); 132 | } 133 | 134 | /** 135 | * Limit the uploaded file to the given file extensions. 136 | * 137 | * @param array|string $extensions 138 | * @return $this 139 | */ 140 | public function extensions($extensions): static 141 | { 142 | $this->allowedExtensions = (array) $extensions; 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * Indicate that the uploaded file should be exactly a certain size in kilobytes. 149 | * 150 | * @return $this 151 | */ 152 | public function size(int|string $size): static 153 | { 154 | $this->minimumFileSize = $this->toKilobytes($size); 155 | $this->maximumFileSize = $this->minimumFileSize; 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * Indicate that the uploaded file should be between a minimum and maximum size in kilobytes. 162 | * 163 | * @return $this 164 | */ 165 | public function between(int|string $minSize, int|string $maxSize): static 166 | { 167 | $this->minimumFileSize = $this->toKilobytes($minSize); 168 | $this->maximumFileSize = $this->toKilobytes($maxSize); 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * Indicate that the uploaded file should be no less than the given number of kilobytes. 175 | * 176 | * @return $this 177 | */ 178 | public function min(int|string $size): static 179 | { 180 | $this->minimumFileSize = (int) $this->toKilobytes($size); 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Indicate that the uploaded file should be no more than the given number of kilobytes. 187 | * 188 | * @return $this 189 | */ 190 | public function max(int|string $size): static 191 | { 192 | $this->maximumFileSize = (int) $this->toKilobytes($size); 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * Specify additional validation rules that should be merged with the default rules during validation. 199 | * 200 | * @param mixed $rules 201 | * @return $this 202 | */ 203 | public function rules($rules): static 204 | { 205 | $this->customRules = array_merge($this->customRules, Arr::wrap($rules)); 206 | 207 | return $this; 208 | } 209 | 210 | /** 211 | * Determine if the validation rule passes. 212 | */ 213 | public function passes(string $attribute, mixed $value): bool 214 | { 215 | $this->messages = []; 216 | 217 | $test = $this->buildValidationRules(); 218 | 219 | $validator = ApplicationContext::getContainer()->get(ValidatorFactory::class)->make( 220 | $this->data, 221 | [$attribute => $test], 222 | $this->validator->customMessages, 223 | $this->validator->customAttributes 224 | ); 225 | 226 | if ($validator->fails()) { 227 | return $this->fail($validator->messages()->all()); 228 | } 229 | 230 | return true; 231 | } 232 | 233 | /** 234 | * Get the validation error message. 235 | */ 236 | public function message(): array|string 237 | { 238 | return $this->messages; 239 | } 240 | 241 | /** 242 | * Set the current validator. 243 | * 244 | * @return $this 245 | */ 246 | public function setValidator(Validator $validator): static 247 | { 248 | $this->validator = $validator; 249 | 250 | return $this; 251 | } 252 | 253 | /** 254 | * Set the current data under validation. 255 | * 256 | * @return $this 257 | */ 258 | public function setData(array $data): static 259 | { 260 | $this->data = $data; 261 | 262 | return $this; 263 | } 264 | 265 | /** 266 | * Convert a potentially human-friendly file size to kilobytes. 267 | * 268 | * @param int|string $size 269 | * @return mixed 270 | */ 271 | protected function toKilobytes($size) 272 | { 273 | if (! is_string($size)) { 274 | return $size; 275 | } 276 | 277 | $value = floatval($size); 278 | 279 | return round(match (true) { 280 | Str::endsWith($size, 'kb') => $value * 1, 281 | Str::endsWith($size, 'mb') => $value * 1000, 282 | Str::endsWith($size, 'gb') => $value * 1000000, 283 | Str::endsWith($size, 'tb') => $value * 1000000000, 284 | default => throw new InvalidArgumentException('Invalid file size suffix.'), 285 | }); 286 | } 287 | 288 | /** 289 | * Build the array of underlying validation rules based on the current state. 290 | * 291 | * @return array 292 | */ 293 | protected function buildValidationRules() 294 | { 295 | $rules = ['file']; 296 | 297 | $rules = array_merge($rules, $this->buildMimetypes()); 298 | 299 | if (! empty($this->allowedExtensions)) { 300 | $rules[] = 'extensions:' . implode(',', array_map('strtolower', $this->allowedExtensions)); 301 | } 302 | 303 | $rules[] = match (true) { 304 | is_null($this->minimumFileSize) && is_null($this->maximumFileSize) => null, 305 | is_null($this->maximumFileSize) => "min:{$this->minimumFileSize}", 306 | is_null($this->minimumFileSize) => "max:{$this->maximumFileSize}", 307 | $this->minimumFileSize !== $this->maximumFileSize => "between:{$this->minimumFileSize},{$this->maximumFileSize}", 308 | default => "size:{$this->minimumFileSize}", 309 | }; 310 | 311 | return array_merge(array_filter($rules), $this->customRules); 312 | } 313 | 314 | /** 315 | * Separate the given mimetypes from extensions and return an array of correct rules to validate against. 316 | * 317 | * @return array 318 | */ 319 | protected function buildMimetypes() 320 | { 321 | if (count($this->allowedMimetypes) === 0) { 322 | return []; 323 | } 324 | 325 | $rules = []; 326 | 327 | $mimetypes = array_filter( 328 | $this->allowedMimetypes, 329 | fn ($type) => str_contains($type, '/') 330 | ); 331 | 332 | $mimes = array_diff($this->allowedMimetypes, $mimetypes); 333 | 334 | if (count($mimetypes) > 0) { 335 | $rules[] = 'mimetypes:' . implode(',', $mimetypes); 336 | } 337 | 338 | if (count($mimes) > 0) { 339 | $rules[] = 'mimes:' . implode(',', $mimes); 340 | } 341 | 342 | return $rules; 343 | } 344 | 345 | /** 346 | * Adds the given failures, and return false. 347 | * 348 | * @param array|string $messages 349 | * @return bool 350 | */ 351 | protected function fail($messages) 352 | { 353 | $messages = collect(Arr::wrap($messages))->map(function ($message) { 354 | return $this->validator->getTranslator()->get($message); 355 | })->all(); 356 | 357 | $this->messages = array_merge($this->messages, $messages); 358 | 359 | return false; 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /publish/en/validation.php: -------------------------------------------------------------------------------- 1 | 'The :attribute must be accepted.', 25 | 'accepted_if' => 'The :attribute must be accepted when :other is :value.', 26 | 'active_url' => 'The :attribute is not a valid URL.', 27 | 'after' => 'The :attribute must be a date after :date.', 28 | 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', 29 | 'alpha' => 'The :attribute may only contain letters.', 30 | 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 31 | 'alpha_num' => 'The :attribute may only contain letters and numbers.', 32 | 'array' => 'The :attribute must be an array.', 33 | 'ascii' => 'The :attribute must only contain single-byte alphanumeric characters and symbols.', 34 | 'before' => 'The :attribute must be a date before :date.', 35 | 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', 36 | 'between' => [ 37 | 'numeric' => 'The :attribute must be between :min and :max.', 38 | 'file' => 'The :attribute must be between :min and :max kilobytes.', 39 | 'string' => 'The :attribute must be between :min and :max characters.', 40 | 'array' => 'The :attribute must have between :min and :max items.', 41 | ], 42 | 'boolean' => 'The :attribute field must be true or false.', 43 | 'confirmed' => 'The :attribute confirmation does not match.', 44 | 'contains' => 'The :attribute is missing a required value.', 45 | 'date' => 'The :attribute is not a valid date.', 46 | 'date_equals' => 'The :attribute must be a date equal to :date.', 47 | 'date_format' => 'The :attribute does not match the format :format.', 48 | 'decimal' => 'The :attribute must have :decimal decimal places.', 49 | 'declined' => 'The :attribute must be declined.', 50 | 'declined_if' => 'The :attribute must be declined when :other is :value.', 51 | 'different' => 'The :attribute and :other must be different.', 52 | 'digits' => 'The :attribute must be :digits digits.', 53 | 'digits_between' => 'The :attribute must be between :min and :max digits.', 54 | 'dimensions' => 'The :attribute has invalid image dimensions.', 55 | 'distinct' => 'The :attribute field has a duplicate value.', 56 | 'doesnt_end_with' => 'The :attribute must not end with one of the following: :values.', 57 | 'doesnt_start_with' => 'The :attribute must not start with one of the following: :values.', 58 | 'email' => 'The :attribute must be a valid email address.', 59 | 'ends_with' => 'The :attribute must end with one of the following: :values.', 60 | 'enum' => 'The selected :attribute is invalid.', 61 | 'exclude' => 'The :attribute field is excluded.', 62 | 'exclude_if' => 'The :attribute field is excluded when :other is :value.', 63 | 'exclude_unless' => 'The :attribute field is excluded unless :other is in :values.', 64 | 'exclude_with' => 'The :attribute field is excluded when :values is present.', 65 | 'exclude_without' => 'The :attribute field is excluded when :values is not present.', 66 | 'exists' => 'The selected :attribute is invalid.', 67 | 'extensions' => 'The :attribute must have one of the following extensions: :values.', 68 | 'file' => 'The :attribute must be a file.', 69 | 'filled' => 'The :attribute field is required.', 70 | 'gt' => [ 71 | 'numeric' => 'The :attribute must be greater than :value', 72 | 'file' => 'The :attribute must be greater than :value kb', 73 | 'string' => 'The :attribute must be greater than :value characters', 74 | 'array' => 'The :attribute must be greater than :value items', 75 | ], 76 | 'gte' => [ 77 | 'numeric' => 'The :attribute must be great than or equal to :value', 78 | 'file' => 'The :attribute must be great than or equal to :value kb', 79 | 'string' => 'The :attribute must be great than or equal to :value characters', 80 | 'array' => 'The :attribute must be great than or equal to :value items', 81 | ], 82 | 'hex_color' => 'The :attribute must be a valid hexadecimal color.', 83 | 'image' => 'The :attribute must be an image.', 84 | 'in' => 'The selected :attribute is invalid.', 85 | 'in_array' => 'The :attribute field does not exist in :other.', 86 | 'integer' => 'The :attribute must be an integer.', 87 | 'ip' => 'The :attribute must be a valid IP address.', 88 | 'ipv4' => 'The :attribute must be a valid IPv4 address.', 89 | 'ipv6' => 'The :attribute must be a valid IPv6 address.', 90 | 'json' => 'The :attribute must be a valid JSON string.', 91 | 'list' => 'The :attribute must be a list.', 92 | 'lowercase' => 'The :attribute must be lowercase.', 93 | 'lt' => [ 94 | 'numeric' => 'The :attribute must be less than :value', 95 | 'file' => 'The :attribute must be less than :value kb', 96 | 'string' => 'The :attribute must be less than :value characters', 97 | 'array' => 'The :attribute must be less than :value items', 98 | ], 99 | 'lte' => [ 100 | 'numeric' => 'The :attribute must be less than or equal to :value', 101 | 'file' => 'The :attribute must be less than or equal to :value kb', 102 | 'string' => 'The :attribute must be less than or equal to :value characters', 103 | 'array' => 'The :attribute must be less than or equal to :value items', 104 | ], 105 | 'mac_address' => 'The :attribute must be a valid MAC address.', 106 | 'max' => [ 107 | 'numeric' => 'The :attribute may not be greater than :max.', 108 | 'file' => 'The :attribute may not be greater than :max kilobytes.', 109 | 'string' => 'The :attribute may not be greater than :max characters.', 110 | 'array' => 'The :attribute may not have more than :max items.', 111 | ], 112 | 'max_digits' => 'The :attribute must not have more than :max digits.', 113 | 'mimes' => 'The :attribute must be a file of type: :values.', 114 | 'mimetypes' => 'The :attribute must be a file of type: :values.', 115 | 'min' => [ 116 | 'numeric' => 'The :attribute must be at least :min.', 117 | 'file' => 'The :attribute must be at least :min kilobytes.', 118 | 'string' => 'The :attribute must be at least :min characters.', 119 | 'array' => 'The :attribute must have at least :min items.', 120 | ], 121 | 'min_digits' => 'The :attribute must have at least :min digits.', 122 | 'missing' => 'The :attribute must be missing.', 123 | 'missing_if' => 'The :attribute must be missing when :other is :value.', 124 | 'missing_unless' => 'The :attribute must be missing unless :other is :value.', 125 | 'missing_with' => 'The :attribute must be missing when :values is present.', 126 | 'missing_with_all' => 'The :attribute must be missing when :values are present.', 127 | 'multiple_of' => 'The :attribute must be a multiple of :value.', 128 | 'not_in' => 'The selected :attribute is invalid.', 129 | 'not_regex' => 'The :attribute cannot match a given regular rule.', 130 | 'numeric' => 'The :attribute must be a number.', 131 | 'present' => 'The :attribute field must be present.', 132 | 'prohibits' => 'The :attribute field must be present.', 133 | 'regex' => 'The :attribute format is invalid.', 134 | 'required' => 'The :attribute field is required.', 135 | 'required_if' => 'The :attribute field is required when :other is :value.', 136 | 'required_unless' => 'The :attribute field is required unless :other is in :values.', 137 | 'required_with' => 'The :attribute field is required when :values is present.', 138 | 'required_with_all' => 'The :attribute field is required when :values is present.', 139 | 'required_without' => 'The :attribute field is required when :values is not present.', 140 | 'required_without_all' => 'The :attribute field is required when none of :values are present.', 141 | 'same' => 'The :attribute and :other must match.', 142 | 'size' => [ 143 | 'numeric' => 'The :attribute must be :size.', 144 | 'file' => 'The :attribute must be :size kilobytes.', 145 | 'string' => 'The :attribute must be :size characters.', 146 | 'array' => 'The :attribute must contain :size items.', 147 | ], 148 | 'starts_with' => 'The :attribute must be start with :values ', 149 | 'string' => 'The :attribute must be a string.', 150 | 'timezone' => 'The :attribute must be a valid zone.', 151 | 'unique' => 'The :attribute has already been taken.', 152 | 'uploaded' => 'The :attribute failed to upload.', 153 | 'uppercase' => 'The :attribute must be uppercase.', 154 | 'url' => 'The :attribute format is invalid.', 155 | 'ulid' => 'The :attribute must be a valid ULID.', 156 | 'uuid' => 'The :attribute is invalid UUID.', 157 | 'max_if' => [ 158 | 'numeric' => 'The :attribute may not be greater than :max when :other is :value.', 159 | 'file' => 'The :attribute may not be greater than :max kilobytes when :other is :value.', 160 | 'string' => 'The :attribute may not be greater than :max characters when :other is :value.', 161 | 'array' => 'The :attribute may not have more than :max items when :other is :value.', 162 | ], 163 | 'min_if' => [ 164 | 'numeric' => 'The :attribute must be at least :min when :other is :value.', 165 | 'file' => 'The :attribute must be at least :min kilobytes when :other is :value.', 166 | 'string' => 'The :attribute must be at least :min characters when :other is :value.', 167 | 'array' => 'The :attribute must have at least :min items when :other is :value.', 168 | ], 169 | 'between_if' => [ 170 | 'numeric' => 'The :attribute must be between :min and :max when :other is :value.', 171 | 'file' => 'The :attribute must be between :min and :max kilobytes when :other is :value.', 172 | 'string' => 'The :attribute must be between :min and :max characters when :other is :value.', 173 | 'array' => 'The :attribute must have between :min and :max items when :other is :value.', 174 | ], 175 | 176 | /* 177 | |-------------------------------------------------------------------------- 178 | | Custom Validation Language Lines 179 | |-------------------------------------------------------------------------- 180 | | 181 | | Here you may specify custom validation messages for attributes using the 182 | | convention "attribute.rule" to name the lines. This makes it quick to 183 | | specify a specific custom language line for a given attribute rule. 184 | | 185 | */ 186 | 187 | 'custom' => [ 188 | 'attribute-name' => [ 189 | 'rule-name' => 'custom-message', 190 | ], 191 | ], 192 | 193 | /* 194 | |-------------------------------------------------------------------------- 195 | | Custom Validation Attributes 196 | |-------------------------------------------------------------------------- 197 | | 198 | | The following language lines are used to swap attribute place-holders 199 | | with something more reader friendly such as E-Mail Address instead 200 | | of "email". This simply helps us make messages a little cleaner. 201 | | 202 | */ 203 | 204 | 'attributes' => [], 205 | 'phone_number' => 'The :attribute must be a valid phone number', 206 | 'telephone_number' => 'The :attribute must be a valid telephone number', 207 | 208 | 'chinese_word' => 'The :attribute must contain valid characters(chinese/english character, number, underscore)', 209 | 'sequential_array' => 'The :attribute must be sequential array', 210 | ]; 211 | -------------------------------------------------------------------------------- /src/Concerns/FormatsMessages.php: -------------------------------------------------------------------------------- 1 | replaceAttributePlaceholder( 34 | $message, 35 | $this->getDisplayableAttribute($attribute) 36 | ); 37 | 38 | $message = $this->replaceInputPlaceholder($message, $attribute); 39 | 40 | if (isset($this->replacers[StrCache::snake($rule)])) { 41 | return $this->callReplacer($message, $attribute, StrCache::snake($rule), $parameters, $this); 42 | } 43 | if (method_exists($this, $replacer = "replace{$rule}")) { 44 | return $this->{$replacer}($message, $attribute, $rule, $parameters); 45 | } 46 | 47 | return $message; 48 | } 49 | 50 | /** 51 | * Get the displayable name of the attribute. 52 | */ 53 | public function getDisplayableAttribute(string $attribute): string 54 | { 55 | $primaryAttribute = $this->getPrimaryAttribute($attribute); 56 | 57 | $expectedAttributes = $attribute != $primaryAttribute 58 | ? [$attribute, $primaryAttribute] : [$attribute]; 59 | 60 | foreach ($expectedAttributes as $name) { 61 | // The developer may dynamically specify the array of custom attributes on this 62 | // validator instance. If the attribute exists in this array it is used over 63 | // the other ways of pulling the attribute name for this given attributes. 64 | if (isset($this->customAttributes[$name])) { 65 | return $this->customAttributes[$name]; 66 | } 67 | 68 | // We allow for a developer to specify language lines for any attribute in this 69 | // application, which allows flexibility for displaying a unique displayable 70 | // version of the attribute name instead of the name used in an HTTP POST. 71 | if ($line = $this->getAttributeFromTranslations($name)) { 72 | return $line; 73 | } 74 | } 75 | 76 | // When no language line has been specified for the attribute and it is also 77 | // an implicit attribute we will display the raw attribute's name and not 78 | // modify it with any of these replacements before we display the name. 79 | if (isset($this->implicitAttributes[$primaryAttribute])) { 80 | return $attribute; 81 | } 82 | 83 | return str_replace('_', ' ', StrCache::snake($attribute)); 84 | } 85 | 86 | /** 87 | * Get the displayable name of the value. 88 | * 89 | * @param mixed $value 90 | */ 91 | public function getDisplayableValue(string $attribute, $value): string 92 | { 93 | if (isset($this->customValues[$attribute][$value])) { 94 | return $this->customValues[$attribute][$value]; 95 | } 96 | 97 | if (is_array($value)) { 98 | return 'array'; 99 | } 100 | 101 | $key = "validation.values.{$attribute}.{$value}"; 102 | 103 | if (($line = $this->translator->trans($key)) !== $key) { 104 | return $line; 105 | } 106 | 107 | if (is_bool($value)) { 108 | return $value ? 'true' : 'false'; 109 | } 110 | 111 | if (is_null($value)) { 112 | return 'empty'; 113 | } 114 | 115 | return (string) $value; 116 | } 117 | 118 | /** 119 | * Get the validation message for an attribute and rule. 120 | */ 121 | protected function getMessage(string $attribute, string $rule): string 122 | { 123 | $inlineMessage = $this->getInlineMessage($attribute, $rule); 124 | 125 | // First we will retrieve the custom message for the validation rule if one 126 | // exists. If a custom validation message is being used we'll return the 127 | // custom message, otherwise we'll keep searching for a valid message. 128 | if (! is_null($inlineMessage)) { 129 | return $inlineMessage; 130 | } 131 | 132 | $lowerRule = StrCache::snake($rule); 133 | 134 | $customMessage = $this->getCustomMessageFromTranslator( 135 | $customKey = "validation.custom.{$attribute}.{$lowerRule}" 136 | ); 137 | 138 | // First we check for a custom defined validation message for the attribute 139 | // and rule. This allows the developer to specify specific messages for 140 | // only some attributes and rules that need to get specially formed. 141 | if ($customMessage !== $customKey) { 142 | return $customMessage; 143 | } 144 | 145 | // If the rule being validated is a "size" rule, we will need to gather the 146 | // specific error message for the type of attribute being validated such 147 | // as a number, file or string which all have different message types. 148 | if (in_array($rule, $this->sizeRules)) { 149 | return $this->getSizeMessage($attribute, $rule); 150 | } 151 | 152 | // Finally, if no developer specified messages have been set, and no other 153 | // special messages apply for this rule, we will just pull the default 154 | // messages out of the translator service for this validation rule. 155 | $key = "validation.{$lowerRule}"; 156 | 157 | if ($key != ($value = $this->translator->trans($key))) { 158 | return $value; 159 | } 160 | 161 | return $this->getFromLocalArray( 162 | $attribute, 163 | $lowerRule, 164 | $this->fallbackMessages 165 | ) ?: $key; 166 | } 167 | 168 | /** 169 | * Get the proper inline error message for standard and size rules. 170 | * 171 | * @return null|string 172 | */ 173 | protected function getInlineMessage(string $attribute, string $rule) 174 | { 175 | $inlineEntry = $this->getFromLocalArray($attribute, StrCache::snake($rule)); 176 | 177 | return is_array($inlineEntry) && in_array($rule, $this->sizeRules) 178 | ? $inlineEntry[$this->getAttributeType($attribute)] 179 | : $inlineEntry; 180 | } 181 | 182 | /** 183 | * Get the inline message for a rule if it exists. 184 | * 185 | * @param null|array $source 186 | * @return null|string 187 | */ 188 | protected function getFromLocalArray(string $attribute, string $lowerRule, $source = null) 189 | { 190 | $source = $source ?: $this->customMessages; 191 | 192 | $keys = ["{$attribute}.{$lowerRule}", $lowerRule]; 193 | 194 | // First we will check for a custom message for an attribute specific rule 195 | // message for the fields, then we will check for a general custom line 196 | // that is not attribute specific. If we find either we'll return it. 197 | foreach ($keys as $key) { 198 | foreach (array_keys($source) as $sourceKey) { 199 | if (Str::is($sourceKey, $key)) { 200 | return $source[$sourceKey]; 201 | } 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * Get the custom error message from translator. 208 | */ 209 | protected function getCustomMessageFromTranslator(string $key): string 210 | { 211 | if (($message = $this->translator->trans($key)) !== $key) { 212 | return $message; 213 | } 214 | 215 | // If an exact match was not found for the key, we will collapse all of these 216 | // messages and loop through them and try to find a wildcard match for the 217 | // given key. Otherwise, we will simply return the key's value back out. 218 | $shortKey = preg_replace( 219 | '/^validation\.custom\./', 220 | '', 221 | $key 222 | ); 223 | 224 | return $this->getWildcardCustomMessages(Arr::dot( 225 | (array) $this->translator->trans('validation.custom') 226 | ), $shortKey, $key); 227 | } 228 | 229 | /** 230 | * Check the given messages for a wildcard key. 231 | */ 232 | protected function getWildcardCustomMessages(array $messages, string $search, string $default): string 233 | { 234 | foreach ($messages as $key => $message) { 235 | if ($search === $key || (Str::contains((string) $key, ['*']) && Str::is($key, $search))) { 236 | return $message; 237 | } 238 | } 239 | 240 | return $default; 241 | } 242 | 243 | /** 244 | * Get the proper error message for an attribute and size rule. 245 | */ 246 | protected function getSizeMessage(string $attribute, string $rule): string 247 | { 248 | $lowerRule = StrCache::snake($rule); 249 | 250 | // There are three different types of size validations. The attribute may be 251 | // either a number, file, or string so we will check a few things to know 252 | // which type of value it is and return the correct line for that type. 253 | $type = $this->getAttributeType($attribute); 254 | 255 | $key = "validation.{$lowerRule}.{$type}"; 256 | 257 | return $this->translator->trans($key); 258 | } 259 | 260 | /** 261 | * Get the data type of the given attribute. 262 | */ 263 | protected function getAttributeType(string $attribute): string 264 | { 265 | // We assume that the attributes present in the file array are files so that 266 | // means that if the attribute does not have a numeric rule and the files 267 | // list doesn't have it we'll just consider it a string by elimination. 268 | if ($this->hasRule($attribute, $this->numericRules)) { 269 | return 'numeric'; 270 | } 271 | if ($this->hasRule($attribute, ['Array'])) { 272 | return 'array'; 273 | } 274 | if ($this->getValue($attribute) instanceof UploadedFile) { 275 | return 'file'; 276 | } 277 | 278 | return 'string'; 279 | } 280 | 281 | /** 282 | * Get the given attribute from the attribute translations. 283 | */ 284 | protected function getAttributeFromTranslations(string $name): string 285 | { 286 | return (string) Arr::get($this->translator->trans('validation.attributes'), $name); 287 | } 288 | 289 | /** 290 | * Replace the :attribute placeholder in the given message. 291 | */ 292 | protected function replaceAttributePlaceholder(string $message, string $value): string 293 | { 294 | return str_replace( 295 | [':attribute', ':ATTRIBUTE', ':Attribute'], 296 | [$value, Str::upper($value), Str::ucfirst($value)], 297 | $message 298 | ); 299 | } 300 | 301 | /** 302 | * Replace the :input placeholder in the given message. 303 | */ 304 | protected function replaceInputPlaceholder(string $message, string $attribute): string 305 | { 306 | $actualValue = $this->getValue($attribute); 307 | 308 | if (is_scalar($actualValue) || is_null($actualValue)) { 309 | $message = str_replace(':input', $this->getDisplayableValue($attribute, $actualValue), $message); 310 | } 311 | 312 | return $message; 313 | } 314 | 315 | /** 316 | * Transform an array of attributes to their displayable form. 317 | */ 318 | protected function getAttributeList(array $values): array 319 | { 320 | $attributes = []; 321 | 322 | // For each attribute in the list we will simply get its displayable form as 323 | // this is convenient when replacing lists of parameters like some of the 324 | // replacement functions do when formatting out the validation message. 325 | foreach ($values as $key => $value) { 326 | $attributes[$key] = $this->getDisplayableAttribute($value); 327 | } 328 | 329 | return $attributes; 330 | } 331 | 332 | /** 333 | * Call a custom validator message replacer. 334 | */ 335 | protected function callReplacer(string $message, string $attribute, string $rule, array $parameters, Validator $validator): ?string 336 | { 337 | $callback = $this->replacers[$rule]; 338 | 339 | if ($callback instanceof Closure) { 340 | return call_user_func_array($callback, func_get_args()); 341 | } 342 | if (is_string($callback)) { 343 | return $this->callClassBasedReplacer($callback, $message, $attribute, $rule, $parameters, $validator); 344 | } 345 | } 346 | 347 | /** 348 | * Call a class based validator message replacer. 349 | * 350 | * @param Validator $validator 351 | */ 352 | protected function callClassBasedReplacer(string $callback, string $message, string $attribute, string $rule, array $parameters, $validator): string 353 | { 354 | [$class, $method] = Str::parseCallback($callback, 'replace'); 355 | 356 | return call_user_func_array([make($class), $method], array_slice(func_get_args(), 1)); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/Concerns/ReplacesAttributes.php: -------------------------------------------------------------------------------- 1 | getDisplayableValue($parameters[0], Arr::get($this->data, $parameters[0])); 25 | 26 | $parameters[0] = $this->getDisplayableAttribute($parameters[0]); 27 | 28 | return str_replace([':other', ':value'], $parameters, $message); 29 | } 30 | 31 | /** 32 | * Replace all place-holders for the declined_if rule. 33 | */ 34 | protected function replaceDeclinedIf(string $message, string $attribute, string $rule, array $parameters): string 35 | { 36 | $parameters[1] = $this->getDisplayableValue($parameters[0], Arr::get($this->data, $parameters[0])); 37 | 38 | $parameters[0] = $this->getDisplayableAttribute($parameters[0]); 39 | 40 | return str_replace([':other', ':value'], $parameters, $message); 41 | } 42 | 43 | /** 44 | * Replace all place-holders for the between rule. 45 | */ 46 | protected function replaceBetween(string $message, string $attribute, string $rule, array $parameters): string 47 | { 48 | return str_replace([':min', ':max'], $parameters, $message); 49 | } 50 | 51 | /** 52 | * Replace all place-holders for the date_format rule. 53 | */ 54 | protected function replaceDateFormat(string $message, string $attribute, string $rule, array $parameters): string 55 | { 56 | return str_replace(':format', $parameters[0], $message); 57 | } 58 | 59 | /** 60 | * Replace all place-holders for the decimal rule. 61 | */ 62 | protected function replaceDecimal(string $message, string $attribute, string $rule, array $parameters): string 63 | { 64 | return str_replace( 65 | ':decimal', 66 | isset($parameters[1]) 67 | ? $parameters[0] . '-' . $parameters[1] 68 | : $parameters[0], 69 | $message 70 | ); 71 | } 72 | 73 | /** 74 | * Replace all place-holders for the different rule. 75 | */ 76 | protected function replaceDifferent(string $message, string $attribute, string $rule, array $parameters): string 77 | { 78 | return $this->replaceSame($message, $attribute, $rule, $parameters); 79 | } 80 | 81 | /** 82 | * Replace all place-holders for the digits rule. 83 | */ 84 | protected function replaceDigits(string $message, string $attribute, string $rule, array $parameters): string 85 | { 86 | return str_replace(':digits', $parameters[0], $message); 87 | } 88 | 89 | /** 90 | * Replace all place-holders for the digits (between) rule. 91 | */ 92 | protected function replaceDigitsBetween(string $message, string $attribute, string $rule, array $parameters): string 93 | { 94 | return $this->replaceBetween($message, $attribute, $rule, $parameters); 95 | } 96 | 97 | /** 98 | * Replace all place-holders for the extensions rule. 99 | */ 100 | protected function replaceExtensions(string $message, string $attribute, string $rule, array $parameters): string 101 | { 102 | return str_replace(':values', implode(', ', $parameters), $message); 103 | } 104 | 105 | /** 106 | * Replace all place-holders for the min rule. 107 | */ 108 | protected function replaceMin(string $message, string $attribute, string $rule, array $parameters): string 109 | { 110 | return str_replace(':min', $parameters[0], $message); 111 | } 112 | 113 | /** 114 | * Replace all place-holders for the min digits rule. 115 | */ 116 | protected function replaceMinDigits(string $message, string $attribute, string $rule, array $parameters): string 117 | { 118 | return str_replace(':min', $parameters[0], $message); 119 | } 120 | 121 | /** 122 | * Replace all place-holders for the max rule. 123 | */ 124 | protected function replaceMax(string $message, string $attribute, string $rule, array $parameters): string 125 | { 126 | return str_replace(':max', $parameters[0], $message); 127 | } 128 | 129 | /** 130 | * Replace all place-holders for the max digits rule. 131 | */ 132 | protected function replaceMaxDigits(string $message, string $attribute, string $rule, array $parameters): string 133 | { 134 | return str_replace(':max', $parameters[0], $message); 135 | } 136 | 137 | /** 138 | * Replace all place-holders for the missing_if rule. 139 | */ 140 | protected function replaceMissingIf(string $message, string $attribute, string $rule, array $parameters): string 141 | { 142 | $parameters[1] = $this->getDisplayableValue($parameters[0], Arr::get($this->data, $parameters[0])); 143 | 144 | $parameters[0] = $this->getDisplayableAttribute($parameters[0]); 145 | 146 | return str_replace([':other', ':value'], $parameters, $message); 147 | } 148 | 149 | /** 150 | * Replace all place-holders for the missing_unless rule. 151 | */ 152 | protected function replaceMissingUnless(string $message, string $attribute, string $rule, array $parameters): string 153 | { 154 | return str_replace([':other', ':value'], [ 155 | $this->getDisplayableAttribute($parameters[0]), 156 | $this->getDisplayableValue($parameters[0], $parameters[1]), 157 | ], $message); 158 | } 159 | 160 | /** 161 | * Replace all place-holders for the missing_with rule. 162 | */ 163 | protected function replaceMissingWith(string $message, string $attribute, string $rule, array $parameters): string 164 | { 165 | return str_replace(':values', implode(' / ', $this->getAttributeList($parameters)), $message); 166 | } 167 | 168 | /** 169 | * Replace all place-holders for the missing_with_all rule. 170 | */ 171 | protected function replaceMissingWithAll(string $message, string $attribute, string $rule, array $parameters): string 172 | { 173 | return $this->replaceMissingWith($message, $attribute, $rule, $parameters); 174 | } 175 | 176 | /** 177 | * Replace all place-holders for the multiple_of rule. 178 | */ 179 | protected function replaceMultipleOf(string $message, string $attribute, string $rule, array $parameters): string 180 | { 181 | return str_replace(':value', $parameters[0] ?? '', $message); 182 | } 183 | 184 | /** 185 | * Replace all place-holders for the in rule. 186 | */ 187 | protected function replaceIn(string $message, string $attribute, string $rule, array $parameters): string 188 | { 189 | foreach ($parameters as &$parameter) { 190 | $parameter = $this->getDisplayableValue($attribute, $parameter); 191 | } 192 | 193 | return str_replace(':values', implode(', ', $parameters), $message); 194 | } 195 | 196 | /** 197 | * Replace all place-holders for the not_in rule. 198 | */ 199 | protected function replaceNotIn(string $message, string $attribute, string $rule, array $parameters): string 200 | { 201 | return $this->replaceIn($message, $attribute, $rule, $parameters); 202 | } 203 | 204 | /** 205 | * Replace all place-holders for the in_array rule. 206 | */ 207 | protected function replaceInArray(string $message, string $attribute, string $rule, array $parameters): string 208 | { 209 | return str_replace(':other', $this->getDisplayableAttribute($parameters[0]), $message); 210 | } 211 | 212 | /** 213 | * Replace all place-holders for the mimetypes rule. 214 | */ 215 | protected function replaceMimetypes(string $message, string $attribute, string $rule, array $parameters): string 216 | { 217 | return str_replace(':values', implode(', ', $parameters), $message); 218 | } 219 | 220 | /** 221 | * Replace all place-holders for the mimes rule. 222 | */ 223 | protected function replaceMimes(string $message, string $attribute, string $rule, array $parameters): string 224 | { 225 | return str_replace(':values', implode(', ', $parameters), $message); 226 | } 227 | 228 | /** 229 | * Replace all place-holders for the required_with rule. 230 | */ 231 | protected function replaceRequiredWith(string $message, string $attribute, string $rule, array $parameters): string 232 | { 233 | return str_replace(':values', implode(' / ', $this->getAttributeList($parameters)), $message); 234 | } 235 | 236 | /** 237 | * Replace all place-holders for the required_with_all rule. 238 | */ 239 | protected function replaceRequiredWithAll(string $message, string $attribute, string $rule, array $parameters): string 240 | { 241 | return $this->replaceRequiredWith($message, $attribute, $rule, $parameters); 242 | } 243 | 244 | /** 245 | * Replace all place-holders for the required_without rule. 246 | */ 247 | protected function replaceRequiredWithout(string $message, string $attribute, string $rule, array $parameters): string 248 | { 249 | return $this->replaceRequiredWith($message, $attribute, $rule, $parameters); 250 | } 251 | 252 | /** 253 | * Replace all place-holders for the required_without_all rule. 254 | */ 255 | protected function replaceRequiredWithoutAll(string $message, string $attribute, string $rule, array $parameters): string 256 | { 257 | return $this->replaceRequiredWith($message, $attribute, $rule, $parameters); 258 | } 259 | 260 | /** 261 | * Replace all place-holders for the size rule. 262 | */ 263 | protected function replaceSize(string $message, string $attribute, string $rule, array $parameters): string 264 | { 265 | return str_replace(':size', $parameters[0], $message); 266 | } 267 | 268 | /** 269 | * Replace all place-holders for the gt rule. 270 | */ 271 | protected function replaceGt(string $message, string $attribute, string $rule, array $parameters): string 272 | { 273 | if (is_null($value = $this->getValue($parameters[0]))) { 274 | return str_replace(':value', $parameters[0], $message); 275 | } 276 | 277 | return str_replace(':value', (string) $this->getSize($attribute, $value), $message); 278 | } 279 | 280 | /** 281 | * Replace all place-holders for the lt rule. 282 | */ 283 | protected function replaceLt(string $message, string $attribute, string $rule, array $parameters): string 284 | { 285 | if (is_null($value = $this->getValue($parameters[0]))) { 286 | return str_replace(':value', $parameters[0], $message); 287 | } 288 | 289 | return str_replace(':value', (string) $this->getSize($attribute, $value), $message); 290 | } 291 | 292 | /** 293 | * Replace all place-holders for the gte rule. 294 | */ 295 | protected function replaceGte(string $message, string $attribute, string $rule, array $parameters): string 296 | { 297 | if (is_null($value = $this->getValue($parameters[0]))) { 298 | return str_replace(':value', $parameters[0], $message); 299 | } 300 | 301 | return str_replace(':value', (string) $this->getSize($attribute, $value), $message); 302 | } 303 | 304 | /** 305 | * Replace all place-holders for the lte rule. 306 | */ 307 | protected function replaceLte(string $message, string $attribute, string $rule, array $parameters): string 308 | { 309 | if (is_null($value = $this->getValue($parameters[0]))) { 310 | return str_replace(':value', $parameters[0], $message); 311 | } 312 | 313 | return str_replace(':value', (string) $this->getSize($attribute, $value), $message); 314 | } 315 | 316 | /** 317 | * Replace all place-holders for the required_if rule. 318 | */ 319 | protected function replaceRequiredIf(string $message, string $attribute, string $rule, array $parameters): string 320 | { 321 | $parameters[1] = $this->getDisplayableValue($parameters[0], Arr::get($this->data, $parameters[0])); 322 | 323 | $parameters[0] = $this->getDisplayableAttribute($parameters[0]); 324 | 325 | return str_replace([':other', ':value'], $parameters, $message); 326 | } 327 | 328 | /** 329 | * Replace all place-holders for the required_unless rule. 330 | */ 331 | protected function replaceRequiredUnless(string $message, string $attribute, string $rule, array $parameters): string 332 | { 333 | $other = $this->getDisplayableAttribute($parameters[0]); 334 | 335 | $values = []; 336 | 337 | foreach (array_slice($parameters, 1) as $value) { 338 | $values[] = $this->getDisplayableValue($parameters[0], $value); 339 | } 340 | 341 | return str_replace([':other', ':values'], [$other, implode(', ', $values)], $message); 342 | } 343 | 344 | /** 345 | * Replace all place-holders for the same rule. 346 | */ 347 | protected function replaceSame(string $message, string $attribute, string $rule, array $parameters): string 348 | { 349 | return str_replace(':other', $this->getDisplayableAttribute($parameters[0]), $message); 350 | } 351 | 352 | /** 353 | * Replace all place-holders for the before rule. 354 | */ 355 | protected function replaceBefore(string $message, string $attribute, string $rule, array $parameters): string 356 | { 357 | if (! strtotime($parameters[0])) { 358 | return str_replace(':date', $this->getDisplayableAttribute($parameters[0]), $message); 359 | } 360 | 361 | return str_replace(':date', $this->getDisplayableValue($attribute, $parameters[0]), $message); 362 | } 363 | 364 | /** 365 | * Replace all place-holders for the before_or_equal rule. 366 | */ 367 | protected function replaceBeforeOrEqual(string $message, string $attribute, string $rule, array $parameters): string 368 | { 369 | return $this->replaceBefore($message, $attribute, $rule, $parameters); 370 | } 371 | 372 | /** 373 | * Replace all place-holders for the after rule. 374 | */ 375 | protected function replaceAfter(string $message, string $attribute, string $rule, array $parameters): string 376 | { 377 | return $this->replaceBefore($message, $attribute, $rule, $parameters); 378 | } 379 | 380 | /** 381 | * Replace all place-holders for the after_or_equal rule. 382 | */ 383 | protected function replaceAfterOrEqual(string $message, string $attribute, string $rule, array $parameters): string 384 | { 385 | return $this->replaceBefore($message, $attribute, $rule, $parameters); 386 | } 387 | 388 | /** 389 | * Replace all place-holders for the date_equals rule. 390 | */ 391 | protected function replaceDateEquals(string $message, string $attribute, string $rule, array $parameters): string 392 | { 393 | return $this->replaceBefore($message, $attribute, $rule, $parameters); 394 | } 395 | 396 | /** 397 | * Replace all place-holders for the dimensions rule. 398 | */ 399 | protected function replaceDimensions(string $message, string $attribute, string $rule, array $parameters): string 400 | { 401 | $parameters = $this->parseNamedParameters($parameters); 402 | 403 | if (is_array($parameters)) { 404 | foreach ($parameters as $key => $value) { 405 | $message = str_replace(':' . $key, $value, $message); 406 | } 407 | } 408 | 409 | return $message; 410 | } 411 | 412 | /** 413 | * Replace all place-holders for the ends_with rule. 414 | */ 415 | protected function replaceEndsWith(string $message, string $attribute, string $rule, array $parameters): string 416 | { 417 | foreach ($parameters as &$parameter) { 418 | $parameter = $this->getDisplayableValue($attribute, $parameter); 419 | } 420 | 421 | return str_replace(':values', implode(', ', $parameters), $message); 422 | } 423 | 424 | /** 425 | * Replace all place-holders for the doesnt_end_with rule. 426 | */ 427 | protected function replaceDoesntEndWith(string $message, string $attribute, string $rule, array $parameters): string 428 | { 429 | foreach ($parameters as &$parameter) { 430 | $parameter = $this->getDisplayableValue($attribute, $parameter); 431 | } 432 | 433 | return str_replace(':values', implode(', ', $parameters), $message); 434 | } 435 | 436 | /** 437 | * Replace all place-holders for the starts_with rule. 438 | */ 439 | protected function replaceStartsWith(string $message, string $attribute, string $rule, array $parameters): string 440 | { 441 | foreach ($parameters as &$parameter) { 442 | $parameter = $this->getDisplayableValue($attribute, $parameter); 443 | } 444 | 445 | return str_replace(':values', implode(', ', $parameters), $message); 446 | } 447 | 448 | /** 449 | * Replace all place-holders for the doesnt_start_with rule. 450 | */ 451 | protected function replaceDoesntStartWith(string $message, string $attribute, string $rule, array $parameters): string 452 | { 453 | foreach ($parameters as &$parameter) { 454 | $parameter = $this->getDisplayableValue($attribute, $parameter); 455 | } 456 | 457 | return str_replace(':values', implode(', ', $parameters), $message); 458 | } 459 | 460 | /** 461 | * Replace all place-holders for the prohibited_with rule. 462 | * @param array $parameters 463 | */ 464 | protected function replaceProhibits(string $message, string $attribute, string $rule, array $parameters): string 465 | { 466 | return str_replace(':other', implode(' / ', $this->getAttributeList($parameters)), $message); 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | data = $this->parseData($data); 185 | 186 | $this->setRules($initialRules); 187 | } 188 | 189 | /** 190 | * Handle dynamic calls to class methods. 191 | * 192 | * @param mixed $method 193 | * @param mixed $parameters 194 | * @throws BadMethodCallException when method does not exist 195 | */ 196 | public function __call($method, $parameters) 197 | { 198 | $rule = StrCache::snake(substr($method, 8)); 199 | 200 | if (isset($this->extensions[$rule])) { 201 | return $this->callExtension($rule, $parameters); 202 | } 203 | 204 | throw new BadMethodCallException(sprintf( 205 | 'Method %s::%s does not exist.', 206 | static::class, 207 | $method 208 | )); 209 | } 210 | 211 | /** 212 | * Parse the data array, converting dots to ->. 213 | */ 214 | public function parseData(array $data): array 215 | { 216 | $newData = []; 217 | 218 | foreach ($data as $key => $value) { 219 | if (is_array($value)) { 220 | $value = $this->parseData($value); 221 | } 222 | 223 | // If the data key contains a dot, we will replace it with another character 224 | // sequence so it doesn't interfere with dot processing when working with 225 | // array based validation rules and array_dot later in the validations. 226 | if (Str::contains((string) $key, '.')) { 227 | $newData[str_replace('.', '->', $key)] = $value; 228 | } else { 229 | $newData[$key] = $value; 230 | } 231 | } 232 | 233 | return $newData; 234 | } 235 | 236 | /** 237 | * Add an after validation callback. 238 | * 239 | * @param callable|string $callback 240 | */ 241 | public function after($callback): self 242 | { 243 | $this->after[] = fn () => call_user_func_array($callback, [$this]); 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * Determine if the data passes the validation rules. 250 | */ 251 | public function passes(): bool 252 | { 253 | $this->messages = new MessageBag(); 254 | 255 | [$this->distinctValues, $this->failedRules] = [[], []]; 256 | 257 | // We'll spin through each rule, validating the attributes attached to that 258 | // rule. Any error messages will be added to the containers with each of 259 | // the other error messages, returning true if we don't have messages. 260 | foreach ($this->rules as $attribute => $rules) { 261 | $attribute = str_replace('\.', '->', $attribute); 262 | 263 | if ($this->shouldBeExcluded($attribute)) { 264 | $this->removeAttribute($attribute); 265 | 266 | continue; 267 | } 268 | 269 | foreach ($rules as $rule) { 270 | $this->validateAttribute($attribute, $rule); 271 | 272 | if ($this->shouldBeExcluded($attribute)) { 273 | break; 274 | } 275 | 276 | if ($this->shouldStopValidating($attribute)) { 277 | break; 278 | } 279 | } 280 | } 281 | 282 | foreach ($this->rules as $attribute => $rules) { 283 | if ($this->shouldBeExcluded($attribute)) { 284 | $this->removeAttribute($attribute); 285 | } 286 | } 287 | 288 | // Here we will spin through all of the "after" hooks on this validator and 289 | // fire them off. This gives the callbacks a chance to perform all kinds 290 | // of other validation that needs to get wrapped up in this operation. 291 | foreach ($this->after as $after) { 292 | call_user_func($after); 293 | } 294 | 295 | return $this->messages->isEmpty(); 296 | } 297 | 298 | /** 299 | * Determine if the data fails the validation rules. 300 | */ 301 | public function fails(): bool 302 | { 303 | return ! $this->passes(); 304 | } 305 | 306 | /** 307 | * Run the validator's rules against its data. 308 | * 309 | * @throws ValidationException if validate fails 310 | */ 311 | public function validate(): array 312 | { 313 | if ($this->fails()) { 314 | throw new ValidationException($this); 315 | } 316 | 317 | return $this->validated(); 318 | } 319 | 320 | /** 321 | * Get the attributes and values that were validated. 322 | * 323 | * @throws ValidationException if invalid 324 | */ 325 | public function validated(): array 326 | { 327 | if ($this->invalid()) { 328 | throw new ValidationException($this); 329 | } 330 | 331 | $results = []; 332 | 333 | $missingValue = Str::random(10); 334 | 335 | foreach ($this->getRules() as $key => $rules) { 336 | $value = data_get($this->getData(), $key, $missingValue); 337 | 338 | if ($this->excludeUnvalidatedArrayKeys 339 | && (in_array('array', $rules) || in_array('list', $rules)) 340 | && $value !== null 341 | && ! empty(preg_grep('/^' . preg_quote($key, '/') . '\.+/', array_keys($this->getRules())))) { 342 | continue; 343 | } 344 | 345 | if ($value !== $missingValue) { 346 | Arr::set($results, $key, $value); 347 | } 348 | } 349 | 350 | return $results; 351 | } 352 | 353 | /** 354 | * Add a failed rule and error message to the collection. 355 | */ 356 | public function addFailure(string $attribute, string $rule, array $parameters = []): void 357 | { 358 | if (! $this->messages) { 359 | $this->passes(); 360 | } 361 | 362 | if (in_array($rule, $this->excludeRules)) { 363 | $this->excludeAttribute($attribute); 364 | return; 365 | } 366 | 367 | $this->messages->add($attribute, $this->makeReplacements( 368 | $this->getMessage($attribute, $rule), 369 | $attribute, 370 | $rule, 371 | $parameters 372 | )); 373 | 374 | $this->failedRules[$attribute][$rule] = $parameters; 375 | } 376 | 377 | /** 378 | * Returns the data which was valid. 379 | */ 380 | public function valid(): array 381 | { 382 | if (! $this->messages) { 383 | $this->passes(); 384 | } 385 | 386 | return array_diff_key( 387 | $this->data, 388 | $this->attributesThatHaveMessages() 389 | ); 390 | } 391 | 392 | /** 393 | * Returns the data which was invalid. 394 | */ 395 | public function invalid(): array 396 | { 397 | if (! $this->messages) { 398 | $this->passes(); 399 | } 400 | 401 | return array_intersect_key( 402 | $this->data, 403 | $this->attributesThatHaveMessages() 404 | ); 405 | } 406 | 407 | /** 408 | * Get the failed validation rules. 409 | */ 410 | public function failed(): array 411 | { 412 | return $this->failedRules; 413 | } 414 | 415 | /** 416 | * Get the message container for the validator. 417 | * 418 | * @return MessageBag 419 | */ 420 | public function messages() 421 | { 422 | if (! $this->messages) { 423 | $this->passes(); 424 | } 425 | 426 | return $this->messages; 427 | } 428 | 429 | /** 430 | * An alternative more semantic shortcut to the message container. 431 | */ 432 | public function errors(): MessageBagContract 433 | { 434 | return $this->messages(); 435 | } 436 | 437 | /** 438 | * Get the messages for the instance. 439 | */ 440 | public function getMessageBag(): MessageBagContract 441 | { 442 | return $this->messages(); 443 | } 444 | 445 | /** 446 | * Determine if the given attribute has a rule in the given set. 447 | * 448 | * @param array|string|Stringable $rules 449 | */ 450 | public function hasRule(string $attribute, mixed $rules): bool 451 | { 452 | return ! is_null($this->getRule($attribute, $rules)); 453 | } 454 | 455 | /** 456 | * Get the data under validation. 457 | */ 458 | public function attributes(): array 459 | { 460 | return $this->getData(); 461 | } 462 | 463 | /** 464 | * Get the data under validation. 465 | */ 466 | public function getData(): array 467 | { 468 | return $this->data; 469 | } 470 | 471 | /** 472 | * Set the data under validation. 473 | */ 474 | public function setData(array $data): self 475 | { 476 | $this->data = $this->parseData($data); 477 | 478 | $this->setRules($this->initialRules); 479 | 480 | return $this; 481 | } 482 | 483 | /** 484 | * Get the validation rules. 485 | */ 486 | public function getRules(): array 487 | { 488 | return $this->rules; 489 | } 490 | 491 | /** 492 | * Set the validation rules. 493 | */ 494 | public function setRules(array $rules): self 495 | { 496 | $this->initialRules = $rules; 497 | 498 | $this->rules = []; 499 | 500 | $this->addRules($rules); 501 | 502 | return $this; 503 | } 504 | 505 | /** 506 | * Parse the given rules and merge them into current rules. 507 | */ 508 | public function addRules(array $rules) 509 | { 510 | // The primary purpose of this parser is to expand any "*" rules to the all 511 | // of the explicit rules needed for the given data. For example the rule 512 | // names.* would get expanded to names.0, names.1, etc. for this data. 513 | $response = (new ValidationRuleParser($this->data)) 514 | ->explode($rules); 515 | 516 | $this->rules = array_merge_recursive( 517 | $this->rules, 518 | $response->rules 519 | ); 520 | 521 | $this->implicitAttributes = array_merge( 522 | $this->implicitAttributes, 523 | $response->implicitAttributes 524 | ); 525 | } 526 | 527 | /** 528 | * Add conditions to a given field based on a Closure. 529 | * 530 | * @param array|string $attribute 531 | * @param array|string $rules 532 | */ 533 | public function sometimes($attribute, $rules, callable $callback): self 534 | { 535 | $payload = new Fluent($this->getData()); 536 | 537 | if (call_user_func($callback, $payload)) { 538 | foreach ((array) $attribute as $key) { 539 | $this->addRules([$key => $rules]); 540 | } 541 | } 542 | 543 | return $this; 544 | } 545 | 546 | /** 547 | * Register an array of custom validator extensions. 548 | */ 549 | public function addExtensions(array $extensions) 550 | { 551 | if ($extensions) { 552 | $keys = array_map('\Hyperf\Stringable\StrCache::snake', array_keys($extensions)); 553 | 554 | $extensions = array_combine($keys, array_values($extensions)); 555 | } 556 | 557 | $this->extensions = array_merge($this->extensions, $extensions); 558 | } 559 | 560 | /** 561 | * Register an array of custom implicit validator extensions. 562 | */ 563 | public function addImplicitExtensions(array $extensions) 564 | { 565 | $this->addExtensions($extensions); 566 | 567 | foreach ($extensions as $rule => $extension) { 568 | $this->implicitRules[] = StrCache::studly($rule); 569 | } 570 | } 571 | 572 | /** 573 | * Register an array of custom implicit validator extensions. 574 | */ 575 | public function addDependentExtensions(array $extensions) 576 | { 577 | $this->addExtensions($extensions); 578 | 579 | foreach ($extensions as $rule => $extension) { 580 | $this->dependentRules[] = StrCache::studly($rule); 581 | } 582 | } 583 | 584 | /** 585 | * Register a custom validator extension. 586 | */ 587 | public function addExtension(string $rule, Closure|string $extension) 588 | { 589 | $this->extensions[StrCache::snake($rule)] = $extension; 590 | } 591 | 592 | /** 593 | * Register a custom implicit validator extension. 594 | */ 595 | public function addImplicitExtension(string $rule, Closure|string $extension) 596 | { 597 | $this->addExtension($rule, $extension); 598 | 599 | $this->implicitRules[] = StrCache::studly($rule); 600 | } 601 | 602 | /** 603 | * Register a custom dependent validator extension. 604 | */ 605 | public function addDependentExtension(string $rule, Closure|string $extension) 606 | { 607 | $this->addExtension($rule, $extension); 608 | 609 | $this->dependentRules[] = StrCache::studly($rule); 610 | } 611 | 612 | /** 613 | * Register an array of custom validator message replacers. 614 | */ 615 | public function addReplacers(array $replacers) 616 | { 617 | if ($replacers) { 618 | $keys = array_map('\Hyperf\Stringable\StrCache::snake', array_keys($replacers)); 619 | 620 | $replacers = array_combine($keys, array_values($replacers)); 621 | } 622 | 623 | $this->replacers = array_merge($this->replacers, $replacers); 624 | } 625 | 626 | /** 627 | * Register a custom validator message replacer. 628 | */ 629 | public function addReplacer(string $rule, Closure|string $replacer) 630 | { 631 | $this->replacers[StrCache::snake($rule)] = $replacer; 632 | } 633 | 634 | /** 635 | * Set the custom messages for the validator. 636 | */ 637 | public function setCustomMessages(array $messages): self 638 | { 639 | $this->customMessages = array_merge($this->customMessages, $messages); 640 | 641 | return $this; 642 | } 643 | 644 | /** 645 | * Set the custom attributes on the validator. 646 | */ 647 | public function setAttributeNames(array $attributes): self 648 | { 649 | $this->customAttributes = $attributes; 650 | 651 | return $this; 652 | } 653 | 654 | /** 655 | * Add custom attributes to the validator. 656 | */ 657 | public function addCustomAttributes(array $customAttributes): self 658 | { 659 | $this->customAttributes = array_merge($this->customAttributes, $customAttributes); 660 | 661 | return $this; 662 | } 663 | 664 | /** 665 | * Set the custom values on the validator. 666 | */ 667 | public function setValueNames(array $values): self 668 | { 669 | $this->customValues = $values; 670 | 671 | return $this; 672 | } 673 | 674 | /** 675 | * Add the custom values for the validator. 676 | */ 677 | public function addCustomValues(array $customValues): self 678 | { 679 | $this->customValues = array_merge($this->customValues, $customValues); 680 | 681 | return $this; 682 | } 683 | 684 | /** 685 | * Set the fallback messages for the validator. 686 | */ 687 | public function setFallbackMessages(array $messages) 688 | { 689 | $this->fallbackMessages = $messages; 690 | } 691 | 692 | /** 693 | * Get the Presence Verifier implementation. 694 | * 695 | * @throws RuntimeException 696 | */ 697 | public function getPresenceVerifier(): PresenceVerifierInterface 698 | { 699 | if (! isset($this->presenceVerifier)) { 700 | throw new RuntimeException('Presence verifier has not been set.'); 701 | } 702 | 703 | return $this->presenceVerifier; 704 | } 705 | 706 | /** 707 | * Get the Presence Verifier implementation. 708 | * 709 | * @throws RuntimeException 710 | */ 711 | public function getPresenceVerifierFor(?string $connection): PresenceVerifierInterface 712 | { 713 | return tap($this->getPresenceVerifier(), function ($verifier) use ($connection) { 714 | $verifier->setConnection($connection); 715 | }); 716 | } 717 | 718 | /** 719 | * Set the Presence Verifier implementation. 720 | */ 721 | public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier) 722 | { 723 | $this->presenceVerifier = $presenceVerifier; 724 | } 725 | 726 | /** 727 | * Get the Translator implementation. 728 | */ 729 | public function getTranslator(): TranslatorInterface 730 | { 731 | return $this->translator; 732 | } 733 | 734 | /** 735 | * Set the Translator implementation. 736 | */ 737 | public function setTranslator(TranslatorInterface $translator) 738 | { 739 | $this->translator = $translator; 740 | } 741 | 742 | /** 743 | * Set the IoC container instance. 744 | */ 745 | public function setContainer(ContainerInterface $container) 746 | { 747 | $this->container = $container; 748 | } 749 | 750 | /** 751 | * Get the value of a given attribute. 752 | */ 753 | public function getValue(string $attribute) 754 | { 755 | return Arr::get($this->data, $attribute); 756 | } 757 | 758 | /** 759 | * Set the value of a given attribute. 760 | * 761 | * @param string $attribute 762 | * @param mixed $value 763 | */ 764 | public function setValue($attribute, $value) 765 | { 766 | Arr::set($this->data, $attribute, $value); 767 | } 768 | 769 | /** 770 | * Determine if the attribute should be excluded. 771 | */ 772 | protected function shouldBeExcluded(string $attribute): bool 773 | { 774 | foreach ($this->excludeAttributes as $excludeAttribute) { 775 | if ($attribute === $excludeAttribute 776 | || Str::startsWith($attribute, $excludeAttribute . '.')) { 777 | return true; 778 | } 779 | } 780 | 781 | return false; 782 | } 783 | 784 | /** 785 | * Remove the given attribute. 786 | */ 787 | protected function removeAttribute(string $attribute): void 788 | { 789 | Arr::forget($this->data, $attribute); 790 | Arr::forget($this->rules, $attribute); 791 | } 792 | 793 | /** 794 | * Add the given attribute to the list of excluded attributes. 795 | */ 796 | protected function excludeAttribute(string $attribute): void 797 | { 798 | $this->excludeAttributes[] = $attribute; 799 | 800 | $this->excludeAttributes = array_unique($this->excludeAttributes); 801 | } 802 | 803 | /** 804 | * Validate a given attribute against a rule. 805 | * 806 | * @param object|string $rule 807 | */ 808 | protected function validateAttribute(string $attribute, $rule) 809 | { 810 | $this->currentRule = $rule; 811 | 812 | [$rule, $parameters] = ValidationRuleParser::parse($rule); 813 | 814 | if ($rule == '') { 815 | return; 816 | } 817 | 818 | // First we will get the correct keys for the given attribute in case the field is nested in 819 | // an array. Then we determine if the given rule accepts other field names as parameters. 820 | // If so, we will replace any asterisks found in the parameters with the correct keys. 821 | if (($keys = $this->getExplicitKeys($attribute)) 822 | && $this->dependsOnOtherFields($rule)) { 823 | $parameters = $this->replaceAsterisksInParameters($parameters, $keys); 824 | } 825 | 826 | $value = $this->getValue($attribute); 827 | 828 | // If the attribute is a file, we will verify that the file upload was actually successful 829 | // and if it wasn't we will add a failure for the attribute. Files may not successfully 830 | // upload if they are too large based on PHP's settings so we will bail in this case. 831 | if ($value instanceof UploadedFile && ! $value->isValid() 832 | && $this->hasRule($attribute, array_merge($this->fileRules, $this->implicitRules)) 833 | ) { 834 | return $this->addFailure($attribute, 'uploaded', []); 835 | } 836 | 837 | // If we have made it this far we will make sure the attribute is validatable and if it is 838 | // we will call the validation method with the attribute. If a method returns false the 839 | // attribute is invalid and we will add a failure message for this failing attribute. 840 | $validatable = $this->isValidatable($rule, $attribute, $value); 841 | 842 | if ($rule instanceof RuleContract) { 843 | return $validatable 844 | ? $this->validateUsingCustomRule($attribute, $value, $rule) 845 | : null; 846 | } 847 | 848 | $method = "validate{$rule}"; 849 | 850 | if ($validatable && ! $this->{$method}($attribute, $value, $parameters, $this)) { 851 | $this->addFailure($attribute, $rule, $parameters); 852 | } 853 | } 854 | 855 | /** 856 | * Determine if the given rule depends on other fields. 857 | * 858 | * @param object|string $rule 859 | */ 860 | protected function dependsOnOtherFields($rule): bool 861 | { 862 | return in_array($rule, $this->dependentRules); 863 | } 864 | 865 | /** 866 | * Get the explicit keys from an attribute flattened with dot notation. 867 | * 868 | * E.g. 'foo.1.bar.spark.baz' -> [1, 'spark'] for 'foo.*.bar.*.baz' 869 | */ 870 | protected function getExplicitKeys(string $attribute): array 871 | { 872 | $pattern = str_replace('\*', '([^\.]+)', preg_quote($this->getPrimaryAttribute($attribute), '/')); 873 | 874 | if (preg_match('/^' . $pattern . '/', $attribute, $keys)) { 875 | array_shift($keys); 876 | 877 | return $keys; 878 | } 879 | 880 | return []; 881 | } 882 | 883 | /** 884 | * Get the primary attribute name. 885 | * 886 | * For example, if "name.0" is given, "name.*" will be returned. 887 | */ 888 | protected function getPrimaryAttribute(string $attribute): string 889 | { 890 | foreach ($this->implicitAttributes as $unparsed => $parsed) { 891 | if (in_array($attribute, $parsed)) { 892 | return $unparsed; 893 | } 894 | } 895 | 896 | return $attribute; 897 | } 898 | 899 | /** 900 | * Replace each field parameter which has asterisks with the given keys. 901 | */ 902 | protected function replaceAsterisksInParameters(array $parameters, array $keys): array 903 | { 904 | return array_map(fn ($field) => vsprintf(str_replace('*', '%s', $field), $keys), $parameters); 905 | } 906 | 907 | /** 908 | * Determine if the attribute is validatable. 909 | * 910 | * @param object|string $rule 911 | * @param mixed $value 912 | */ 913 | protected function isValidatable($rule, string $attribute, $value): bool 914 | { 915 | if (in_array($rule, $this->excludeRules)) { 916 | return true; 917 | } 918 | 919 | return $this->presentOrRuleIsImplicit($rule, $attribute, $value) 920 | && $this->passesOptionalCheck($attribute) 921 | && $this->isNotNullIfMarkedAsNullable($rule, $attribute) 922 | && $this->hasNotFailedPreviousRuleIfPresenceRule($rule, $attribute); 923 | } 924 | 925 | /** 926 | * Determine if the field is present, or the rule implies required. 927 | * 928 | * @param object|string $rule 929 | * @param mixed $value 930 | */ 931 | protected function presentOrRuleIsImplicit($rule, string $attribute, $value): bool 932 | { 933 | if (is_string($value) && trim($value) === '') { 934 | return $this->isImplicit($rule); 935 | } 936 | 937 | return $this->validatePresent($attribute, $value) 938 | || $this->isImplicit($rule); 939 | } 940 | 941 | /** 942 | * Determine if a given rule implies the attribute is required. 943 | * 944 | * @param object|string $rule 945 | */ 946 | protected function isImplicit($rule): bool 947 | { 948 | return $rule instanceof ImplicitRule 949 | || in_array($rule, $this->implicitRules); 950 | } 951 | 952 | /** 953 | * Determine if the attribute passes any optional check. 954 | */ 955 | protected function passesOptionalCheck(string $attribute): bool 956 | { 957 | if (! $this->hasRule($attribute, ['Sometimes'])) { 958 | return true; 959 | } 960 | 961 | $data = ValidationData::initializeAndGatherData($attribute, $this->data); 962 | 963 | return array_key_exists($attribute, $data) 964 | || array_key_exists($attribute, $this->data); 965 | } 966 | 967 | /** 968 | * Determine if the attribute fails the nullable check. 969 | * 970 | * @param object|string $rule 971 | */ 972 | protected function isNotNullIfMarkedAsNullable($rule, string $attribute): bool 973 | { 974 | if ($this->isImplicit($rule) || ! $this->hasRule($attribute, ['Nullable'])) { 975 | return true; 976 | } 977 | 978 | return ! is_null(Arr::get($this->data, $attribute, 0)); 979 | } 980 | 981 | /** 982 | * Determine if it's a necessary presence validation. 983 | * 984 | * This is to avoid possible database type comparison errors. 985 | * 986 | * @param object|string $rule 987 | */ 988 | protected function hasNotFailedPreviousRuleIfPresenceRule($rule, string $attribute): bool 989 | { 990 | return in_array($rule, ['Unique', 'Exists']) ? ! $this->messages->has($attribute) : true; 991 | } 992 | 993 | /** 994 | * Validate an attribute using a custom rule object. 995 | * @param mixed $value 996 | */ 997 | protected function validateUsingCustomRule(string $attribute, $value, RuleContract $rule) 998 | { 999 | if ($rule instanceof ValidatorAwareRule) { 1000 | $rule->setValidator($this); 1001 | } 1002 | 1003 | if ($rule instanceof DataAwareRule) { 1004 | $rule->setData($this->data); 1005 | } 1006 | 1007 | if (! $rule->passes($attribute, $value)) { 1008 | $this->failedRules[$attribute][$rule::class] = []; 1009 | 1010 | $messages = $rule->message() ? (array) $rule->message() : [$rule::class]; 1011 | 1012 | foreach ($messages as $message) { 1013 | $this->messages->add($attribute, $this->makeReplacements( 1014 | $message, 1015 | $attribute, 1016 | $rule::class, 1017 | [] 1018 | )); 1019 | } 1020 | } 1021 | } 1022 | 1023 | /** 1024 | * Check if we should stop further validations on a given attribute. 1025 | */ 1026 | protected function shouldStopValidating(string $attribute): bool 1027 | { 1028 | if ($this->hasRule($attribute, ['Bail'])) { 1029 | return $this->messages->has($attribute); 1030 | } 1031 | 1032 | if (isset($this->failedRules[$attribute]) 1033 | && array_key_exists('uploaded', $this->failedRules[$attribute])) { 1034 | return true; 1035 | } 1036 | 1037 | // In case the attribute has any rule that indicates that the field is required 1038 | // and that rule already failed then we should stop validation at this point 1039 | // as now there is no point in calling other rules with this field empty. 1040 | return $this->hasRule($attribute, $this->implicitRules) 1041 | && isset($this->failedRules[$attribute]) 1042 | && array_intersect(array_keys($this->failedRules[$attribute]), $this->implicitRules); 1043 | } 1044 | 1045 | /** 1046 | * Generate an array of all attributes that have messages. 1047 | */ 1048 | protected function attributesThatHaveMessages(): array 1049 | { 1050 | return collect($this->messages()->toArray())->map(fn ($message, $key) => explode('.', $key)[0])->unique()->flip()->all(); 1051 | } 1052 | 1053 | /** 1054 | * Get a rule and its parameters for a given attribute. 1055 | * 1056 | * @param array|string|Stringable $rules 1057 | */ 1058 | protected function getRule(string $attribute, mixed $rules): ?array 1059 | { 1060 | if (! array_key_exists($attribute, $this->rules)) { 1061 | return null; 1062 | } 1063 | 1064 | $rules = (array) $rules; 1065 | 1066 | foreach ($this->rules[$attribute] as $rule) { 1067 | [$rule, $parameters] = ValidationRuleParser::parse($rule); 1068 | 1069 | if (in_array($rule, $rules)) { 1070 | return [$rule, $parameters]; 1071 | } 1072 | } 1073 | return null; 1074 | } 1075 | 1076 | /** 1077 | * Call a custom validator extension. 1078 | */ 1079 | protected function callExtension(string $rule, array $parameters): ?bool 1080 | { 1081 | $callback = $this->extensions[$rule]; 1082 | 1083 | if (is_callable($callback)) { 1084 | return call_user_func_array($callback, $parameters); 1085 | } 1086 | if (is_string($callback)) { 1087 | return $this->callClassBasedExtension($callback, $parameters); 1088 | } 1089 | } 1090 | 1091 | /** 1092 | * Call a class based validator extension. 1093 | */ 1094 | protected function callClassBasedExtension(string $callback, array $parameters): bool 1095 | { 1096 | [$class, $method] = Str::parseCallback($callback, 'validate'); 1097 | 1098 | return call_user_func_array([make($class), $method], $parameters); 1099 | } 1100 | } 1101 | --------------------------------------------------------------------------------