Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Validation with Symfony2

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
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Hugo HAMON Head of training at SensioLabs Book author Speaker

    at Conferences Symfony contributor @hhamon
  2. Symfony2 is a set of reusable, standalone, decoupled, and cohesive

    PHP components that solve common web development problems.
  3. Symfony is also a full stack web framework made of

    bundles and third party libraries.
  4. 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
  5. 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
  6. 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';
  7. Validator The validator validates a value, an object property or

    a whole object against a set of constraints.
  8. Constraint A constraint is an object that describes an assertive

    statement to trigger on the value to validate.
  9. Constraint class Regex extends Constraint { public $message = 'This

    value is not valid.'; public $pattern; public $htmlPattern = null; public $match = true; } Options
  10. ConstraintValidator A constraint validator is the object that triggers the

    validation logic on the value against its corresponding validation constraints.
  11. 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) ); } } }
  12. 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'));
  13. 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, ]);
  14. 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 ]);
  15. Date & Time Constraints $constraint = new Date(); $constraint =

    new DateTime(); $constraint = new Time();
  16. 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' ], ]);
  17. 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, ]);
  18. 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, ]);
  19. Collection Constraints $collection = new Collection([ 'allowExtraFields' => true, 'fields'

    => [ 'fullName' => [ new NotBlank(), new Length([ 'min' => 2, 'max' => 30 ]), ], 'emailAddress' => [ new NotBlank(), new Email(), ], ], ]);
  20. 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(), ]]);
  21. $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() ;
  22. 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()); } }
  23. Shop\Order: properties: reference: - NotBlank: ~ - Length: { min:

    10, max: 10 } - Regex: "/[A-Z0-9]+/" customer: - NotBlank: ~ - Email: ~ YAML validation mapping
  24. <?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
  25. 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; }
  26. 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; }
  27. 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.
  28. $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', ]);
  29. 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(); }
  30. Each item set into the order is an associative array.

    We want to validate the data in each array.
  31. 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(); }
  32. o_O

  33. 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; }
  34. $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
  35. class Order { /** * @Assert\Count( * min = 1,

    * max = 10, * minMessage = "You must ... one item.", * maxMessage = "You can't ... 10 items." * ) * @Assert\Valid */ private $lines = array(); }
  36. namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class Order { /**

    * @Assert\EqualTo( * value = "DRUPALCON2014" * message = "Coupon code is not valid." * ) */ private $coupon; // ... }
  37. The coupon is also valid with at least 3 ordered

    items and a minimum purchase of $850 USD.
  38. 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
  39. In an address, if the country is United States or

    Canada, the state (or province) must be set as well.
  40. class Order { /** * @Assert\NotBlank * @Assert\Valid */ private

    $billingAddress; /** * @Assert\NotBlank * @Assert\Valid */ private $deliveryAddress; }
  41. 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) { // ... } }
  42. 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); } }
  43. 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
  44. 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.'); } } }
  45. 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
  46. 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; } }
  47. 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) { // ... } }
  48. 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); } } }
  49. namespace Shop; // ... use Shop\Validator\Constraints as ShopAssert; /** *

    @ShopAssert\NorthAmericaState */ class Address { // ... }
  50. 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
  51. /** * @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; // ... }
  52. Class inheritance The validator is able to execute validation rules

    located in both a child class and its parent.
  53. namespace Shop; use Symfony\Component\Validator\Constraints as Assert; class ComplimentaryOrder extends Order

    { /** @Assert\EqualTo(0) */ protected $total; /** @Assert\EqualTo(0) */ protected $vat; // ... }
  54. <?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>
  55. 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() ;
  56. 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.