ikkez / f3-validation-engine

Validation engine for Cortex and the PHP Fat-Free Framework
GNU General Public License v3.0
12 stars 2 forks source link
cortex fat-free-framework validation

F3 Validation engine

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:

Table of Contents

  1. Installation
  2. Getting started
  3. Validation
    1. Cortex Mapper Validation
    2. Data Array Validation
  4. Rules
    1. Validators
    2. Filters
    3. Validation Dependencies
    4. Validation Level
    5. Array Validation
    6. Contains Check
  5. Error handling
    1. Error context
    2. Field names
    3. Frontend integration
    4. Custom Errors
  6. Extend
  7. License

Installation

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.

Getting started

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"

Validation

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.

Cortex Mapper Validation

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.

Data Array Validation

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);

Rules

Validators

The default validators from GUMP are:

Additional validators:

Filters

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:

Additional filters:

Validation Dependencies

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.

Validation Level

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.

Array Validation

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'],
    ]
]

Contains check

- 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 handling

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.

Error context

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.

Field names

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

Default field labels

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

Custom model context name conversion

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.

Frontend integration

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.

Custom errors

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');
}

Extend

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');

License

GPLv3