Symfony Forms: Build, Render & Conquer!

Ah, forms: one of the most powerful - but sometimes hated - features in all of Symfony. Here's the deal: forms are just plain hard. You need to manage the HTML form elements, validation, data transformation and a lot more. The Form component is probably the most complex part of Symfony. But the more you work with it, the more you'll like it.

In this episode, we'll learn how to use Symfony forms to handle simple situations as well as some complex setups. Most importantly, I'll show you how to avoid the pitalls that many developers often fall into that causes the form component to spiral into complexity hell. The form system is a tool: in this tutorial, we'll put the joy back into it:

  • Creating a basic form
  • Basic form rendering and customization
  • Handling a form submit
  • Backing your form with an entity
  • Adding validation
  • Form field guessing
  • Pre-filling with default data
  • Creating a form "type" class
  • Using forms to handle an API POST request
  • Understanding how forms really work
  • Flash messages

... and form tips and tricks stuck in all over!


Your Guides

Ryan Weaver Leanna Pelham

Questions? Conversation?

  • 2016-08-03 Victor Bocharsky

    Hey Maciej,

    That's a good idea indeed! You should always set the lowest visibility for class methods, i.e. starts from private. If you see that some inherited class require access to this method - make it protected. And only if this method should be publicly accessible outside the class - make it public. Private ( protected ) methods make your class less responsible for your code. In this case you could easily refactor this methods. I love refactor private methods, because I 100% ensure that these methods are used only inside the class.

    Some controller methods should be "public", e.g. all your actions, but for other helper methods - always start with "private" and reveal visibility only if it necessary.

    Cheers!

  • 2016-08-02 Maciej Osytek

    Hey folks. What do you think about private functions in controller class?

  • 2016-07-25 weaverryan

    I've moved this up higher on our list - there's obviously some interest here! :D

  • 2016-07-23 Sławek Grochowski

    yeah please do this example https://symfony.com/doc/master...

  • 2016-07-06 Asia

    Yeah, I would love to see it too (dynamic forms especially) ;)

  • 2016-07-04 weaverryan

    Awesome, thanks for sharing!

  • 2016-07-02 JLChafardet

    hey weaverryan indeed i went with the textfile option! after analizing myself yesterday the code I realized that was the best way to go.

    so indeed, im not using fileType, but textType, and hooking the text-field with a javascript to open the bootstrap modal with the filemanager. and on select, that text-field is filled with the file value.

    the text-field is a read-only file, so the user cant actually interact manually with the name of the file.
    form is rendered normally.
    modal



    <div class="modal-body">
    <iframe width="100%" height="450" src="{{ asset('vendor/responsive-filemanager/filemanager/dialog.php') }}?type=0&amp;field_id=blog_form_mainImage" frameborder="0" style="overflow: scroll; overflow-x: hidden; overflow-y: scroll;
    ">
    </iframe>
    </div>


    //builder stuff
    ->add('mainImage', TextType::class, [
    'attr' => [
    'readonly' => 'readonly',
    'data-target' => '#browser',
    'data-toggle' => 'modal'
    ],
    'label' => 'main_image'
    ])
  • 2016-07-02 weaverryan

    Hey JLChafardet!

    I'm not sure exactly what you're doing wrong - it depends on what your form theme looks like - but we're going to cover a lot more about this in our form themes tutorial. But more importantly, I have a simpler approach for you, which I use all the time:

    A) Don't use the FileType. If you have a "file manager" system, where the user is uploading inside some modal and ultimately choosing some file that they have *already* uploaded, then when the user finishes this process and submits the form, all you should need to send is the name of the file that was selected (or perhaps, it's an "id" - if you have some sort of File entity that your file manager manages). Said differently, you do not really have a file "upload" field in your form - the file is uploaded somewhere else, and this "file" field in your form is really more of a "text" field - in that you are just submitting some value - like foo.png or 10 (the id of a File entity). With this in mind, you could go a direction that would look like this: Use TextType, but give it a class that makes it hidden *and* some other class - like js-file-selector-field - that you can find with JavaScript

    B) On page load, look for all inputs with the class js-file-selector-field, and append your button markup manually after the input, and hook up any modal JS functionality.

    C) After the modal is finished, use JavaScript to set the value on your hidden "text" field to whatever filename/id was just selected.

    D) Now, when you submit, you will be submitting a simple string value, which is really what you want.

    Depending on your setup - especially if you have some File entity - you may want to use a "data transformer" to transform the id (10) into the File entity object whose id is 10, so that this is ultimately set on your object. Data transformers are a little more advanced, but this is a pretty common use for them.

    I hope this helps man!

  • 2016-06-30 JLChafardet

    This is great! indeed this solution is far more flexible than mine, as mine i somewhat hardcoded. or i guess i can overwrite it too, lemme test.
    //update
    yes i can overwrite it, so it works great, not a bad idea to have auto-renders, but if i need to overwrite something, there is a way to do so.

    thanks @weaverryan

  • 2016-06-30 JLChafardet

    I'm moving now to highly different and "seemly" more complicated waters, reading the documentation as we speak.

    What i want:



    <div class="form-group">
    <label for="mainImage" class="col-sm-2 control-label">
    {{ 'image'|trans ~ ' ' ~ 'main'|trans|lower }}
    </label>
    <div class="col-sm-10">
    <div class="input-group">
    <input type="text" id="blog_form_mainImage" name="blog_form[mainImage]" class="form-control" readonly="" placeholder="">

    <button class="btn btn-default" data-target="#browser" data-toggle="modal" type="button">
    {{ 'search'|trans }}
    </button>

    </div>
    </div>
    </div>

    I am trying to make a custom widget, because the FileType just wont work, as im looking for a input-group, with a read-only text field, bundled with a button, that opens a custom file manager inside a modal lol

    I'm working atm on the widget template (no luck whatsoever yet), so i can render in place of


    {{ form_row(myForm.fieldName) }}


    print


    {% form_theme myForm 'widget-template.html.twig' %}
    {{ form_widget(myForm.myinputGroup) }}

    yet im having no luck at all! and attempting to print the field in html, just breaks the page. for one, attempts to print the FieldType below the form (not sure why) and if i remove it from the FormType class it tells me "the form cannot have additional fields"
    also its erroring me on the form token tho, no clue why.

  • 2016-06-30 weaverryan

    Glad you figured it out! The code you posted will be helpful for other people :).

    For those people, here's the working code for setting a label:


    {# the 2nd argument of *form_label* is just the string label #}
    {{ form_label(form.title, 'title') }}

    {# the 2nd argument if *form_row* is an array of variables #}
    {{ form_row(form.title, {'label': 'title'}) }}

    Cheers!

  • 2016-06-30 Victor Bocharsky

    Haha, yes, you got it!

    It comes with Symfony out of the box :)

    Very nice research!

  • 2016-06-30 JLChafardet

    still trying to figure out the label part tho.

    An exception has been thrown during the rendering of a template ("Catchable Fatal Error: Argument 3 passed to Symfony\Component\Form\FormRenderer::searchAndRenderBlock() must be of the type array, string given, called in /var/www/vhosts/lockandbefree.s3/var/cache/dev/classes.php(3641) : eval()'d code on line 26 and defined") in :admin/blog:_form.html.twig at line 2. 

    while using

    {{ form_row(myForm.title, 'title' }}


    as suggested in the documentation http://symfony.com/doc/current...

    found it


    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    ->add('title', TextType::class, [
    'label' => 'title'
    ])

    //other fields

    ->add('isVisible', ChoiceType::class, [
    'choices' => [
    'yes' => true,
    'no' => false,
    ],
    'choice_label' => function ($value, $key, $index) {
    return $key;
    },
    ]);
    }

    Sorry for the spam tho! hope this works for other people wondering about the same thing.

  • 2016-06-30 JLChafardet

    here! haha


    form_themes:
    - 'bootstrap_3_horizontal_layout.html.twig'
  • 2016-06-30 JLChafardet

    http://symfony.com/blog/new-in... <-- ok going to get it working asap.

  • 2016-06-30 JLChafardet

    Hey Victor Bocharsky thanks a lot! that did it.
    went with only $key tho :D


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

    -//other fields

    ->add('isVisible', ChoiceType::class, [
    'choices' => [
    'yes' => true,
    'no' => false,
    ],
    'choice_label' => function ($value, $key, $index) {
    return $key;
    },
    ]);
    }


    as the key for simple words in my messages.es.yml is the word itself
    indeed I had enabled translation tho thanks for the reminder.


    parameters:
    locale: es

    framework:
    #esi: ~
    translator: { fallbacks: ["%locale%"] }

    Now, how would we go about getting rid of the label guessing? I tried this:


    {{ form_start(myForm) }}
    {{ form_label(myForm.title, 'title') }}
    {{ form_row(myForm.title) }}
    {# other fields #}
    {{ form_end(myForm) }}

    no luck it renders the label translated as it should, but below the field with the untranslated (guessed) label.

    is there a way to actually get a full horizontal form (bootstrap) using the form builder yet using my own labels? maybe I should go through this whole chapter again to take a peak :S

  • 2016-06-30 Victor Bocharsky

    Hey there!

    Yes, you can translate choices directly in your form type using `choice_label` option:

        public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    // other fields
    ->add('isVisible', ChoiceType::class, [
    'choices' => [
    'yes' => true,
    'no' => false,
    ],
    'choice_label' => function ($value, $key, $index) {
    return 'form.choice.'.$key;
    },
    ]);
    }

    For more information about `choice_label` option see ChoiceType Field. And don't forget to enable translator in your config.yml:

    framework:
    translator: ~

    Cheers!

  • 2016-06-29 JLChafardet

    hey guys!

    Lets go with a question here and I wont lie, I'm too lazy to google LOL sorry!

    we have a formtype, great! now, choices....


    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    ->add('title')
    ->add('mainImage')
    ->add('description')
    ->add('content')
    ->add('isVisible', ChoiceType::class, [
    'choices' => [
    'yes' => true,
    'no' => false,
    ]
    ]);
    }

    how would I go around to put there a string that I'm planning on enclosing in single quotes to then translate it?


    {{ 'string'|trans }}

    in twig?

  • 2016-06-29 Schmalitz

    Super nice - it definitely helps! Thank you so much!
    You gals and guys are doing a fantastic job!

  • 2016-06-29 weaverryan

    Thanks Schmalitz for the really nice comments!

    And you are 100% thinking in the right direction - you even explained it really well in my opinion :p. We've gotten a few requests to explain this further, so I think we will in a future tutorial. But, your GenusAdminController would ultimately look something like this (this is totally, fake code - just use it for inspiration!). In this example, I have a "Genus edit form" with just 2 fields: the Genus name and the Genus's SubFamily name. If you changed the SubFamily name, you would actually update that record in the database (it's kind of a weird example):


    public function editAction(Genus $genus)
    {
    // create the model object and "transfer" the data to it from other entities
    $genusModel = new GenusModel(); // some non-entity class
    $genusModel->setName($genus->getName());
    $genusModel->setSubFamilyName($genus->getSubFamily()->getName());

    $form = $this->createForm(GenusModelForm::class, $genusModel);

    if ($form->isValid()) {
    // now, transfer the submitted data back to the entities
    $genus->setName($genusModel->getName());
    $genus->getSubFamily()->setName($genusModel->getSubFamilyName());

    // save the $genus and $genus->getSubFamily() objects
    }

    // ...
    }

    I hope that helps!

  • 2016-06-29 Schmalitz

    Hi Leanna and Ryan. This course is huge (all of your courses are, by the way...)! Thank you so much!

    Re ModelClass (multiple entities -> one form): Could you maybe give an example of how to design such a ModelClass? I could imagine these to be - lets say - a pseudo-entity looking like an entity (without database connection) which holds specific properties from different entities. These properties are defined by setters custom querying data from the actual entity.

    Am I thinking in the right direction? Could you maybe drop such a ModelClass and also the Controller-lines (GenusAdminController) as of line 67 to save this data-collection? Or just paste in a knowledge-resource which you do recommend.

    Thank you so much!
    Cheers

  • 2016-06-01 weaverryan

    Hey Sergio!

    No worries - this is a pretty common thing that people ask. First, there is no performance difference - the choice will be entirely subjective: whatever you like better. Here's a quick summary:

    Annotations:
    PROS: the route is right above your action method
    CONS: slight performance degradation in the dev environment (something I've been meaning to "fix" in SensioFrameworkExtraBundle)

    YAML
    PROS: All of your routes in one file (or a set of files) - though due to debug:router, you can always get a list of routes with annotations or YAML. Also, it's a little easier to re-order routes if you need to
    CONS: The route is not right next to the action method, so you need to manage/edit 2 files. Also, the _controller syntax is ugly for beginners

    I like annotations because everything is in one file and I hate the _controller syntax (well, mostly, I hate teaching it to beginners).

    You'll be in good shape either way!

    Cheers!

  • 2016-06-01 Sergio Medina

    Hi Ryan, quick question and maybe a little bit of topic but... What's better routes as annotation or routes as yml?? I'm working on a project at the moment and we use yml, just was wondering if there's any gaing on one over the other. Great work teaching us Symfony, thanks mate! =)

  • 2016-06-01 Shairyar Baig

    Makes sense, thanks

  • 2016-05-31 weaverryan

    Hey Baig!

    I like and don't like "form handlers". Basically, we all know that putting logic in a controller is "bad" and putting logic in a service is "good". That's fundamentally why form handlers are created: they hold the form handling logic so that it doesn't live in your controller. Third-party bundles also do it for another reason: if they put the logic in a service, it's typically easier for an end-user to extend/override parts of the functionality (however, this can be done even better by firing events, like FOSUserBundle does).

    I don't love form handlers personally (which is why you don't find them here or on the official docs) because that logic typically isn't re-used, shouldn't be unit-tested and is fundamental to the "flow" of your app (e.g. setting flash messages, redirecting, etc). However, if I actually need to do a bit of work before setting the flash and redirecting (e.g. instead of just "saving" an entity, I need to do some data manipulation, send an email, etc), then isolating some of all of this into a service makes sense. However, even in that case, I usually make a service that's special to what it's doing (e.g. NotificationManager::createNotification) instead of being something that just handles the "form logic".

    It's certainly subjective - but there ya go!

    Cheers!

  • 2016-05-31 Shairyar Baig

    Hi,

    I have been seeing a lot of 3rd party bundle the that make use of FormHandlers. What is this and why we need this? I also read about it in the book An year with Symfony. Official documentation does not mention this. I actually don't want to personally use FormHandlers as I think we have Events that does pretty nice job.

    Are you going to cover this topic as I just want to understand this.

    Baig

  • 2016-05-25 weaverryan

    Ha, glad you figured it out one way or another! This setup makes sense to me :)

  • 2016-05-24 Kosta Andonovski

    I had to enter the $user object into the form builder and after the form->handlerequest() I had to delete the $data object, it was causing the problems. the fix is below, thank anyway ryan, your a legend.

         $user = $em->getRepository('AppBundle\Entity\User')
    ->find($userId);

    $form = $this->createFormBuilder($user)
    ->add('firstName', TextType::class)
    ->add('lastName', TextType::class)
    ->add('address', TextType::class)
    ->add('EmailAddress', TextType::class, array(
    'required' => true,
    ))
    ->add('password', TextType::class)
    ->add('phoneNumber', IntegerType::class)
    ->add('title', TextType::class)
    ->add('role', EntityType::class, array(
    'class' => 'AppBundle\Entity\Role',
    'choice_label' => 'role',
    'placeholder' => 'Choose Role',
    ))
    ->add('sites', EntityType::class, array(
    'class' => 'AppBundle\Entity\Site',
    'choice_label' => 'name',
    'placeholder' => 'Choose Sites',
    'required' => false,
    'expanded' => true,
    'multiple' => true,
    ))
    ->add('drivingLicence', TextType::class)
    ->add('drivingLicenceExpiryDate', DateType::class)
    ->add('securityLicence', TextType::class)
    ->add('securityLicenceExpiryDate', DateType::class)
    ->add('goodQuality', TextType::class)
    ->add('create', SubmitType::class, array('label' => 'Edit User'))
    ->getForm();

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
    $em->persist($user);
    $this->addFlash(
    'success',
    'User details have been updated'
    );
    $em->flush();

    return $this->redirectToRoute('users_list');
    }

    //I had to delete $data = $form->getData(); and then all the setters after that.


  • 2016-05-22 weaverryan

    Hey Kosta!

    Hmm, it looks fine to me! In fact, it's curious: if you set multiple to false, I would expect this to *fail*, as the EntityType would pass an individual *object* to your User.sites property. Do you have a setSites() method on User? That's the only piece I don't see here. It's also odd that Doctrine is complaining about the ArrayCollection - as it implements the Collection interface class that it says it actually wants :).

    Cheers!

  • 2016-05-22 weaverryan

    Hi Tom!

    Haha, well, we don't want that :). At least I'm glad you're excited about the tutorial. I can temporarily publish the finished code for this tutorial - as you suggested - to keep people productive over the next week or so before we get this out! You can find it here: https://github.com/knpuniversi.... If you look at the commits (https://github.com/knpuniversi... you can find the individual steps.

    Cheers!

  • 2016-05-22 weaverryan

    Ah, too bad! This is coming out so soon! Well, it'll be ready if/when you come back to Symfony :).

  • 2016-05-20 Laravel Guy

    Half a year after official Symfony3 release and you still have not finished basic Symfony3 course. I actually started learning laravel... :(

  • 2016-05-20 Kosta Andonovski

    Hey Ryan, I have gone through the form tutorials on symphony 2 but cant find out how to submit a EntityChoice with multiple set to true (site). If I set multiple to false it works fine, but when I put multiple => true one of the following happens. a) if I choose just one option in the select box I get an error "Expected value of type "Doctrine\Common\Collections\Collection|array" for association field "AppBundle\Entity\User#$sites", got "Doctrine\Common\Collections\ArrayCollection" instead" b) if I tick all available options and press submit it just reloads the page. I have 2 tables users and sites. on the user entity I have

    /**
    * @ORM\ManyToMany(targetEntity="Site", inversedBy="users")
    */
    private $sites;

    public function __construct()
    {
    $this->sites = new arrayCollection();
    }

    */ * @return ArrayCollection

    /**
    public function getSites()
    {
    return $this->sites;
    }

    My site entity is as follows:

    /**
    * @ORM\ManyToMany(targetEntity="User", mappedBy="sites")
    */
    private $users;

    /**
    * @return mixed
    */
    public function getUsers()
    {
    return $this->users;
    }

    My form element that is causing problems is below

    $form = $this->createFormBuilder()
    ->add('sites', EntityType::class, array(
    'class' => 'AppBundle\Entity\Site',
    'choice_label' => 'name',
    'placeholder' => 'Choose Sites',
    'expanded' => true,
    'multiple' => true,
    ))
    ->add('create', SubmitType::class, array('label' => 'Create User'))
    ->getForm();

    $form->handleRequest($request);

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

    $user->getSites()->add($data['sites']);
    $em->persist($user);
    $em->flush();

  • 2016-05-19 Tom Olson

    How about this... Even if you do not have the video done, how about releasing the lesson and code.

  • 2016-05-19 Tom Olson

    Seriously, I am about to abandon KNP for codereviewvideos.com if this does not come out this week.

  • 2016-05-18 weaverryan

    Hi Bjorn! This will come out later this month - so in May! So, within the next 2 weeks :)

  • 2016-05-17 Bjorn Larson

    Please when is this coming out. I really need it !!!!!!

  • 2016-05-04 Raphael Schubert

    Here is what happens and the solution: http://stackoverflow.com/quest...

    Cya!!

  • 2016-05-04 weaverryan

    Hey, congrats! And that's a great idea! We have some documentation about it on symfony.com (I'm sure you saw that), but I don't know how complete it is. Any words of wisdom you could add here quickly to help others?

    Cheers!

  • 2016-05-04 Raphael Schubert

    Yeah!!!! Finally i finished Azure configuration with Symfony....
    If you can, create an video about Upload an Symfony Website to Azure.... It take me 2 days just because little things... now i can configure in 30 minutes... lol... others peoples will like an explanation....

  • 2016-05-03 weaverryan

    You rock! And welcome! I see you got an answer on that SO - did it solve your problem?

  • 2016-05-03 Raphael Schubert

    Hey !!! Good news!!! i subscribed the KNPU... Amazing vídeos.!!!!

    Now i will ask for one more help.... thank you for answer me.... i was with trouble to insert datatype field in forms... i asked for help in Stackoverflow and no one answered yet... if you can, can you explain more to us? i spent 1 day in this problem and can't solve it.... here is my explanation and details about...

    http://stackoverflow.com/quest...

    Best regards!

  • 2016-05-02 weaverryan

    Hi Raphael!

    Yes - you'll attach your annotations via options on the fields: http://symfony.com/doc/current...

    Cheers!

  • 2016-05-02 Raphael Schubert

    hello there again!!! i have an question about forms... can i validate an no entity related form?

    I created an form to find something in database, but if i send it clear, will return all, i don't want it....

  • 2016-04-30 Diego Aguiar

    Hi there,
    Would be nice to see something about of how to manage file uploads in entity forms, like images for users avatar

    cheers ;]

  • 2016-04-14 Ömür Yanıkoğlu

    Last track is over quickly :( but thank you very much!
    I am looking forward the next one :)

  • 2016-02-29 gabf hann

    Hey Ryan,

    Would be great if advance tutorials like FormEvents are also covered in this Chapter or the next.

  • 2016-01-10 weaverryan

    Thanks for sharing - I like this solution a lot!

  • 2016-01-10 Bettinz

    Maybe this help someone :) I've solved using a macro

    {% macro purchasedProduct(product) %}
    <div class="row">
    <div class="col-md-3">
    {{ form_row(product.name)}}
    </div>
    </div>
    {% endmacro %}

    and then {% import "form/bootstrap_supplier.html.twig" as supplierForm %}

    and in data-prototype="{{ supplierForm.purchasedProduct(form.products.vars.prototype) |e }}"

    Now I can use col-md-3 for my fields instead the default col-md-10 with a prototype and a collection :)

  • 2016-01-09 Bettinz

    Hello, I'm starting with forms, and I've a question: I'm using bootstrap_3_horizontal_layout in a form with a collection; In fact, I've an Invoice and a row for each product in this invoice (ProductInvoice). I've followed http://symfony.com/doc/current... and now I'm able to add a row and delete a row; if I understand correctly, I'm adding an object in real time with a prototype. Everything is working except that I need to show each form_row of ProductInvoice with style class="col-md-3" instead the default class="col-md-10".

    I've tried this: http://symfony.com/doc/current... but the problem is the form_row name since it's dynamic like purchase_invoice_products_1_name.

    How can I customize the form_row of a prototype dynamically generated via javascript?
    Thank you :)