Handling the Form Submit

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Creating the form class and rendering was... easy! Now it's time to talk about handling the form submit. Notice: we haven't configured anything on our form about what URL it should submit to. When we rendered it, we used form_start() and... that's it! Inspect element on the form. By default, form_start() creates a form tag with no action attribute. And when a form tag has no action=, it means that it will submit right back to this same URL.

That's the most common way of handling forms in Symfony: the same controller is responsible for both rendering the form on a GET request and handling the form submit on a POST request. The way you do this always follows a similar pattern.

The Form Submit Logic

First, get the $request object by type-hinting Request - the one from HttpFoundation. Next, add $form->handleRequest($request) and then if ($form->isSubmitted() && $form->isValid()). Inside the if, dd($form->getData().

... lines 1 - 13
class ArticleAdminController extends AbstractController
{
... lines 16 - 19
public function new(EntityManagerInterface $em, Request $request)
{
... lines 22 - 23
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dd($form->getData());
}
... lines 28 - 31
}
... lines 33 - 41
}

Okay, so... this requires a little bit of explanation. First, yea, the $form->handleRequest() makes it look like the submitted data is being read and processed on every request, even the initial GET request that renders the form. But, that's not true! By default, handleRequest() only processes the data when this is a POST request. So, when the form is being submitted. When the form is originally loaded, handleRequest() sees that this is a GET request, does nothing, $form->isSubmitted() returns false, and then the un-submitted form is rendered by Twig.

But, when we POST the form, ah, that's when handleRequest() does its magic. Because the form knows all of its fields, it grabs all of the submitted data from the $request automatically and isSubmitted() returns true. Oh, and later, we'll talk about adding validation to our form. As you can guess, when validation fails, $form->isValid() returns false.

So, wow! This controller does a lot, with very little code. And there are three possible flows. One: if this is a GET request, isSubmitted() returns false and so the form is passed to Twig. Two, if this is a POST request but validation fails, isValid() returns false and so the form is again passed to Twig, but now it will render with errors. We'll see that later. And three: if this is a POST request and validation passes, both isSubmitted() and isValid() are true, and we finally get into the if block. $form->getData() is how we access the final, normalized data that was submitted.

Phew! So, let's try it! Find your browser and create a very important article about the booming tourism industry on Mercury. Submit!

Yes! It dumps out exactly what we probably expected: an array with title and content keys. It's not too fancy yet, but it works nicely.

Saving the Form Data

To insert a new article into the database, we need to use this data to create an Article object. There is a super cool way to do this automatically with the form system. But, to start, let's do it the manual way. Add $data = $form->getData(). Then, create that object: $article = new Article(), $article->setTitle($data['title']);, $article->setContent($data['content']), and the author field is also required. How about, $article->setAuthor() with $this->getUser(): the current user will be the author.

... lines 1 - 19
public function new(EntityManagerInterface $em, Request $request)
{
... lines 22 - 24
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$article = new Article();
$article->setTitle($data['title']);
$article->setContent($data['content']);
$article->setAuthor($this->getUser());
... lines 31 - 35
}
... lines 37 - 40
}
... lines 42 - 52

To save this to the database, we need the entity manager. And, hey! We already have it thanks to our EntityManagerInterface argument. Save with the normal $em->persist($article), $em->flush().

Awesome! The last thing we always do after a successful form submit is redirect to another page. Let's use return this->redirectToRoute('app_homepage').

... lines 1 - 24
if ($form->isSubmitted() && $form->isValid()) {
... lines 26 - 31
$em->persist($article);
$em->flush();
return $this->redirectToRoute('app_homepage');
}
... lines 37 - 52

Time to test this puppy out! Refresh to re-post the data. Cool! I... think it worked? Scroll down... Hmm. I don't see my article. Ah! But that's because only published articles are shown on the homepage.

Adding an Article List Page

What we really need is a way to see all of the articles in an admin area. We have a "new" article page and a work-in-progress edit page. Now, create a new method: public function list(). Above it, add the annotation @Route("/admin/article"). To fetch all of the articles, add an argument: ArticleRepository $articleRepo, and then say $articles = $articleRepo->findAll(). At the bottom, render a template - article_admin/list.html.twig- and pass this an articles variable.

... lines 1 - 52
/**
* @Route("/admin/article")
*/
public function list(ArticleRepository $articleRepo)
{
$articles = $articleRepo->findAll();
return $this->render('article_admin/list.html.twig', [
'articles' => $articles,
]);
}
... lines 64 - 65

Oh, and I'll cheat again! If you have the Symfony plugin installed, you can put your cursor in the template name and press Alt+Enter to create the Twig template, right next to the other one.

Because we're awesome at Twig, the contents of this are pretty boring. In fact, I'm going to cheat again! I'm on a roll! I'll paste a template I already prepared. You can get this from the code block on this page.

{% extends 'content_base.html.twig' %}
{% block content_body %}
<a href="{{ path('admin_article_new') }}" class="btn btn-primary pull-right">
Create <span class="fa fa-plus-circle"></span>
</a>
<h1>All Articles</h1>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Published?</th>
</tr>
</thead>
<tbody>
{% for article in articles %}
<tr>
<td>{{ article.title }}</td>
<td>{{ article.author.email }}</td>
<td>
<span class="fa fa-{{ article.isPublished ? 'check' : 'times' }}"></span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

And... yea! Beautifully boring! This loops over the articles and prints some basic info about each. I also added a link on top to the new article form page.

Oh, there is one interesting part: the article.isPublished code, which I use to show a check mark or an "x" mark. That's interesting because... we don't have an isPublished property or method on Article! Add public function isPublished(), which will return a bool, and very simply, return $this->publishedAt !== null.

... lines 1 - 15
class Article
{
... lines 18 - 127
public function isPublished(): bool
{
return $this->publishedAt !== null;
}
... lines 132 - 253
}

If you want to be fancier, you could check to see if the publishedAt date is not null and also not a future date. It's up to how you want your app to work.

Time to try it! Manually go to /admin/article and... woohoo! There is our new article on the bottom.

And... yea! We've already learned enough to create, render and process a form submit! Nice work! Next, let's make things a bit fancier by rendering a success message after submitting.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "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.2.7
        "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
    }
}