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. Validation with Symfony2
    from a Drupal perspective
    Tuesday, June 3rd 2014 – Austin, TX – United States

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  4. Introduction

    View full-size slide

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

    View full-size slide

  6. Check data consistency
    Check data format
    Filter data
    Check data integrity

    View full-size slide

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

    View full-size slide

  8. What is
    Symfony2?

    View full-size slide

  9. Framework
    Philosophy
    Community

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. 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

    View full-size slide

  13. 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

    View full-size slide

  14. Getting Started

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. Object
    Validation
    Mapping

    View full-size slide

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

    View full-size slide

  38. ValidatorBuilder
    The validator builder builds
    and configures the validator
    object.

    View full-size slide

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

    View full-size slide

  40. PHP Validation
    Metadata
    Mapping

    View full-size slide

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

    View full-size slide

  42. YAML Validation
    Metadata
    Mapping

    View full-size slide

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

    View full-size slide

  44. XML Validation
    Metadata
    Mapping

    View full-size slide








  45. 8


    32




    /[A-Z0-9]+/









    Constraint + options

    View full-size slide

  46. Annotations
    Validation
    Metadata
    Mapping

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  49. Let’s take a real
    world example!

    View full-size slide

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

    View full-size slide

  51. Validating the
    reference &
    customer email
    address

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  55. 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.

    View full-size slide

  56. Validating the
    number of
    ordered items

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  60. Validating the
    collection of
    ordered items

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  66. $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

    View full-size slide

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

    View full-size slide

  68. Validating a
    coupon code

    View full-size slide

  69. The coupon must equal a
    specific code to be valid.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  72. 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

    View full-size slide

  73. Validating the
    customer’s billing
    and delivery
    address

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  78. 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

    View full-size slide

  79. Dealing with the
    validation
    ExecutionContext

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  83. 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

    View full-size slide

  84. Creating a custom
    validator
    constraint

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  90. Advanced
    Mapping

    View full-size slide

  91. Validation
    Groups

    View full-size slide

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

    View full-size slide

  93. 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

    View full-size slide

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

    View full-size slide

  95. Group
    Sequences

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  99. Class
    Inheritance

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  102. Translations

    View full-size slide






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




    View full-size slide

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

    View full-size slide

  105. 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.

    View full-size slide

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

    View full-size slide