KEMBAR78
Symfony CoP: Form component | PDF
Symfony
Form component
Plan
1. Basic usage
2. Validation
3. Custom types
4. Events
5. Data Transformers
6. Form type extensions
7. Rendering overview
What's the Form
component?
Basic usages
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(),
];
}
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) }}
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(),
];
}
Validation
Validation
$form = $this->createFormBuilder()
->add('comment', 'textarea', [
'required' => true,
'constraints' => [
new NotBlank(),
],
])
->getForm()
;
Basic usage (Validation on the object)
use SymfonyComponentValidatorConstraints as Assert;
class MyObject
{
/**
* @AssertNotBlank
* @AssertLength(
* min=10,
* minMessage="The comment have to be useful"
* )
*/
private $comment;
// Required methods
public function getComment();
public function setComment($comment);
}
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.
}
Hey!
31 built-in types
46 built-in validators
Custom types
Custom type definition
class MyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('comment', 'textarea')
;
}
}
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(),
];
}
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' => 'AppModelObject',
));
}
}
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,
));
}
}
Form Type as a service
Register the form type
<service id="app.form.type.gender" class="AppFormTypeGenderType">
<argument>%genders%</argument>
<tag name="form.type" />
</service>
Use the form type
class MyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('gender', GenderType::class)
;
}
}
Events
Or how to dynamically update the FormType based on
data.
Form workflow
It dispatches different events
while handling the requests.
» PRE_SET_DATA
» POST_SET_DATA
» PRE_SUBMIT
» SUBMIT
» POST_SUBMIT
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);
}
});
}
Event subscribers
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$builder->addEventSubscriber(new AddNameFieldSubscriber());
}
 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);
}
}
}
List based on another field
1. Initial Form Type
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('sport', EntityType::class, array(
'class' => 'AppBundle:Sport',
'placeholder' => '',
))
;
}
List based on another field
2. 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,
));
};
List based on another field
3. 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);
});
Data Transformers
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().
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('#<brs*/?>#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);
}));
}
An integer to an object
1. 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' => 'AppModelTask'
));
}
// ...
}
An integer to an object
2. 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);
}
An integer to an object
3. From model to view
public function transform($issue)
{
return null !== $issue ? $issue->getId() : '';
}
An integer to an object
3. 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;
}
An integer to an object
4. 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));
}
}
Form Type Extensions
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)
}
An optional extra icon
class IconTypeExtension extends AbstractTypeExtension
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'icon' => null,
]);
}
}
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.
Register the extension
<service
id="app.image_type_extension"
class="AppFormExtensionImageTypeExtension">
<tag name="form.type_extension"
extended-type="SymfonyComponentFormExtensionCoreTypeChoiceType" />
</service>
 Rendering
Using a form theme
{% form_theme form 'form/fields.html.twig' %}
{# or.. #}
{% form_theme form _self %}
{{ form(form) }}
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 %}
Creating a form theme
Using 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.
FormType's buildView
class GenderType extends AbstractType
{
private $genderChoices;
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['genders'] = $this->genderChoices;
}
}
FormType's buildView
# form/fields.html.twig
{% block gender_widget %}
{% spaceless %}
{# We can use the `gender` variable #}
{% endspaceless %}
{% endblock %}
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;
}
Creating a form without name
1. The difference?
Your form properties won't be namespaced
Example: comment instead of my_form[comment]
You might need/want to use it for:
- Legacy applications compatibility
- APIs (with the FOS Rest Body Listener)
Creating a form without name
2. 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);
// ...
}
Wow, we're done!

Symfony CoP: Form component

  • 1.
  • 2.
    Plan 1. Basic usage 2.Validation 3. Custom types 4. Events 5. Data Transformers 6. Form type extensions 7. Rendering overview
  • 3.
  • 4.
  • 5.
    Basic usage (Symfonyway) 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(), ]; }
  • 6.
    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) }}
  • 7.
    Basic usage (Objectinstead 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(), ]; }
  • 8.
  • 9.
    Validation $form = $this->createFormBuilder() ->add('comment','textarea', [ 'required' => true, 'constraints' => [ new NotBlank(), ], ]) ->getForm() ;
  • 10.
    Basic usage (Validationon the object) use SymfonyComponentValidatorConstraints as Assert; class MyObject { /** * @AssertNotBlank * @AssertLength( * min=10, * minMessage="The comment have to be useful" * ) */ private $comment; // Required methods public function getComment(); public function setComment($comment); }
  • 11.
    Basic usage (Getvalidation 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. }
  • 12.
    Hey! 31 built-in types 46built-in validators
  • 13.
  • 14.
    Custom type definition classMyFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('comment', 'textarea') ; } }
  • 15.
    Using the FormType 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(), ]; }
  • 16.
    Form Type options classMyFormType extends AbstractType { //! Replace `setDefaultOptions` since Symfony 3.0 /! public function configureOptions(OptionsResolver $resolver) { $resolver->setRequired(['my-custom-option']); $resolver->setDefaults(array( 'data_class' => 'AppModelObject', )); } }
  • 17.
    Form Type asa 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, )); } }
  • 18.
    Form Type asa service Register the form type <service id="app.form.type.gender" class="AppFormTypeGenderType"> <argument>%genders%</argument> <tag name="form.type" /> </service> Use the form type class MyFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('gender', GenderType::class) ; } }
  • 19.
    Events Or how todynamically update the FormType based on data.
  • 20.
    Form workflow It dispatchesdifferent events while handling the requests. » PRE_SET_DATA » POST_SET_DATA » PRE_SUBMIT » SUBMIT » POST_SUBMIT
  • 21.
    The name fieldonly 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); } }); }
  • 22.
    Event subscribers public functionbuildForm(FormBuilderInterface $builder, array $options) { // ... $builder->addEventSubscriber(new AddNameFieldSubscriber()); }
  • 23.
     An event subscriber classAddNameFieldSubscriber 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); } } }
  • 24.
    List based onanother field 1. Initial Form Type public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('sport', EntityType::class, array( 'class' => 'AppBundle:Sport', 'placeholder' => '', )) ; }
  • 25.
    List based onanother field 2. 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, )); };
  • 26.
    List based onanother field 3. 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); });
  • 27.
  • 28.
    Normalization flow 1. Modeldata. 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().
  • 29.
    Using the CallbackTransformer publicfunction 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('#<brs*/?>#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); })); }
  • 30.
    An integer toan object 1. 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' => 'AppModelTask' )); } // ... }
  • 31.
    An integer toan object 2. 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); }
  • 32.
    An integer toan object 3. From model to view public function transform($issue) { return null !== $issue ? $issue->getId() : ''; }
  • 33.
    An integer toan object 3. 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; }
  • 34.
    An integer toan object 4. 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)); } }
  • 35.
  • 36.
    An extension class classIconTypeExtension 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) }
  • 37.
    An optional extraicon class IconTypeExtension extends AbstractTypeExtension { public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'icon' => null, ]); } }
  • 38.
    An optional extraicon 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.
  • 39.
    Register the extension <service id="app.image_type_extension" class="AppFormExtensionImageTypeExtension"> <tagname="form.type_extension" extended-type="SymfonyComponentFormExtensionCoreTypeChoiceType" /> </service>
  • 40.
  • 41.
    Using a formtheme {% form_theme form 'form/fields.html.twig' %} {# or.. #} {% form_theme form _self %} {{ form(form) }}
  • 42.
    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 %}
  • 43.
    Creating a formtheme Using 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.
  • 44.
    FormType's buildView class GenderTypeextends AbstractType { private $genderChoices; public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars['genders'] = $this->genderChoices; } }
  • 45.
    FormType's buildView # form/fields.html.twig {%block gender_widget %} {% spaceless %} {# We can use the `gender` variable #} {% endspaceless %} {% endblock %}
  • 46.
    FormType's finishView Called whenthe 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; }
  • 47.
    Creating a formwithout name 1. The difference? Your form properties won't be namespaced Example: comment instead of my_form[comment] You might need/want to use it for: - Legacy applications compatibility - APIs (with the FOS Rest Body Listener)
  • 48.
    Creating a formwithout name 2. 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); // ... }
  • 49.