Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Symfony 4 Forms: Build, Render & Conquer!

4:45:40

What you'll be learning

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.6
        "symfony/console": "^4.0", // v4.1.6
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.6
        "symfony/framework-bundle": "^4.0", // v4.1.6
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.6
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.6
        "symfony/validator": "^4.0", // v4.1.6
        "symfony/web-server-bundle": "^4.0", // v4.1.6
        "symfony/yaml": "^4.0", // v4.1.6
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
        "symfony/dotenv": "^4.0", // v4.1.6
        "symfony/maker-bundle": "^1.0", // v1.8.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.6
    }
}

Yep, forms! One of the most powerful and... confusing features in all of Symfony. Here's the truth: forms are just plain hard. You need to manage the HTML form elements, validation, data transformation and a lot more. The Form component might be the most complex part of Symfony. But the more you work with it, the more you'll like it.

So, let's work with some forms! We'll learn how to use Symfony forms to handle both simple & complex/ugly situations. 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
  • Filling with default data
  • Creating a form "type" class
  • Understanding how forms really work
  • Flash messages

... and bad jokes + form tips & tricks!


Your Guides

Ryan Weaver

Buy Access
Login or register to track your progress!

Join the Conversation?

97
Login or Register to join the conversation
Mohamed K. Avatar
Mohamed K. Avatar Mohamed K. | posted 4 years ago

Can’t wait until This is availble. Been loving symfony 4 because of symfonycasts🙂

1 Reply
Default user avatar
Default user avatar Влада Петковић | posted 1 year ago | edited

I would like to ask one question if someone can answer me.
I am trying to use Symfony Forms (FormType) in my web application. This is my snipet of code where I'm trying to assign a name to the FoleUpload control:


    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('firstName')
            ->add('lastName')
            ->add('profileImagePath',FileType::class,[
                'attr' =>[
                    'name' => 'imgUpload',
                    'css' =>'black_box'
                ]
            ])
        ;
    }

but in the web browser the fileUpload control is rendered as:


<input type="file" id="author_form_profileImagePath" name="author_form[profileImagePath]" css="black_box" class="form-control">

Why can't the name attribute override the existing value and the css works correctly?
Is there a possibility that when building the form, we can set our attribute name od some field to some value that we want?

Thanks in advance.

Reply

Hey Vlada,

The problem is that there's no "css" attribute in HTML, probably you mean "style" or "class" attribute? You need to pass the valid attributes to the "attr" key as an array, otherwise they will be rendered as in your case with 'css="black_box"', but nothing will be changed as browser does not know them.

Cheers!

1 Reply
Default user avatar
Default user avatar Влада Петковић | Victor | posted 1 year ago

Thank you for your answer, Victor.
Yes, my mistake is css, but I mentioned it as an example that we can use the attr option to enter it in the attribute declaration of the field. However, my main problem is the inability to set the name attribute of a field that I define using a FormType class (in this case FileType). Is it even possible to define the name attribute using FormType elements, or in that case are we forced to define the html element via standard html? FormType offers so many options and possibilities that I wonder why it doesn't allow us programmers to define the name attribute?! So, in my code, I define the name="imgUpload" attribute, but when rendering the html element, my name is ignored and the name probably assigned by Symfony itself is retained, i.e. name = author_form[profileImagePath].

Reply

Hey Vlada,

Ah, I see. Well, you can't change the name via "attr" because it's a spacial (system) name of the field, and Symfony requires it to be exactly as the field name in the form. If you want to change it - change the field name in the form and in the entity. However, you may change the field name in the form only, just use the field name you want in the form builder, i.e. ->add('yourCustomFieldName') and then point it to the specific entity property via "property_path", see https://symfony.com/doc/cur... . in theory, this should work.

But it will result in something like "author_form[yourCustomFieldName]". In case you want to get rid of that automatic form type prefix, i.e. "author_form" in your case - you can override getBlockPrefix() method in your form type, just return an empty string there and it will do the trick, no prefix in the form anymore. You can read more about that method on this page I think: https://symfony.com/doc/cur...

I hope this helps!

Cheers!

1 Reply
Default user avatar
Default user avatar Влада Петковић | Victor | posted 1 year ago

Yes, you helped me, Mr. Victor and the problem is solved.
I didn't know about the trick with the method - getBlockPrefix() . Now the field has the name I assigned to it with builder->add('my-name') method and getBlockPrefix() return empty string .

Thank you very much.

Reply

Hey Vlada,

Awesome, thanks for confirming it works for you this way. Yeah, probably a little-know trick, but IIRC we covered this in our forms tutorial.

Cheers!

Reply

Hi and thank you for the great Symfony tutorials.

I wanted to follow this course and code along but i've tried many ways to update it (i'm kind of a noob) and it still didn't work.
Is there a way to run it on my system (php 8.1 and Symfony 6)? (I'm not ready to configure a docker file alone with all what will be needed to make this work.)
I hope there is a way because your tutos are great teaching tools to add with reading the docs.

Reply
Victor Avatar Victor | SFCASTS | Manolis | posted 1 year ago | edited

Hey Mano,

Thank you for your interest in SymfonyCasts tutorials and kind words about them! Yeah, unfortunately, this course works only on PHP 7.x - that's a limitation of dependencies that were installed in this course on the time we recorded it. We do recommend our users to download the course code and code along with us starting from start/ directory. But if you have a newer PHP version installed on your system - you have a few options: use Docker, or downgrade your PHP version, or install a legacy PHP version. For example, if you're on Mac - you can install legacy (alternative) 7.4 via Brew with:


$ brew install php@7.4

and use it to run the website. If you're on a different OS - good about how to install a legacy version on your system.

Unfortunately, we cannot change the code of already recorded tutorials, but we can add some notes to make is easier to follow it on a newer version of Symfony. We already added some notes that should help you to follow this course on a newer Symfony version. Please, if all the options I listed above does not fit you - then you can download a brand new Symfony version and try to follow this course on it. And of course, if you get stuck somewhere following the course on a newer version - let us know in the comments below the video and we will help you!

Also, we're going to release a new tutorial about Symfony Form (that will be based on Symfony 6) - but we don't have any specific date when it may happen. Thank you for your understanding!

Cheers!

Reply

Thank you for the answer. I'm on ubuntu but I finally (hardly) success.

Reply

Hey Mano!

I'm glad to hear you managed to do this, well done! If you get stuck following the course - don't hesitate to ask questions in comments below the video

Cheers!

Reply
Ruslan Avatar

Can I use this tutorial for Symfony 5 ?

Reply

Hey @Ruslan Gr.!

Absolutely :). Not a ton has changed in the forms system between Symfony 4 and Symfony 5. We will likely record (probably in the fall) a Sf5 version of this tutorial, but we haven't been in a rush since not a ton has changed.

Cheers!

1 Reply
Fernando A. Avatar
Fernando A. Avatar Fernando A. | posted 2 years ago

Hi!
Is this course compatible with Symfony 5? or are there to much changes between versions?

Thank you

Reply

Hey Fernando A.

You may find a few deprecations but in general, the main concepts of the tutorial are still valid. Symfony Forms component hasn't changed much since then

Cheers!

Reply
Default user avatar
Default user avatar mario31 | posted 3 years ago

Guys i ran into problem, i skipped ($this->createForm()). Is there a way back on track after this?

I broke the form creation chain by building 3 separate html forms myself in one page. I needed to do it since I need to add some form manipulation with javascript( add several event listeners, etc. In practice user hits the option and as a result one of 3 forms appear. I didn't know how to do it with form component. )

Reply

Hey @mario31

When you have a complex form situation is recommended to handle it outside of Symfony's Form component because it will just make it harder. If validations are your concern, you can still use them, you only need to add another argument to your route and type for Symfony\Component\Validator\Validator\ValidatorInterface
If you want to learn more about the validator component, you can check the docs here: https://symfony.com/doc/current/validation.html

I hope it helps! Cheers!

Reply
Christina V. Avatar
Christina V. Avatar Christina V. | posted 3 years ago | edited

Hi folks !

I have this entity, with some relations: (NB: only the relevant fields)

`

/**

  • @ORM\Entity(repositoryClass="App\Repository\PageRepository")
    / class Page { /*

    • @ORM\Id()
    • @ORM\GeneratedValue()
    • @ORM\Column(type="integer")
      */
      private $id;

    /**

    • @ORM\ManyToMany(targetEntity="App\Entity\Content", inversedBy="pages", cascade={"persist"})
      */
      private $contents;

    /**

    • Page constructor.
      */
      public function __construct()
      {
      $this->contents = new ArrayCollection();
      }

    /**

    • @return int|null
      */
      public function getId(): ?int
      {
      return $this->id;
      }

    /**

    • @param Content $content
    • @return $this
      */
      public function addContent(Content $content)
      {
      if (!$this->contents->contains($content)) {

      $this->contents[] = $content;
      

      }

      return $this;
      }

    /**

    • @param Content $content
    • @return $this
      */
      public function removeContent(Content $content)
      {
      if ($this->contents->contains($content)) {

       $this->contents->removeElement($content);
      

      }

      return $this;
      }

    /**

    • @return Collection
      */
      public function getContents(): Collection
      {
      return $this->contents;
      }
      }
      `

And here, the Content entity:

`/**

  • @ORM\Entity(repositoryClass="App\Repository\ContentRepository")
    / class Content { /*

    • @ORM\Id()
    • @ORM\GeneratedValue()
    • @ORM\Column(type="integer")
      */
      private $id;

    /**

    • @ORM\Column(type="text", nullable=true)
      */
      private $content;

    /**

    • @ORM\ManyToOne(targetEntity="App\Entity\BlockType")
    • @ORM\JoinColumn(nullable=true)
      */
      private $blockType;

    /**

    • @ORM\ManyToMany(targetEntity="App\Entity\Page", mappedBy="contents", cascade={"persist"})
      */
      private $pages;

    /**

    • Content constructor.
      */
      public function __construct()
      {
      $this->pages = new ArrayCollection();
      }

    /**

    • @return int|null
      */
      public function getId(): ?int
      {
      return $this->id;
      }

    /**

    • @return string|null
      */
      public function getContent(): ?string
      {
      return $this->content;
      }

    /**

    • @param string|null $content
    • @return $this
      */
      public function setContent(?string $content): self
      {
      $this->content = $content;

      return $this;
      }

    /**

    • @return BlockType|null
      */
      public function getBlockType(): ?BlockType
      {
      return $this->blockType;
      }

    /**

    • @param BlockType|null $blockType
    • @return $this
      */
      public function setBlockType(?BlockType $blockType): self
      {
      $this->blockType = $blockType;

      return $this;
      }

    /**

    • @return Collection|Page[]
      */
      public function getPages(): Collection
      {
      return $this->pages;
      }

    /**

    • @param Page $page
    • @return $this
      */
      public function addPage(Page $page): self
      {
      if (!$this->pages->contains($page)) {

       $this->pages[] = $page;
       $page->addContent($this);
      

      }

      return $this;
      }

    /**

    • @param Page $page
    • @return $this
      */
      public function removePage(Page $page): self
      {
      if ($this->pages->contains($page)) {

       $this->pages->removeElement($page);
       $page->removeContent($this);
      

      }

      return $this;
      }
      }
      `

<b>My goal is to provide a nice form, where the user can fill what he needs for a page:</b>

  • pages informations
  • content via a textarea

So > embedded form ;)

Here, my PageForm:

`class PageType extends AbstractType
{

/**
 * @param FormBuilderInterface $builder
 * @param array $options
 */
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('title')
        ->add('contents', CollectionType::class,
            [
                'label'         => 'Manage the content for this page',
                'entry_type'    => PageContentEmbedded::class,
                'allow_add'     => true,
                'allow_delete'  => true,
                'by_reference'  => false,
                'entry_options' => ['label' => false],
            ]
        )
    ;
}

/**
 * @param OptionsResolver $resolver
 */
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'data_class' => Page::class,
    ]);
}

}
`

And here my PageContentEmbedded:

`/**

  • Class PageContentEmbedded
  • @package App\Form
    / class PageContentEmbedded extends AbstractType { /*

    • @param FormBuilderInterface $builder
    • @param array $options
      */
      public function buildForm(FormBuilderInterface $builder, array $options)
      {
      /** @var Page|null $page */
      $page = $options['data'] ?? null;
      $isEdit = $page && $page->getId();

      $builder

       ->add('blockType')
       ->add('content', TextareaType::class);
      

    }

    /**

    • @param OptionsResolver $resolver
      */
      public function configureOptions(OptionsResolver $resolver)
      {
      $resolver->setDefaults(
       ['data_class' => Content::class]
      

      );
      }
      }
      `

It's working fine so tested and approved.

But.... I'm trying to go a little further:

<b>If the value of blockType == "textarea", I want to be able to change the FieldType for content to CkEditorType.</b>

I wanted to use the FormEvents for this, but I'm quite lost:

<b>> which event to chose ?

where should I place the event ? On the field ?</b>

I tried several things, but it's not really clear.

Can you guide ?

Thanks !

Reply

Hey Christina V.

Why you can't do that check in the build() method? I think you can drop that switch-case there and configure the blockType field depending on its content

Cheers!

Reply
Christina V. Avatar
Christina V. Avatar Christina V. | MolloKhan | posted 3 years ago | edited

Hi MolloKhan , and first for the answer.

Just to be clear, I don't need to configure the blocktype field.
I need to configure the content field, depending of the choice made in blocktype.

> if blockType value = text >> content is a classic text field.
> if blockType value = textarea >> content is a textare field (ckEditor).

Reply

Hey Christina V.

So my question applied to the content field. You could check for the value of blockType and then cofigure the content field. Something like this


public function buildForm(FormBuilderInterface $builder, array $options)
{
    $entity = $options['data'];
    if ($entity->getBlockType) === 'textarea') {
        // configure content field to work with ckEditor
    } else {
        // normal configuration for the content field
    }
}

Does it makes any sense to you?

Reply
Christina V. Avatar
Christina V. Avatar Christina V. | MolloKhan | posted 3 years ago | edited

MolloKhan totally make sense !
Before I could see your answer, I did something similar.

But thanks for your explanations ;)

1 Reply
Francesco P. Avatar
Francesco P. Avatar Francesco P. | posted 3 years ago

Hi!
Great learning material.
Is there a Symfony4 or 5 tutorial regarding the topic "embedding forms"? I found on in Symfony 3, but would be interested in a more recent version, if available.
Thanks!
Francesco

Reply

Hey Francesco,

Unfortunately, this tutorial about Symfony forms is the latest for now: https://symfonycasts.com/sc... . We've started working on Symfony 5 courses, but no course about Symfony Forms yet, but it will be released some day.

Btw, could you tell us what exactly topics you would like to see in that course? We will consider including them in the new tutorial! :)

Cheers!

Reply
Francesco P. Avatar
Francesco P. Avatar Francesco P. | Victor | posted 3 years ago

Hello Victor,

Thanks for the reply! I know already the Symfony4 tutorial about forms that you mention, but I was wondering if and where there is a part in it about specifically "embedding forms", which was available in the Symfony3 tutorial.

I am learning now how to use Symfony and find this part powerful but a little confusing.

Francesco

Reply

Hey Francesco,

I'll try to keep embedding form in mind for our upcoming Symfony 5 Forms tutorial. For now, the most resent tutorial about Symfony Forms is: https://symfonycasts.com/sc... - it's written in Symfony 4. If you haven't seen it yet - I'd recommend you to check it out. Otherwise, follow Symfony docs for more deep info about it: https://symfony.com/doc/cur...

I hope this helps! If you have more topics that you're interested in to see in the new Symfony 5 Forms course but haven't seen in Symfony 4 Forms one - please, let us know, and we will try to cover them too.

Cheers!

Reply
Evald N. Avatar
Evald N. Avatar Evald N. | posted 3 years ago

Hello. I have rendered a User Registration Form, but when the user tries to register with the same email,
it shows up an error "There is already an account with this email" . How can I customize this message,
so that it looks something different, for instance 'The email is taken!'.

Reply

Hi Evald N.

It's pretty easy, and you can find it in chapter 16 of this course, Here is a link to exact codeblock with message configuration
https://symfonycasts.com/sc...

Cheers!

Reply

Hi Ryan and Victor,
First of all, i'm a big fan of what you do, it's awesome.
I would like to know why the form folder (FormType) is been excluded from autowiring?

`

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
    resource: '../src/*'
    exclude: '../src/{DependencyInjection,<u><b>Form</b></u>,Entity,Migrations,Tests,Kernel.php}'

`

Is it bad practice to Inject Services to FormTypes?
Can I just unexclude Form?

Thank you, and have a good day

Reply

Hey Moulaye!

Hmm. I think this might just be excluded in your project. Because, you're 100% correct! It's totally ok (and I recommend it!) to autowire / use dependency injection in your form classes :).

In reality, when you start a new Symfony project, the Form directory is NOT excluded: https://github.com/symfony/recipes/blob/e76843f80e1927488cc27df9c3c4abc5b11d6bf2/symfony/framework-bundle/4.2/config/services.yaml#L18

So I think this was maybe just done in your project for some reason? And if so... it probably should not be excluded.

Cheers!

1 Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 3 years ago

How to debug when after handleRequest() some data becomes null? That data is in $_POST. Going inside handleRequest looks unproductive. Form is very big, posting here all form - not sure. Reducing the amount of fields for debugging, also looks like lot of work. I feel that somebody having more experience knows where to look for but I am out of ideas now. I have problem with symfony 3.4 but same principle probalby it is as with 4.

Later found out that I have made mistake by modifying request - $request->request->set('checkout', $postData['checkout']); (I needed to unset some data).
I passed $postData instead of $postData['checkout'] and while stepping inside handleRequest() method saw error that there is extra field 'checkout'.

Reply

Hey Lijana Z.

First you can debug your data through symfony web profiler there is a Form tab which allows you to see whole form data lifecycle and can help with debugging.

And about extra fields, can say exactly because I don't see your code, and probably you have a reason of modifying request directly, but for me is better to map all fields inside Form class to get forms work better.

PS also you can use some form listeners for modifying request data and form data. You can find some info in this chapter https://symfonycasts.com/sc...

Cheers.

1 Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | sadikoff | posted 3 years ago | edited

Hi, today I need to modify request data but I did not see in this video how to do it. I have tried
`
$builder->addEventListener(

        FormEvents::PRE_SET_DATA,
        function (FormEvent $event) {

            $data = $event->getData();
            if (!$data) {
                return;
            }}
    );

`
It calls this line to add a listener in the form where is this field but does not go into callback fuction, it just gives error "The form should not contain extra fields". Which code part exactly modifies the request data?

Reply

Hey Lijana Z.

IIRC it should be FormEvents::PRE_SUBMIT event, you can read more <a href="https://symfony.com/doc/current/form/events.html#registering-event-listeners-or-event-subscribers&quot;&gt;here&lt;/a&gt;

Cheers!

Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 3 years ago

This should teach me a lot for symfony 3.x, because they are not that different, right? I am working with 3.x and might need to improve my form skills. But also plan is to go to symfony 4 in the future so by watching this I would like to learn for both.

Reply

Hey Coder,

Yeah, it should be not too much different. Though I'd recommend you to watch 3.x specific course about forms first, here it is: https://symfonycasts.com/sc... . And then before upgrading your app to Symfony 4.x watch this one as well. This way it would be easier to notice some BC breaks, and also discover new features that was added in 4.x. But yeah, if you want to watch only one course - better to watch this one :)

Also, we have a few Symfony Form related courses that are based on Symfony 3.x but you might be interested in it as well, here there are:

https://symfonycasts.com/sc...

https://symfonycasts.com/sc...

I hope this helps!

Cheers!

1 Reply
Anton B. Avatar
Anton B. Avatar Anton B. | posted 3 years ago

Hey guys,
This is becomes really important to cover Google's Recaptcha. Thank you in advance

Reply

Hey Anton!

Thank you for this idea, we will add it to our ideas pool for the future screencasts.

Cheers!

Reply
Oleksandr K. Avatar
Oleksandr K. Avatar Oleksandr K. | posted 4 years ago

Hi. This course is amazing!
I would like to ask you a question about Dynamic TypeChoice fields. When I have only 2 TypeChoice fields it works no problem. Select one will update second one. But when I want to use 3 TypeChoice fields and each depend on what was selected in previous one It is not working and end up with Violation exception in field 3.
I am adding POST_SUBMIT event listeners to first field and second field and PRE_SET_DATA to the form to update second field and third field. But when press submit button on a form PRE_SET_DATA event is triggered and will remove second field and POST_SUBMIT event listener for second field as well. Then POST_SUBMIT event is triggered on first field and will add second field but adding POST_SUBMIT event listener for second field is too late as Form system started iterating through registered listeners already and this new added will be ignored. So field number 3 will not be updated with new value and throws Violation Exception.

How this problem can be sorted?
Any suggestions are appreciated.
Thank you
Alex

Reply

Hey Oleksandr K.

That's advanced ;) To be honest I'm not sure how to actually do it using Symfony Form component, probably you would be better doing it with another tool (Maybe ReactJS?), you can handle all the logic for updating input fields in your frontend, and on submit, you can rely on Symfony Validation component so you don't miss validations

Cheers!

Reply
Oleksandr K. Avatar
Oleksandr K. Avatar Oleksandr K. | MolloKhan | posted 4 years ago

Thank you @Diego Aguiar
But I would like to do it on a server and I am not friendly with any java script frameworks

Reply
Default user avatar

Personally bypassed that issue using manual resetting properties of underlaying model data object that is then passed to the form, in controller action processing post request, before creating and initializing of the form, with new values from request. Meaning just fields (their values) that affect others. But that is very "unclean" resolution and hoped to find here instructions to solve it correctly and using appropriate Symfony's resources.

Reply
MolloKhan Avatar MolloKhan | SFCASTS | Ben | posted 4 years ago | edited

This is the answer Ben is talking about: https://symfonycasts.com/sc...

Reply
Oleksandr K. Avatar
Oleksandr K. Avatar Oleksandr K. | Ben | posted 4 years ago | edited

Hi @Ben.
Look the previous question from Alex. By mistake I asked two the same questions. Ben found the wounderful solution.

Reply
Default user avatar

Hi Alex, thanks for notification, I checked full thread of question and replied there.

Reply
Default user avatar

Hi. The course is amazing.
I have a question about Dynamic ChoiceType choices. It works with two dropdowns with no problem. You select element in one and next dropdown will have all choices related to element was selected in dropdown one. But imagine that you have 3 dropdowns and each next related to what you selected before.
I am trying to add PRE_SUBMIT event listener to 2 previous dropdowns and not to the last. When PRE_SET_DATA event is triggered after pressing submit on a form It will remove the second dropdown and of course the PRE_SUBMIT event listener for this dropdown will be removed as well. When the PRE_SUBMIT will be triggered on the first dropdown and will add second dropdown but adding PRE_SUBMIT at this point to this dropdown number 2 does not make sense as Form system already iterating through the list of registered listeners and will ignore it. So dropdown number 3 will have always Violation Exception.

How to deal with problem? What be the easiest way?

Thank you

Alex

Reply

Hey Alex,

Hm, it's similar to the https://symfonycasts.com/sc... - did you follow that thread?

Fairly speaking, I've never try this before, but I think 3 dropdowns should work the same! So, you just add one listener for PRE_SET_DATA as we do and in anon function at first call setupSpecificLocationNameField(), and after call another method that depends on specificLocationNameField, where in the last you do all the check base on the specificLocationNameField. Did you try this?

I hope this helps.

Cheers!

Reply
Oleksandr K. Avatar
Oleksandr K. Avatar Oleksandr K. | Victor | posted 4 years ago | edited

Hi victor
Thank you for your reply. I have followed your thread. And it works with two fields no problem. I am fighting with this problem for good few weeks and cannot get working. POST_SUBMIT event listener on first and second field should do all the magic. But if you remove field to pass validation and them add field the POST_SUBMIT listener is removed and cannot be added.

I am adding POST_SUBMIT event listeners to first field and second field and PRE_SET_DATA to the form to update second field and third field. But when press submit button on a form PRE_SET_DATA event is triggered and will remove second field and POST_SUBMIT event listener for second field as well. Then POST_SUBMIT event is triggered on first field and will add second field but adding POST_SUBMIT event listener for second field is too late as Form system started iterating through registered listeners already and this new added will be ignored. So field number 3 will not be updated with new value and throws Violation Exception

I hope i am wrong and there is simple solution. Thank you.
Alex

Reply

Hey Alex,

Here's an example I made for you:


namespace App\Form;

use App\Entity\Article;
use App\Entity\User;
use App\Repository\ArticleRepository;
use App\Repository\UserRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ArticleFormType extends AbstractType
{
    private $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        /** @var Article|null $article */
        $article = $options['data'] ?? null;
        $isEdit = $article && $article->getId();

        $builder
            ->add('title', TextType::class, [
                'help' => 'Choose something catchy!'
            ])
            ->add('content', null, [
                'rows' => 15
            ])
            ->add('author', UserSelectTextType::class, [
                'disabled' => $isEdit
            ])
            ->add('location', ChoiceType::class, [
                'placeholder' => 'Choose a location',
                'choices' => [
                    'The Solar System' => 'solar_system',
                    'Near a star' => 'star',
                    'Interstellar Space' => 'interstellar_space'
                ],
                'required' => false,
            ])
        ;

        if ($options['include_published_at']) {
            $builder->add('publishedAt', null, [
                'widget' => 'single_text',
            ]);
        }

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) {
                /** @var Article|null $data */
                $data = $event->getData();
                if (!$data) {
                    return;
                }

                $this->setupSpecificLocationNameField(
                    $event->getForm(),
                    $data->getLocation()
                );

                $this->setupSpecificLocationNameField2(
                    $event->getForm(),
                    $data->getSpecificLocationName()
                );
            }
        );

        $builder->get('location')->addEventListener(
            FormEvents::POST_SUBMIT,
            function(FormEvent $event) {
                $form = $event->getForm();
                $this->setupSpecificLocationNameField(
                    $form->getParent(),
                    $form->getData()
                );
            }
        );
        if ($builder->has('specificLocationName')) {
            $builder->get('specificLocationName')->addEventListener(
                FormEvents::POST_SUBMIT,
                function(FormEvent $event) {
                    $form = $event->getForm();
                    $this->setupSpecificLocationNameField2(
                        $form->getParent(),
                        $form->getData()
                    );
                }
            );
        }
    }

    private function setupSpecificLocationNameField(FormInterface $form, ?string $location)
    {
        if (null === $location) {
            $form->remove('specificLocationName');
            $form->remove('specificLocationName2');

            return;
        }

        $choices = $this->getLocationNameChoices($location);

        if (null === $choices) {
            $form->remove('specificLocationName');
            $form->remove('specificLocationName2');

            return;
        }

        $form->add('specificLocationName', ChoiceType::class, [
            'placeholder' => 'Where exactly?',
            'choices' => $choices,
            'required' => false,
        ]);
    }

    private function setupSpecificLocationNameField2(FormInterface $form, ?string $specificLocation)
    {
        if (null === $specificLocation) {
            $form->remove('specificLocationName2');

            return;
        }

        $choices = $this->getLocationNameChoices2($specificLocation);

        if (null === $choices) {
            $form->remove('specificLocationName2');

            return;
        }

        $form->add('specificLocationName2', ChoiceType::class, [
            'placeholder' => 'Where exactly 2?',
            'choices' => $choices,
            'required' => false,
        ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Article::class,
            'include_published_at' => false,
        ]);
    }

    private function getLocationNameChoices(string $location)
    {
        $planets = [
            'Mercury',
            'Venus',
            'Earth',
            'Mars',
            'Jupiter',
            'Saturn',
            'Uranus',
            'Neptune',
        ];

        $stars = [
            'Polaris',
            'Sirius',
            'Alpha Centauari A',
            'Alpha Centauari B',
            'Betelgeuse',
            'Rigel',
            'Other'
        ];

        $locationNameChoices = [
            'solar_system' => array_combine($planets, $planets),
            'star' => array_combine($stars, $stars),
            'interstellar_space' => null,
        ];

        return $locationNameChoices[$location] ?? null;
    }

    private function getLocationNameChoices2(string $specificLocation)
    {
        $locationNameChoices = [
            'Earth' => [
                'The USA' => 'The USA',
                'Ukraine' => 'Ukraine',
                'Mexico' => 'Mexico',
                'Moldova' => 'Moldova',
            ],
        ];

        return $locationNameChoices[$specificLocation] ?? null;
    }
}

You need to add a new field Article::$specificLocationName2 with getter and setter. And render this field in your template like:


    {# ... #}
    {{ form_row(articleForm.location) }}
    {%  if articleForm.specificLocationName is defined %}
        {{ form_row(articleForm.specificLocationName) }}
    {% endif %}
    {%  if articleForm.specificLocationName2 is defined %}
        {{ form_row(articleForm.specificLocationName2) }}
    {% endif %}
    {# ... #}

I hope this helps, at least you can start with it. Good luck!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

userVoice