This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
04.

Multiple Submit Buttons

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Alright, we've got our "Create" button working like a charm, but what if we want a couple of different workflows: "Create and Close" to save the part and jump back to the list, and "Create and Add Another" to save the part but stay on the form page for quick data entry.

No worries, Symfony is fully capable of handling this. Let's dive into the second method of adding submit buttons to a form - using the SubmitType.

Adding a Second Submit Button Inside the Form Type

First things first, let's tweak the name of our current button in the new.html.twig template to "Create and Close":

// ... lines 1 - 4
{% block body %}
<div class="max-w-4xl mx-auto">
// ... lines 7 - 8
{{ form_start(form) }}
// ... lines 10 - 11
<button type="submit" class="text-white bg-green-700 hover:bg-green-800 rounded-lg px-5 py-2.5 me-2 mb-2 cursor-pointer">Create and close</button>
{{ form_end(form) }}
</div>
{% endblock %}

Now, pop open your form type class in src/Form/StarshipPartType.php. It's time to drop in a second button below the fields. Add this using ->add('createAndAddNew', SubmitType::class):

36 lines | src/Form/StarshipPartType.php
// ... lines 1 - 8
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
// ... lines 10 - 12
class StarshipPartType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// ... lines 18 - 24
->add('createAndAddNew', SubmitType::class)
;
}
// ... lines 28 - 34
}

This handy SubmitType::class tells Symfony to render it as a <button type="submit">.

If you hop over to the browser and refresh, you'll see our two buttons. The new one doesn't quite look like a button - that's because we've reset styles. But technically, in the code, it's <button type="submit">. We'll spruce up the styles later.

Accessing Unmapped Fields in Symfony

For now, in the controller, we're already aware that $form->getData() hands us a mapped entity, which in our case is StarshipPart:

39 lines | src/Controller/AdminController.php
// ... lines 1 - 13
class AdminController extends AbstractController
{
#[Route('/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()) {
/** @var StarshipPart $part */
$part = $form->getData();
// ... lines 26 - 31
}
// ... lines 33 - 36
}
}

This time, though, we need to get our hands on an unmapped field - the submit button we just added. This field doesn't have a matching property on the entity, which is why we call it "unmapped".

No sweat, we can access raw form data. Directly below the addFlash(), cook up a $createAndAddNewBtn variable that equals $form->get('createAndAddNew'). This should match the button name on your Form type. Let's give it a quick test run first. Down below, dd($createAndAddNewBtn):

42 lines | src/Controller/AdminController.php
// ... lines 1 - 13
class AdminController extends AbstractController
{
#[Route('/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()));
$createAndAddNewBtn = $form->get('createAndAddNew');
dd($createAndAddNewBtn);
// ... lines 33 - 34
}
// ... lines 36 - 39
}
}

Back to the browser, filling the form notes is optional, so just the name and price will do. Then hit our "Create and Add New" button... and... there's our dump. It's a Submit Button with some intriguing fields. Take a closer look and you'll spot the magical clicked = true. Believe it or not, this little gem is how we can tell which button was actually clicked, and we can harness this to power different business logic.

Harnessing Button Clicks to Execute Business Logic

Back to the code, get rid of the dd() and give it PhpStorm little help. To solve the autocomplete, above the button, add a docblock with /** @var SubmitButton $createAndAddNewBtn */. Now, inside the form submit logic, write $createAndAddNewBtn->isClicked(). That's exactly what we need. Wrap it in an if statement, and if the button was clicked, let's return $this->redirectToRoute('app_admin_starship_part_new'):

46 lines | src/Controller/AdminController.php
// ... lines 1 - 8
use Symfony\Component\Form\SubmitButton;
// ... lines 10 - 14
class AdminController extends AbstractController
{
#[Route('/starship-part/new', name: 'app_admin_starship_part_new', methods: ['GET', 'POST'])]
public function newStarshipPart(
// ... lines 19 - 20
): Response {
// ... lines 22 - 23
if ($form->isSubmitted()) {
// ... lines 25 - 31
/** @var SubmitButton $createAndAddNewBtn */
$createAndAddNewBtn = $form->get('createAndAddNew');
if ($createAndAddNewBtn->isClicked()) {
return $this->redirectToRoute('app_admin_starship_part_new');
}
// ... lines 37 - 38
}
// ... lines 40 - 43
}
}

Now, back in the browser, refresh and resubmit. Hmm, we see the successful flash message twice... That's because it was added during the dd() request, and again, when we resubmitted. But it did work - we're back on the blank form, ready to add another part. Sweet!

The Intricacies of Symfony's Form Type: Field Type Guessing

Let's zip back to the Form type for a second. We're using $builder->add() to add fields to our form. Sometimes we specify the second argument, but others, we don't. This second one is the field type, and it's null by default. So, why don't we need to always specify it?

Well, Symfony has this nifty feature called field type guessing. When the type is null, Symfony will inspect the underlying data class - in our case, the StarshipPart entity. It will look for a property that matches the field name. Based on the found property's type (guessed from type-hints and metadata), Symfony will automatically select the most appropriate form field type.

For example, since price is an integer, Symfony will pick the IntegerType. Since name is a string, Symfony will opt for a TextType. If we had, let's say, an isActive boolean, then Symfony would select a CheckboxType. Pretty cool, eh?

Most of the time, Symfony guesses exactly what you want. But when it can't guess correctly, or when you want something different, you can always override the default guessed type by passing the type explicitly.

If we change price to IntegerType::class, when we refresh the form, nothing will change, because that was the guessed type.

Exploring Symfony's Built-in Form Field Types

But what built-in form field types does Symfony have and where can we find them? Great question, and I'm glad you asked! Symfony docs will always be there to help with that. But Symfony also comes with a super nerd-friendly tool for discovering everything about built-in field types.

At your terminal, run:

symfony console debug:form

This dumps all available form types, including your own form type classes, which we can see here.

If you want to inspect a specific type, just specify it as an argument to the command. For example, run:

symfony console debug:form TextType

And you'll see all options that TextType supported, the options that are required, and which options come from parent types it extends. Try another one:

symfony console debug:form EntityType

We use this in our StarshipPartType for the ship field. With this one, you'll see that the class option is required. If we look at our form type... the MakerBundle already filled that for us:

36 lines | src/Form/StarshipPartType.php
// ... lines 1 - 12
class StarshipPartType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// ... lines 18 - 20
->add('starship', EntityType::class, [
'class' => Starship::class,
// ... line 23
])
// ... line 25
;
}
// ... lines 28 - 34
}

So smart! Options are set as the third argument of $builder->add().

And by the way, Symfony doesn't only guess field types, it also guesses field type options. For example, if a Doctrine entity property is nullable: true like for this notes field:

86 lines | src/Entity/StarshipPart.php
// ... lines 1 - 10
class StarshipPart
{
// ... lines 13 - 25
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $notes = null;
// ... lines 28 - 84
}

Then Symfony will automatically make it optional. And the reverse for the other fields, the required HTML attribute will be added if the field is not nullable, like for name and price:

86 lines | src/Entity/StarshipPart.php
// ... lines 1 - 10
class StarshipPart
{
// ... lines 13 - 19
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column]
private ?int $price = null;
// ... lines 25 - 84
}

You can see this with the HTML inspector... If we inspect the notes field, we see it doesn't have the required attribute. If we inspect the name field, it does! Same with price.

That's why when you submit an empty form, we see HTML5 validation errors for the required fields.

And of course, you can easily override this behaviour in that third $builder->add() argument array. To see this, add required => false to our price field's options. Back in the browser, refresh and inspect the price field - the required attribute is gone!

Revert that change - it really is required!

We'll dive deeper into form field types later in this course. For now, let's switch gears to something fun and stylish: dressing up our form for the ball by applying a built-in Symfony form theme to it. That's next!