bluem/validation

Simple, stand-alone validation library

3.0.1 2015-07-07 12:41 UTC

README

SensioLabsInsight

Synopsis

Validation is a simple, stand-alone validation and (to some extent) normalization library for PHP 5.3 or higher. It can be used to validate a single value, a single object’s property or all of an object’s properties – either be defining the validation rules in an array, or by using property annotations in the class (domain object) whose properties should be validated.

Why yet another validation library?

Very good question, glad you asked. Indeed, there are plenty of PHP validation libraries, some of them really good.

For instance, there is the nice Symfony Validator component (which I do use myself, when it makes sense). It is easy to integrate when using Symfony2 or the so-called “fat” Silex distribution, but if not, you have to wire things yourself, which (unless you’re happy with the English localization) at the very least means using also the Translator component. And even then, it cannot handle some localized values so you have to add constraints yourself. The same is true when, for example, you want to make sure that a date is smaller than another date.

This is where this Validator library is more convenient: it does not have any dependencies, will handle localized input such as numbers (if you have ever copied a monetary value such as a German “1.234,56” to an input field of an application that doesn’t know how to handle localized decimal and thousands separator, you know what I’m talking about) or dates and is able to check dependencies (i.e.: value1 is smaller|larger than value2 etc.). The obvious downside is that this library has far fewer constraints to choose from (I’m attempted to say: it does not have all those constraints you’ll hardly ever need) and that its localization support is currently pretty limited (English and German) – albeit it’s easily extendable.

So, to conclude: depending on your needs and your project, it can be a handy solution, but it’s probably not an “always and everywhere” validation library.

Installation

The preferred way to install this library is through Composer. For this, add "bluem/validation": "~2.0" to the requirements in your composer.json file. As Validation uses semantic versioning, you will get fixes and feature additions when running composer update, but not changes which break the API.

Alternatively, you can clone the repository using git.

Validating using annotations

Step 1: Define constraints by writing annotations

#!php

/**
 * @var string
 * @validation-type email
 * @validation-label E-mail address
 * @validation-mandatory IF NOT $phone
 */
protected $email = '';

/**
 * @var string
 * @validation-type string
 * @validation-label Phone
 * @validation-maxlength 15
 * @validation-pattern /^[0-9 ()+-]$/
 * @validation-mandatory IF NOT $email
 */
protected $phone = '';

/**
 * @var string
 * @validation-type url
 * @validation-label Website URL
 */
protected $url = '';

Step 2: Implement your setters

#!php
/**
 * @param string $value
 * @throws InvalidArgumentException
 */
public function setEmail($value)
{
   // The call to validatePropertyValue() will throw an InvalidArgumentException
   // if it does not fulfill the rules/constraints.
   $this->email = $this->validatePropertyValue($this, 'email', $value);
}

Optional step 3: Add validation for the whole class.

#!php
/**
 * @param string $value
 * @return array
 */
public function validate()
{
   return $this->validator->validateObject($this);
}

That’s it. The setters make sure that the properties cannot be set to invalid values and the class-wide makes sure that the current values adhere to these constraints and, moreoever, checks conditional constraints such as @validation-mandatory above. In this example, setting @validation-mandatory to IF NOT $email and IF NOT $phone makes sure that either the e-email address or phone number has to be set.

Validating using annotations not only enables you to easily validate an object by calling validateObject(), but is especially handy if you prefer to implement setters using the property overloading approach based on implementing a __set() method in your class. In other words: you pass property name and value to __set(), and inside __set(), you only have to make sure that the property name exists (and is intended to be validated by Validator) and tvalidatePropertyValueteProperty($this, $name, $value)`, and you have validation for all properties.

A nice feature of annotation-based validation is that the “minimum” and “maximum” constraints accept the names of other properties as value. This means that if you have two instance variables $startmonth and $endmonth, you can add a constraint @validation-minimum $startmonth to $endmonth (and vice versa) to make sure the dates are logically correct.

Validating with explicit constraints

If you do not like the annotation-based approach or need constraints that cannot be expressed by annotations alone (for instance, “the minimum value for this date is the second Tuesday in next month”), you can explicitly call validate(). This is also useful in situations where you do not even deal with a model class, but need simple, direct validation for data provided by a user.

Instantiating the validator works as usual, but instead of validatePropertyValue(), you call validate() with the constraints as argument 1 and the value as argument 2:

#!php
$validator = new \BlueM\Validation\Validator(
    new BlueM\Validation\I18n\En()
);

try {
    $value = $validator->validate(
        array(
            'type'      => 'string',
            'minlength' => 10,
            'mandatory' => true,
        ),
        $value
    );
    // $value is now the transformed/cleaned value (in case of a
    // string: trimmed, vertical whitespace removed)
} catch (\InvalidArgumentException $e) {
    // Validation failed
}

Validating collection datatypes

In case you have instance variables that should be an array holding several items of the same type and the same constraints, you can use a collection. For example, in the case of a collection of e-mail addresses, this would be the code when using annotations:

#!php
/**
 * @var array
 * @validation-type email[]
 */
protected $emailAddresses = array();

Now, when letting Validator validate the value array('user1@example.com', 'user2@example.com'), validation will pass, while validating "Hello world" or array(123, 'Not e-mail') will fail.

ArrayCollections (an indexed array of associative arrays) work similarly, except that in this case, you have to specify the datatype as array[] and can add constraints for the fields in the associative arrays.

As an example, let’s create an instance variable that holds an array of events, each one contained in an array with keys "startdate" (date, mandatory), "enddate" (date, optional) and "title" (string consisting of 10 ... 50 characters, mandatory):

#!php
/**
 * @var array
 * @validation-type array[]
 * @validation-type:startdate date
 * @validation-mandatory:startdate
 * @validation-type:enddate date
 * @validation-type:title string
 * @validation-mandatory:title
 * @validation-minlength:title 10
 * @validation-maxlength:title 50
 */
protected $events = array();

The Validator should, for instance, regard this value as valid:

#!php
array(
	array(
	    'startdate' => '2013-04-07',
	    'enddate'   => '2013-04-11',
	    'title'     => 'Title of event 1',
    ),
	array(
	    'startdate' => '2014-02-25',
	    'title'     => 'Title of event 2',
    ),
)

On the other hand trying to validate this value should fail, as the first event’s startdate is invalid and the second event’s title is too short:

#!php
array(
	array(
	    'startdate' => '2013-02-31',
	    'enddate'   => '2013-04-11',
	    'title'     => 'Title of event 1',
    ),
	array(
	    'startdate' => '2014-02-25',
	    'title'     => 'Short',
    ),
)

Catching ArrayCollection validation exceptions

When validating an ArrayCollections (an indexed array of associative arrays), you cannot simply use the setter and catch exceptions of type \InvalidArgumentException, as if you did, you would not know which value(s) exactly caused the validation to fail.

The more informative way is to catch an InvalidCollectionArgumentException (for the fully-qualified class name, see code below), which has a getErrors() method which will return the precise information, so you can display the error string next to the element.

#!php
try {
    $object->seEvents($events, $validator);
} catch (\BlueM\Validation\InvalidCollectionArgumentException $e) {
    $errors = $e->getErrors();
    // $errors is now an array with the numeric row index as key and
    // assocative array(s) as value. For instance, in the last example
    // above, $e->getErrors() would return this:
    // array(
    //    0 => array('startdate' => 'The date is invalid.')
    //    1 => array('title' => 'The value must not have less than 10 characters.')
    // )
} catch (\InvalidArgumentException $e) {
    $error = $e->getMessage();
    // $error is now a newline-separated string containing all errors
}

Defining field labels for “blank” or “mandatory” conditional constraints

When conditional constraints of type Blank or Mandatory exist, they may need to reference other properties in the error message (e.g.: “This value is mandatory, if field foobar is not empty.”).

For this, you can pass an array of labels as third argument to validate() and validateObject(), or as fourth argument to validatePropertyValue(), validateProperty(). The array is expected to be an associative array that has the same string as key that is used in the conditional constraint’s definition, and the field label as value.

Example: When your property annotation contains something like @validation-mandatory IF $firstname, the array would have to be like array('$firstname' => 'First name').

Datatypes

Currently the following datatypes are supported:

  • ArrayCollection: A collection (indexed array) of associative arrays, where the latter ones have certain constraints
  • Bool: A boolean value
  • Collection: A type that holds one or more items of same type/with identical constraints
  • Date: A scalar type that holds a date representation (internally in “Y-m-d” format) which can be set from a localized string
  • Email: A string of at most 254 characters that roughly has to look like an e-mail address, i.e.: The tests performed are not RFC-compliant, but rather check for the most typical mistakes
  • Float: A float, which can be set from a localized number
  • HourMinute: Hour plus minute (not a time), internally represented by a string in “H:m” format
  • Int: A float, which can be set from a localized number
  • Month: A string that specifies a month, such as “03/2012” or “2010-8”
  • String: A single-line string containing no more than 250 characters and from which leading and trailing whitespace are automatically trimmed
  • Text: A single-line or multi-line string with no length limitation, from which leading and trailing whitespace are automatically trimmed. Linebreaks are normlized to Unix style (“\n”)
  • Url: A single-line string that represents an HTTP or HTTPS URL. By default, this string may not contain more than 150 characters, but this can be changed by using a maxlength constraint.
  • Xml: An XML string (only checked if it is well-formed, currently no further validations)

With the exception of ArrayCollection and Collection, the you can directly use the lowercased name of the datatype to tell Validator which datatype to expect. For ArrayCollection and Collection however, it’s a little different (see examples above): you have to use array[] (ArrayCollection) and string[] (Colletion) respectively, where in the case of Collection, it could of course also be email[], int[] and so on.

Constraints

There are more constraints than the ones listed below, but the ones excluded are merely useful for internal use, not for using them in annotations or for specifying validation rules.

  • decimalplaces: Makes sure that the given value (expected to be a float) has no more than a given number of decimal places (digits after the decimal separator)
  • mandatory: Makes sure that the given value is not null and not an empty string and not an empty array.
  • maximum: Makes sure that the given value is at most as large (whatever that means, depending on the datatype) as the argument given to the constructor.
  • maxlength: Makes sure that the given string (which may be a string representation of a number) contains at most the given number of characters
  • mincount: Minimum number of items in a collection (this might change in a future version)
  • minimum: Makes sure that the given value is not smaller (whatever that means, depending on the datatype) as the argument given to the constructor.
  • minlength: Makes sure that the given string (which may be a string representation of a number) contains at least a minimum number of characters
  • notlocalized: Tells the validator that this value must not be interpreted as a localized value
  • pattern: Makes sure that the value matches a regular expression (which should include delimiters and modifiers)
  • valuelist: The value must be in the specified list of values. This list is expected to be either a whitespace-separated string (typically useful in annotations) or an indexed array.
  • wellformed: Makes sure that the given string is well-formed. Naturally, this only makes sense to use for strings which are expected to be XML.

Conditional constraints

Conditional constraints are mandatory constraints that are applied or ignored based on whether another instance variable is empty or not.

Typical usage scenarios are:

  • A form that includes name and an optional address; while it is acceptable to have no address, you will not want to have only a ZIP code or only a city – instead, if either of the two is non-empty, you will need both (as well as the street). To do that, add an annotation @validation-mandatory IF city to the ZIP code and @validation-mandatory IF zip to the city.

  • A form where a user may leave either his e-mail address or his phone number or both – but not neither of them. To do that, add an annotation @validation-mandatory IF NOT email to the phone property and @validation-mandatory IF NOT phone to the e-mail property.

Attention: When using conditional constraints, you may have to pay attention to the order in which you set properties. If you have a property $b, which is mandatory if property $a is not empty, make that $a has its value set before $b is validated.

Extending Validation with custom datatypes

If you want to validate data which cannot be handled by the built-in datatypes, you can define custom datatypes that perform arbitrary tasks for validating.

To do that, these are the required steps:

  • Create the validator (as usual)
  • Call the validator’s addNamespace() method and pass to it a namespace and (optionally) an array of additional constructor arguments
  • Implement the type(s) you need (which must inherit from BlueM\Validation\Type) in the namespace (plus “Type”) you passed to addNamespace()
  • Use the type’s name as you do with any of the built-in types

Example

First the setup:

#!php

$validator = new \BlueM\Validation\Validator(
    new \BlueM\Validation\I18n\En()
);

$validator->addNamespace('Mynamespace\Validation', array($this));

Now, the type:

#!php

namespace Mynamespace\Validation\Type;

use BlueM\Validation\Constraint\Scalar;
use BlueM\Validation\I18n;
use BlueM\Validation\Transformer\Trim;
use BlueM\Validation\Type;

class FooType extends Type
{
    public function __construct(I18n $i18n, $localized)
    {
        $this->addConstraint(new Scalar());
        $this->addTransformer(new Trim());
        ...
        ...
    }
}

Please note that the class name needs the suffix “Type” (as in the code above) to work with version 3 of this library, and may not have that suffix to work with version 1/2.

Now, you can use “foo” as a datatype, either in annotations or in an array describing the validation constraints.

It is your responsibility to make the type class(es) loadable. This means you either have to provide an autoloading mechanism or include the classes explicitly.

Adding constraints

In addition to declaring custom types, you can add custom constraints. (In fact, custom types often only make sense when using custom constraints.)

A constraint class must inherit from BlueM\Validation\Constraint and should throw a BlueM\Validation\ValidationFailedException in its check() method if the check failed. You can pass an arbitrary exception code to ValidationFailedException’s constructor, but it is recommendable to either one of the constants from BlueM\Validation\Validator (in case none is appropriate, there is still a generic FAIL_GENERIC constant) or to define an own class constant and then overwrite the getExceptionMessage() method in your custom type which can act use this constant for determining which kind of validation failed.

Author & License

This code was written by Carsten Blüm (www.bluem.net) and licensed under the BSD 2-Clause license.

Changes from earlier versions

From 2.1.1 to 3.0

  • PHP7 compatibility. PHP7 adds several reserved words that conflict with generic class names in this library, such as “String” or “Float”. Therefore, all Type and Transformer classes got a “Type” or “Transformer” suffix. This is a backwards-incompatible change (hence the big leap from version 2.1.1 to version 3) if some client code added a custom type, in which case the suffix “Type” must be added to the type’s class name. If no custom type is used, version 3 should be completely backwards compatible, as far as the public API is concerned.

From 2.1 to 2.1.1

  • Types Text and String are normalized (i.e.: processed by Normalizer::normalize(), if the intl extension is available.

From 2.0 to 2.1

  • Add the Json type, which simply verifies if the given string can be decoded as JSON

From 1.2 to 2.0

  • To validate dependencies, validateObject() is no longer necessary. Instead, everything will be validated automatically.
  • Added method validateProperty, which acts on the current value of a property. This method can be given an array of constraints instead of using annotations. * validateObject() can be called with an array of constraints as second argument
  • Dependencies inside an ArrayCollection are supported. These dependencies must be defined using two Dollar characters. Example: IF NOT $$arraykey.
  • Changed the way how labels are defined when using dependencies with Blank or Mandatory constraints.