This course is still being released! Check back later for more chapters.
Processing the Submitted Form
Alright, we've built, created, rendered, and styled our form. I've given it my all and now our form is ready for submission. Now, as any seasoned backend developer will tell you, the real fun begins when we start dealing with that submitted data. Let's jump back into our controller and make this form functional.
Updating Controller to Handle Form Data
Open up src/Controller/AdminController.php, and in the newStarshipPart() method, right below the $form object, add $form->handleRequest(). To pull this off, we need to pass the current request object to this method. You're familiar with this by now. Inject Request from the HttpFoundation as the method argument $request and pass it to handleRequest():
| // ... lines 1 - 7 | |
| use Symfony\Component\HttpFoundation\Request; | |
| // ... lines 9 - 12 | |
| class AdminController extends AbstractController | |
| { | |
| ('/starship-part/new', name: 'app_admin_starship_part_new', methods: ['GET', 'POST']) | |
| public function newStarshipPart( | |
| Request $request, | |
| ): Response { | |
| $form = $this->createForm(StarshipPartType::class); | |
| $form->handleRequest($request); | |
| // ... lines 21 - 24 | |
| } | |
| } |
You might be wondering what this handleRequest() is all about. It simply grabs the submitted data from the request, applies that data to your form, and now your form contains the user's submitted values.
Checking Form Submission
Next, we want to know whether the form has actually been submitted or if we just loaded the form page. That's a breeze — just write if ($form->isSubmitted()). Then within that if, we can retrieve the submitted data with $form->getData():
| // ... lines 1 - 12 | |
| class AdminController extends AbstractController | |
| { | |
| ('/starship-part/new', name: 'app_admin_starship_part_new', methods: ['GET', 'POST']) | |
| public function newStarshipPart( | |
| Request $request, | |
| ): Response { | |
| $form = $this->createForm(StarshipPartType::class); | |
| $form->handleRequest($request); | |
| if ($form->isSubmitted()) { | |
| $part = $form->getData(); | |
| } | |
| // ... lines 24 - 27 | |
| } | |
| } |
Since our form type has a data_class option set to StarshipPart::class:
| // ... lines 1 - 11 | |
| class StarshipPartType extends AbstractType | |
| { | |
| // ... lines 14 - 26 | |
| public function configureOptions(OptionsResolver $resolver): void | |
| { | |
| $resolver->setDefaults([ | |
| 'data_class' => StarshipPart::class, | |
| ]); | |
| } | |
| } |
the data you retrieve here isn't a straightforward PHP array. Instead, it's a StarshipPart entity instance with the fields already filled in for us. Skeptical? Go ahead and check it yourself. Let's assign it to a $part variable and below dd($part):
| // ... lines 1 - 12 | |
| class AdminController extends AbstractController | |
| { | |
| ('/starship-part/new', name: 'app_admin_starship_part_new', methods: ['GET', 'POST']) | |
| public function newStarshipPart( | |
| Request $request, | |
| ): Response { | |
| // ... lines 19 - 20 | |
| if ($form->isSubmitted()) { | |
| $part = $form->getData(); | |
| dd($part); | |
| } | |
| // ... lines 25 - 28 | |
| } | |
| } |
Testing Our Form
Back in the browser, I'll quickly fill in the form. Hit Create to submit it... and voila, a shiny new StarshipPart object with the data we sent. Notice that no ID is set because Doctrine hasn't saved it in the database yet. I'll quickly add a PHPDoc above the variable to make PhpStorm's autocomplete happier, and delete the dd() statement:
| // ... lines 1 - 12 | |
| class AdminController extends AbstractController | |
| { | |
| ('/starship-part/new', name: 'app_admin_starship_part_new', methods: ['GET', 'POST']) | |
| public function newStarshipPart( | |
| Request $request, | |
| ): Response { | |
| // ... lines 19 - 20 | |
| if ($form->isSubmitted()) { | |
| /** @var StarshipPart $part */ | |
| $part = $form->getData(); | |
| } | |
| // ... lines 25 - 28 | |
| } | |
| } |
Saving Data with Doctrine's EntityManager
To save the new part, we need Doctrine's EntityManager. Inject it with EntityManagerInterface $entityManager in the method signature. Then, back in the if, add $entityManager->persist(), passing the $part object and next $entityManager->flush():
| // ... lines 1 - 6 | |
| use Doctrine\ORM\EntityManagerInterface; | |
| // ... lines 8 - 13 | |
| class AdminController extends AbstractController | |
| { | |
| ('/starship-part/new', name: 'app_admin_starship_part_new', methods: ['GET', 'POST']) | |
| public function newStarshipPart( | |
| // ... line 18 | |
| EntityManagerInterface $entityManager, | |
| ): Response { | |
| // ... lines 21 - 22 | |
| if ($form->isSubmitted()) { | |
| /** @var StarshipPart $part */ | |
| $part = $form->getData(); | |
| $entityManager->persist($part); | |
| $entityManager->flush(); | |
| } | |
| // ... lines 29 - 32 | |
| } | |
| } |
Back to the browser, I'll set the name to: "Legacy Hyperdrive". Give it a fair price, and don’t forget about an important note:
Be careful with high revs!
Now, hit the submit button again. Did it finally work? Well, at least there are no errors. Head to the $part page and search for "Legacy Hyperdrive". There it is, your new shiny Starship part, ready for sale.
Celebrating Success with Flash Messages
Let's celebrate this moment properly. I'm going to add a successful flash message. Flash messages are temporary messages stored in the session and shown exactly once. They are perfect for things like:
Your part was created successfully!
If you peek into templates/base.html.twig - you'll see we already have code that loops over flash messages and renders them with nice styling depending on the type: success, warning, error, default:
| <html> | |
| // ... lines 3 - 13 | |
| <body class="text-white" style="background: radial-gradient(102.21% 102.21% at 50% 28.75%, #00121C 42.62%, #013954 100%);"> | |
| {% set flashBgColors = { | |
| 'success': 'bg-green-400/30', | |
| 'warning': 'bg-yellow-400/30', | |
| 'error': 'bg-red-400/30', | |
| 'default': 'bg-blue-400/30' | |
| } %} | |
| {% for label, messages in app.flashes %} | |
| {% for message in messages %} | |
| <div class="relative isolate flex items-center gap-x-6 overflow-hidden {{ flashBgColors[label]|default(flashBgColors['default']) }} px-6 py-2.5 after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-white/10 sm:px-3.5 sm:before:flex-1"> | |
| <div class="flex flex-wrap items-center gap-x-4 gap-y-2"> | |
| <p class="text-sm/6 text-gray-100"> | |
| {{ message }} | |
| </p> | |
| </div> | |
| <div class="flex flex-1 justify-end"> | |
| <button type="button" class="-m-3 p-3 focus-visible:-outline-offset-4"> | |
| <span class="sr-only">Dismiss</span> | |
| <svg viewBox="0 0 20 20" fill="currentColor" data-slot="icon" aria-hidden="true" class="size-5 text-gray-100"> | |
| <path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| {% endfor %} | |
| // ... lines 40 - 67 | |
| </body> | |
| </html> |
Back in our controller, after saving the part entity to the database, write: $this->addFlash(). First argument: the message "type" - it helps to control styling. Write success here. Second argument: the content of the message. How about: sprintf('The part "%s" was successfully created!', $part->getName()):
| // ... lines 1 - 13 | |
| class AdminController extends AbstractController | |
| { | |
| ('/starship-part/new', name: 'app_admin_starship_part_new', methods: ['GET', 'POST']) | |
| public function newStarshipPart( | |
| // ... lines 18 - 19 | |
| ): Response { | |
| // ... lines 21 - 22 | |
| if ($form->isSubmitted()) { | |
| // ... lines 24 - 28 | |
| $this->addFlash('success', sprintf('The part "%s" was successfully created.', $part->getName())); | |
| } | |
| // ... lines 31 - 34 | |
| } | |
| } |
Avoiding Duplication and Redirecting User
To avoid duplicate form submissions when the user refreshes the page - which might lead to unwanted parts floating around in space - let's finish the process with a redirect. This is a classic best practice for POST forms. Let's return $this->redirectToRoute().
We can redirect anywhere, but I'll send users back to the part list for convenience. This route name is app_part_index.:
| // ... lines 1 - 13 | |
| class AdminController extends AbstractController | |
| { | |
| ('/starship-part/new', name: 'app_admin_starship_part_new', methods: ['GET', 'POST']) | |
| public function newStarshipPart( | |
| // ... lines 18 - 19 | |
| ): Response { | |
| // ... lines 21 - 22 | |
| if ($form->isSubmitted()) { | |
| // ... lines 24 - 28 | |
| $this->addFlash('success', sprintf('The part "%s" was successfully created.', $part->getName())); | |
| return $this->redirectToRoute('app_part_index'); | |
| } | |
| // ... lines 33 - 36 | |
| } | |
| } |
Testing the Overall Flow
Alright, let's create a new part again. How about a "Quantum Reactor" for the name, set a price, and for notes, I'll say:
Do not exceed 120% core flux
OK, submit the form again, and there it is. Our flash message, announcing that:
The part "Quantum Reactor" was successfully created!
And if I try to refresh the page, the message is gone, so it was shown only once, and Chrome does not ask me if I want to resubmit the form again, so it was redirected properly too. Sweet!
Adding a Second Submit Button
Right now we only have one submit button: Create. But imagine this, if you're feeling productive, caffeinated, on a roll, and you want to create multiple parts quickly, one after the other, you could do this with a few extra clicks each time, clicking on the link to return to the form. But wouldn't it be so much faster to have a second submit button, that, instead of creating and going back to the list, creates and stays on this page with an empty form open, so you can immediately create another part?
Well, it might not be much faster, but it still could save someone a few hours of their life over the course of many years of adding those parts.
You might be wondering, is that even possible? Absolutely! And we'll figure out how in the next chapter.