ikkez/f3-validation-engine

Validation engine for Cortex and the PHP Fat-Free Framework

v1.7.3 2023-01-23 22:06 UTC

This package is auto-updated.

Last update: 2024-10-24 03:40:37 UTC


README

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:

  • Validator based on GUMP
  • multi-level validation
  • define Cortex validation within $fieldConf
  • nested array & dependency validation
  • pre- & post-validation filters
  • F3-compatible language dictionaries included
  • context-based language overwrites per field
  • also usable without Cortex
  • it's extendable

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:

  • required
    Ensures the specified key value exists and is not empty
  • valid_email
    Checks for a valid email address
  • max_len,n
    Checks key value length, makes sure it's not longer than the specified length. n = length parameter.
  • min_len,n
    Checks key value length, makes sure it's not shorter than the specified length. n = length parameter.
  • exact_len,n
    Ensures that the key value length precisely matches the specified length. n = length parameter.
  • alpha
    Ensure only alpha characters are present in the key value (a-z, A-Z)
  • alpha_numeric
    Ensure only alpha-numeric characters are present in the key value (a-z, A-Z, 0-9)
  • alpha_dash
    Ensure only alpha-numeric characters + dashes and underscores are present in the key value (a-z, A-Z, 0-9, _-)
  • alpha_space
    Ensure only alpha-numeric characters + spaces are present in the key value (a-z, A-Z, 0-9, \s)
  • numeric
    Ensure only numeric key values
  • integer
    Ensure only integer key values
  • boolean
    Checks for PHP accepted boolean values, returns TRUE for "1", "true", "on" and "yes"
  • float
    Checks for float values
  • valid_url
    Check for valid URL or subdomain
  • url_exists
    Check to see if the url exists and is accessible
  • valid_ip
    Check for valid generic IP address
  • valid_ipv4
    Check for valid IPv4 address
  • valid_ipv6
    Check for valid IPv4 address
  • guidv4
    Check for valid GUID (v4)
  • valid_cc
    Check for a valid credit card number (Uses the MOD10 Checksum Algorithm)
  • valid_name
    Check for a valid format human name
  • contains,n
    Verify that a value is contained within the pre-defined value set
  • contains_list,n
    Verify that a value is contained within the pre-defined value set. The list of valid values must be provided in semicolon-separated list format (like so: value1;value2;value3;..;valuen). If a validation error occurs, the list of valid values is not revelead (this means, the error will just say the input is invalid, but it won't reveal the valid set to the user.
  • doesnt_contain_list,n
    Verify that a value is not contained within the pre-defined value set. Semicolon (;) separated, list not outputted. See the rule above for more info.
  • street_address
    Checks that the provided string is a likely street address. 1 number, 1 or more space, 1 or more letters
  • date
    Determine if the provided input is a valid date (ISO 8601)
  • min_numeric
    Determine if the provided numeric value is higher or equal to a specific value
  • max_numeric
    Determine if the provided numeric value is lower or equal to a specific value
  • min_age,n
    Ensure that the field contains a date with a minimum age, i.e. min_age,18. Input date needs to be in ISO 8601 / strtotime compatible.
  • starts,n
    Ensures the value starts with a certain character / set of character
  • equalsfield,n
    Ensure that a provided field value equals current field value.
  • iban
    Check for a valid IBAN
  • phone_number
    Validate phone numbers that match the following examples: 555-555-5555 , 5555425555, 555 555 5555, 1(519) 555-4444, 1 (519) 555-4422, 1-555-555-5555
  • regex
    You can pass a custom regex using the following format: 'regex,/your-regex/'
  • valid_json_string
    validate string to check if it's a valid json format

Additional validators:

  • empty
    Check if a field value is empty.
  • notempty
    Ensure that a field value is not empty, no empty string, no string "0".
  • notnull
    Ensure that a field value is not NULL. It's a less-strict "required" check.
  • email_host
    Checks the MX domain record from a given email address. Fails if the host system does not accept emails. This validator does only work on valid emails, but does not validate the email address itself (combine this with valid_email).
  • unique - Cortex only -
    Check if the given field value is unique in the database table (i.e. for usernames).

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:

  • sanitize_string
    Remove script tags and encode HTML entities, similar to GUMP::xss_clean();
  • urlencode
    Encode url entities`
  • htmlencode
    Encode HTML entities
  • sanitize_email
    Remove illegal characters from email addresses
  • sanitize_numbers
    Remove any non-numeric characters
  • sanitize_floats
    Remove any non-float characters
  • trim
    Remove spaces from the beginning and end of strings
  • base64_encode
    Base64 encode the input
  • base64_decode
    Base64 decode the input
  • sha1
    Encrypt the input with the secure sha1 algorithm
  • md5
    MD5 encode the input
  • noise_words
    Remove noise words from string
  • json_encode
    Create a json representation of the input
  • json_decode
    Decode a json string
  • rmpunctuation
    Remove all known punctuation characters from a string
  • basic_tags
    Remove all layout orientated HTML tags from text. Leaving only basic tags
  • whole_number
    Ensure that the provided numeric value is represented as a whole number
  • ms_word_characters
    Converts MS Word special characters [“”‘’–…] to web safe characters
  • lower_case
    Converts to lowercase
  • upper_case
    Converts to uppercase
  • slug
    Creates web safe url slug

Additional filters:

  • website
    Prepend http:// protocol to URI, if no protocol was given.

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