Share
Embed
Validation with Symfony2
When was the last time you wrote a web application without having to validate some data or user input? Probably long ago. In fact, validation is so important that most developers take a profound validation system for granted. Maybe that also explains why few people even realize that Drupal 8 uses a completely new validation engine underneath: The tried and tested Symfony2 Validator component.
In my talk, I will show you how to use the Validator component both in regular PHP applications and in Drupal 8. You will learn how the component works internally and how a few simple mechanisms will help you to fulfill most of your validation needs - from small websites to big enterprise applications.
Hugo Hamon
June 03, 2014
Transcript
112 slides
-
Validation with Symfony2 from a Drupal perspective Tuesday, June 3rd
2014 – Austin, TX – United States -
Validation with Symfony2 from a Drupal perspective Tuesday, June 3rd
2014 – Austin, TX – United States -
Hugo HAMON Head of training at SensioLabs Book author Speaker
at Conferences Symfony contributor @hhamon -
Symfony2 is a set of reusable, standalone, decoupled, and cohesive
PHP components that solve common web development problems. -
Symfony is also a full stack web framework made of
bundles and third party libraries. -
Dependency Injection BrowserKit ClassLoader Config Console CssSelector Debug DomCrawler EventDispatcher
ExpressionLanguage Filesystem Finder Form HttpFoundation HttpKernel Locale Intl Icu OptionsResolver Process PropertyAccess Routing Security Serializer Stopwatch Templating Translation Validator Yaml -
Dependency Injection BrowserKit ClassLoader Config Console CssSelector Debug DomCrawler EventDispatcher
ExpressionLanguage Filesystem Finder Form HttpFoundation HttpKernel Locale Intl Icu OptionsResolver Process PropertyAccess Routing Security Serializer Stopwatch Templating Translation Validator Yaml -
Validating a single value use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Validation; $validator =
Validation::createValidator(); $errors = $validator->validateValue('[email protected]', new Email()); echo count($errors) ? 'Invalid' : 'Valid'; -
Combining constraints use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Regex; $errors = $validator->validateValue('hhamon', array(
new Length(array('min' => 5, 'max' => 15)), new Regex(array('pattern' => '/^[a-z]+/i')), )); -
Validator The validator validates a value, an object property or
a whole object against a set of constraints. -
Validator $user = new User('[email protected]'); $validator->validateValue($user->email, new Email()); $validator->validateProperty($user, 'email');
$validator->validatePropertyValue($user, 'email', '[email protected]'); $validator->validate($user); -
ConstraintViolationList The constraint violation list is a collection of validation
failures generated by the validator object. -
Constraint A constraint is an object that describes an assertive
statement to trigger on the value to validate. -
Constraint class Regex extends Constraint { public $message = 'This
value is not valid.'; public $pattern; public $htmlPattern = null; public $match = true; } Options -
ConstraintValidator A constraint validator is the object that triggers the
validation logic on the value against its corresponding validation constraints. -
class RegexValidator extends ConstraintValidator { public function validate($value, Constraint $constraint)
{ if (null === $value || '' === $value) { return; } if ($constraint->match xor preg_match($constraint->pattern, $value)) { $this->context->addViolation( $constraint->message, array('{{ value }}' => $value) ); } } } -
Basic Constraints $constraint = new NotBlank(); $constraint = new Blank();
$constraint = new NotNull(); $constraint = new Null(); $constraint = new True(); $constraint = new False(); $constraint = new Type(array('type' => 'int')); -
String Constraints $constraint = new Url(); $constraint = new Ip();
$constraint = new Regex([ 'pattern' => '#\d+#' ]); $constraint = new Email([ 'checkMX' => true ]); $constraint = new Length([ 'min' => 1, 'max' => 6, ]); -
Comparison Constraints $constraint = new EqualTo([ 'value' => 6 ]);
$constraint = new NotEqualTo([ 'value' => 6 ]); $constraint = new IdenticalTo([ 'value' => 6 ]); $constraint = new NotIdenticalTo([ 'value' => 6 ]); $constraint = new LessThan([ 'value' => 6 ]); $constraint = new LessThanOrEqual([ 'value' => 6 ]); $constraint = new GreaterThan([ 'value' => 6 ]); $constraint = new GreaterThanOrEqual([ 'value' => 6 ]); -
Date & Time Constraints $constraint = new Date(); $constraint =
new DateTime(); $constraint = new Time(); -
Number & Financial Constraints $constraint = new Currency(); $constraint =
new Luhn(); $constraint = new Iban(); $constraint = new Range([ 'min' => 1, 'max' => 6 ]); $constraint = new Issn([ 'caseSensitive' => true ]); $constraint = new Isbn([ 'isbn10' => true, 'isbn13' => true, ]); $constraint = new CardScheme([ 'schemes' => [ 'AMEX', 'VISA' ], ]); -
File Constraints $constraint = new File([ 'mimeTypes' => [ 'application/pdf',
'text/plain' ], 'maxSize' => '8M', ]); $constraint = new Image([ 'mimeTypes' => [ 'image/jpg', 'image/png' ], 'maxSize' => '8M', 'minWidth' => 120, 'maxWidth' => 1024, 'maxHeight' => 120, 'minHeight' => 768, ]); -
Collection Constraints $constraint = new Language(); $constraint = new Locale();
$constraint = new Country(); $constraint = new Count(['min' => 2, 'max' => 5 ]); $constraint = new Choice([ 'choices' => range(10, 99), 'multiple' => true, 'min' => 2, 'max' => 5, ]); -
Collection Constraints $collection = new Collection([ 'allowExtraFields' => true, 'fields'
=> [ 'fullName' => [ new NotBlank(), new Length([ 'min' => 2, 'max' => 30 ]), ], 'emailAddress' => [ new NotBlank(), new Email(), ], ], ]); -
Other Constraints $constraint = new Valid(); $constraint = new UserPassword();
$constraint = new Callback([ 'callback' => 'checkPassword' ]); $constraint = new Expression([ 'expression' => "this.isTierStatus() in ['gold', 'platinum']", 'message' => 'Customer is not premium member', ]); $constraint = new All(['constraints' => [ new NotBlank(), new Email(), ]]); -
The validator component supports four validation mapping drivers: PHP, YAML,
XML & Annotations. -
$loader = require_once __DIR__.'/vendor/autoload.php'; use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Component\Validator\Validation; // Required
to load annotations AnnotationRegistry::registerLoader([ $loader, 'loadClass' ]); $validator = Validation::createValidatorBuilder() ->addMethodMapping('loadValidatorMetadata') ->addYamlMapping(__DIR__.'/validation.yml') ->addXmlMapping(__DIR__.'/validation.xml') ->enableAnnotationMapping() ->getValidator() ; -
namespace Shop; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Mapping\ClassMetadata; class Order { private $reference; private $customer; // ... public static function loadValidatorMetadata(ClassMetadata $md) { $md->addPropertyConstraint('reference', new NotBlank()); $md->addPropertyConstraint('reference', new Length(['min' => 10, 'max' => 10])); $md->addPropertyConstraint('reference', new Regex(['pattern' => '/[A-Z0-9]+/' ])); $md->addPropertyConstraint('customer', new NotBlank()); $md->addPropertyConstraint('customer', new Email()); } } -
Shop\Order: properties: reference: - NotBlank: ~ - Length: { min:
10, max: 10 } - Regex: "/[A-Z0-9]+/" customer: - NotBlank: ~ - Email: ~ YAML validation mapping -
<?xml version="1.0" encoding="UTF-8"?> <constraint-mapping> <class name="Shop\Order"> <property name="reference"> <constraint name="NotBlank"/>
<constraint name="Length"> <option name="min"> <value>8</value> </option> <option name="max"> <value>32</value> </option> </constraint> <constraint name="Regex"> <option name="pattern"> <value>/[A-Z0-9]+/</value> </option> </constraint> </property> <property name="customer"> <constraint name="NotBlank"/> <constraint name="Email"/> </property> </class> </constraint-mapping> Constraint + options -
namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class Order { /**
* @Assert\NotBlank * @Assert\Length(min = 10, max = 10) * @Assert\Regex("/[A-Z0-9]+/") */ private $reference; /** * @Assert\NotBlank * @Assert\Email */ private $customer; } -
namespace Shop; class Order { private $reference; private $customer; private
$lines = array(); // ... } -
namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class Order { /**
* @Assert\NotBlank * @Assert\Length(min = 10, max = 10) * @Assert\Regex(pattern = "/[A-Z0-9]+/") */ private $reference; /** * @Assert\NotBlank * @Assert\Email */ private $customer; } -
Shop\Order.reference: This value should have exactly 10 characters. Shop\Order.reference: This
value is not valid. Shop\Order.customer: This value is not a valid email address. -
The order must be valid if it contains at least
one lines and less than ten. -
$order = new \Shop\Order('XXX0123YYY', '[email protected]'); $order->addLine([ 'quantity' => 2, 'price'
=> 650, 'reference' => 'SF2C1', 'designation' => 'Getting Started with Symfony', ]); $order->addLine([ 'quantity' => 1, 'price' => 990, 'reference' => 'SF2C2', 'designation' => 'Mastering Symfony', ]); -
namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class Order { /**
* @Assert\Count( * min = 1, * max = 10, * minMessage = "You must have at least one item.", * maxMessage = "You can't order more than 10 items." * ) */ private $lines = array(); } -
Each item set into the order is an associative array.
We want to validate the data in each array. -
$order->addLine([ 'quantity' => 2, 'price' => 650, 'reference' => 'SF2C1',
'designation' => 'Getting ... ', ]); -
class Order { /** * @Assert\Count(...) * @Assert\All( * @Assert\Collection(
* fields = { * "reference" = @Assert\Required(@Assert\NotBlank), * "quantity" = @Assert\Required({ * @Assert\NotBlank, * @Assert\Type("int"), * @Assert\Range(min = 1) * }), * "price" = @Assert\Required({ * @Assert\Type("double"), * @Assert\Range(min = 0) * }), * "designation" = @Assert\Optional * } * ) * )) */ private $lines = array(); } -
Of course, there is a better path to achieve this
with objects validation. -
namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class OrderLine { /**
@Assert\NotBlank */ private $reference; /** * @Assert\NotBlank * @Assert\Type("int") * @Assert\Range(min = 1) */ private $quantity; /** * @Assert\NotNull * @Assert\Type("double") * @Assert\Range(min = 0) */ private $price; private $designation; } -
$item1 = new OrderLine('SF2C1', 650, 2, 'Get...'); $item2 = new
OrderLine('SF2C2', 990, 1); $order = new Order('XXX0123YYY', '[email protected]'); $order->addLine($item1); $order->addLine($item2); Using OrderLine objects -
class Order { /** * @Assert\Count( * min = 1,
* max = 10, * minMessage = "You must ... one item.", * maxMessage = "You can't ... 10 items." * ) * @Assert\Valid */ private $lines = array(); } -
namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class Order { /**
* @Assert\EqualTo( * value = "DRUPALCON2014" * message = "Coupon code is not valid." * ) */ private $coupon; // ... } -
The coupon is also valid with at least 3 ordered
items and a minimum purchase of $850 USD. -
class Order { /** * @Assert\EqualTo( * value = "DRUPALCON2014"
* message = "Coupon code is not valid." * ) * * @Assert\Expression( * expression = "this.getNbItems() > 2 and this.getSubtotal() > 850", * message = "Coupon is valid with 3+ items and at $850+ purchase." * ) */ private $coupon; // ... } Symfony > 2.4 -
In an address, if the country is United States or
Canada, the state (or province) must be set as well. -
class Order { /** * @Assert\NotBlank * @Assert\Valid */ private
$billingAddress; /** * @Assert\NotBlank * @Assert\Valid */ private $deliveryAddress; } -
namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class Address { /**
@Assert\NotBlank */ private $street; /** @Assert\NotBlank */ private $zipCode; /** @Assert\NotBlank */ private $city; /** @Assert\Country */ private $country; private $state; public function __construct($street, $zipCode, $city, $country, $state = null) { // ... } } -
class Address { /** * @Assert\False( * message = "State/province
is required for USA/Canada." * ) */ public function isStateRequiredAndFilled() { if (!in_array($this->country, [ 'CA', 'US' ])) { return false; } return empty($this->state); } } -
Shop\Order.lines[0].price: This value should be of type double. Shop\Order.lines[1].price: This
value should be of type double. Shop\Order.coupon: Coupon is valid with 3 or more items and at least $850 purchase. Shop\Order.billingAddress.stateRequiredAndFilled: State/province is required for USA/Canada. Method Getter Constraint -
The getter method constraint doesn’t allow to attach the violation
to a particular property (ie: state). -
This can be done with the Class Callback validation constraint
and the ExecutionContext object. -
namespace Shop; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\ExecutionContext; /** @Assert\Callback("enforceStateInNorthAmerica")
*/ class Address { function enforceStateInNorthAmerica(ExecutionContext $context) { if (!in_array($this->country, [ 'CA', 'US'])) { return; } if (empty($this->state)) { $context->addViolationAt('state', 'State is mandatory.'); } } } -
Shop\Order.lines[0].price: This value should be of type double. Shop\Order.lines[1].price: This
value should be of type double. Shop\Order.coupon: Coupon is valid with 3 or more items and at least $850 purchase. Shop\Order.billingAddress.state: State is mandatory. Method Getter Constraint -
What about validating the state/province code is a valid North
America state/province code? -
namespace Shop\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"CLASS", "ANNOTATION"})
*/ class NorthAmericaState extends Constraint { public $message = 'State code is not valid.'; public function getTargets() { return self::CLASS_CONSTRAINT; } } -
namespace Shop\Constraints; use Shop\Address; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class NorthAmericaStateValidator extends ConstraintValidator { private static $regions = [ 'US' => [ 'NV', 'CA', 'DC', 'FL', 'VG', 'NY', 'OR', … ], 'CA' => [ 'QC', 'AB', … ], ]; public function validate($value, Constraint $constraint) { // ... } } -
class NorthAmericaStateValidator extends ConstraintValidator { // ... public function validate($value,
Constraint $constraint) { if (!$value instanceof Address) { throw new UnexpectedTypeException($value, 'Shop\Address'); } $country = $value->getCountry(); if (!isset(self::$regions[$country])) { return; } $regions = self::$regions[$country]; if (!in_array($value->getState(), $regions)) { $this->context->addViolationAt('state', $constraint->message); } } } -
namespace Shop; // ... use Shop\Validator\Constraints as ShopAssert; /** *
@ShopAssert\NorthAmericaState */ class Address { // ... } -
Validation groups Validation groups provide a simple way to contextualize
the validation of an object. -
class Order { /** * @Assert\NotBlank(groups = "Create") * @Assert\Length(min
= 10, max = 10, groups = "Create") * @Assert\Regex(pattern = "/[A-Z0-9]+/", groups = "Create") */ private $reference; // ... } Attach validation groups -
Group Sequences Group Sequences allow to validate an object in
multiple steps. -
/** * @Assert\GroupSequence(groups = { * "Order", "Shipping", "Payment" *
}) */ class Order { /** @Assert\NotBlank */ private $reference; /** @Assert\NotBlank(groups = "Shipping") */ private $billingAddress; /** @Assert\NotBlank(groups = "Shipping") */ private $deliveryAddress; /** @Assert\NotBlank(groups = "Payment") */ private $payment; // ... } -
Class inheritance The validator is able to execute validation rules
located in both a child class and its parent. -
namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class ComplimentaryOrder extends Order
{ /** @Assert\EqualTo(0) */ protected $total; /** @Assert\EqualTo(0) */ protected $vat; // ... } -
<?xml version="1.0"?> <xliff version="1.2"> <file source-language="en" datatype="plaintext"> <body> <trans-unit id="1">
<source>State code is not valid.</source> <target>L'Etat est invalide.</target> </trans-unit> </body> </file> </xliff> -
use Symfony\Component\Validator\Validation; use Symfony\Component\Translation\Loader\XliffFileLoader; use Symfony\Component\Translation\Translator; $file = __DIR__.'/config/validators.fr.xlf'; $translator
= new Translator('fr'); $translator->addLoader('xlf', new XliffFileLoader()); $translator->addResource('xlf', $file, 'fr'); $validator = Validation::createValidatorBuilder() ->enableAnnotationMapping() ->setTranslator($translator) ->getValidator() ; -
Shop\ComplimentaryOrder.lines[0].price: This value should be of type double. Shop\ComplimentaryOrder.lines[1].price: This
value should be of type double. Shop\ComplimentaryOrder.billingAddress.state: L'Etat ou la province est invalide.