If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeAlright, here's the issue and it is super technical. If we change the Location from "Near a Star" to "Solar System", even if we "hack" the specificLocationName
field so that it submits the value "Earth", it doesn't work! It fails validation!
This is a real problem, because, in a few minutes, we're going to add JavaScript to the page so that when we change location
to "The Solar System", it will dynamically update the specificLocationName
dropdown down to be the list of planets. But for that to work, our form system needs to be smart enough to realize - at the moment we're submitting - that the location has changed. And then, before it validates the ChoiceType
, it needs to change the choices to be the list of planets.
Don't worry if this doesn't make complete sense yet - let's see some code!
There's one piece of the form system that we haven't talked about yet: it has an event system, which we can use to hook into the form loading & submitting process.
At the end of the form, add $builder->get('location')->addEventListener()
and pass this FormEvents::POST_SUBMIT
. This FormEvents
class holds a constant for each "event" that we can hook into for the form system. Pass a callback as a second argument: Symfony will pass that a FormEvent
object.
Let's dd()
the $event
so we can see what it looks like.
... lines 1 - 26 | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
... lines 29 - 68 | |
$builder->get('location')->addEventListener( | |
FormEvents::POST_SUBMIT, | |
function(FormEvent $event) { | |
dd($event); | |
} | |
); | |
} | |
... lines 76 - 117 |
But before we check it out, two important things. First, when you build a form, it's actually a big form tree. We've seen this inside of the form profiler. There's a Form
object on top and then each individual field below is itself a full Form
object. The same is true with the "form builder": we normally just interact with the top-level $builder
by adding fields to it. When we call $builder->add()
, that creates another "form builder" object for that field, and you can fetch it later by saying $builder->get()
.
Second, we're attaching the event to only the location field - not the entire form. So, when the form submits, Symfony will call this function, but the $event
object will only have information about the location
field - not the entire form.
Let's actually see this! Refresh to re-submit the form. There it is! The FormEvent
contains the raw, submitted data - the solar_system
string - and the entire Form
object for this one field.
This gives us the hook we need: we can use the submitted data to dynamically change the specificLocationName
field to use the correct choices, right before validation occurs. Actually, this hook happens after validation - but we'll use a trick where we remove and re-add the field, to get around this.
To start, create a new private function
called setupSpecificLocationNameField()
. The job of this function will be to dynamically add the specificLocationName
field with the correct choices. It will accept a FormInterface
- we'll talk about that in a minute - and a ?string $location
, the ?
part so this can be null
.
... lines 1 - 81 | |
private function setupSpecificLocationNameField(FormInterface $form, ?string $location) | |
{ | |
... lines 84 - 102 | |
} | |
... lines 104 - 145 |
Inside, first check if $location
is null
. If it is, take the $form
object and actually ->remove()
the specificLocationName
field and return
. Here's the idea: if when I originally rendered the form there was a location set, then, thanks to our logic in buildForm()
, there will be a specificLocationName
field. But if we changed it to "Choose a location", meaning we are not selecting a location, then we want to remove the specificLocationName
field before we do any validation. We're kind of trying to do the same thing in here that our future JavaScript will do instantly on the frontend: when we change to "Choose a location" - we will want the field to disappear.
... lines 1 - 81 | |
private function setupSpecificLocationNameField(FormInterface $form, ?string $location) | |
{ | |
if (null === $location) { | |
$form->remove('specificLocationName'); | |
return; | |
} | |
... lines 89 - 102 | |
} | |
... lines 104 - 145 |
Next, get the $choices
by using $this->getLocationNameChoices()
and pass that $location
. Then, similar to above, if (null === $choices)
remove the field and return. This is needed for when the user selects "Interstellar Space": that doesn't have any specific location name choices, and so we don't want that field at all.
... lines 1 - 81 | |
private function setupSpecificLocationNameField(FormInterface $form, ?string $location) | |
{ | |
... lines 84 - 89 | |
$choices = $this->getLocationNameChoices($location); | |
if (null === $choices) { | |
$form->remove('specificLocationName'); | |
return; | |
} | |
... lines 97 - 102 | |
} | |
... lines 104 - 145 |
Finally, we do want the specificLocationName
field, but we want to use our new choices. Scroll up and copy the $builder->add()
section for this field, paste down here, and change $builder
to $form
- these two objects have an identical add()
method. For choices
pass $choices
.
... lines 1 - 81 | |
private function setupSpecificLocationNameField(FormInterface $form, ?string $location) | |
{ | |
... lines 84 - 97 | |
$form->add('specificLocationName', ChoiceType::class, [ | |
'placeholder' => 'Where exactly?', | |
'choices' => $choices, | |
'required' => false, | |
]); | |
} | |
... lines 104 - 145 |
Nice! We created this new function so that we can call it from inside of our listener callback. Start with $form = $event->getForm()
: that gives us the actual Form
object for this one field. Now call $this->setupSpecificLocationNameField()
and, for the first argument, pass it $form->getParent()
.
... lines 1 - 27 | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
... lines 30 - 69 | |
$builder->get('location')->addEventListener( | |
FormEvents::POST_SUBMIT, | |
function(FormEvent $event) { | |
$form = $event->getForm(); | |
$this->setupSpecificLocationNameField( | |
$form->getParent(), | |
... line 76 | |
); | |
} | |
); | |
} | |
... lines 81 - 145 |
This is tricky. The $form
variable is the Form object that represents just the location
field. But we want to pass the top level Form
object into the function so that the specificLocationName
field can be added or removed from it.
The second argument is the location
itself, which will be $form->getData()
, or $event->getData()
.
... lines 1 - 73 | |
$this->setupSpecificLocationNameField( | |
$form->getParent(), | |
$form->getData() | |
); | |
... lines 78 - 145 |
Okay guys, I know this is craziness, but we're ready to try it! Refresh to resubmit the form. It saves. Now change the Location to "Near a Star". In a few minutes, our JavaScript will reload the specificLocationName
field with the new options. To fake that, inspect the element. Let's go copy a real star name - how about Sirius
. Change the selected option's value to that string.
Hit update! Yes! It saved! We were able to change both the location
and specificLocationName
fields at the same time.
And that means that we're ready to swap out the field dynamically with JavaScript. But first, we're going to leverage another form event to remove some duplication from our form class.
Hey Jan,
Good job! Thanks for sharing this little example with other users! I think it might be useful for someone.
Cheers!
I am trying to change the value of a given field when a form is submitted. The following example represents what I'm looking for with a PRE_SUBMIT Event:
`public function onPreSubmit(FormEvent $event) {
$formData = $event->getData();
$formData["emailGroups"] = "example@example.com";
$event->setData($formData);
}`
I get the correct value with $event->getData(); But when I try to set the new value it is set null.
Any idea or is this the correct way to update the data sent by the user?
Hey @isGzmn,
the code looks correct, here is my example from test code I created to test this solution
class TestEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email')
->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'preSubmit']);
;
}
public function preSubmit(FormEvent $event) {
$data = $event->getData();
$data['email'] = 'email@dd.d';
$event->setData($data);
}
//....
}
and here in my controller action
public function new(Request $request, TestEntityRepository $testEntityRepository): Response
{
$testEntity = new TestEntity();
$form = $this->createForm(TestEntityType::class, $testEntity);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dd($testEntity); // here we have updated value
// ...
}
return $this->renderForm('test_entity/new.html.twig', [
'test_entity' => $testEntity,
'form' => $form,
]);
}
Cheers!
Hello,
I have an issue on a project. I have three entities (Country, City, Neighborhood).
Country has many cities and Cities is related to one Country. Same thing, City has many neighborhoods and Neghborhood is related to one City.
I created a form with Country Entity which get 3 fields (Choose Country, choose City, choose Neighborhood)
When i choose a Country, i can get his cities, that's works. But i can't do the same things between City and neighborhoods.
My goal is, when i choose a city, all his neighborhoods displayed.
This is my form :
$builder
->add('name', EntityType::class, [
'class' => Country::class,
'placeholder' => '',
'label' => 'Votre pays'
])
->add('cities', ChoiceType::class, [
'placeholder' => 'Villes (Choisir un pays)',
'label' => 'Votre ville'
])
->add('neighborhoodname', ChoiceType::class, [
'placeholder' => 'Quartier (Choisir une ville)',
'label' => 'Votre quartier',
'mapped' => false
])
->add('Valider', SubmitType::class)
;
$formModifier = function(FormInterface $form, Country $country = null) {
$cities = null === $country ? [] : $country->getCities();
$form->add('cities', EntityType::class, [
'class' => City::class,
'choices' => $cities,
'choice_label' => 'name',
'placeholder' => 'Villes (Choisir un pays)',
'label' => 'Votre ville',
'mapped' => false
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data);
}
);
$builder->get('name')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$country = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $country);
}
);
Thanks a lot and sorry for my english.
Hi ZakiZak!
Hmm. In theory, this should not be any more difficult, but I admit that I have not tried this before! So it could have some hidden surprises. What I would try first is pretty simple: duplicate $formModifier
, name it $formModifierNeighborhoods
and then update it to handle the city->neighborhood situation
$formModifier = function(FormInterface $form, City $city = null) {
$neighborhoods = null === $city ? [] : $city->getNeighborhoods();
$form->add('neighboorhoodname', EntityType::class, [
'class' => Neighborhood::class,
'choices' => $neighborhoods,
// ...
]);
};
Now we need to make sure this is included in our 2 listeners. First, inside the PRE_SET_DATA
listener, just call both modifiers. Note, I think there was a small bug in this part of the code you posted:
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
// I tweaked these next 2 lines - this was the mistake I think I saw
// it looked like you were passing the WHOLE data in, not just the Country
$country = $data->getName();
$formModifier($event->getForm(), $country);
// new stuff here!
$city = $data->getCities();
$formModifierNeighborhoods($event->getForm(), $city);
}
);
Finally, for the POST_SUBMIT
listener, duplicate that, change name
to cities
then call the new modifier function:
$builder->get('cities')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$city = $event->getForm()->getData();
$formModifierNeighborhoods($event->getForm()->getParent(), $city);
}
);
Let me know if that helps! Btw, the code was a bit weird because the field is called cities
but the user is only selecting one city. But hopefully the solution is clear. Let me know if it works!
Cheers!
Hi weaverryan,
Thank you for your help. I tried to do what you said but i have this error message :
App\Form\CountryType::App\Form\{closure}(): Argument #2 ($city) must be of type ?App\Entity\City, Doctrine\Common\Collections\ArrayCollection given, called in C:\xampp\htdocs\FormEvents\formEvents\src\Form\CountryType.php
I added $formModifierNeighborhoods
$formModifierNeighborhoods = function(FormInterface $form, City $city = null) {
$neighborhoods = null === $city ? [] : $city->getNeighborhoods();
$form->add('cities', EntityType::class, [
'class' => Neighborhood::class,
'choices' => $neighborhoods,
'choice_label' => 'name',
// ...
]);
};
The PRE_SET_DATA
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier, $formModifierNeighborhoods) {
$data = $event->getData();
$country = $data->getName();
$formModifier($event->getForm(), $country);
$city = $data->getCities();
$formModifierNeighborhoods($event->getForm(), $city);
}
);
The POST_SUBMIT
$builder->get('cities')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifierNeighborhoods) {
$city = $event->getForm()->getData();
$formModifierNeighborhoods($event->getForm()->getParent(), $city);
}
);
I don't know why it works for $country second argument but not for $city second argument.
Note : You are right. I will have change cities
field to city
Hey ZakiZak!
Hmm. It seems like $data->getCities();
is returning a collection of City
objects, instead of just one City
object. On your top-level entity (whatever entity is "bound" to this form), does it relate to just ONE City
or many City
objects? It seems like, currently, it relates to MANY objects... but then in the form the user is selecting just one City
object. Is that on purpose? Do you need to, perhaps, change the property on your top level entity from $cities
to $city
and change it to a singular relationship (e.g. from ManyToMany
to ManyToOne
)?
Cheers!
Hi weaverryan,
Thank you for your reply. I have only ManyToOne relationship between all my entities (Country, City, Neighborhood).
Country
entity has many Cities
& cities is attached to one Country (OneToMany)City
entity has many neighborhoods & neighborhoods
is attached to one City (OneToMany)
The entity that is bound to this form is Country
(CountryType) and his properties are the following :
#[ORM\Column(length: 150)]
private ?string $name = null;
#[ORM\OneToMany(mappedBy: 'country', targetEntity: City::class)]
private Collection $cities;
For City :
#[ORM\Column(length: 150)]
private ?string $name = null;
#[ORM\ManyToOne(inversedBy: 'cities')]
private ?Country $country = null;
#[ORM\OneToMany(mappedBy: 'city', targetEntity: Neighborhood::class, orphanRemoval: true)]
private Collection $neighborhoods;
For Neighborhood :
#[ORM\Column(length: 150)]
private ?string $name = null;
#[ORM\ManyToOne(inversedBy: 'neighborhoods')]
#[ORM\JoinColumn(nullable: false)]
private ?City $city = null;
Hey ZakiZak!
Ah, this helps! But I have one question:
The entity that is bound to this form is Country(CountryType) and his properties are the following :
This confused me, because you mentioned your form looked like this:
->add('name', EntityType::class, [
'class' => Country::class,
'placeholder' => '',
'label' => 'Votre pays'
])
Having a name
field in your form makes sense, because Country
has a name
property. But it doesn't make sense for this to be an EntityType
... because the name
is a string. Something isn't quite making sense here...
The error from earlier is somewhat related to this question:
App\Form\CountryType::App\Form{closure}(): Argument #2 ($city) must be of type ?App\Entity\City, Doctrine\Common\Collections\ArrayCollection given, called in C:\xampp\htdocs\FormEvents\formEvents\src\Form\CountryType.php
Currently, your form is set up so that the user is selecting ONE city. The problem is that, if your form truly is bound to the Country
entity, then when the form is initializing, we want to prefill the cities
property with the ONE City
that is related to that Country
. But each Country
relates to Many cities... not just one. So, something here isn't making sense.
This leaves me with two main questions:
A) In the form, is the user truly meant to select just one city? Or do you actually intend for them to select multiple?
B) And, if you DO want the form to allow selecting just one city, is it possible that the form should not be bound to the Country
entity? What I mean is: maybe your form is not meant to edit/create a Country
but is used in some other way? What will you do with this data after a successful form submit?
Cheers!
Hi Weaverryan !
You're true. I have chosen Entity on this field because i didn't know how can i access to all countries with ChoiceType. ($country=>getName()
). It works with EntityType but if there are others solutions i take it.
A) The user select one City among some cities
B) Yes the form should not be bound to the Country. My goal is to create a user profile with those informations like :
Welcome username
, from city
(country
) and your neighborhood is (neighborhood
).
This project is essentially to learn form events.
I don't know if i reply to your question
Thank you !
Hey.
I'm trying to do something pretty difficult (for me) with Form Component. I haven't found a solution yet so I'm sending an SOS here. I hope my explaination will be clear:
I have 2 entities with ManyToOne relation: Task and Tech. Of course, each technician could have many tasks...
In TaskType, I want to choose a technician AND if needed directly update some data about them (address, phone number). So I'm using a custom TechType and I did persist cascade from Task to Tech (and even @Assert\Valid on tech property of Task). IMO, everything is okay here.
In TechType, in addition to the classic fields (address, phone...), I have a special "name" field (TextType with autocomplete) used to CHOOSE the technician, not to update the name. After selecting a technician in the dropdown list, I'm reloading the TechType part with Ajax. Then, if needed, I can update some data in other fields.
I did many things (relatively) deep inside Form Component to create this functionality: EventListeners, DataTransformers, Underlying data... Everything seems to be OK.
BUT when submitting the form, I have an unexpected behavior: the technician is not updated but a new one is created. As I have a UniqueEntity(name) for Tech, I have a validation error "This name is already used for another technician". It seems like Doctrine is tracking a specific Tech and can't replace it with another one loaded by Ajax.
Did anyone have already done such a thing? Is there a Doctrine ID for tracking (I saw #id while dumping data to compare) ? Or maybe there's another way to do this ? If someone have an idea, it will be helpful!
Thanks a lot.
Cyril, from France.
Hey Cyril S.!
Oh boy - form events. This stuff is hard for me too :p. But wow, you've already accomplished a ton! Nice work!
I may not have the correct or best solution, bit maybe I can point you in the right direction - you seem pretty capable of figuring stuff out ;).
When you ultimately submit the form (after all the ajax fanciness), you are submitting some "task" data and also some "task[tech]" data for the technician. But, i believe that you are not actually sending the technician "id" anywhere in the POST data. I could totally be wrong about that part - and I hope I am. Somehow you DO need to communicate the "tech id" in the POST data somewhere. It could even be a random hidden input field that is not even part of your form. Because... in your controller, when you create the new Task()
(or maybe you let the form create this object automatically - if you do, please start creating it manually), you can THEN read that POST'ed tech id value, query for the Tech object, and call $task->setTech($tech)
. That, I believe, will fix your problem :). Ultimately, when the form starts processing, if the Task
object does not have a Tech
object on it, then the TechType will automatically create one for you, which is why you're ultimately hitting this issue.
Let me know if this helps you solve it :).
Cheers!
Yes! It worked! You're the King, Ryan!
Thank you for your help and, once again, for your great tutorials. I'm looking forward to the next one on "Turbo".
Cyril
Hey. Why do we use the POST_SUBMIT event ? And not PRE_SUBMIT? I mean, would it not be better, to do the change before validation?
Hey Michael B.!
That's an excellent question :). So... the true answer is... I don't know. Or, at least, I don't remember. This stuff is SO complex (one of my least favorite parts of Symfony) and I can't remember all the effects of the events. But, it's definitely worth a try. You would need to get the submitting data in a different way (it's passed to the event), but that shouldn't be a problem. There is also a SUBMIT event as well, which is in the middle.
And, about validation, I can't remember if that was something I had checked into or not. As I'm guessing you already know, validation is performed on a listener to POST_SUBMIT. But is that called before our listener or after? I don't remember. As it's written, but have a priority of 0. Anyways, if your code *were* being called after validation, we could change the priority on our event listener to guarantee it comes first.
So... that's a bunch of wishy-washy answers :P. But I hope it helps.
Cheers!
Hey weaverryan .
Yes, the form component is and always has been one of the most complicated things about Symfony.
In the last year I did a lot with the JAM stack and dealt with Laravel (wrote 2 packages). At Laravel in particular, it was the first thing that struck me - how super easy it is to validate forms there. Totally painless.
On the other hand, one of Symfony's strengths is that everything is a piece and everything interlocks. I have also implemented very complex forms with Symfony. Timetables for schools, for example, everything is possible if you work your way into it.
Anyway. I think all the other technologies are cool, but now, having looked elsewhere, I come back to Symfony and will stay. You have to choose. I've been around too long and there are many areas where I just like Symfony a lot better.
I would also like to write more about Symfony in the future and help spread the framework. Because I have the feeling that it doesn't look good for the future if nobody does that. The Laravel community is much more active. And new developers are more likely to use Laravel at the moment, because it is simply much more present.
Hi,
I was coding my custom application along with your videos. I wanted to clean up my PersonFormType so I transfered EventListeners into separate "Listener" file. (like the ones from ArticleFormType in you videos)
Now I am getting this error which I cannot resolve. Can you help?
Code is exactly the same as in you videos.
Argument 1 passed to App\EventSubscriber\NumberTypeByCitizenshipSubscriber::setupNumberField() must implement interface Symfony\Component\Form\FormInterface, null given, called in C:\WAMP\www\blablabla\src\EventSubscriber\NumberTypeByCitizenshipSubscriber.php on line 43
PersonFormType.php
https://pastebin.com/W0krQjyG
NumberTypeByCitizenshipSubscriber.php
https://pastebin.com/gCcfKbXw
Thank you!
Hey SentinelBL,
It looks like that somewhere in your code where you call that "setupNumberField()" method you passes a null instead of a form object. Why? That's the correct question! Please, try to debug your code, basically you can add "dd($form)" before calling that setupNumberField() in all places and see in which cases it's null. It should give you some clue about what's going on.
I hope this helps!
Cheers!
Hi,
it's possible to do that for 3 feld?
Example: I had Country, City and Partner
City depends on Country and Partner depends on City
Hey aosow!
I don't see why not! You will just need to add a new event listener to this new dynamic form field and repeat the process feeding into your City field. Let us know if you find any unforeseen difficulties in doing this!
<?php
namespace App\Form;
use App\Entity\City;
use App\Entity\Country;
use App\Entity\Partner;
use App\Entity\Recipient;
use App\Entity\Transfert;
use App\Repository\CityRepository;
Use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Intl\Countries;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
class TransfertType extends AbstractType
{
private $cityRepo;
public function __construct(CityRepository $cityRepo)
{
$this->cityRepo = $cityRepo;
}
/**
* Configuration de base d'un champ (cc: champ configuration)
*
*
*/
private function cc($label, $placeholder, $class, $options = [])
{
return array_merge([
'label' => $label,
'attr' => [
'placeholder' => $placeholder,
'class' => $class
]
], $options);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('amount', TextType::class, $this->cc("Montant à envoyer *", "", ""))
->add('withdrawalAmount', TextType::class, $this->cc("Montant à recevoir *", "", ""))
->add('fees', TextType::class, $this->cc("Frais *", "", ""))
->add('total', TextType::class, $this->cc("Total à payer *", "", ""))
->add('paymentCurrency', ChoiceType::class, [
'choices' => [
'EUR' => 'EUR',
'USD' => 'USD',
'CAD' => 'CAD',
'XOF' => 'XOF'
],
'label' => 'Monnaie depart *'
])
->add('recipient', EntityType::class, [
'class' => Recipient::class,
'choice_label' => function ($recipient) {
return $recipient->getFirstname() . ' ' . $recipient->getLastname();
},
'label' => 'Destinataire',
'placeholder' => 'Selectionner un bénéficiare',
])
->add('withdrawalType', ChoiceType::class, [
'choices' => [
'Retrait en espèces' => 'Retrait en espèces',
'Virement bancaire' => 'Virement bancaire',
'MTN Mobile Money' => 'MTN Mobile Money',
'Orange Money' => 'Orange Money'
],
'label' => 'Type de retrait*',
])
->add('country', EntityType::class, [
'class' => Country::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->where('c.isAvailable = :val')
->setParameter('val', true)
->orderBy('c.id', 'ASC');
},
'choice_label' => function ($country) {
return Countries::getName($country->getName(), 'fr');
},
'label' => 'Pays d\'établissement',
'placeholder' => 'Selectionner un pays',
])
;
$this->handleCity($builder);
$this->handlePartner($builder);
}
private function handleCity(FormBuilderInterface $builder)
{
$formAddCity = function (FormInterface $form, Country $country = null) {
$cities = null === $country ? [] : $country->getCities();
$form->add('city', EntityType::class, [
'class' => 'App\Entity\City',
'placeholder' => 'Selectionner la ville',
'choices' => $cities,
'choice_label' => 'name',
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formAddCity) {
$transfert = $event->getData();
$formAddCity($event->getForm(), $transfert->getCountry());
}
);
$builder->get('country')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formAddCity) {
$country = $event->getForm()->getData();
$formAddCity($event->getForm()->getParent(), $country);
}
);
}
private function handlePartner(FormBuilderInterface $builder)
{
$formAddPartner = function (FormInterface $form, City $city = null) {
$partners = null === $city ? [] : $city->getPartners();
$form->add('partner', EntityType::class, [
'class' => 'App\Entity\Partner',
'placeholder' => 'Selectionner un point retrait',
'choices' => $partners,
'choice_label' => 'comercialMane',
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formAddPartner) {
$transfert = $event->getData();
$formAddPartner($event->getForm(), $transfert->getCity());
}
);
$builder->get('city')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formAddPartner) {
$city = $event->getForm()->getData();
$formAddPartner($event->getForm()->getParent(), $city);
}
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Transfert::class,
]);
}
}
Hi this is what i tried to do but it doesn't work
cityRepo = $cityRepo;
}
/**
* Configuration de base d'un champ (cc: champ configuration)
*
*
*/
private function cc($label, $placeholder, $class, $options = [])
{
return array_merge([
'label' => $label,
'attr' => [
'placeholder' => $placeholder,
'class' => $class
]
], $options);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('amount', TextType::class, $this->cc("Montant à envoyer *", "", ""))
->add('withdrawalAmount', TextType::class, $this->cc("Montant à recevoir *", "", ""))
->add('fees', TextType::class, $this->cc("Frais *", "", ""))
->add('total', TextType::class, $this->cc("Total à payer *", "", ""))
->add('paymentCurrency', ChoiceType::class, [
'choices' => [
'EUR' => 'EUR',
'USD' => 'USD',
'CAD' => 'CAD',
'XOF' => 'XOF'
],
'label' => 'Monnaie depart *'
])
->add('recipient', EntityType::class, [
'class' => Recipient::class,
'choice_label' => function ($recipient) {
return $recipient->getFirstname() . ' ' . $recipient->getLastname();
},
'label' => 'Destinataire',
'placeholder' => 'Selectionner un bénéficiare',
])
->add('withdrawalType', ChoiceType::class, [
'choices' => [
'Retrait en espèces' => 'Retrait en espèces',
'Virement bancaire' => 'Virement bancaire',
'MTN Mobile Money' => 'MTN Mobile Money',
'Orange Money' => 'Orange Money'
],
'label' => 'Type de retrait*',
])
->add('country', EntityType::class, [
'class' => Country::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->where('c.isAvailable = :val')
->setParameter('val', true)
->orderBy('c.id', 'ASC');
},
'choice_label' => function ($country) {
return Countries::getName($country->getName(), 'fr');
},
'label' => 'Pays d\'établissement',
'placeholder' => 'Selectionner un pays',
])
;
$this->handleCity($builder);
$this->handlePartner($builder);
}
private function handleCity(FormBuilderInterface $builder)
{
$formAddCity = function (FormInterface $form, Country $country = null) {
$cities = null === $country ? [] : $country->getCities();
$form->add('city', EntityType::class, [
'class' => 'App\Entity\City',
'placeholder' => 'Selectionner la ville',
'choices' => $cities,
'choice_label' => 'name',
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formAddCity) {
$transfert = $event->getData();
$formAddCity($event->getForm(), $transfert->getCountry());
}
);
$builder->get('country')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formAddCity) {
$country = $event->getForm()->getData();
$formAddCity($event->getForm()->getParent(), $country);
}
);
}
private function handlePartner(FormBuilderInterface $builder)
{
$formAddPartner = function (FormInterface $form, City $city = null) {
$partners = null === $city ? [] : $city->getPartners();
$form->add('partner', EntityType::class, [
'class' => 'App\Entity\Partner',
'placeholder' => 'Selectionner un point retrait',
'choices' => $partners,
'choice_label' => 'comercialMane',
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formAddPartner) {
$transfert = $event->getData();
$formAddPartner($event->getForm(), $transfert->getCity());
}
);
$builder->get('city')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formAddPartner) {
$city = $event->getForm()->getData();
$formAddPartner($event->getForm()->getParent(), $city);
}
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Transfert::class,
]);
}
}
Hey @aosow!
Sorry for the slow reply! This got escalated to me because... this stuff is *super* complex (one of my least favorite things to do inside of Symfony, tbh) and it's been a crazy week here :).
Anyways, are you still having the issue? If so, with the above code, what is the behavior you're seeing? Are you getting an error? Is one of the fields not being added? Is it not being added with the correct options? Does it seem to work, but you get a validation error when you submit?
Let me know!
Cheers!
Is it possible to remove children of a collection inside an event listener? I ran a little experiment and it doesn't seem possible.
Say I create a form that has one field 'deck' which is a collection of 'cards'. For testing, I manually add two cards when I create the form. Then I added this PRE_SUBMIT event listener to my form:<br />$form = $event->getForm();<br />$form->get('deck')->remove(0);<br />dump($form);<br />
All is good according to the debug output. I have a form with a 'deck' collection that contains exactly one 'card' (which had 2 prior to this event). However, head on over to the controller and all is not well. Here a snippet of the controller:<br />$form->handleRequest($form);<br />dump($form); <br />
That last dump shows two cards in the deck! What the...? Seems like shenanigans from handleRequest! So is it not possible to remove children from the form or am I doing something wrong?
Hey Christopher
You have a tiny error in the way you're trying to remove a form field. Just do this instead $form->remove('deck);
if that doesn't work, then double-check that hook into the event correctly (dump something inside your event listener function)
Cheers!
I got this working with two important changes:
1. I also had to remove the data associated with the card I was removing from the form.
2. I had to take logic for removing the form fields and data out of the event listener and instead put it in an event subscriber. I then added the event subscriber to the 'deck' field of the form. Now I can successfully remove cards and that removal survives handlRequest.
Can you shed some light on why this doesn't work in the event listener but it does work in the event subscriber? Is there a video on this topic that might help?
Hey @CMarcus!
I wish I could explain that, but I can’t :). Event listeners and subscribers are *identical* in how they work: they are *nothing* more than different ways to register which event you want to listen to. But ultimately, you are called in the same way.
So... my guess might be (because this is easy to do with forms) that maybe you attached the event listener and subscriber to slightly different parts of the form / fields. Or, in theory (though this seems unlikely), the listener vs subscriber caused your code to be called at a slightly different time due to the priority that it was registered. But I’m *really* reaching for this :).
Anyways, this stuff is hard. I still need to sit and think hard about how to solve these situations. Subtle changes (as you’ve seen) do make a difference. So, while I can’t fully explain it, nice job getting it working.
Cheers!
Thanks for the response, Diego. I was not clear in my intent here. I actually don't want to remove 'deck'. Deck is a collection of two cards and I want to remove one of the cards.
To give further details, 'deck' is a collection of CardType forms. The CardType form actually doesn't contain any fields until two fields get added in a PRE_SET_DATA event listener of CardType. In that event listener, a selection (checkbox) field is added along with an order field. In the DeckType form, the 'deck' collection of CardType forms is added as well as PRE_SUBMIT event listener where it iterates over each item in the collection and if the selection box is not checked, it attempts to remove that card. This removal of that card appears to work - if I dump the form as the last step of the PRE_SUBMIT event listener, I see that the card has been removed. But when I dump the form after returning from handleRequest in the controller, the card has been added back!
Also, DeckType does not have a data class. I haven't yet figured out if that's at play here or not.
One last point that may clarify things, I'm not attempting to remove cards from the collection that have already been persisted.
Hello everyone!
I have a question about dynamic forms.
I want to allow user to select some things that require three select inputs at all. But it must work like:
if ($builder->has('process')) {
$builder->get('process')->addEventListener(
FormEvents::POST_SUBMIT,
static function (FormEvent $event) use ($machiningOperationFormModifier) {
// $machiningOperationFormModifier is just closure that add new field to the form
$process = $event->getForm()->getData(); // this is the value from 'second' select
$frameworkProcess = $event->getForm()->getParent()->getData(); // this is the value from 'first' select
$machiningOperationFormModifier($event->getForm()->getParent()->getParent(), $process, $frameworkProcess);
}
);
but above code is never entered by php (I have dumped).
Is it possible to make such complicated form for working well?
Hey igor_wnek!
Oh boy! These dynamic forms can be complex. First, I really don't know why it's not working for you. My guess is that, because you're adding the 2nd field (process
) in POST_SUBMIT, when you try to look for that 2nd field, it doesn't appear to be there. That's just a wild guess based on what you're telling me :/.
One possible workaround is to always have the three fields (including process), just make the 2nd and 3rd empty (and hidden via JS) until you're reading to show them. Then, $builder->has(process')
might return true. Or, it might just create more problems 🙃
Another fix might be to always add this POST_SUBMIT
listener - do NOT bit it in an if statement. Instead, put the if statement INSIDE of the callback - e.g. $event->getForm->has('process')
. That might be enough to fix things.
Good luck and let me know how it goes!
Cheers!
What about changing same field?
I have EntityType field rendered as select, which is intentionally empty on start. Then I'm using select2 to populate it with one option.
Next - in EventListener I'm changing choices list - to just one - the same that was sent from select on form submit.
And problems start here.
After populating choices with one option - Invalid value error dissapear, but 'This value should not be blank' firing.
private function setupManagerField(FormInterface $form, ?int $manager)
{
$choices = $this->userRepository->findBy(['id' => $manager]);
$form->add('manager', EntityType::class, [
'class' => User::class,
'choice_label' => function (User $user) {
return sprintf('%s (%s)', $user->getFirstName(), $user->getUsername());
},
'choices' => $choices,
'attr' => ['class' => 'select2-dropdown']
]);
$form->get('manager')->setData($this->userRepository->find($manager));
}
Am I doing something wrong or this is not possible?
Hey Krzysztof K.!
Hmm, I'm not sure - this stuff is so complex - it's very easy to get some little detail wrong :/. Can you tell me more about the "why" behind what you're building? Like why there is a drop-down field with one item that is filled in dynamically with JS? I don't know the exact reasons behind what you're doing, but one other way to accomplish this (since it's not a "true" drop-down - you are always just populating it for the user with one option) is to make manager
a HiddenType field. On the front-end, you an show a select visually, of course, but behind the scenes, you will truly be setting the id onto the value of that hidden field. Then, the only magic you'll need on submit is a custom data transformer on that field which will transform from the Manager object to its id and its id back to the Manager object. The whole thing might give you less trouble :).
Cheers!
The "why"
I have about 1000 users in my database. I've used select2 before and started to using it also in this case. I can't load every user to select (there are so many) on form initialization, so I came up with this solution (maybe it is crazy, but seems to me to be the most obvious one).
I have a form for project. Each project has relation to manager (User) which is required field (verified by Assert\NotBlank() in Entity). So I choose EntityType. On "new" form I initialize empty (only with placeholder) field "manager", then it is populated by select2 with Ajax with one, selected option.
Then - with no event listener - validator throws error 'invalid value' (which is pretty obvious - choices option on initialization was empty - so there is no valid values).
I came up with idea that I use EventListener to populate choices on *_SUBMIT event. I'm getting event value for manager, then querying for option with UserRepository and bringing choices to field with setupManagerField function.
Now validator throws 'empty value error'. Choices are populated by one item on form render, profiler shows that submitted value exist, but still - empty value error.
Hey Krzysztof K.!
That's not crazy at all - it's the most popular reason that you need to get into all this "form events" mess :). What's curious is that (really) you're doing the exact same thing I'm doing in this tutorial - you're pre-populating the "correct" choice(s) for a drop-down menu on submit so that you do not get the "This value is not valid" from the field. The only big difference is that I'm using ChoiceType and you're using EntityType - but these are effectively the same thing.
So, you're getting the:
This value should not be blank
validation error. I can suggest 2 things:
1) Check out the profiler for your form AFTER the submit (so when you're looking at the page with the validation error) - specifically look at the part of the form profiler for your "manager" field. What does the "submitted data" say for that field? It seems like the field thinks that there is no submitted data. I would also be interested in what dump($form['manager']->getData())
looks like after "handling the form" inside your controller. Is that null?
2) Or... you can just try a different strategy :p. What you're doing is certainly not a wrong strategy. But since you are not truly ever using the "drop down" functionality of the EntityType, I would probably have done this initially as a "hidden" input field (which select2 populates) and then used a custom data transformer to go from entity -> id and id -> entity. The downside is that you're not re-using the built-in "entity transformation" that EntityType gives you... but the positive is that dealing with a "hidden" input field is simpler: no need to mess around with events. In your data transformer, you would simply need to take the submitted "id" and query for a manager object. If there was some restriction where only some users can be selected as "managers" and a "bad" user hacked the HTML and submitted a manager id that should not be used, simply detect that in your data tranformer.
Let me know what you do and how it goes!
Cheers!
This is real crazyness :) I dont get how do know when we need builder, and when we need form object. For example $builder->get('location') why not $form->get('location') ? :) and isnt this one of example of when to not use symfony form ?:)
Hey Lijana Z.
Haha, yes, this is madness, and yes, in this situation you may want to use something different than Symfony Forms. About the differences between the FormBuilder and FormInterface is that the FormBuilder is who creates instances of FormInterfaces
. So, when you are inside a FormType, you will be working with a FormBuilder instance. In this case the method setupSpecificLocationNameField()
is called inside an EventForm, and that event gets called after the building time, that's why you have to work directly with a FormInterface instance. I hope I didn't leave you more confused than you actually were :p
Cheers!
It should be easy, whenever you feel like it's just so complex, you should use something else :)
Tools are for making our job easier not harder
Cheers!
yes, I also think, that tools should do job easier, not harder. But maybe sometimes its harder because you dont know tool well. Otherwise, for me it looks like using symfony framework should not be an option :) when I worked with codeigniter and laravel 4, it felt much easier than symfony :) But maybe if I would not use things like forms and other features, then it could be similar to laravel by dificulty.
> But maybe sometimes its harder because you dont know tool well
That's so true, not knowing how a tool works makes things harder but it should only happen in the beginning, as you keep working with it, you get more proficient, and you should feel the benefits of using the tool
Hello !
I'm struggling with a form that has both a dynamic field1 (depending on the connected user) for which I use$builder->addEventListener(FormEvents::PRE_SET_DATA [...]
and a field2 that depends on the value selected in field1 like the example of this page.
How can I handle that ?
Hey Nicoschwartz,
Take a look at the next chapter: https://symfonycasts.com/sc... - I think it might help you to implement this.
Cheers!
Thank you but I think I might have missed something.
When I try to do $builder->get('field1')->addEventListener(...)
I get an error The child with the name "field1" does not exist.
since it is only added with the EventListener $builder->addEventListener(FormEvents::PRE_SET_DATA [...]{
Hey Nicoschwartz!
Awesome! Thank you for the feedback that you were able to solve it. If you have an extra solution that was not covered in our videos - it would be great if you were able to share it in your comment - it might help others a lot.
Also, I think this thread might me interesting for people as well: https://symfonycasts.com/sc... - just wanted to add a cross link to it.
Cheers!
// 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
}
}
Hi there,
here is a working example of 3 inputs based on PRE_SUBMIT events
Symfony 6, PHP 8
https://github.com/kapcus/s...
jan