This is an extension for the PHP Fat-Free Framework that offers a validation system, especially tailored for validating Cortex models. The validation system is based on the well known GUMP validator, with additional sugar of course:
$fieldConf
To install with composer, just run composer require ikkez/f3-validation-engine
.
In case you do not use composer, add the src/
folder into your AUTOLOAD
path and install Wixel/GUMP separately.
By default, the validation engine is silent. In order to make use of errors that where found during validation, you have to wire some things together.
This is done in the onError
handler, where you define what to do with errors. An example:
$validation = \Validation::instance();
$validation->onError(function($text,$key) {
\Base::instance()->set('error.'.$key, $text);
\Flash::instance()->addMessage($text,'danger');
});
In the example above, we've registered an onError handler that sets an error key in the Hive as well as adding a flash message to the user Session. Feel free to define this globally or change the onError handler on the fly.
For the error text to appear correctly, you need to load the included language dictionaries. To do so, you can add the lang folders to your dictionary path, like
LOCALES = app/your/lang/,vendor/ikkez/f3-validation-engine/src/lang,vendor/ikkez/f3-validation-engine/src/lang/ext,
Or you simply call a little helper for this one:
\Validation::instance()->loadLang();
This uses an optimized loading method and also includes some caching technique when you give $ttl
as 1st argument.
NP: Caching like \Validation::instance()->loadLang(3600*24);
requires the latest F3 edge-version, or it can lead to weird lexicon issues. Use at least:
"bcosca/fatfree-core": "dev-master#b3bc18060f29db864e00bd01b25c99126ecef3d6 as 3.6.6"
To use the validation engine, you can choose between a simple array validation against a defined set of rules or take a cortex mapper with predefined rules in its field configuration.
To use the mapper validation, you have to define the validation rules in the $fieldConf
array. This can look like this:
protected $fieldConf = [
'username' => [
'type' => Schema::DT_VARCHAR128,
'filter' => 'trim',
'validate' => 'required|min_len,10|max_len,128|unique',
],
'email' => [
'type' => Schema::DT_VARCHAR256,
'filter' => 'trim',
'validate' => 'required|valid_email|email_host',
],
];
To start validation you can either use this method:
$valid = $validation->validateCortexMapper($mapper);
or add the appropriate validation trait to your model class:
namespace Model;
class User extends \DB\Cortex {
use \Validation\Traits\CortexTrait;
// ...
}
and then just call $mapper->validate();
on your model directly.
If you just have a simple array and want to validate that against some rules you can do so too:
$data = [
'username' => 'Jonny',
'email' => 'john.doe@domain.com',
];
$rules = [
'username' => [
'filter' => 'trim',
'validate' => 'required|min_len,10|max_len,128|unique',
],
'email' => [
'filter' => 'trim',
'validate' => 'required|valid_email|email_host',
]
];
$valid = $validation->validate($rules, $data);
The validation methods always return a boolean value to indicate if the data is valid (TRUE
) or any error was found (FALSE
);
The default validators from GUMP are:
required
valid_email
max_len,n
min_len,n
exact_len,n
alpha
alpha_numeric
alpha_dash
alpha_space
numeric
integer
boolean
float
valid_url
url_exists
valid_ip
valid_ipv4
valid_ipv6
guidv4
valid_cc
valid_name
contains,n
contains_list,n
doesnt_contain_list,n
street_address
date
min_numeric
max_numeric
min_age,n
min_age,18
. Input date needs to be in ISO 8601 / strtotime
compatible.starts,n
equalsfield,n
iban
phone_number
regex
'regex,/your-regex/'
valid_json_string
Additional validators:
empty
empty
.notempty
empty
, no empty string, no string "0"
.notnull
NULL
. It's a less-strict "required" check.email_host
valid_email
).unique
- Cortex only -Filters can be used to sanitize the input data before validating it. Basically a filter is a simple PHP function that returns a string.
You can set up normal a filter
that is applied before validation and post_filter
that's applied afterwards:
'foo' => [
'filter' => 'trim|base64_decode',
'validate' => 'required|valid_url',
'post_filter' => 'base64_encode',
],
Default GUMP filters:
sanitize_string
urlencode
htmlencode
sanitize_email
sanitize_numbers
sanitize_floats
trim
base64_encode
base64_decode
sha1
md5
noise_words
json_encode
json_decode
rmpunctuation
basic_tags
whole_number
ms_word_characters
lower_case
upper_case
slug
Additional filters:
website
http://
protocol to URI, if no protocol was given.You can skip a validation rule and only take it into account when a certain condition to another field is met.
In the following example, foo
is only required when bar = 2
:
'foo' => [
'validate' => 'required',
'validate_depends' => [
'bar' => 2
]
]
You can also use some more advanced dependency checks, like nested validation rules:
'foo' => [
'validate' => 'required',
'validate_depends' => [
'bar' => ['validate', 'min_numeric,2']
]
]
When the nested validation is TRUE
(valid), then the dependency is met and the validation of foo
goes on, otherwise it's skipped.
If that's not enough, you can also add a custom function:
'validate_depends' => [
'bar' => ['call', function($value,$input) { return (bool) $value % 3 }]
]
An F3 callstring like ['call', 'Foo->bar']
is also possible.
Similar to a validation dependency, you can also define validation levels. If a validation level is defined on a field, its validation is completely skipped (filters however are applied).
Validation levels are useful when you want to validate a record in certains steps, i.e. a "draft" might need less validation rules before it is considered to be published.
You can define a validation level like this:
'foo' => [
'validate_level'=>2,
'validate' => 'required',
]
By default, all validations are made with level = 0
. To trigger this validation rule, you need to call the validate function with a new $level
:
$validation->validateCortexMapper($mapper, $level);
// OR
$validation->validate($rules, $data, $level);
The default comparison operator for the level check is <=
. So a level-2 validation also triggers level 0 and level 1 validation rules. If you only want to check level-2 alone, set a new $level_op
parameter as well.
In case you have an array field in your data or model, you can validate certain array keys as well:
'contact' => [
'type' => self::DT_JSON,
'validate_array' => [
'name' => ['validate'=>'required'],
'address' => ['validate'=>'required|street_address'],
'region' => ['validate'=>'required'],
'zip' => ['validate'=>'required'],
]
]
Nested arrays:
It's also possible to validate a nested array / an array of assoc arrays:
'partners' => [
'type' => self::DT_JSON,
'validate_nested_array' => [
'name' => ['validate'=>'required'],
'profession' => ['validate'=>'required'],
'phone' => ['validate'=>'required'],
'image' => ['filter'=>'upload'],
]
]
- work in progress -
It is also possible to define the values that contains will check (contains
validator) with a simple item
key in your $fieldConf
(or $rules
array):
'foo' => [
'type' => Schema::DT_VARCHAR128,
'item' => [
'one',
'two',
'three',
]
]
If the value does not match one of the items defined, the validation will fail.
It's also possible to put a string to item
, which is a F3 hive key, that contains the array with items.
Error messages are loaded from the dictionary files into the F3 hive.
The default hive location is at: {PREFIX}error.validation.{type}
That means, in case your PREFIX is ll.
, the default text for the required
validator is at ll.error.validation.required
.
Of course you can overwrite that in your custom language files for the whole project, but sometimes you might only want to change that for a single field in your model. Here we can use error contexts.
When you validate a Cortex model, it'll automatically have a context according to the class namespace. So in case you validate \Model\User
, the error context is at error.model.user
.
The trick to overwrite the default error message for a specific field and a specific validator is to create a new language key at this context:
[error.model.user]
username.required = You Shall Not Pass!
username.unique = Doh! It's already taken.
Most error messages contain a field identifier / field name, the error belongs to. By default this name is build from the array key of that field but that's mostly not enough. To have a proper translated field name in your error message too, the system looks for a language key according to the model context and the field.
In example, our username field in user model should be labeled at:
model.user.username.label = Nick name
While you're at it, you could also think about placeholder labels, help text and more that might fit into this schema, which can potentially improve the frontend wiring as well.
When you get an error within validating an array using the validate_array
or validate_nested_array
rules, the field labels are moved one key below the entry field.
I.e. when you have an array address field on your user model that includes a zip-code field, the label context would be:
model.user.address.zip.label = Zip Code
If you have common fields that are present on multiple models and don't want to repeat yourself defining those translated labels for each and every model again, you can define a default fallback label at model.base
:
[model.base]
deleted_at.label = deleted at
enabled_from.label = visible from
enabled_until.label = visible until
By default the model name is converted to a lower-case string. I.e. this class \Model\BackendUser
is converted to the context: model.backenduser
. You can adjust this behaviour by setting a custom function:
\Validation::instance()->setStringifyModel(function($mapper) {
return implode('.',array_map('lcfirst',explode('\\',get_class($mapper))));
});
This example will transform the classnames to model.backendUser
.
When you've set the error key within the onError
handler like in the sample from the beginning, you can easily use those to display custom error messages or add classes to your markup:
<div class="uk-card uk-card-default {{ @@error.model.user.username ? 'frame-red' : 'frame-grey' }}">
<div class="uk-card-body">
<h3 class="uk-card-title">{{ @ll.model.user.username.label }}</h3>
<div class="uk-margin">
<input class="uk-input {{ @@error.model.user.username ? 'uk-form-danger' : '' }}"
type="text" name="username" placeholder="{{ @@ll.model.user.username.placeholder }}">
</div>
</div>
</div>
If you like, you can also add customized help texts depending on the failed validator.
For example add a <F3:check if="{{@@error.model.user.username.unique}}">
or a different one for the required validator and you can build complex and functional form states. For additional flash messages, maybe have a look at f3-flash.
If you want to set a custom error message only, read about Error context above.
In case you need to trigger an error manually, because there are parts to validate that are out of scope here at the moment, you can make your own validation and emit an error yourself like this:
$valid = $userModel->validate();
if (!$userModel->foo1 && !$userModel->foo2) {
\Validation::instance()->emitError('foo','required','model.user.foo');
$valid = false;
}
if ($valid) {
// continue
}
emitError( string $field, string $type, string|array $context = NULL, string $fallback = NULL)
To use error messages from a different field, you can use an array as $context
parameter:
\Validation::instance()->emitError('phone_mail_copy','required',[
'model.employerprofile.phone_mail_copy', // context
'model.employerprofile.phone_mail_orig', // context label
]);
You can also use params on custom errors:
if ($f3->get('POST.password') !== $f3->get('POST.password2')) {
// The {0} field does not equal {1} field
\Validation::instance()->emitError(['password','Password repeat'],'equalsfield','model.user');
}
Extending the system is easy. To add a new validator:
\Validation::instance()->addValidator('is_one', function($field, $input, $param=NULL) {
return $input[$field] === 1;
}, 'The field "{0}" must be 1');
Also add the new translation key at error.validation.is_one
, otherwise only the fallback text is shown, or the context, when no text was given at all.
And to add a new filter:
\Validation::instance()->addFilter('nl2br',function($value,$params=NULL) {
return nl2br($value);
});
You can also use a F3 callstring too:
$validator = \Validation::instance();
$validator->addValidator('is_one','App\MyValidators->is_one');
$validator->addFilter('nl2br','App\MyFilters->nl2br');
GPLv3