Wednesday, April 11, 2012

Custom form validation constraint with Callback in Symfony2

Symfony2 forms provides many embedded validators to constrain data passed in form fields. Here is the way to define a constraint in a callback function, useful to determine a data validity relative to another field...

Given a form asking the user to chose its favorite payment ways, in a shopping application :
<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

/**
 * Selection of preferred payment ways form
 */
class PreferredPaymentWays extends AbstractType
{

   public function buildForm(FormBuilder $builder, array $options)
   {
      $builder
         ->add(
            'paypal', 'checkbox',
            array (
                 'label' => 'PayPal',
                 'required' => false,
            )
         )
         ->add(
            'cc', 'checkbox',
            array (
                 'label' => 'CreditCard',
                 'required' => false,
            )
         )
         ->add(
            'banktransfer', 'checkbox',
            array (
                 'label' => 'Bank Transfer',
                 'required' => false,
            )
         )
         ->add(
            'check', 'checkbox',
            array (
                 'label' => 'Personal Check',
                 'required' => false,
            )
         )
         ->add(
            'paypalEmailAddress', 'email',
            array (
                 'label' => 'PayPal email',
                 'required' => false
            )
         )
      ;
   }

   /**
    * Returns the name of this type.
    *
    * @return string The name of this type
    */
   public function getName()
   {
      return 'acme_demo_payment_ways';
   }

}
Given a class mapped to this form :
<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ExecutionContext;

/**
 * Class mapped on PaymentWaysType form, where object constraints are defined
 *
 * @Assert\Callback(methods={"arePaymentWaysValid"})
 */
class PaymentWays
{

   public $paypal;
   public $cc;
   public $banktransfer;
   public $check;
   /**
    * @Assert\Email()
    */
   public $paypalEmailAddress;

   /**Validate paypalEmailAddress if paypal is selected
    * @param ExecutionContext $context
    * @return void
    */
   public function arePaymentWaysValid(ExecutionContext $context)
   {
      if ($this->paypal && empty($this->paypalEmailAddress))
      {
         $propertyPath = $context->getPropertyPath() . '.paypalEmailAddress';
         $context->setPropertyPath($propertyPath);
         $context->addViolation('Please fill your PayPal email address if you want to select PayPal payment way !', array (), null);
      }
   }

}
I've defined a Callback validation on the class, through the method arePaymentWaysValid. This method is called each time the class is validated (through the form or not). Now embed this form in a controller, here integrated in the AcmeDemoBundle :
<php

namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\RedirectResponse;
// these import the "@Route" and "@Template" annotations
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

use Acme\DemoBundle\Form\PreferredPaymentWays;
use Acme\DemoBundle\FormData\PaymentWays;

class CallableController extends Controller
{

   /**
    * @Route("/", name="_callable")
    * @Template()
    */
   public function indexAction()
   {
      $formData = new PaymentWays;

      $form = $this->createForm(new PreferredPaymentWays, $formData);

      if ($this->getRequest()->getMethod() == 'POST')
      {
         $form->bindRequest($this->getRequest());
      }

      $viewVars = array ('form' => $form->createView());

      return $viewVars;
   }

}

Try to check Paypal choice without filling a Paypal email address, you'll get a validation error !



You'll find more informations about Callback validation at official documentation.

3 comments :

Anonymous said...

awesome post and exactly what i was looking for even down to the reason i need to use it (paypal vs check) :D thanks!

Unknown said...

Hi there. Thank you for the hint but what if the paypal checkbox is not mapped?

AlterPHP said...

Hi Cyril,
For not mapped fields, you should define the CallbackValidator on the Form, NOT on the entity. You can add it on a POST_SUBMIT FormEvent.

Post a Comment

Comments are moderated before being published.