tobento / service-validation
Easily validating data.
Requires
- php: >=8.0
- psr/container: ^2.0
- psr/log: ^1.1 || ^2.0 || ^3.0
- tobento/service-autowire: ^1.0
- tobento/service-collection: ^1.0
- tobento/service-dater: ^1.0
- tobento/service-message: ^1.0
Requires (Dev)
- monolog/monolog: ^2.3
- phpunit/phpunit: ^9.5
- tobento/service-container: ^1.0
- tobento/service-translation: ^1.0
- vimeo/psalm: ^4.0
Suggests
- tobento/service-translation: Support for translating messages
README
The Validation Service provides an easy way to validate data.
Table of Contents
- Getting started
- Documentation
- Credits
Getting started
Add the latest version of the validation service running this command.
composer require tobento/service-validation
Requirements
- PHP 8.0 or greater
Highlights
- Framework-agnostic, will work with any project
- Decoupled design
Documentation
Validating
Single value
Easily validate a single value.
use Tobento\Service\Validation\Validator; use Tobento\Service\Validation\ValidatorInterface; use Tobento\Service\Validation\ValidationInterface; $validator = new Validator(); var_dump($validator instanceof ValidatorInterface); // bool(true) $validation = $validator->validating( value: 'foo', rules: 'alphaStrict|minLen:2', data: [], key: null ); var_dump($validation instanceof ValidationInterface); // bool(true)
Check out Validator to learn more about the Validator.
Check out Validation to learn more about the ValidationInterface.
Parameters explanation
Multiple values
Validate multiple values.
use Tobento\Service\Validation\Validator; use Tobento\Service\Validation\ValidatorInterface; use Tobento\Service\Validation\ValidationInterface; $validator = new Validator(); var_dump($validator instanceof ValidatorInterface); // bool(true) $validation = $validator->validate( data: [ 'title' => 'Product', 'color' => 'blue', ], rules: [ 'title' => 'alpha', 'color' => 'in:blue:red:green', ] ); var_dump($validation instanceof ValidationInterface); // bool(true)
Check out Validator to learn more about the Validator.
Check out Validation to learn more about the ValidationInterface.
Parameters explanation
Nested values
If the incoming values contains "nested" data, you may specify these attributes in your rules using "dot" syntax:
use Tobento\Service\Validation\Validator; use Tobento\Service\Validation\ValidatorInterface; use Tobento\Service\Validation\ValidationInterface; $validator = new Validator(); var_dump($validator instanceof ValidatorInterface); // bool(true) $validation = $validator->validate( data: [ 'title' => 'Product', 'meta' => [ 'color' => 'blue', ], ], rules: [ 'title' => 'alpha', 'meta.color' => 'required|in:blue:red:green', ] ); var_dump($validation instanceof ValidationInterface); // bool(true)
Check out Validator to learn more about the Validator.
Check out Validation to learn more about the ValidationInterface.
Parameters explanation
Rules Definition
The Default Rules Parser supports the following rules definition.
You may also check out the Default Rules to learn more about the rules it provides.
If you add rules "lazy" with dependencies you will need to use the AutowiringRuleFactory for resolving see Default Rules.
string definition
use Tobento\Service\Validation\Validator; $validation = (new Validator())->validate( data: [ 'title' => 'Product', ], rules: [ 'title' => 'minLen:2|alpha', ] );
array definition with string rules
If you need to define additional rule parameters or custom error messages, wrap the rule into an array:
use Tobento\Service\Validation\Validator; $validation = (new Validator())->validate( data: [ 'title' => 'Product', ], rules: [ 'title' => [ // single or multiple rules 'required|alpha', // using array with string rule and custom parameters. ['minLen:3', 'error' => 'Custom error message'], // using array with string rule but seperate rule parameters and custom parameters. ['minLen', [3], 'error' => 'Custom error message'], ], ] );
object rules
You may define object rules implementing the Rule Interface:
use Tobento\Service\Validation\Validator; use Tobento\Service\Validation\Rule\Same; $validation = (new Validator())->validate( data: [ 'title' => 'Product', ], rules: [ 'title' => [ // single or multiple rules 'required|minLen:2', new Same(), // using array for custom parameters: [new Same(), 'error' => 'Custom error message'], // using array for lazy rule: [[Rule::class], [3], 'error' => 'Custom error message'], // lazy rule with unresolvable class params: // [[Rule::class, ['name' => 'value']], [3], 'error' => 'Custom error message'], ], ] );
object rules with different validation method
You may define object rules with different validation methods:
use Tobento\Service\Validation\Validator; use Tobento\Service\Validation\Rule\Length; $validation = (new Validator())->validate( data: [ 'title' => 'Product', ], rules: [ 'title' => [ // single or multiple rules 'required|alpha', // calls the min method for validation: [[new Length(), 'min'], [3], 'error' => 'Custom error message'], // lazy rule [[Length::class, 'min'], [3], 'error' => 'Custom error message'], // lazy rule with unresolvable class params: [[Length::class, 'min', ['name' => 'value']], [3], 'error' => 'Custom error message'], // lazy rule with unresolvable class params without method to call: // [[Rule::class, ['name' => 'value']], [3], 'error' => 'Custom error message'], ], ] );
Parameters
For each rule you can define custom parameters.
use Tobento\Service\Validation\Validator; use Tobento\Service\Validation\Rule\Length; $validation = (new Validator())->validate( data: [ 'title' => 'Product', ], rules: [ 'title' => [ [ 'minLen:3', 'error' => ':attribute must at least contain :parameters[0] chars', // you might want a custom value for the attribute: ':attribute' => 'The TITLE', // you might need a custom value: ':parameters[0]' => 3, // global modifier parameters: 'limit_length' => 100, ], ], ] );
Global parameters
Sometimes you may need custom parameters for all rules.
use Tobento\Service\Validation\Validator; use Tobento\Service\Validation\Rule\Length; $validation = (new Validator())->validate( data: [ 'title' => 'Product', ], rules: [ 'title' => [ // single or multiple rules 'required|alpha', // calls the min method for validation: [[new Length(), 'min'], [3]], // global error message: 'error' => 'Error message', // global replacement parameters for messages: ':attribute' => 'The TITLE', // global modifier parameters: 'limit_length' => 100, ], ] );
Validator
Create Validator
use Tobento\Service\Validation\Validator; use Tobento\Service\Validation\ValidatorInterface; use Tobento\Service\Validation\RulesInterface; use Tobento\Service\Validation\RulesParserInterface; use Tobento\Service\Validation\RulesAware; use Tobento\Service\Message\MessagesFactoryInterface; $validator = new Validator( rules: null, // null|RulesInterface rulesParser: null, // null|RulesParserInterface messagesFactory: null // null|MessagesFactoryInterface ); var_dump($validator instanceof ValidatorInterface); // bool(true) var_dump($validator instanceof RulesAware); // bool(true)
Parameters explanation
Validator Interface
use Tobento\Service\Validation\ValidatorInterface; use Tobento\Service\Validation\ValidationInterface; use Tobento\Service\Collection\Collection; interface ValidatorInterface { public function validating( mixed $value, string|array $rules, array|Collection $data = [], null|string $key = null ): ValidationInterface; public function validate(mixed $data, array $rules): ValidationInterface; }
Check out Validating to learn more about the methods.
Check out Collection Service to learn more it.
Rules Aware
use Tobento\Service\Validation\RulesInterface; interface RulesAware { public function rules(): RulesInterface; }
Check out Rules Interface to learn more about the RulesInterface.
Validation
Validation Interface
use Tobento\Service\Validation\ValidationInterface; use Tobento\Service\Validation\RuleInterface; use Tobento\Service\Message\MessagesInterface; use Tobento\Service\Collection\Collection; interface ValidationInterface { public function isValid(): bool; public function errors(): MessagesInterface; public function data(): Collection; public function valid(): Collection; public function invalid(): Collection; public function rule(): null|RuleInterface; }
Methods explanation
Error Messages
By default, the rules keys are used as the :attribute parameter when defined in the error messages.
For translation reason, it is not recommended to write messages like "The :attribute must ...", it is better to add "The title" in the :attribute parameter because "The" might be different for an attribute name depending on the language.
use Tobento\Service\Validation\Validator; use Tobento\Service\Message\MessagesInterface; $validation = (new Validator())->validate( data: [ 'title' => 'Pr', 'color' => 'green', ], rules: [ 'title' => 'minLen:3|alpha', 'color' => 'in:blue:red', ] ); $errors = $validation->errors(); var_dump($errors instanceof MessagesInterface); // bool(true)
Retrieving the first error message for a data key
use Tobento\Service\Message\MessageInterface; $errors = $validation->errors(); $error = $errors->key('title')->first(); var_dump($error instanceof MessageInterface); // bool(true) echo $error; // The title must at least contain 3 chars.
Check out the Message Service to learn more about messages in general.
Retrieving all error messages for all data keys
use Tobento\Service\Message\MessageInterface; $errors = $validation->errors(); foreach($errors->key('title')->all() as $error) { var_dump($error instanceof MessageInterface); // bool(true) }
Check out the Message Service to learn more about messages in general.
Determine if messages exist for a data key
$errors = $validation->errors(); var_dump($errors->key('title')->has()); // bool(true)
Check out the Message Service to learn more about messages in general.
Custom error message
use Tobento\Service\Validation\Validator; $validation = (new Validator())->validate( data: [ 'title' => 'Pr', 'color' => 'green', ], rules: [ 'title' => [ 'alpha', ['minLen', [3], 'error' => ':attribute must contain at least :parameters[0] chars!'] ], 'color' => 'in:blue:red', ] ); $errors = $validation->errors(); echo $errors->key('title')->first(); // The title must contain at least 3 chars!
Custom error message parameters
use Tobento\Service\Validation\Validator; $validation = (new Validator())->validate( data: [ 'title' => 'Pr', 'color' => 'green', ], rules: [ 'title' => [ 'alpha', [ 'minLen', [3], ':attribute' => 'The TITLE', ':parameters[0]' => 3, 'error' => ':attribute must contain at least :parameters[0] chars!' ] ], 'color' => 'in:blue:red', ] ); $errors = $validation->errors(); echo $errors->key('title')->first(); // The TITLE must contain at least 3 chars!
Global custom error message parameters
You might want to define global message parameters for all rules defined:
use Tobento\Service\Validation\Validator; $validation = (new Validator())->validate( data: [ 'title' => 'Pr', 'color' => 'green', ], rules: [ 'title' => [ 'minLen:3|alpha', ':attribute' => 'The TITLE', // you might even set a global message for all rules. 'error' => ':attribute is invalid', ], 'color' => 'in:blue:red', ] ); $errors = $validation->errors(); echo $errors->key('title')->first(); // The TITLE is invalid.
Skipping first parameter
You might need to skip the first value of the parameters by declaring it as :parameters[-1].
use Tobento\Service\Validation\Validator; $validation = (new Validator())->validate( data: [ 'title' => '', 'color' => 'green', ], rules: [ 'title' => [ 'required_ifIn:color:green:red', 'error' => ':attribute is required as :parameters[0] is in list :parameters[-1].', ], 'color' => 'in:blue:red', ] ); $errors = $validation->errors(); echo $errors->key('title')->first(); // The title is required as color is in list green, red.
Validated Data
use Tobento\Service\Validation\Validator; use Tobento\Service\Collection\Collection; $validation = (new Validator())->validate( data: [ 'title' => 'Pr', 'color' => 'green', ], rules: [ 'title' => 'minLen:3|alpha', 'color' => 'in:blue:red', ] ); // all validated data: var_dump($validation->data() instanceof Collection); // bool(true) // all valid data: var_dump($validation->valid() instanceof Collection); // bool(true) // all invalid data: var_dump($validation->invalid() instanceof Collection); // bool(true)
Check out the Collection Service to learn more about Collection in general.
Rules
Rules Interface
use Tobento\Service\Validation\RulesInterface; use Tobento\Service\Validation\RuleInterface; use Tobento\Service\Validation\RuleNotFoundException; use Tobento\Service\Validation\InvalidRuleException; /** * RulesInterface */ interface RulesInterface { /** * Add a rule. * * @param string $name * @param mixed $rule * @return static $this */ public function add(string $name, mixed $rule): static; /** * Returns the rule based on the specified rule. * * @param mixed $rule * @return RuleInterface * * @throws RuleNotFoundException * @throws InvalidRuleException */ public function get(mixed $rule): RuleInterface; }
Default Rules
use Tobento\Service\Validation\DefaultRules; use Tobento\Service\Validation\RulesInterface; use Tobento\Service\Validation\RuleFactoryInterface; $rules = new DefaultRules( ruleFactory: null, //null|RuleFactoryInterface ); var_dump($rules instanceof RulesInterface); // bool(true)
With autowiring rule factory
The autowiring rule factory is needed if you define or add rules "lazy" with dependencies.
use Tobento\Service\Validation\DefaultRules; use Tobento\Service\Validation\AutowiringRuleFactory; // Any PSR-11 container $container = new \Tobento\Service\Container\Container(); $rules = new DefaultRules( ruleFactory: new AutowiringRuleFactory($container); );
Available Rules
The following rules are available out of the box:
Adding Rules
You may add additional rules by the following way. If you add rules "lazy" with dependencies you will need to use the AutowiringRuleFactory for resolving.
use Tobento\Service\Validation\DefaultRules; use Tobento\Service\Validation\RulesInterface; use Tobento\Service\Validation\AutowiringRuleFactory; use Tobento\Service\Validation\Rule\Same; use Tobento\Service\Validation\Rule\Type; // Any PSR-11 container $container = new \Tobento\Service\Container\Container(); $rules = new DefaultRules( ruleFactory: new AutowiringRuleFactory($container); ); $rules = new DefaultRules(); $rules->add('same', new Same()); // Lazy: $rules->add('same', Same::class); // Custom method: $rules->add('bool', [new Type(), 'bool']); // Lazy custom method: $rules->add('bool', [Type::class, 'bool']); // Lazy custom method with unresolvable parameters: // $rules->add('rule', [Rule::class, 'bool', ['name' => 'value']]); // Lazy with unresolvable parameters: // $rules->add('rule', [Rule::class, ['name' => 'value']]);
Custom Rules
You may write your own rules class or adjusting the default rules for your needs.
use Tobento\Service\Validation\DefaultRules; class CustomDefaultRules extends DefaultRules { protected function getDefaultRules(): array { $rules = parent::getDefaultRules(); // adding or overwriting rules. $rules['bool'] = [\Tobento\Service\Validation\Rule\Type::class, 'bool']; return $rules; } } $rules = new CustomDefaultRules();
Rule
Rule Interface
use Tobento\Service\Validation\RuleInterface; /** * RuleInterface */ interface RuleInterface { /** * Skips validation depending on value and rule method. * * @param mixed $value The value to validate. * @param string $method * @return bool Returns true if skip validation, otherwise false. */ public function skipValidation(mixed $value, string $method = 'passes'): bool; /** * Determine if the validation rule passes. * * @param mixed $value The value to validate. * @param array $parameters Any parameters used for the validation. * @return bool */ public function passes(mixed $value, array $parameters = []): bool; /** * Returns the validation error messages. * * @return array */ public function messages(): array; }
Passes Rule
With the Passes rule you can create any custom rule.
use Tobento\Service\Validation\Rule\Passes; $validation = $validator->validating( value: 'foo', rules: [ // rule does pass: new Passes(passes: true), // rule does not pass: new Passes(passes: false), // rule does pass: new Passes(passes: fn (mixed $value): bool => $value === 'foo'), // using static new method: Passes::new(passes: true), ], );
Passes parameters
The following parameters are available:
use Tobento\Service\Validation\Rule\Passes; use Tobento\Service\Validation\ValidatorInterface; use Tobento\Service\Validation\ValidationInterface; $rule = new Passes(passes: function( mixed $value, array $parameters, ValidatorInterface $validator, ValidationInterface $validation): bool { return true; });
If you have set up the validator with the autowiring rule factory, the passes
and skipValidation
callable are autowired:
use Tobento\Service\Validation\Rule\Passes; $rule = new Passes(passes: function(mixed $value, SomeService $service): bool { return true; });
Ensure Declared Closure Type
By default, the declared type of a closure $value
parameter will be automatically verified. If it does not match the input value type, the rule does not pass and the closure will never be executed.
use Tobento\Service\Validation\Rule\Passes; $rule = new Passes(passes: fn (string|int $value): bool { return true; }); // you may deactivate it, but then you will need to declare the value type as mixed: $rule = new Passes( passes: fn (mixed $value): bool { return true; }, verifyDeclaredType: false, );
Custom error message
You may specify a custom error message:
use Tobento\Service\Validation\Rule\Passes; $rule = new Passes( passes: true, errorMessage: 'Custom error message', );
Skip validation
You may use the skipValidation parameter in order to skip validation under certain conditions:
$validation = $validator->validating( value: 'foo', rules: [ // skips validation: new Passes(passes: true, skipValidation: true), // does not skip validation: new Passes(passes: true, skipValidation: false), // skips validation: new Passes(passes: true, skipValidation: fn (mixed $value): bool => $value === 'foo'), ], );
Custom Rule
use Tobento\Service\Validation\Rule; class ListRule extends Rule { /** * The error messages. */ public const MESSAGES = [ 'passes' => ':attribute must be in list :parameters', ]; /** * Determine if the validation rule passes. * * @param mixed $value The value to validate. * @param array $parameters Any parameters used for the validation. * @return bool */ public function passes(mixed $value, array $parameters = []): bool { return in_array($value, $parameters); } }
With multiple validation methods
See Rule\Strings for demo.
Needs Validation for validation
See Rule\Arr for demo.
Needs Validator for validation
See Rule\Arr for demo.
Skip validation when passes
See rules for demo.
Rules Parser
The role of the rules parser is to parse the Rules Definition.
Default Rules Parser
See the Rules Definition for more detail.
Custom Rules Parser
You may write your own parser for your needs implementing the following interface.
use Tobento\Service\Validation\RulesParserInterface; use Tobento\Service\Validation\ParsedRule; use Tobento\Service\Validation\RulesParserException; interface RulesParserInterface { /** * Parses the rules. * * @param string|array $rules * @return array<int, ParsedRule> * * @throws RulesParserException */ public function parse(string|array $rules): array; }
Messages
Messages are used for the validation Error Messages.
Check out the Message Service to learn more about messages in general.
Messages Factory
With the message factory you can fully control how messages are created and modified.
use Tobento\Service\Validation\Message\MessagesFactory; use Tobento\Service\Message\MessagesFactoryInterface; use Tobento\Service\Message\MessageFactoryInterface; use Tobento\Service\Message\ModifiersInterface; use Psr\Log\LoggerInterface; $messagesFactory = new MessagesFactory( messageFactory: null, // null|MessageFactoryInterface modifiers: null, // null|ModifiersInterface logger: null, // null|LoggerInterface ); var_dump($messagesFactory instanceof MessagesFactoryInterface); // bool(true) $modifiers = $messagesFactory->modifiers(); var_dump($modifiers instanceof ModifiersInterface); // bool(true)
Parameters explanation
Default modifiers
If you do not set modifiers on the factory, the following modifiers are added:
use Tobento\Service\Message\Modifiers; use Tobento\Service\Validation\Message\RuleParametersModifier; use Tobento\Service\Message\Modifier\ParameterReplacer; $modifiers = new Modifiers( // maps :attribute, :value, :parameters // based on the rule parameters new RuleParametersModifier(), // Default parameter replacer new ParameterReplacer(), );
Message Translation
If you want to translate messages you may use the translator modifiers:
First you will need to install Translation Service though.
use Tobento\Service\Validation\Validator; use Tobento\Service\Validation\Message\MessagesFactory; use Tobento\Service\Validation\Message\RuleParametersModifier; use Tobento\Service\Translation; use Tobento\Service\Message\Modifier\Translator; use Tobento\Service\Message\Modifier\ParameterTranslator; use Tobento\Service\Message\Modifiers; use Tobento\Service\Message\Modifier\ParameterReplacer; $translator = new Translation\Translator( new Translation\Resources( new Translation\Resource('*', 'de', [ 'The :attribute must only contain letters [a-zA-Z]' => ':attribute muss aus Buchstaben [a-zA-Z] bestehen.', 'title' => 'Titel', ]), ), new Translation\Modifiers(), new Translation\MissingTranslationHandler(), 'de', ); $messagesFactory = new MessagesFactory( modifiers: new Modifiers( new Translator(translator: $translator, src: '*'), new RuleParametersModifier(), new ParameterTranslator( parameters: [':attribute'], translator: $translator, src: '*' ), new ParameterReplacer(), ) ); $validator = new Validator(messagesFactory: $messagesFactory); $validation = $validator->validate( data: [ 'title' => 'P3', ], rules: [ 'title' => 'alpha', ] ); $errors = $validation->errors(); var_dump($errors->key('title')->first()->message()); // string(44) "Titel muss aus Buchstaben [a-zA-Z] bestehen."