tobento/service-validation

1.0.2 2024-03-09 16:01 UTC

This package is auto-updated.

Last update: 2024-11-09 17:30:09 UTC


README

The Validation Service provides an easy way to validate data.

Table of Contents

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."

Credits