Symfony CoP: Form component

49
Symfony Form component

Transcript of Symfony CoP: Form component

Page 1: Symfony CoP: Form component

Symfony

Form component

Page 2: Symfony CoP: Form component

Plan1. Basic usage

2. Validation

3. Custom types

4. Events

5. Data Transformers

6. Form type extensions

7. Rendering overview

Page 3: Symfony CoP: Form component

What's the Form component?

Page 4: Symfony CoP: Form component

Basic usages

Page 5: Symfony CoP: Form component

Basic usage (Symfony way)

public function createAction(Request $request){ $form = $this ->createFormBuilder([]) ->add('comment', 'textarea') ->getForm() ;

$form->handleRequest($request); if ($form->isValid()) { $data = $form->getData();

// Do what ever you want with the data... $comment = $data['comment']; }

return [ 'form' => $form->createView(), ];}

Page 6: Symfony CoP: Form component

Basic usage (rendering)

# create.html.twig{{ form_start(form) }} {{ form_errors(form) }}

{{ form_row(form.comment) }}

<input type="submit" name="Let's go" />

{{ form_end(form) }}

Page 7: Symfony CoP: Form component

Basic usage (Object instead of array)

public function createOrUpdateAction(Request $request, MyObject $myObject = null){ $myObject = $myObject :? new MyObject();

$form = $this ->createFormBuilder($myObject, [ 'data_class' => MyObject::class, ]) ->add('comment', 'textarea') ->getForm() ;

$form->handleRequest($request); if ($form->isValid()) { // Do what ever you want with the updated object... $comment = $myObject->getComment(); }

return [ 'form' => $form->createView(), ];}

Page 8: Symfony CoP: Form component

Validation

Page 9: Symfony CoP: Form component

Validation

$form = $this->createFormBuilder() ->add('comment', 'textarea', [ 'required' => true, 'constraints' => [ new NotBlank(), ], ]) ->getForm();

Page 10: Symfony CoP: Form component

Basic usage (Validation on the object)

use Symfony\Component\Validator\Constraints as Assert;

class MyObject{ /** * @Assert\NotBlank * @Assert\Length( * min=10, * minMessage="The comment have to be useful" * ) */ private $comment;

// Required methods public function getComment(); public function setComment($comment);}

Page 11: Symfony CoP: Form component

Basic usage (Get validation errors)

...that's rendered for you!

But if you want access to them...

$form->handleRequest($request);if (!$form->isValid()) { $errors = form->getErrors();

// `$errors` is an array of `FormError` objects.}

Page 12: Symfony CoP: Form component

Hey!31 built-in types

46 built-in validators

Page 13: Symfony CoP: Form component

Custom types

Page 14: Symfony CoP: Form component

Custom type definition

class MyFormType extends AbstractType{ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('comment', 'textarea') ; }}

Page 15: Symfony CoP: Form component

Using the Form Type

public function createAction(Request $request){ $form = $this->createForm(new MyFormType()); $form->handleRequest($request);

if ($form->isValid()) { $data = $form->getData();

// Do what ever you want with the data... $comment = $data['comment']; }

return [ 'form' => $form->createView(), ];}

Page 16: Symfony CoP: Form component

Form Type options

class MyFormType extends AbstractType{ //!\ Replace `setDefaultOptions` since Symfony 3.0 /!\\ public function configureOptions(OptionsResolver $resolver) { $resolver->setRequired(['my-custom-option']);

$resolver->setDefaults(array( 'data_class' => 'App\Model\Object', )); }}

Page 17: Symfony CoP: Form component

Form Type as a service

class GenderType extends AbstractType{ private $genderChoices;

public function __construct(array $genderChoices) { $this->genderChoices = $genderChoices; }

public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'choices' => $this->genderChoices, )); }}

Page 18: Symfony CoP: Form component

Form Type as a service

Register the form type<service id="app.form.type.gender" class="App\Form\Type\GenderType"> <argument>%genders%</argument> <tag name="form.type" /></service>

Use the form typeclass MyFormType extends AbstractType{ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('gender', GenderType::class) ; }}

Page 19: Symfony CoP: Form component

EventsOr how to dynamically update the FormType based on

data.

Page 20: Symfony CoP: Form component

Form workflowIt dispatches different events while handling the requests.

» PRE_SET_DATA

» POST_SET_DATA

» PRE_SUBMIT

» SUBMIT

» POST_SUBMIT

Page 21: Symfony CoP: Form component

The name field only for a new product

public function buildForm(FormBuilderInterface $builder, array $options){ // ...

$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { $product = $event->getData(); $form = $event->getForm();

// The product name is only updatable for a new `Product` if (!$product || null === $product->getId()) { $form->add('name', TextType::class); } });}

Page 22: Symfony CoP: Form component

Event subscribers

public function buildForm(FormBuilderInterface $builder, array $options){ // ...

$builder->addEventSubscriber(new AddNameFieldSubscriber());}

Page 23: Symfony CoP: Form component

 An event subscriber

class AddNameFieldSubscriber implements EventSubscriberInterface{ public static function getSubscribedEvents() { return array(FormEvents::PRE_SET_DATA => 'preSetData'); }

public function preSetData(FormEvent $event) { $product = $event->getData(); $form = $event->getForm();

if (!$product || null === $product->getId()) { $form->add('name', TextType::class); } }}

Page 24: Symfony CoP: Form component

List based on another field1. Initial Form Type

public function buildForm(FormBuilderInterface $builder, array $options){ $builder ->add('sport', EntityType::class, array( 'class' => 'AppBundle:Sport', 'placeholder' => '', )) ;}

Page 25: Symfony CoP: Form component

List based on another field2. Our form modifier

$formModifier = function (FormInterface $form, Sport $sport = null) { $positions = null === $sport ? array() : $sport->getAvailablePositions();

$form->add('position', EntityType::class, array( 'class' => 'AppBundle:Position', 'placeholder' => '', 'choices' => $positions, ));};

Page 26: Symfony CoP: Form component

List based on another field3. The listeners

$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($formModifier) { $data = $event->getData();

$formModifier($event->getForm(), $data->getSport());});

$builder->get('sport')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($formModifier) { // It's important here to fetch $event->getForm()->getData(), as // $event->getData() will get you the client data (that is, the ID) $sport = $event->getForm()->getData();

// since we've added the listener to the child, we'll have to pass on // the parent to the callback functions! $formModifier($event->getForm()->getParent(), $sport);});

Page 27: Symfony CoP: Form component

Data Transformers

Page 28: Symfony CoP: Form component

Normalization flow

1. Model data. Our object returned by getData().

2. Internal representation, mostly our model data. Almost never used by the developer.

3. View data. The data structure sent to submit().

Page 29: Symfony CoP: Form component

Using the CallbackTransformer

public function buildForm(FormBuilderInterface $builder, array $options){ $builder->add('description', TextareaType::class);

$builder->get('description')->addModelTransformer(new CallbackTransformer(function ($originalDescription) { // transform <br/> to \n so the textarea reads easier return preg_replace('#<br\s*/?>#i', "\n", $originalDescription); }, function ($submittedDescription) { // remove most HTML tags (but not br,p) $cleaned = strip_tags($submittedDescription, '<br><br/><p>');

// transform any \n to real <br/> return str_replace("\n", '<br/>', $cleaned); }));}

Page 30: Symfony CoP: Form component

An integer to an object1. Our task Form Type

class TaskType extends AbstractType{ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('description', TextareaType::class) ->add('issue', TextType::class) ; }

public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'App\Model\Task' )); }

// ...}

Page 31: Symfony CoP: Form component

An integer to an object2. The transformer

class IssueToNumberTransformer implements DataTransformerInterface{ private $issueRepository; public function __construct(IssueRepository $issueRepository) { $this->issueRepository = $issueRepository; }

// Two methods to implement... public function transform($issue); public function reverseTransform($issueNumber);}

Page 32: Symfony CoP: Form component

An integer to an object3. From model to view

public function transform($issue){ return null !== $issue ? $issue->getId() : '';}

Page 33: Symfony CoP: Form component

An integer to an object3. From view to model

public function reverseTransform($issueNumber){ if (empty($issueNumber)) { return; }

if (null === ($issue = $this->issueRepository->find($issueNumber))) { throw new TransformationFailedException(sprintf('An issue with number "%s" does not exist!', $issueNumber)); }

return $issue;}

Page 34: Symfony CoP: Form component

An integer to an object4. Voilà!

class TaskType extends AbstractType{ private $issueRepository; public function __construct(IssueRepository $issueRepository) { $this->issueRepository = $issueRepository; }

public function buildForm(FormBuilderInterface $builder, array $options) { // ...

$builder->get('issue')->addModelTransformer(new IssueToNumberTransformer($this->issueRepository)); }

}

Page 35: Symfony CoP: Form component

Form Type Extensions

Page 36: Symfony CoP: Form component

An extension class

class IconTypeExtension extends AbstractTypeExtension{ public function getExtendedType() { return ChoiceType::class; }

// We can now declare the following methods... public function configureOptions(OptionsResolver $resolver); public function buildForm(FormBuilderInterface $builder, array $options); public function buildView(FormView $view, FormInterface $form, array $options); public function finishView(FormView $view, FormInterface $form, array $options)}

Page 37: Symfony CoP: Form component

An optional extra icon

class IconTypeExtension extends AbstractTypeExtension{ public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'icon' => null, ]); }

}

Page 38: Symfony CoP: Form component

An optional extra icon

class IconTypeExtension extends AbstractTypeExtension{ public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars['icon'] = $options['icon']; }}

You'll need to extend the choice_widget block using a form theme, so you can display an icon when the local icon variable is defined.

Page 39: Symfony CoP: Form component

Register the extension

<service id="app.image_type_extension" class="App\Form\Extension\ImageTypeExtension">

<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\ChoiceType" />

</service>

Page 40: Symfony CoP: Form component

 Rendering

Page 41: Symfony CoP: Form component

Using a form theme

{% form_theme form 'form/fields.html.twig' %}{# or.. #}{% form_theme form _self %}

{{ form(form) }}

Page 42: Symfony CoP: Form component

Example theme

# form/fields.html.twig

{% block form_row %}{% spaceless %} <div class="form_row"> {{ form_label(form) }} {{ form_errors(form) }} {{ form_widget(form) }} </div>{% endspaceless %}{% endblock form_row %}

Page 43: Symfony CoP: Form component

Creating a form themeUsing Twig blocks.

» [type]_row

» [type]_widget

» [type]_label

» [type]_errors

If the given block of type is not found, it will use the parent's type.

Page 44: Symfony CoP: Form component

FormType's buildView

class GenderType extends AbstractType{ private $genderChoices;

public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars['genders'] = $this->genderChoices; }}

Page 45: Symfony CoP: Form component

FormType's buildView

# form/fields.html.twig{% block gender_widget %}{% spaceless %} {# We can use the `gender` variable #}{% endspaceless %}{% endblock %}

Page 46: Symfony CoP: Form component

FormType's finishView

Called when the children's view is completed.

public function finishView(FormView $view, FormInterface $form, array $options){ $multipart = false;

foreach ($view->children as $child) { if ($child->vars['multipart']) { $multipart = true; break; } }

$view->vars['multipart'] = $multipart;}

Page 47: Symfony CoP: Form component

Creating a form without name1. The difference?

Your form properties won't be namespacedExample: comment instead of my_form[comment]

You might need/want to use it for:- Legacy applications compatibility- APIs (with the FOS Rest Body Listener)

Page 48: Symfony CoP: Form component

Creating a form without name2. How?

private $formFactory;

public function __construct(FormFactoryInterface $formFactory){ $this->formFactory = $formFactory;}

public function createAction(Request $request){ $form = $this->formFactory->createNamed(null, new MyFormType()); $form->handleRequest($request);

// ...}

Page 49: Symfony CoP: Form component

Wow, we're done!