marwanalsoltany / mighty
The last validation library you will ever need!
Fund package maintenance!
Ko-Fi
Installs: 1 262
Dependents: 0
Suggesters: 0
Security: 0
Stars: 60
Watchers: 3
Forks: 2
Open Issues: 0
Requires
- php: >=8.1
- ext-ctype: *
- ext-dom: *
- ext-intl: *
- ext-json: *
- ext-mbstring: *
Requires (Dev)
- code-lts/doctum: ^5.5
- friendsofphp/php-cs-fixer: ^3.9
- phpbench/phpbench: ^1.2
- phpstan/phpstan: ^1.8
- phpunit/phpunit: ^9.5
This package is auto-updated.
Last update: 2024-11-09 19:17:13 UTC
README
The last validation library you will ever need!
Table of Contents
Installation
About Mighty
Quickstart
mVEL
Examples
Constraints
Validations
Documentation
Specification
Changelog
If you like this project and would like to support its development, giving it a ⭐ would be appreciated!
Key Features
- Zero dependencies.
- Framework agnostic, can be integrated in any codebase.
- Expressive, intuitive and easy to get along with over 250 built-in validation rules.
Installation
composer require marwanalsoltany/mighty
About Mighty
Validation is a common task in any web application. Data passed to the application via forms —or any type of input for that matter— must always be validated against a set of rules. Mighty can handle in an easy and expressive way.
Mighty is fast, powerful, robust, and easy to use validation library that is just fun to work with, it makes validating any data a breeze. Thanks to the power of the Mighty Validation Expression Language (mVEL) it is not like anything you've seen before. With its validation approach and over 250 built-in rules there is pretty much nothing that you cannot validate with it, in a very expressive and compact manner. Simply put, Mighty is validation on steroids! It is indeed the last validation library you will ever need.
Mighty provides several different approaches to validate data. It's most common use-case is validating incoming data via HTTP requests, but it of course is not limited to that; Mighty provides also attributes in the form of constraints to easily validate models and/or any kind of objects.
Mighty includes a wide variety of convenient validation rules that you may apply as a single rule or combine them with each other using operators to build up even more complex validations.
Quickstart
To learn about Mighty's powerful validation features, let's cut straight to the point and take a look at some examples:
General Data Validation
Validating form data using the Validator::class
:
use MAKS\Mighty\Validator; $validator = new Validator(); $validator ->setData([ 'name' => 'John Doe', 'username' => 'john.doe', 'password' => 'Super@Secret#123', 'email' => 'john@doe.com', 'hobbies' => ['coding', 'design', 'sports'], ]) ->setValidations([ // required&string&between:3,255 'name' => $validator->validation()->required()->string()->between(3, 255), // required&string&matches:/[a-z0-9._-]/i 'username' => $validator->validation()->required()->string()->matches('/[a-z0-9._-]/i'), // required&string&min:8 'password' => $validator->validation()->required()->string()->min(8), // required&email 'email' => $validator->validation()->required()->email(), // null^(required&array&max:5) 'hobbies' => $validator ->validation() ->null() ->xor() ->group(fn ($validation) => $validation ->array() ->max(5) ), // null|(if:${hobbies.validations.array}&(string&min:3)) // hobby can be null or a string with at least 3 characters if hobbies is an array 'hobbies.*' => $validator ->validation() ->null() ->or() ->group(fn ($validation) => $validation ->if('${hobbies.validations.array}') ->open() ->string() ->min(3) ->close() ), ]) ->validate(); $result = $validator->isOK(); // boolean result of the overall validation result $errors = $validator->getErrors(); // an array of results of validations that failed $results = $validator->getResults(); // an array of results of all validations $validator->check(); // void or throws an exception with a nicely formatted message of what exactly went wrong
Objects Validation
Validating the state of an object using Constraint::class
attributes:
use MAKS\Mighty\Validation\Strategy; use MAKS\Mighty\Validation\Behavior; use MAKS\Mighty\Validation\Operator; use MAKS\Mighty\Validation\Constraint; use MAKS\Mighty\Validation\Constraint as Assert; use MAKS\Mighty\Validation\Constraint\ValidatableObjectInterface; use MAKS\Mighty\Validation\Constraint\ValidatableObjectTrait; class ValidatableObject implements ValidatableObjectInterface { use ValidatableObjectTrait; #[Assert\Rule\Equals('CONST')] public const CONST = 'CONST'; #[Assert\Rule\In(['STATIC', 'VAR'])] public static $static = 'VAR'; #[Assert\Rule\StringConstraint] #[Assert\Rule\StringCharset('UTF-8')] #[Assert\Rule\Between(3, 99)] public $default = 'DEFAULT'; #[Assert\Rule\StringConstraint] #[Assert\Rule\StringContains('<element>')] #[Assert\Rule\Xml] public $xml = '<?xml version="1.0"?><element></element>'; #[Assert\Rule\ArrayConstraint] #[Assert\Shape([ 'string' => new Assert\Rule\Str, 'array' => new Assert\Rule\Arr, ])] public $array = [ 'string' => 'value', 'array' => [], ]; #[Assert\Rule\ObjectConstraint] #[Assert\Rule\ObjectIsInstanceOf(ValidatableObjectInterface::class)] #[Assert\Valid(message: 'Not valid')] public $object; #[Assert\Callback('is_scalar', 'Data is not scalar')] #[Constraint('string&min:3', strategy: Strategy::FailLazy, messages: [ 'string' => 'Must be string.', 'min' => 'Must be longer than ${@arguments.0}.', ])] public function getDefault() { return $this->default; } #[Assert\Compound([ new Assert\Rule\Str, new Assert\Compound([ new Assert\Rule\Arr, new Assert\Compound([ new Assert\Rule\Blank, ], Operator::Not), ], Operator::And), ], Operator::Xor, Behavior::Pessimistic, Strategy::FailLazy)] public static function getStaticProperty() { return static::$static; } } $object = new ValidatableObject(); $result = $object->isValid(); // boolean result of the overall validation result $results = $object->validate(); // an array of results of all validations $object->check(); // void or throws an exception with a nicely formatted message of what exactly went wrong
An example of the output of validating a validatable object would look like this:
// check out the previous snippet see the used constraints $object = new ValidatableObject(); $object->object = new class implements ValidatableObjectInterface { use ValidatableObjectTrait; // some properties and their validation constraints ... }; $object->default = null; // this must be a string $object->check(); // ValidationFailedException::class // Data failed to pass the validation. // (01) The value (null) of the "ValidatableObject->default" property failed to pass the validation [string]. Problem: Value must be a string. // (02) The value (null) of the "ValidatableObject->default" property failed to pass the validation [string.charset:"UTF-8"]. Problem: Value must be encoded in one of the following charsets: ["UTF-8"]. // (03) The value (null) of the "ValidatableObject->default" property failed to pass the validation [between:3,99]. Problem: Value must be between 3 and 99 or have a value/count/length that is between 3 and 99. // (04) The return value (null) of the "ValidatableObject->getDefault()" method failed to pass the validation [callback]. Problem: Data is not scalar. // (05) The return value (null) of the "ValidatableObject->getDefault()" method failed to pass the validation [string&min:3]. Problems: Must be string; Must be longer than 3.
Check also ValidatableObject
and ValidatableObjectChild
.
Hint: More examples can be found in the Examples section.
Mighty Validation Expression Language
Mighty has the concept of Validation Expression. The Validation Expression in its simplest forms is just a string that describes how Mighty should validate the given data. These strings are based on the Mighty Validation Expression Language Specification (mVEL). mVEL is pretty simple, human-readable and easy to cope with. It is a combination of well established concepts and/or specifications like Boolean Algebra, Bitwise Operators, JSON, and CSV.
Therefore, Validation Expression may be defined as a string that contains some rules separated by Bitwise Operators which will build an expression that when evaluated using Boolean Algebra logic, will result in the final result of the validation. The rules can have arguments, the types of these arguments can be denoted using the same rules of JSON types. A rule can also have multiple arguments and the arguments are separated by commas (CSV).
For example required&string&between:2,255|null
is a valid Validation Expression, this expression can be understood as the following:
- The expression has four rules.
- The expression contains the rules:
required
Asserts that the input is present.string
Asserts that the input is a string.between:2,255
Asserts that the input is a string with a length between 2 and 255.null
Asserts that the input is null.
- The final result is the result of evaluating the expression resulting from the result of each rule glued together using bitwise operators.
The required&string&between:2,255|null
expression means that the input must be present; AND of type string; AND between 2 and 255 in length; OR null. So it's a nullable string that when is not null must be between 2 and 255 characters long.
Lets say the the input was "Mighty is Awesome!"
, the result of the expression required&string&between:2,255|null
against that input would be 1&1&1|0
which will result in 1
which is true
, if the input was null
the result would be 0&0&0|1
= 1
, if the input was X
the result would be 0&0&0|0
= 0
, etc ...
Unlike other validations implementations, the concept of Boolean Algebra using Bitwise Operators, gives the possibility to build up complex validations that are very readable and compact while keeping the rules to a minimum number, reusing existing logic in reversed or compound manner, and finally keeping the code base as DRY as it can be. The benefits can be summarized in the following points:
- A rule can be NOTed (using
~
) to do the exact opposite of what it normally does. - A complex rule can be the result of ANDing (using
&
), ORing (using|
), or XORing (using^
), two or more simple rules. - Rules can be grouped together and/or given higher precedence by using parentheses, namely OPEN (using
(
) and CLOSE (using)
). - A Validation Expression can also have behavior, which is a character the prefixes the Validation Expression string that will affect all rules. Available behaviors are:
- NORMAL: execute all rules, default behavior (no prefix).
- OPTIMISTIC: stop executing rules after the first success (
?
prefix). - PESSIMISTIC: stop executing rules after the first failure (
!
prefix).
- Readability can be improved by aliasing some rules or adding rules sets as macros and executing them using the
[macro]
syntax.
Also the concept of JSON ensures arguments data-types safety, and the concept of CSV makes sure the arguments list has clear parsing rules.
The nicest thing, you don't have to memorize all the rules nor the Validation Expression Language syntax. The Validation
class is a fluent interface that can be used to build a Validation Expression. It knows about all Mighty available rules and has full IDE-Intellisense support to make it as easy as it gets. For example:
use MAKS\Mighty\Validation; // the validation expression: `required&string&between:2,255|null` // can be constructed using the Validation::class as follows: $validation = (new Validation())->required()->string()->between(2, 255)->or()->null(); // AND is the default operator // or statically: $validation = Validation::required()->string()->between(2, 255)->or()->null();
Fact: It usually takes more words to describe what a validation expression does than the validation expression itself!
Examples
Here are some examples of real world scenarios:
Validating a Single Value
use MAKS\Mighty\Validator; $result = ($validator = new Validator()) ->validateOne( '123', $validator ->validation() // can be an integer or float or a string that is numeric // this example is only for demonstration only, // the same result can be achieved using numeric() only ->integer()->or()->float()->or()->group( fn ($validation) => $validation->string()->and()->numeric() ) ) ->toArray(); // $result would look something like this: [ 'value' => '123', 'result' => true, 'validations' => [ 'integer' => false, 'float' => false, 'string' => true, 'numeric' => true, ], 'errors' => [], 'metadata' => [ 'basis' => 'integer|float|(string&numeric)', 'rules' => 'integer|float|(string&numeric)', 'expression' => '0|0|(1&1)', ], ]; // you can also simply use the static helper Validator::validateData($data, $validation);
Validating Structured Data
use MAKS\Mighty\Validator; use App\Service\HaveIBeenPwnedService as PasswordService; $validator = new Validator(); $data = [ 'name' => 'John Doe', 'age' => 32, 'email' => 'john.doe@domian.tld', 'username' => 'john.doe', 'password' => 'Secret@123', 'image' => '/path/to/image.png', 'submission' => 'now', 'consent' => 'yes', 'data' => [ 'nickname' => 'JOE', 'number' => 7, 'hobbies' => [ 'coding', 'cooking', 'reading', ] ], ]; $validations = [ 'name' => $validator->validation()->required()->string()->stringCharset(['UTF-8', 'ASCII'])->pessimistic(), // or using mVEL => required&string&string.charset:'["UTF-8","ASCII"]' 'age' => $validator->validation()->required()->integer()->min(18), // or using mVEL => required&integer&min:18 'email' => $validator->validation()->required()->email()->macro('gmail'), // or using mVEL => required&email&[gmail] 'username' => $validator->validation()->required()->username(), // or using mVEL => required&username 'password' => $validator->validation()->required()->password()->callback(fn ($input) => !PasswordService::isPwned($input)), // or using mVEL => required&password (NOTE: callback ist not possible, it requires a Validation::class instance that is bound to the Validator::class instance) 'image' => $validator->validation()->null()->xor()->group(fn () => $this->image()->imageDimensions(1920, 1080, '<=')), // or using mVEL => null^(image&image.dimensions:1920,1080,"<=") 'submission' => $validator->validation()->required()->datetime()->datetimeLt('2022-12-07'), // or using mVEL => required&datetime&datetime.lt:"2022-12-07" 'consent' => $validator->validation()->assert('${age.value}', 18, '>=')->or()->accepted()->or()->assertEquals('${this}', 'granted')->optimistic(), // or using mVEL => ?assert:${age.value},18,">="|accepted|assert.equals:${this},"granted" 'data' => $validator->validation()->required()->array()->arrayHasKey('nickname'), // or using mVEL => required&array&array.hasKey:"nickname" 'data.*' => $validator->validation()->scalar()->or()->array()->optimistic(), // or using mVEL => ?scalar|array 'data.nickname' => $validator->validation()->string()->min(2)->max(32), // or using mVEL => string&min:2&max:32 'data.hobbies.*' => $validator->validation()->ifEq('${data.hobbies.validations.array}', false)->or()->group(fn () => $this->string()->min(3)), // or using mVEL => if.eq:${data.hobbies.validations.array},false|(string&min:3) ]; $labels = [ 'name' => 'Name', 'age' => 'Age', 'email' => 'E-Mail', 'password' => 'Password', 'image' => 'Image', 'data' => 'Data', 'data.*' => 'Value of data', 'consent' => 'Consent', ]; $messages = [ '*' => [ // this will be expanded for all fields 'required' => '${@label} is a required field.', ], 'age' => [ 'min' => '${@label} must be at least ${@arguments.0}.', ], 'username' => [ 'matches' => '${@label} must contain letters, numbers, and the following characters ".-_" only.', ], 'consent' => [ 'assert' => 'You must be at least ${@arguments.1} years old to submit this form.', ] ]; $validator ->setData($data) ->setValidations($validations) ->setMessages($messages) ->setLabels($labels) ->validate(); $results = $validator->getResults(); // $result should look something like this: [ // this will actually be a Result object // array syntax is used here for demonstration purposes 'name' => [ 'key' => 'name', 'value' => 'John Doe', 'result' => true, 'validations' => [ 'required' => true, 'string' => true, 'string.charset' => true, ], 'errors' => [], 'metadata' => [ 'basis' => '!required&string&string.charset:["UTF-8","ASCII"]', 'rules' => 'required&string&string.charset:["UTF-8","ASCII"]', 'expression' => '1&1&1', ], ], // other validations ... ]; // you can also simply use the static helper Validator::validateData($data, $validations);
Hint: When providing message overrides to the Validator::class
, it is advised to use the Rule\Validation::class
to set array keys. This class contains all Mighty built-in rules names as class constants.
Extending the Validator
The validator can be extended in three ways:
- By adding a rule.
- By adding an alias.
- By adding a macro.
use MAKS\Mighty\Validator; use MAKS\Mighty\Rule; $validator = new Validator(); // adding a new rule $validator->addRule( (new Rule()) ->name('equals') ->arguments(['string']) ->callback(fn (string $input, mixed $expected): bool => $input == $expected) ->parameters(['@input', '@arguments.0']) ->comparison(['@output', '===', true]) ->example('equals:value') ->description('Asserts that the input is equal to the given value.') ); // adding a new rule alias $validator->addRuleAlias('eq', 'equals'); // adding a new rules macro $validator->addRuleMacro('gmail', 'string&email&matches:"/@gmail\.com$/i"'); $results = $validator->validateAll( [ 'name' => 'John', 'email' => 'john@doe.com', ], [ 'name' => 'eq:John', 'email' => 'required&[gmail]', ] ); // $results should look like this: [ // items will actually be a Result object // array syntax is used here for demonstration purposes 'name' => [ 'key' => 'name', 'value' => 'John', 'result' => true, 'validations' => [ 'eq' => true, ], 'errors' => [], 'metadata' => [ 'basis' => 'eq:John', 'rules' => 'eq:John', 'expression' => '1', ], ], 'email' => [ 'key' => 'email', 'value' => 'john@gmail.com', 'result' => false, 'validations' => [ 'required' => true, 'string' => true, 'email' => true, 'matches' => false, ],, 'errors' => [], 'metadata' => [ 'basis' => 'required&[gmail]', 'rules' => 'required&(string&email&matches:"/@gmail\.com$/i")', 'expression' => '1&(1&1&0)', ], ], ];
Hint: Check out the default rules
, aliases
, and macros
of the Validator
to see more examples.
Constraints
Mighty consists of over 250 rules/attributes that can be used to validate any data or the values of classes, class constants, properties, and methods.
The attributes are split into three main groups:
- Generic Constraint Attributes
- Special Constraint Attributes
- Rule Constraint Attributes
Generic Constraint Attributes Group
The Generic Constraint Attributes are located under the MAKS\Mighty\Validation
namespace.
This group consists currently of one attribute only; that is the Constraint
attribute. This attribute takes a Validation Expression to validate the data it is applied to. It is also the base class for all other attributes.
Special Constraint Attributes Group
The Special Constraint Attributes are located under the MAKS\Mighty\Validation\Constraint
namespace.
This group contains attributes that do a specific job that is available only in the context of attributes. It consists of the following attributes:
Rule
: This attribute is used to validate any data using a single validation rule. It is also the base class for all attributes in the Rule Constraint Attributes Group.Callback
: This attribute is used to validate any data using a callback function.Valid
: This attribute is used to validate the validity of a validatable object.Shape
: This attribute is used to validate the shape of an array or object. Note that this is the only attribute that validates a set of values (structured data) rather than a single value.Compound
: This attribute is used to combine a set of constraints to build up a Validation Expression. The constraints can be combined using any operator, and can also have a behavior. It serves as an object-oriented way to build up a Validation Expression.
Note: Note that the constraints that are allowed to be used with the Shape::class
and Compound::class
attributes must be an actual instances of the Constraint::class
, Rule::class
, or Compound::class
. The Callback::class
, Valid::class
, or Shape::class
of the Special Constraint Attributes Group are NOT allowed. If you have a need for this feature, open an issue and we'll discuss implementing it
Rule Constraint Attributes Group
The Rule Constraint Attributes are located under the MAKS\Mighty\Validation\Constraint\Rule
namespace.
This group contains attributes that are based on a single validation rule. It consists of most of the attributes Mighty provides. Refer to the Validations section for the full list.
Adding a Custom Constraint
Mighty has a huge list of built-in constraints, it's really rare that you will need anything other than what Mighty provides. Nonetheless, sometimes the need arises for a custom constraint, the is how you can achieve that:
<?php declare(strict_types=1); namespace App\Validation\Constraint; use Attribute; use MAKS\Mighty\Rule; use MAKS\Mighty\Result; use MAKS\Mighty\Validation\Strategy; use MAKS\Mighty\Validation\Constraint; use MAKS\Mighty\Validation\Constraint\ValidatesOne; // use the ValidatesMany interface if your Constraint returns a collection of Result objects use MAKS\Mighty\Validation\Constraint\ValidatesMany; #[Attribute( Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD )] class MyCustomConstraint extends Constraint implements ValidatesOne { public function __construct( ?string $message = null, Strategy $strategy = Strategy::FailFast, ) { parent::__construct( validation: 'app.myCustomConstraint', messages: ['app.myCustomConstraint' => $message], strategy: $strategy ); } public function validate(mixed $value = null): Result { // it is really up to you, how you handle this // you will just work with a normal Mighty Validator // here we're just preparing the data to pass to the Validator $name = ''; $data = [$name => $value]; $validations = [$name => $this->validation]; $messages = [$name => [$this->validation => $this->messages[$this->validation] ?? null]]; $labels = [$name => static::class]; // you can reuse the built-in rules or // add you own Rule that handles your custom logic $result = $this ->getValidator() ->addRule( // see MAKS\Mighty\Rule for more info (new Rule()) ->setName('app.myCustomConstraint') ->setCallback(static fn ($input) => $input /* here comes your logic */) ->setParameters(['@input']) // rule callback dependencies ->setMessage('${@label} must follow my custom constraint validation.') // this is the default message ) ->setData($data) ->setValidations($validations) ->setMessages($messages) ->setLabels($labels) ->validate(); return $result[$name]; // if you implement ValidatesMany, you will just return $result } }
Note: Custom constraints are considered a part of the Special Constraint Attributes Group (i.e. not allowed to be used with/inside the Shape::class
and Compound::class
constraints)
Validations
The following table lists all available rules including their Attribute and Method equivalents:
Rules and Aliases
Macros
Benchmarks
By now it may seem like Mighty is doing too much and performance concerns are starting to arise. Well, there is no need to worry about that. Mighty is really fast and is optimized to provide the best performance. Here are some benchmarks of the performance of the validator:
Not So Scientific Benchmark
The performance of Mighty Validator and Laravel Validator in a laravel application. The test was carried out using an array of 50000 elements, half of them are integers and the other half are numeric strings. Each validator was tested 10 times (consecutively) and the average result of these 10 was collected:
$data = array_merge(range(1, 25000), array_map('strval', range('25001', '50000'))); // Mighty Validator with XDebug disabled [ // required&integer 'preparationTime' => '1.32ms', // the time required to build the array 'validationTime' => '1107.29ms', // the time required to validate the array 'totalTime' => '1108.61ms', // the time required for the whole process ] // Mighty Validator with XDebug enabled [ // required&integer 'preparationTime' => '9.09ms', 'validationTime' => '6085.04ms', 'totalTime' => '6094.13ms', ] // Laravel Validator with XDebug disabled [ // required|integer 'preparationTime' => '1.33ms', 'validationTime' => '13882.72ms', 'totalTime' => '13884.05ms', ] // Laravel Validator with XDebug enabled [ // required|integer 'preparationTime' => '9.33ms', 'validationTime' => '24010.60ms', 'totalTime' => '24019.93ms', ]
So Mighty is about 12.5X times faster than Laravel Validator with XDebug disabled and about 4X times faster with XDebug enabled.
Scientific Benchmark
The benchmark is done using PHPBench. Here is a quick overview:
PHPBench (1.2.6) running benchmarks...
with configuration file: mighty/phpbench.json.dist
with PHP version 8.1.9, xdebug ❌, opcache ❌
\MAKS\Mighty\Benchmarks\ConstraintBench
benchAValidValidatableObject............I4 ✔ Mo305.595074ops/s (±0.75%)
benchAnInvalidValidatableObject.........I4 ✔ Mo326.708522ops/s (±1.02%)
\MAKS\Mighty\Benchmarks\ValidatorBench
benchSingleValidationString.............I4 ✔ Mo0.02212ms (±1.59%)
benchSingleValidationObject.............I4 ✔ Mo0.126929ms (±1.63%)
benchBulkValidationObject...............I4 ✔ Mo9.345847ms (±0.62%)
benchBulkValidationString...............I4 ✔ Mo6.734188ms (±0.40%)
Subjects: 6, Assertions: 6, Failures: 0, Errors: 0
Fact: The most recent benchmark result can also be found in the CI pipeline, which will be updated with each Push/PR to the upsteam.
Notes
- Mighty generates really friendly error messages by default (currently only in English). These messages can be easily overwritten on Validator/Constraint bases. You may want to have these messages in different languages, for that, the
MAKS\Mighty\Rule::setMessageTranslator()
method can be used. This method is a convenient way to set a global message translator, it takes a closure that gets the raw message (with placeholders) as an argument and must return the translated version of that message. - Refer to the Mighty Validation Expression Language Specification to learn more about the theory behind Mighty and to get a grasp of how the language works.
- Refer to Mighty API Documentation to learn more about the PHP API. The API is well-documented and there shouldn't be anything that is not covered there.
- Mighty is currently a pure validation library, it doesn't do any kind of transformation on the data. The engine and the current design is flexible enough and can be used to easily implement a Sanitizer on top of it, there is no plan to make this at the moment but it may be a future milestone.
License
Mighty is an open-source project licensed under the MIT license.
Copyright (c) 2022 Marwan Al-Soltany. All rights reserved.