mizmoz / validate
Validation inspired by React Prop Types
Requires
- php: >=8.3
- ext-intl: >=1.1
- ext-json: >=1.2
- guzzlehttp/guzzle: ^7.9
- symfony/console: ^7.0
Requires (Dev)
- mockery/mockery: ^1.4
- phpstan/phpstan: ^1.12
- phpunit/phpunit: ^9.5
README
Validation for PHP 7 that tries to suck less.
We've used a lot of different validation libraries over time and I've yet to be truly happy with any of them.
The main aim for this is to create a validator that can handle complex items, resolve them and also create nice descriptions of themselves. The dream is to create a validator that can handle REST API data, send useful error messages to the user and also a nice description of the endpoint. This will be the face of the Mizmoz API.
Table of Contents
Getting started
Composer installation
It's probably worth pointing out the API is really new and very likely to change.
composer require mizmoz/validate
Keeping the resources up to date
If you‘re using the IsEmailDisposable validator you‘ll want to make sure you‘re using an up to date list of disposable email host names.
Best practice is you create a cron job that executes the update below.
php bin/mizmoz update
See the resources folder for an example cron file that should be placed in /etc/cron.d.
Basic validation
Validate::isString()->validate('Hello there!'); // true Validate::isObject()->validate(new DateTime); // true Validate::isArray()->validate([]); // true Validate::isArray()->validate(new ObjectWithToArray); // true Validate::isOneOf(['on', 'off'])->validate('on'); // true Validate::isOneOf(['on', 'off'])->validate('oops'); // false // Validate a value and return it's object $result = Validate::isObjectOfType(DateTime::class)->validate('2016-01-01 00:00:00'); $result->isValid(); // true $result->getValue(); // DateTime 2016-01-01 00:00:00
Resolving a validator to a new value
$result = Validate::isSame('me') ->toValue(\User::current()->userId) ->validate($userId); // get the user id $userId = $result->getValue();
More complex and useful examples
// Validate a set of items $result = Validate::set([ 'name' => Validate::isString() ->setDescription('Subscriber name') ->isRequired(), 'email' => Validate::isEmail() ->isRequired(), 'sendNewsletter' => Validate::isBoolean() ->setDescription('Subscribe the email address to the newsletter?') ->setDefault(false) ])->validate($_POST); // Get the sanitised data as an array. $values = $result->getValue();
Validate ACL
Whilst there are no validators for ACL (they might come later). There are exceptions that can be returned or thrown in your application. This way you can use a standard set of Exceptions to handle ACL failures. For example we use validation on our API and will catch the AclException to display an error message.
// Add custom validator ValidateFactory::setHelper('aclOwner', function () { // add your custom validator return new IsOwner(); }); Validate::aclAuthenticated(\User::current())->validate(\User::get(1));
Testing
Any validator or resolver can be mocked using the ValidatorFactory::mock()
method.
The mocking is pretty simple and just allows for the return Result object to be set by calling methods on the mock object.
# Setup the Mock of isString $mock = ValidatorFactory::mock('isString') ->valid(false) ->value('fail-value') ->message('boo'); Validator::isString()->validate('hello')->isValid(); // false as we've overriden the result # Reset the container ValidatorFactory::unMock('isString'); # ... or using the original mock $mock->unMock(); # You can also make a mock go away after it's called by adding `once()` when setting it up ValidatorFactory::mock('isString') // ... extra setup ->once();
Getting the parameters passed to the Mocked object
# return an array with the param names and the called method either __constructor, validate or resolve $mock->getCallArguments();
Validators
IsArray
Check the value is an array
(new IsArray()) ->validate([1, 2, 3]); // true
IsArrayOf
Check the array contains only the provided values. Useful for checking enums that are allowed multiple values. For only 1 value set IsOneOf
$validate = (new IsArrayOf(['yes', 'no', 'maybe'])); $validate->validate(['yes']); // pass $validate->validate(['no']); // pass $validate->validate(['yes', 'no']); // pass $validate->validate(['definitely']); // fail
IsArrayOfShape
Same as IsShape except the $value must be an array.
IsArrayOfType
Check the array contains only items of the particular type
Checking against a single type
$validate = (new IsArrayOfType( new IsString() )); $validate->validate(['Hello']); // pass $validate->validate([1, 'World']); // fail
Checking against multiple types
$validate = (new IsArrayOfType([ new IsString(), new IsInteger(), ])); $validate->validate(['Hello']); // pass $validate->validate([1, 'World']); // pass
IsBoolean
Check the value is boolean-ish, that is 1, '1', true, 'true' & 0, '0', false, 'false'.
(new IsBoolean()) ->validate(true); // true
IsDate
Check the value is a valid date in the format provided.
$options
string format
- the date format the value is expected to be inbool setValueToDateTime
- set the value from the result to the value when the date is valid.bool strict
- using strict will force empty strings to fail
(new IsDate()) ->validate('2016-01-01'); // true (new IsDate(['format' => 'd/m/Y'])) ->validate('01/01/2016'); // true
IsEmailDisposable
Check if the value is a disposable email address like guerillamail.com etc
(new IsEmailDisposable()) ->validate('bob@guerillamail.com'); // true
IsEmail
Check if the value is a valid email address
(new IsEmail()) ->validate('support@mizmoz.com'); // true
Don‘t allow disposable email addreses
(new IsEmail(['allowDisposable' => false]))
IsFilter
The filter is a pretty cool helper for parsing strings for filtering.
Basic hash tags with example usage
We use the filter to map to column names for things like statuses etc. Only @tag
& #tag
are
supported anything else will be returned in the filter key as plain text
$validate = new IsFilter([ '#active|#deleted' => 'userStatus' ]); $result = $validate->validate('#deleted')->getValue(); // returns ['userStatus' => ['delete'], 'filter' => ''] $model = User::create(); foreach ($result as $column => $value) { // we have some magic attached to our models for filtering also but you get the idea of how this can be used ;) $model->where($column, $value); } $model->fetch();
Special :isInteger
tagging
$validate = new IsFilter([ '@:isInteger' => 'userId' ]); $result = $validate->validate('@123 @456')->getValue(); // returns returns ['userId' => [123, 456], 'filter' => '']
The filter value has any tags removed
$validate = new IsFilter([ '#subscribed' => 'userStatus' ]); $result = $validate->validate('Bob')->getValue(); // returns ['filter' => 'Bob'] // or with tags $result = $validate->validate('Bob #subscribed')->getValue(); // returns ['userStatus' => ['subscribed'], 'filter' => 'Bob']
Default tags when no tags are present using the *
// active is marked as the default $validate = new IsFilter([ '#active*|#inactive' => 'status' ]); $validate->validate('')->getValue(); // returns ['status' => ['active']] // Or with a filter $validate->validate('Bob')->getValue(); // returns ['status' => ['active'], 'filter' => 'Bob']
Defaults are for the defined group so you can have other tags without defaults
// active is marked as the default $validate = new IsFilter([ '#active*|#inactive' => 'status', '#admin|#user' => 'role', ]); $validate->validate('')->getValue(); // returns ['status' => ['active']] // Or with a tag $validate->validate('#admin')->getValue(); // returns ['status' => ['active'], 'role' => ['admin']]
IsInteger
Check the value is an integer
bool $strict
- set to strict to only allow integers and not number strings or floats.
(new IsInteger()) ->validate(1); // valid
IsNumeric
Check the value is a number
(new IsNumeric()) ->validate(1); // valid (new IsNumeric()) ->validate('100'); // valid
IsObject
IsOneOf
IsOneOfType
IsReCaptcha
Validate a reCAPTCHA response
(new IsReCaptcha($secret)) ->validate($response);
IsRequired
IsSame
IsShape
Check the value is a particular shape, sometimes is easier to explain with an example...
(new IsShape([ 'name' => new IsString(), 'age' => new IsInteger(), ]))->validate([ 'name' => 'Bob', 'age' => 45, ]); // valid
Shapes can be nested to validate shapes of shapes.
Using the Validate::set()
provides a helper to return nice descriptions of the shape.
$validate = Validate::set([ 'name' => Validate::isString() ->setDescription('Full name') ->isRequired(), ]); // return an array describing the set / shape. $validate->getDescription();
IsString
Check the value is a string
(new IsString) ->validate('Hello world'); // valid
Road map
On the todo list
- Formalise the API
- Optional descriptions in OpenAPI format: https://github.com/OAI/OpenAPI-Specification
- Create validators as ReactJS components. Parse the description from Chain to form components.
- Add docs for all remaining validators... there are quite a few more than listed here so be sure to have a look in the src/Validator directory.
- Add more validators!
Tasks
- Create description for isOneOfType
General
Allow positive or negative matching. Possibly like this:
// Positive match Validate::is()->email(); // Negative match Validate::not()->email();
Validators
IsPassword
Check a string matches the requirements for the password.
- Minimum length
- Uppercase characters
- Lowercase characters
- Special characters
- Numbers
Resolvers
ToHash
Create a hash of the given data with various techniques. MD5, SHA1, password_hash etc.
Generate API releases
At Mizmoz we already use the validator chain to generate our API schema in JSON.
The next step would be to create new versions or releases by comparing the new and old API endpoints. This would allow us to create discrete versions for each change and also help with documentation by highlighting each of the changes. We could even go further and highlight breaking changes to the API when defaults etc are changed.