Slide 1

Slide 1 text

Validation with Symfony2 from a Drupal perspective Tuesday, June 3rd 2014 – Austin, TX – United States

Slide 2

Slide 2 text

Validation with Symfony2 from a Drupal perspective Tuesday, June 3rd 2014 – Austin, TX – United States

Slide 3

Slide 3 text

Hugo HAMON Head of training at SensioLabs Book author Speaker at Conferences Symfony contributor @hhamon

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

Introduction

Slide 7

Slide 7 text

Don’t trust ANY user inputs! https://www.flickr.com/photos/ferran-jorda

Slide 8

Slide 8 text

Check data consistency Check data format Filter data Check data integrity

Slide 9

Slide 9 text

https://www.flickr.com/photos/87913776@N00/5129625865/sizes/l/in/photostream/

Slide 10

Slide 10 text

What is Symfony2?

Slide 11

Slide 11 text

Framework Philosophy Community

Slide 12

Slide 12 text

Symfony2 is a set of reusable, standalone, decoupled, and cohesive PHP components that solve common web development problems.

Slide 13

Slide 13 text

Symfony is also a full stack web framework made of bundles and third party libraries.

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Getting Started

Slide 17

Slide 17 text

Installation # composer.json { "require": { "symfony/validator": "~2.4" } }

Slide 18

Slide 18 text

Getting a validator use Symfony\Component\Validator\Validation; $validator = Validation::createValidator();

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

The API

Slide 22

Slide 22 text

Validator The validator validates a value, an object property or a whole object against a set of constraints.

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

ConstraintViolationList The constraint violation list is a collection of validation failures generated by the validator object.

Slide 25

Slide 25 text

ConstraintViolationList $errors = $validator->validate($user); foreach ($errors as $error) { echo $error; echo "\n"; }

Slide 26

Slide 26 text

Constraint A constraint is an object that describes an assertive statement to trigger on the value to validate.

Slide 27

Slide 27 text

Constraint class Regex extends Constraint { public $message = 'This value is not valid.'; public $pattern; public $htmlPattern = null; public $match = true; } Options

Slide 28

Slide 28 text

ConstraintValidator A constraint validator is the object that triggers the validation logic on the value against its corresponding validation constraints.

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Date & Time Constraints $constraint = new Date(); $constraint = new DateTime(); $constraint = new Time();

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Collection Constraints $collection = new Collection([ 'allowExtraFields' => true, 'fields' => [ 'fullName' => [ new NotBlank(), new Length([ 'min' => 2, 'max' => 30 ]), ], 'emailAddress' => [ new NotBlank(), new Email(), ], ], ]);

Slide 38

Slide 38 text

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(), ]]);

Slide 39

Slide 39 text

Object Validation Mapping

Slide 40

Slide 40 text

The validator component supports four validation mapping drivers: PHP, YAML, XML & Annotations.

Slide 41

Slide 41 text

ValidatorBuilder The validator builder builds and configures the validator object.

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

PHP Validation Metadata Mapping

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

YAML Validation Metadata Mapping

Slide 46

Slide 46 text

Shop\Order: properties: reference: - NotBlank: ~ - Length: { min: 10, max: 10 } - Regex: "/[A-Z0-9]+/" customer: - NotBlank: ~ - Email: ~ YAML validation mapping

Slide 47

Slide 47 text

XML Validation Metadata Mapping

Slide 48

Slide 48 text

8 32 /[A-Z0-9]+/ Constraint + options

Slide 49

Slide 49 text

Annotations Validation Metadata Mapping

Slide 50

Slide 50 text

Installation { "require": { "doctrine/cache": "~1.3", "doctrine/annotations": "~1.1", "symfony/validator": "~2.4" } }

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Let’s take a real world example!

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

namespace Shop; class Order { private $reference; private $customer; private $lines = array(); // ... }

Slide 55

Slide 55 text

Validating the reference & customer email address

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

$order = new Order( 'ççç', 'foo@bar' );

Slide 58

Slide 58 text

$errors = $validator ->validate($order) ;

Slide 59

Slide 59 text

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.

Slide 60

Slide 60 text

Validating the number of ordered items

Slide 61

Slide 61 text

The order must be valid if it contains at least one lines and less than ten.

Slide 62

Slide 62 text

$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', ]);

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Validating the collection of ordered items

Slide 65

Slide 65 text

Each item set into the order is an associative array. We want to validate the data in each array.

Slide 66

Slide 66 text

$order->addLine([ 'quantity' => 2, 'price' => 650, 'reference' => 'SF2C1', 'designation' => 'Getting ... ', ]);

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

o_O

Slide 69

Slide 69 text

Of course, there is a better path to achieve this with objects validation.

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

$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

Slide 72

Slide 72 text

class Order { /** * @Assert\Count( * min = 1, * max = 10, * minMessage = "You must ... one item.", * maxMessage = "You can't ... 10 items." * ) * @Assert\Valid */ private $lines = array(); }

Slide 73

Slide 73 text

Validating a coupon code

Slide 74

Slide 74 text

The coupon must equal a specific code to be valid.

Slide 75

Slide 75 text

namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class Order { /** * @Assert\EqualTo( * value = "DRUPALCON2014" * message = "Coupon code is not valid." * ) */ private $coupon; // ... }

Slide 76

Slide 76 text

The coupon is also valid with at least 3 ordered items and a minimum purchase of $850 USD.

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

Validating the customer’s billing and delivery address

Slide 79

Slide 79 text

In an address, if the country is United States or Canada, the state (or province) must be set as well.

Slide 80

Slide 80 text

class Order { /** * @Assert\NotBlank * @Assert\Valid */ private $billingAddress; /** * @Assert\NotBlank * @Assert\Valid */ private $deliveryAddress; }

Slide 81

Slide 81 text

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) { // ... } }

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

Dealing with the validation ExecutionContext

Slide 85

Slide 85 text

The getter method constraint doesn’t allow to attach the violation to a particular property (ie: state).

Slide 86

Slide 86 text

This can be done with the Class Callback validation constraint and the ExecutionContext object.

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

Creating a custom validator constraint

Slide 90

Slide 90 text

What about validating the state/province code is a valid North America state/province code?

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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) { // ... } }

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

namespace Shop; // ... use Shop\Validator\Constraints as ShopAssert; /** * @ShopAssert\NorthAmericaState */ class Address { // ... }

Slide 95

Slide 95 text

Advanced Mapping

Slide 96

Slide 96 text

Validation Groups

Slide 97

Slide 97 text

Validation groups Validation groups provide a simple way to contextualize the validation of an object.

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

$errors = $validator->validate( $order, [ 'Default', 'Create' ] ); Validating against groups

Slide 100

Slide 100 text

Group Sequences

Slide 101

Slide 101 text

Group Sequences Group Sequences allow to validate an object in multiple steps.

Slide 102

Slide 102 text

/** * @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; // ... }

Slide 103

Slide 103 text

$errors = $validator->validate($order, [ 'Default', 'Shipping', 'Payment', ]); Validating against sequences

Slide 104

Slide 104 text

Class Inheritance

Slide 105

Slide 105 text

Class inheritance The validator is able to execute validation rules located in both a child class and its parent.

Slide 106

Slide 106 text

namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class ComplimentaryOrder extends Order { /** @Assert\EqualTo(0) */ protected $total; /** @Assert\EqualTo(0) */ protected $vat; // ... }

Slide 107

Slide 107 text

Translations

Slide 108

Slide 108 text

State code is not valid. L'Etat est invalide.

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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.

Slide 111

Slide 111 text

No content

Slide 112

Slide 112 text

Questions? h"p://www.flickr.com/photos/mkrigsman/