Share
Embed
Leveraging forms and validation with Symfony
Forms are everywhere and they are key elements for interacting with applications users. Unfortunately, designing forms is a very tedious task. Indeed, forms may contain complex behaviors and business logic to validate data. This talk will explain how to bring the Symfony forms and validation components into PHP applications to ease forms management.
Hugo Hamon
March 01, 2013
Transcript
106 slides
-
About me… Hugo HAMON Head of training at SensioLabs Book
author Speaker at conferences Symfony contributor @hhamon -
Form engine Core CSRF DI Doctrine Propel Twig PHP Smarty
… Zend … Rendering Extensions Foundation -
Bootstrapping { "require": { "doctrine/common": "2.*", "symfony/form": "2.2.*", "symfony/yaml": "2.2.*",
"symfony/http-foundation": "2.2.*", "symfony/validator": "2.2.*", "symfony/config": "2.2.*", "symfony/translation": "2.2.*", "symfony/twig-bridge": "2.2.*" } } -
use Symfony\Bridge\Twig\Form\TwigRendererEngine; use Symfony\Component\Form\Forms; use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Form\TwigRenderer; // Define
some constants to the main resources define('VENDOR_DIR', realpath(__DIR__ . '/../vendor')); define('DEFAULT_FORM_THEME', 'form_div_layout.html.twig'); define('VENDOR_TWIG_BRIDGE_DIR', VENDOR_DIR . '/symfony/twig-bridge/Symfony/Bridge/Twig'); define('VIEWS_DIR', realpath(__DIR__ . '/../views')); // Initialize a Twig compatible rendering engine $twig = new Twig_Environment(new Twig_Loader_Filesystem(array( VIEWS_DIR, VENDOR_TWIG_BRIDGE_DIR . '/Resources/views/Form', ))); $formEngine = new TwigRendererEngine(array(DEFAULT_FORM_THEME)); $formEngine->setEnvironment($twig); // Register the Twig Form extension $twig->addExtension(new FormExtension(new TwigRenderer($formEngine))); // Set up the Form component $formFactoryBuilder = Forms::createFormFactoryBuilder(); $formFactory = $formFactoryBuilder->getFormFactory(); -
Creating a simple form $form = $formFactory ->createBuilder() ->add('name') ->add('bio',
'textarea') ->add('gender', 'choice', array( 'choices' => array( 'm' => 'Male', 'f' => 'Female' ), )) ->getForm(); -
The form tree form form bio textarea name
text gender choice Field name Field type -
Form handling $name = $form->getName(); if (!empty($_POST[$name])) { $form->bind($_POST[$name]); $data
= $form->getData(); print_r($data); } -
Prepolutation $form->setData(array( 'name' => 'Jane Smith', 'bio' => 'I do
great things!', 'gender' => 'f', )); -
Core field types § Birthday § Checkbox § Choice §
Collection* § Country § Date § DateTime § File § Hidden § Integer § Language § Locale § Money § Number § Password § Percent § Radio § Repeated* § Search § Textarea § Text § Time § Timezone § Url -
Native form types Text Choice Password File Form Date Country
Language Timezone Birthday DateTime … -
The repeated type $builder ->add('name') ->add('password', 'repeated', array( 'type' =>
'password', 'invalid_message' => 'Passwords are not same.', 'first_options' => array('label' => 'Password'), 'second_options' => array('label' => 'Confirmation'), )) ->add('bio', 'textarea') // [...] ->getForm() ; -
Rendering a form // PHP rendering echo $engine->render('profile.php', array( 'form'
=> $form->createView(), )); // Twig rendering echo $twig->render('profile.twig', array( 'form' => $form->createView(), )); -
Prototyping (PHP) <form action="#" method="post"> <fieldset> <legend>Your profile</legend> <?php echo
$view['form']->widget($form) ?> </fieldset> <div class="form-actions"> <button type="submit">Save changes</button> <button type="button">Cancel</button> </div> </form> -
Prototyping (Twig) <form action="#" method="post"> <fieldset> <legend>Your profile</legend> {{ form_widget(form)
}} </fieldset> <div class="form-actions"> <button type="submit">Save changes</button> <button type="button">Cancel</button> </div> </form> -
// Form rendering <?php echo $view['form']->enctype($form) ?> <?php echo $view['form']->widget($form)
?> <?php echo $view['form']->errors($form) ?> <?php echo $view['form']->rest($form) ?> // Field rendering <?php echo $view['form']->row($form['bio']) ?> <?php echo $view['form']->errors($form['bio']) ?> <?php echo $view['form']->label($form['bio']) ?> <?php echo $view['form']->widget($form['bio'], array( 'attr' => array('class' => 'editor'), )) ?> Piece by piece rendering (PHP) -
{# General rendering #} {{ form_enctype(form) }} {{ form_widget(form) }}
{{ form_errors(form) }} {{ form_rest(form) }} {# Field rendering #} {{ form_row(form.bio) }} {{ form_errors(form.bio) }} {{ form_label(form.bio, 'Biography') }} {{ form_widget(form.bio, { 'attr': { 'class': 'editor' }}) }} Piece by piece rendering (Twig) -
Public properties mapping class Person { public $name; public $password;
public $bio; public $gender; } -
Public properties mapping $person = new Person(); $person->name = 'John
Doe'; $person->password = 'S3cR3T$1337'; $person->bio = 'Born in 1970...'; $person->gender = 'm'; $person->active = true; // The form reads & writes // the person object $form->setData($person); -
Private properties mapping class Person { private $name; // ...
function setName($name) { $this->name = $name; } function getName() { return $this->name; } } -
Private properties mapping class Person { private $active; // ...
public function isActive() { return $this->active; } } -
Private properties mapping $person = new Person(); $person->setName('John Doe'); $person->setPassword('S3cR3T$1337');
$person->setBio('Born in 1970...'); $person->setGender('m'); $person->setActive(true); // The form reads & writes // the person object $form->setData($person); -
Private properties mapping $form = $formFactory ->createBuilder('form', $person, array( 'data_class'
=> 'Person', )) // ... ->getForm() ; -
Enabling CSRF protection # bootstrap.php // ... use Symfony\Component\Form\Extension\Csrf\CsrfExtension; use
Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider; use Symfony\Bridge\Twig\Form\TwigRenderer; define('CSRF_SECRET', 'c2ioeEU1n48QF2WsHGWd2HmiuUUT6dxr'); // Set up the CSRF provider $csrfProvider = new DefaultCsrfProvider(CSRF_SECRET); $renderer = new TwigRenderer($formEngine, $csrfProvider); // ... $formFactory = $formFactoryBuilder ->addExtension(new CsrfExtension($csrfProvider)) ->getFormFactory(); -
Enabling CSRF protection <form method="post" action="#"> <fieldset> <legend>Your profile</legend> <div
id="form"> <!-- ... fields ... --> <input type="hidden" value="0219d[…]61d69" name="form[_token]"> </div> </fieldset> <div class="form-actions"> <button type="submit">Save changes</button> <button type="button">Cancel</button> </div> </form> -
Creating a custom type use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class PersonType
extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array('data_class' => 'Person')); } public function getName() { return 'person'; } } -
// ... use Symfony\Component\Form\FormBuilderInterface; class PersonType extends AbstractType { public
function buildForm( FormBuilderInterface $builder, array $options ) { $builder ->add('name') ->add('password', 'repeated', array(...)) ->add('bio', 'textarea') ->add('gender', 'choice', array(...)) ->add('active', 'checkbox') ; } } -
Creating a custom type $person = new Person(); $person->setName('John Doe');
// ... $options = array('trim' => true); $form = $formFactory ->create(new PersonType(), $person, $options) ; -
Register new custom types # in your code... $form =
$formFactory->create('person', $person); # bootstrap.php $formFactory = Forms::createFormFactoryBuilder() // [...] ->addType(new PersonType(), 'person') ->getFormFactory() ; -
Handling file uploads # bootstrap.php // ... use Symfony\…\HttpFoundation\HttpFoundationExtension; $formFactoryBuilder
= Forms::createFormFactoryBuilder() $formFactory = $formFactoryBuilder ->addExtension(new HttpFoundationExtension()) // [...] ->getFormFactory() ; -
Handling file uploads class PersonType extends AbstractType { function buildForm(FormBuilderInterface
$builder, …) { $builder // […] ->add('picture', 'file', array( 'required' => false, )) ->add('active', 'checkbox') ; } } -
Rendering the enctype attribute <form {{ form_enctype(form) }} …> <!--
render fields --> </form> -
Handling the form use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals() $request->overrideGlobals(); if
($request->isMethod('post')) { $form->bind($request); var_dump($form->getData()); } -
Handling the form object(Person) private 'name' => 'John Doe' private
'picture' => object(Symfony\Component\HttpFoundation\File\UploadedFile) private 'test' => false private 'originalName' => '445.jpg' private 'mimeType' => 'image/jpeg' private 'size' => 21645 private 'error' => 0 private 'password' => 'secret' private 'bio' => 'Famous actor!' private 'gender' => 'm' private 'active' => true -
Handling the form $file = $form->get('picture')->getData(); $target = __DIR__. '/uploads';
if ($file->isValid()) { $new = $file->move($target, 'jdoe.jpg'); } -
array( 'name' => 'John Doe' 'password' => 'secret' 'bio' =>
'Actor!' 'gender' => 'm' 'picture' => null 'active' => true ) Unmapping fields No « rules » data -
Embedding a fields collection $builder // ... ->add('hobbies', 'collection', array(
'allow_add' => true, 'allow_delete' => true, )) // ... ; -
Embedding a fields collection $person = new Person(); $person->setName('John Doe');
$person->addHobby('music'); $person->addHobby('movies'); $person->addHobby('travels'); -
Coding conventions class Person { public function addHobby($hobby) { $this->hobbies[]
= $hobby; } public function removeHobby($hobby) { $key = array_search($hobby, $this->hobbies); if (false !== $key) { unset($this->hobbies[$key]); } } } -
Embedding a form into another class Address { private $street;
private $zipCode; private $city; private $state; private $country; // ... } -
class AddressType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array
$options) { $builder ->add('street', 'textarea') ->add('zipCode') ->add('city') ->add('state') ->add('country', 'country') ; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array('data_class' => 'Address')); } public function getName() { return 'address'; } } -
Embedding a form into another class PersonType extends AbstractType {
function buildForm(FormBuilderInterface $builder, …) { $builder // ... ->add('address', new AddressType()) ; } } -
Embedding a form into another class PersonType extends AbstractType {
function buildForm(FormBuilderInterface $builder, …) { $builder // ... ->add('address', 'address') ; } } -
Embedding a form into another $formBuilder = Forms::createFormFactoryBuilder(); $formFactory =
$formBuilder // ... ->addType(new AddressType(), 'address') ->addType(new PersonType(), 'person') ->getFormFactory() ; -
The form tree form form bio textarea name
text … ... Field Type address Address zipCode text address textarea city text country country state text -
Person Object ( [name] => Hugo Hamon [picture] => [username]
=> hhamon [password] => secret [address] => Address Object ( [street] => 42 Sunshine Street [zipCode] => 12345 [city] => Miami [state] => Florida [country] => US ) [bio] => Speaker at conference [gender] => m [active] => 1 [hobbies] => Array ( [1] => movies [2] => travels [3] => conferences ) ) -
$builder ->add('birthdate', 'birthday', array('format' => 'd/M/y')) ->add('salary', 'money', array('currency' =>
'EUR')) ->add('language', 'language', array( 'preferred_choices' => array('fr'), ) ->add('country', 'country', array( 'preferred_choices' => array('FR'), ) ->add('timezone', 'timezone', array( 'preferred_choices' => array('Europe/Paris') ) ; Localized fields -
{% form_theme form _self %} {% block password_widget %} <div
class="input-append"> {{ block('field_widget') }} <span class="add-on"> <i class="icon-lock"></i> </span> </div> {% endblock password_widget %} Changing all password fields -
{% form_theme form _self %} {% block _person_username_widget %} <div
class="input-append"> {{ block('field_widget') }} <span class="add-on"> <i class="icon-user"></i> </span> </div> {% endblock _person_username_widget %} Changing one single field -
// ... use Symfony\Component\Validator\Validation; use Symfony\Component\Form\Extension\Validator\ValidatorExtension; $validator = Validation::createValidatorBuilder() ->getValidator()
; $formFactoryBuilder = Forms::createFormFactoryBuilder() $formFactory = $formFactoryBuilder // ... ->addExtension(new ValidatorExtension($validator)) ->getFormFactory() ; Configuring the validator -
Constraint & Validator For each validation rule, the component ships
a Constraint class and its associated Validator class. The Constraint object describes the rule to check and the Validator implementation runs that validation logic. -
// ... use Symfony\Component\Validator\Constraints\NotBlank as Assert; class PersonType extends AbstractType
{ public function buildForm(...) { $builder->add('name', 'text', array( 'constraints' => array( new Assert\NotBlank(), new Assert\Length(array('min' => 5, 'max' => 40)), ), )); // ... } } Field constraints mapping -
$genders = array('m' => 'Male', 'f' => 'Female'); $builder->add('gender', 'choice',
array( 'choices' => $genders, 'constraints' => array( new Assert\NotBlank(), new Assert\Choice(array( 'choices' => array_keys($genders), 'message' => 'Alien genders are not supported.', )), ), )); Field constraints mapping -
Validating the form $request = Request::createFromGlobals(); $request->overrideGlobals(); if ($request->isMethod('POST')) {
$form->bind($request); if ($form->isValid()) { $data = $form->getData(); // ... handle sanitized data } } -
Core constraints § All § Blank § Callback § Choice
§ Collection § Count § Country § Date § DateTime § Email § False § File § Image § Ip § Language § Length § Locale § NotBlank § NotNull § Null § Range § Regex § Time § True § Type § Url § Valid… -
# bootstrap.php // ... $validator = Validation::createValidatorBuilder() ->addMethodMapping('loadValidatorMetadata') ->addXmlMapping(__DIR__.'/config/validation.xml') ->addYamlMapping(__DIR__.'/config/validation.yml')
->enableAnnotationMapping() ->getValidator() ; Configuring the validator -
use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Length; class Person { private
$name; // ... public static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addPropertyConstraint('name', new NotBlank()); $metadata->addPropertyConstraint('name', new Length(array( 'min' => 5, 'max' => 40, ))); // ... } } PHP validation mapping -
# src/config/validation.yml Person: properties: name: - NotBlank: ~ - Length:
{ 'min': 5, 'max': 40 } # ... getters: # ... constraints: # ... YAML validation mapping -
<!-- src/config/validation.xml --> <?xml version="1.0" ?> <constraint-mapping ...> <class name="Person">
<property name="name"> <constraint name="NotBlank"/> <constraint name="Length"> <option name="min">5</option> <option name="max">40</option> </constraint> </property> </class> </constraint-mapping> XML validation mapping -
use Symfony\Component\Validator\Constraints as Assert; class Person { /** * @Assert\NotBlank()
* @Assert\Length(min = 5, max = 40) */ private $name; // ... } Annotations validation mapping -
Property constraints use Symfony\Component\Validator\Constraints\Image; class Person { private $picture; //
... static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addPropertyConstraint('picture', new Image(array( 'minWidth' => 100, 'maxWidth' => 150, 'minHeight' => 100, 'maxHeight' => 150, ))); // ... } } -
class Person { //... static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addConstraint(new
Unique(array( 'field' => 'username', 'message' => 'This username already exist.', ))); //... } } Class constraints -
use Symfony\Component\Validator\Constraints\True; class Person { private $username; private $password; public
static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addGetterConstraint('passwordValid', new True(array( 'message' => 'Password and username cannot be same.', ))); // ... } public function isPasswordValid() { return strtolower($this->username) !== strtolower($this->password); } } Method constraints -
Method constraints The error message is not mapped to the
password eld. It’s a global error… -
class PersonType extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array( 'data_class' => 'Person', 'error_mapping' => array('passwordValid' => 'password'), )); } } Method constraints -
Method constraints The error message is now reaffected to the
password concrete eld. -
use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Loader\XliffFileLoader; use Symfony\Bridge\Twig\Extension\TranslationExtension; $t = new Translator('fr');
$t->addLoader('xlf', new XliffFileLoader()); // Built in translations $t->addResource('xlf', FORM_DIR . '/Resources/translations/validators.fr.xlf', 'fr', 'validators'); $t->addResource('xlf', VALIDATOR_DIR . '/Resources/translations/validators.fr.xlf', 'fr', 'validators'); // Your translations $t->addResource('xlf', __DIR__. '/translations/validators.fr.xlf', 'fr', 'validators'); // ... $twig->addExtension(new TranslationExtension($t)); // ... Enabling translations -
<?xml version="1.0"?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body>
<trans-unit id="1"> <source>Password and username cannot be same.</source> <target>Le mot de passe et le nom d'utilisateur doivent être différents.</target> </trans-unit> </body> </file> </xliff> Translating messages -
Validation groups class Person { public static function loadValidatorMetadata(ClassMetadata $metadata)
{ $metadata->addPropertyConstraint('password', new NotBlank(array( 'groups' => array('Signup'), ))); $metadata->addGetterConstraint('passwordValid', new True(array( 'message' => 'Password and username cannot be same.', 'groups' => array('Signup', 'Profile'), ))); // ... } } -
class RegistrationType extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array( // ... 'validation_groups' => array('Signup'), )); } } class EditAccountType extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( // ... 'validation_groups' => array('Profile'), )); } } -
Official resources § http://symfony.com/doc/current/book/forms.html § http://symfony.com/doc/current/reference/forms/types.html § http://symfony.com/doc/current/book/validation.html § http://symfony.com/doc/current/reference/constraints.html
§ http://symfony.com/doc/current/reference/forms/twig_reference.html § https://github.com/bschussek/standalone-forms Useful resources