Usage
An important factor of a library is the interface. We can keep all information on one page because it’s really straight froward.
Initialize
Usually you need validation and filtering in controllers where you control the application flow and pass user (or third party) input to your business models. To test these controllers there are now two options:
- Test with edge cases (a lot of different input value combinations)
- Test that the expected validators get created and used.
We suggest the second method. So put your new Verja\Gate()
to a factory and replace this factory to mock this object
in tests.
use DependencyInjector\DI;
use Verja\Gate;
DI::set('verja', function() {
return new Gate();
}, false);
function controller() {
/** @var Gate $gate */
$gate = DI::get('verja');
}
Note: we are using
tflori\dependency-injector
here. The third parameter means that the object should not be cached. Instead every time a newGate
will be initialized.
Define Fields
A Field
can be initialized as a usual object and passed to Gate
with Gate::addField()
. You can also pass an
array of fields to Gate::addFields()
. These methods are literally aliased by Gate::accept()
and Gate::accepts()
.
Note: The
Gate
will not let any input pass that is not allowed to pass. That means that you also have to define fields that you don’t want to validate or filter.
These methods also allow the short form of defining fields to write code much easier and faster. So the following
calls are equal and allow any value for the key 'comment'
:
use Verja\Gate;
use Verja\Field;
$gate = new Gate();
$gate->accept('comment');
$gate->accept('comment', []);
$gate->accept('comment', new Field());
$gate->accepts(['comment']);
$gate->accepts(['comment' => []]);
$gate->accepts(['comment' => new Field()]);
Note: the later calls just overwrite the comment definition.
Add Filters To Field
You can add any object of a class that is implementing Verja\FilterInterface
to a Field
with Field::addFilter()
.
Because the order might be significant you can also prepend a filter with either Field::prependFilter()
or the
parameter $prepend
set to true
.
The methods to add a filter to an field also allow $filter
to be a string or a callable. To pass a callable is a
shortcut for passing new \Verja\Filter\Callback($callable)
. A string will be converted to a filter with
\Verja\Filter::fromString()
which is searching in LIFO order (last in first out) in all registered namespaces for a
matching filter and colons divide the parameters.
Filters can also be passed to the constructor of Field
in an array where the order is maintained.
use Verja\Field;
use Verja\Filter;
$field = new Field([new Filter\Trim('/')]); // trim slashes
$field->prependFilter('trim'); // first trim whitespace
$field->appendFilter('replace:foo:bar'); // after trim whitespace and trim slashes replace foo with bar
$field->addFilter(function ($value) {
return substr($value, 0, 3); // get the first 3 chars of the result
});
Add Validators To Field
The identical methods exists for the Verja\ValidatorInterface
. The order is also maintained and is relevant when you
try to get value from a field that is not valid. In this case the error message of the first validator that fails is
in the exception message.
The converter method \Verja\Validator::fromString()
allows an exclamation mark in front of the validator name that
will invert the validator.
use Verja\Field;
use Verja\Validator;
use DependencyInjector\DI;
$field = new Field([new Validator\Contains('@')]); // validate that it contains an @
$field->prependValidator('notEmpty'); // prepend not empty check
$field->appendValidator('emailAddress'); // append that it is an email address
$field->addValidator(function ($value) { // validate that the email is unknown
if (DI::get('db')->select('user')->where('email', $value)->count()) {
return Validator::buildError('EMAIL_TAKEN', $value, 'Email address already taken');
}
return true;
});
Type Validation And Filtering
For validating types (integer, boolean or even classes) often it is required to validate first and then filter. For
example a numeric value: $number = is_numeric($_POST['alpha']) ? (double)$_POST['alpha'] : 1;
. To allow this a filter
can throw a InvalidValue
exception or use the defined method validate
to do so. This method executes the validator
and throws if the value is not valid. The exception will be caught and no other filters or validators are executed
because the value could not be filtered.
For example you could write a filter for getting a User
object:
use Verja\Filter;
use Verja\Gate;
use App\User; // a user entity class
class UserFilter extends Filter {
public function filter($value, array $context = []) {
Gate::assert('integer', $value); // this ensures the $value is an integer
return User::findOrFail($value);
}
}
Required Fields
Fields can be required which means that an error leads to an exception when you try to get data for this field and the value is validated even if it is empty. On the other hand it means that a required field is ok to be empty if no validator is defined.
The required attribute can be set via Field::required()
or the string 'required'
in the array of definitions.
use Verja\Field;
$field = new Field([ 'required', 'notEmpty' ]);
Alternative declaration
Filters and validators can be defined in three different ways:
- As string to
new Field()
or explicitFilter::fromString()
andValidator::fromString()
- With keyword
new
directly - With magic
__callStatic()
method
The shortest way to write is as string - however it does not allow auto completion and can not be intuitive. Especially new developers will find it hard to find out how the filter or validator is called. Also you might be wondering when defining a new field and get a filter instead of a validator what happens when they have the same name.
The most understandable way is to create the objects by yourself - it’s also the fastest script as it is the most strait forward way. But you can’t call directly method on the object. Instead you have to surround it with parenthesis and so on. Also it is the the longest way to write.
Another approach are the magic methods. We can type hint these and then they allow auto completion. But it’s harder to get auto completion for custom validators and filters.
Here is a very short example with all three methods:
use Verja\Field;
use Verja\Validator;
use Verja\Filter;
// use strings
$field = new Field(['trim', 'notEmpty']);
$field->addValidator('numeric'); // explicit a validator
// use keyword new
$field = new Field([new Filter\Trim(), new Validator\NotEmpty(), new Validator\Numeric()]);
// use magic method
$field = new Field([Filter::trim(), Validator::notEmpty(), Validator::numeric()]);
Filter And Validate
As there is no magic you can use filters and validators directly or directly use a field for combined filters and validators for a specific value. And last but not least you can pass multiple values to the gate to validate and filter all of them at once.
use Verja\Gate;
use Verja\Field;
use Verja\Validator;
use Verja\Filter;
$validator = new Validator\Contains('foo');
var_dump($validator->validate('some foo may happen')); // true
$filter = new Filter\Trim('/');
var_dump($filter->filter('/relative/path')); // "relative/path"
$field = new Field(['trim:/ ', 'notEmpty']);
var_dump($filtered = $field->filter(' /relative/path /')); // "relative/path"
var_dump($field->validate($filtered)); // true
$gate = new Gate(['foo' => 'from constructor']);
$gate->accepts([
'foo' => [ 'contains:bar' ],
'pw' => [ 'required', 'strLen:3', 'equals:pw_conf' ],
]);
var_dump($gate->validate()); // false (no pw given, foo does not contain bar)
var_dump($gate->validate(['foo' => 'bar', 'pw' => 'abc', 'pw_conf' => 'abc'])); // true
$gate->setData(['foo' => 'bar', 'pw' => '123', 'pw_conf' => 'abc']);
$gate->getData(); // throws "Invalid pw: value should be equal to contexts pw_conf"
Assert Valid Values
You can also directly assert that a value is valid against a list of filters and validators with the static method
Gate::assert()
. This is also used in filters that require specific value. It throws an InvalidValue
exception if
the value is not valid. Example:
use Verja\Gate;
$id = Gate::assert('integer', $_GET['id']);
Show Errors
The Validator
may contain an Verja\Error
after validating an invalid value that you can retrieve with
Validator::getError()
.
The Field
contains an array of all errors occurred during Field::validate()
and the Gate
contains an array with
all arrays of errors from the fields. The method Gate::getErrors()
may return something like this:
return [
'foo' => [
new \Verja\Error(
'NOT_CONTAINS',
'any string',
'value should contain "bar"',
[ 'subString' => 'bar' ]
)
],
'pw' => [
new \Verja\Error(
'STRLEN_TOO_SHORT',
'abc123',
'value should be at least 8 characters long',
[ 'min' => 8, 'max' => 0 ]
),
new \Verja\Error(
'NOT_EQUAL',
'abc123',
'value should be equal to contexts pw_conf',
[ 'opposite' => 'pw_conf', 'jsonEncode' => true ]
)
],
];
You can then serialize this data to this json:
{
"foo": [
{
"key": "NOT_CONTAINS",
"message": "value should contain \"bar\"",
"parameters": {
"subString": "bar",
"value": "any string"
}
}
],
"pw": [
{
"key": "STRLEN_TOO_SHORT",
"message": "value should be at least 8 characters long",
"parameters": {
"min": 8,
"max": 0,
"value": "abc123"
}
},
{
"key": "NOT_EQUAL",
"message": "value should be equal to contexts pw_conf",
"parameters": {
"opposite": "pw_conf",
"jsonEncode": true,
"value": "abc123"
}
}
]
}
Example
This is a basic real world example:
use Verja\Gate;
use Verja\Validator;
use Verja\Error;
// imagine a class that persists somewhere somehow (maybe ORM\Entity from tflori\orm?)
use App\Model\User;
// imagine a dependency injection system (maybe tflori/dependency-injector?)
use DependencyInjector\DI;
$gate = new Gate();
$gate->accepts([
'username' => [ 'required', 'trim', 'strLen:3:20', new Validator\Callback(function ($value) {
return DI::get('em')->fetch(User::class)->where('username', $value)->count() ?
new Error('USERNAME_TAKEN', $value, 'value should be unique in user.username') :
true;
})],
'password' => [ 'required', 'strLen:8', 'equals:password_confirmation' ],
'email' => [ 'required', 'trim', 'emailAddress' ],
'age' => [ 'integer' ],
]);
if ($_SERVER['REQUEST_METHOD'] === 'post' && $gate->validate($_POST)) {
$user = new User;
$user->username = $gate->get('username');
$user->password = password_hash($gate->get('password'), PASSWORD_BCRYPT);
$user->email = $gate->get('email');
$user->age = $gate->get('age');
$user->save();
}