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!

  • 2020-03-31 Deniz Cetin

    Ah, that clears the last missunderstanding. Thanks for the quick reply. I asked a few other fellow programmers and ultimately this example made me understand it: https://www.w3schools.com/j...
    It might be a Javascript example, but I just needed a very simple example of what it does. As stupid as it sounds ;)

  • 2020-03-30 Diego Aguiar

    Hey Deniz Cetin

    Thanks for your kind words :)

    About $this in PHP. $this is a reserved keyword that refers to the current object. It's defined inside of any of its declared *non-static* methods. So, by using $this you have access to everything that's contained by such class (that's not static). When working with inheritance, the only thing you won't have access to is to private things (methods, attributes, constants) that live inside the parent class. Did this help you?

    Cheers!

  • 2020-03-30 Deniz Cetin

    Hi SymfonyCasts-Team!
    I've been folloing your screencasts from the very start of the Symfony 4 tutorial until now and while I had absolutely no problems following your guides (they are excellent btw), I am still having so much struggle with "$this". I don't know how often I read about it, how often I searched and studied tutorials, I just can't get my head around it for some reason. I'm struggling SO hard to visualize what "$this" means, or what it stands for WHILE coding. I know it's supposed to be a reference to the calling object, but my brain absolutely starts farting away as soon as I see that. Can you guys give me something to work with here, maybe a tutorial of yours, where you explain it? I would be really glad. Thank you in advance and greetings from Germany!

  • 2020-02-24 horlyk
    ...and it totally depends on your browser


    It was the point that I wanted to know, thanks)

    Cheers!

  • 2020-02-24 Vladimir Sadicov

    Hey horlyk

    Yeah it's a pretty weird situation, and it totally depends on your browser, some of them shows confirm eg. resend POST request or not, but some just resend previous request completely. You can refresh page without resending POST with clicking on address bar and hitting enter key. Or use redirects after successful form submission.

    Cheers!

  • 2020-02-24 horlyk

    Hello everyone. Does anyone know something about the behavior when you do not redirect after submitting a form and after refreshing the page it submits again? Tried to google, but can't find an exact information about why the data is submitting again (does a POST request at the time when I'm trying to make a GET request by just refreshing a page).

  • 2020-02-03 Diego Aguiar

    Hey Mike

    I like to only add validations to fields that get populated via a form (or from the body of a POST request), so in this case, since your code is in charge of setting the user on the comment, I would disable the validation for that field and I would create a "manager" service that would be in charge of creating new comments

    Cheers!

  • 2020-02-02 Mike

    Goal:
    Hi SF Team!
    I'm coding a "add a new comment to an article" via Ajax via SF Forms.

    To save an comment, SF wants:
    comment content
    author entity
    article entity

    In my form I have one field:
    comment content

    The other two (author & article entity) are set inside my newComment() method:


    /**
    * This method creates a new comment
    *
    * @Route("/article/{slug}/comment/new", name="article_comment_new", methods={"POST"})
    */
    public function newComment(Article $article, Request $request, EntityManagerInterface $em)
    {
    $articleCommentForm = $this->createForm(ArticleCommentFormType::class);

    $articleCommentForm->handleRequest($request);
    if ($articleCommentForm->isSubmitted() && $articleCommentForm->isValid()) {

    /** @var articleComment $articleComment */
    $articleComment = $articleCommentForm->getData();
    $articleComment->setArticle($article);
    $articleComment->setAuthor($this->getUser());
    ...

    Problem:
    isValid() always fails, because SF Forms want to have Article & Author to be populated.
    But both of them are filled AFTER this check, like in the tutorials on SymfonyCasts.

    Possible solution:
    I think the problem relates to the new auto validation since SF 4.3:
    Source

    I currently have change my annotation of "author" and "article" in my ArticleComment Entity like that:

    /**
    * Custom Code:
    * Needed for SF Forms to ignore this field (Otherwise "data.field should not be null" error gets thrown
    * @Assert\DisableAutoMapping()
    *
    * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="recipeReviews")
    * @ORM\JoinColumn(nullable=false)
    */
    private $author;

    With that, isValid() returns true and everything works as expected.
    What I don't like about this method is, if at any time "author" or "article" aren't set, SF throws an "insert into failed" exception instead of a "author should be set" one.

    Question:
    What is the proper way to handle this situation?
    Is it correct to disableAutoMapping() and leaving the error handling as it is?
    Or is there any "@Assert()" rule which I can apply (That isValid() still returns true, but when $em->persist it throws a error that author have to be set)?

    //UPDATE:
    I think Ive found the correct solution: Just create a new ArticleComment Entity and prefill this with the data I want:


    /**
    * This method creates a new comment
    *
    * @Route("/article/{slug}/comment/new", name="article_comment_new", methods={"POST"})
    */
    public function newComment(Article $article, Request $request, EntityManagerInterface $em)
    {
    $articleComment = new ArticleComment();
    $articleComment->setArticle($article);
    $articleComment->setAuthor($this->getUser());

    $articleCommentForm = $this->createForm(ArticleCommentFormType::class, $articleComment);

    $articleCommentForm->handleRequest($request);
    if ($articleCommentForm->isSubmitted() && $articleCommentForm->isValid()) {

    /** @var articleComment $articleComment */
    $articleComment = $articleCommentForm->getData();
    ...

    The DisableAutoMapping Annotation is now no longer necessary.
    Is this the correct & best solution?

  • 2019-09-10 k_e_v

    Hi, thanks for the response :)

  • 2019-08-21 Victor Bocharsky

    Hey Duilio,

    > What do you recommend/use to insert data in the database when writing Functional Tests or tests that interact with the database (i.e. unit testing a repository)?

    If you interact with DB layer - it's not a unit test anymore, it's integration test. Most of the time I use entity manager to fill the DB with some data for testing. Like create a few entities, persist() them and then do flush(). After this I test my queries and check if they return the correct data. of course, not all the queries, just important ones are tested this way. As an alternative, you can look at https://github.com/liip/Lii... to fill in the DB with some data, depends on your case, but most of the time doing it manually is enough.

    > Do you recommend going to Behat/Mink or using the built in test capabilities in Symfony or it really depends on the project?

    Hm, it's probably more matter of taste. We love Behat and we use it a lot to tests things in SymfonyCasts. It allows to test even javascript features using Selenium server. And we even have a tutorial about it: https://symfonycasts.com/sc... - so, it is a great tool for testing. But mostly we do functional/integration testing with it. For unit testing, we, of course, using PHPUnit - it's kinda standard. But no matter what tools you use in your project, you just need to be confident in your features and know that those features work because tests are green.

    Also, I've not used it a lot yet, but Symfony has a new great tool for testing called Panther: https://github.com/symfony/... - it's new, but you can give it a try because it's cool.

    Cheers!

  • 2019-08-19 Duilio Palacios

    Thank you, Victor Bocharsky. Could you please help me out with the following 2 questions:

    - What do you recommend/use to insert data in the database when writing Functional Tests or tests that interact with the database (i.e. unit testing a repository)?

    - Do you recommend going to Behat/Mink or using the built in test capabilities in Symfony or it really depends on the project?

  • 2019-08-19 Victor Bocharsky

    Hey Duilio,

    Nice job! I really like it ;) Anyway, a good way to practice with Twig extensions! And nice discovering that "is_safe" option, though be careful about it - it's OK while you understand how it works and that users does not have access to pass those "$icon" argument. Otherwise it might be an XSS injection from malicious users. But as far as users can't affect on the argument value it should be good, i.e. when you explicitly passing arguments like in your example: "{{ icon(article.published ? 'check' : 'times') }}" but NOT sometimes like "{{ icon(user.customField) }}" where customField might be any string that user may set. Otherwise, you need a really good validation and escaping

    Cheers!

  • 2019-08-17 Duilio Palacios

    I've added a new Function to Twig called "icon" using the AppExtension class created in a previous lesson, just for fun:


    public function getFunctions()
    {
    return [
    new TwigFunction('icon', [$this, 'processFontAwesomeIcon'], ['is_safe' => ['html']]),
    ];
    }

    public function processFontAwesomeIcon($icon)
    {
    return sprintf('<span class="fa fa-%s"></span>', $icon);
    }

    And it can be used in this way:


    <td>{{ icon(article.published ? 'check' : 'times') }}</td>

    I guess I could pass a prefix (green-check or red-times) or add a second argument to pass a custom color or class to the icons.

    Also I think it could be a good idea to either escape or validate the icon parameter.

  • 2019-07-25 weaverryan

    Phew! That's the best possible news :). Thanks for the update.

  • 2019-07-25 Neal Ostrander

    Thanks for the reply Ryan. Found out that the last article was hiding behind the made by knp footer so it was listing just hiding.

  • 2019-07-24 weaverryan

    Yo Neal Ostrander!

    Ha! Wow. No, that IS weird. You did a good job debugging to dump() the articles in the controller. So... doing a <code{{ dump(articles)="" }}<="" code=""> in Twig DOES correctly show all articles? But then, when you loop, suddenly one is missing?

    Let me know. Cheers!

  • 2019-07-23 Neal Ostrander

    Any Idea why the list page would not be returning all the articles in the database? The last article added is not displayed on the page if I do dump and die on articles before the return statement in the list function all articles are listed in the array Doing a dump inside the twig template results with all articles being dumped but the last one doesn't display in the list.

  • 2019-07-12 Diego Aguiar

    Hey k_e_v

    I wouldn't move the logic for handling the request into a service, that's a Controller job. But you can move the rest of the logic into a service. It's easy, you just have to create a new class, add a new method with all the desired logic, and use DI (dependency injection) for injecting all the dependencies that your service needs

    Cheers!

  • 2019-07-12 k_e_v

    Any idea how to move the handleRequest() and saving in the database into a service? What's the best approach?

  • 2019-06-25 Diego Aguiar

    If you don't want to change your backend code, what you can do is to add some JS code that finds the form element, and then triggers the submit action


    $('#some-button').on('click', function() {
    const myForm = $('#some-form');
    myForm.trigger('submit');
    });
  • 2019-06-25 Ionut Stoican

    At first it was a POST request though a form and actually I found a way to do it. Now I am wondering how you can submit through AJAX that form, process it in the controller and return an appropriate response, is it the same as a normal submit form?

  • 2019-06-25 Diego Aguiar

    Hey Ionut Stoican

    How are you planning to manage those submits? As a traditional website, or you will have like an API where you can submit POST/PUT requests?

    Cheers!

  • 2019-06-23 Ionut Stoican

    How do we handle multiple different forms on the same page? I couldn't find anywhere a documentation.

    I have a page with a category name and article list where I want to submit the edit/change category title, and also for each article to add it to another category.

  • 2019-03-15 Victor Bocharsky

    Hey Mike,

    Haha, good catch :) Well, CSRF token should not be unique for each page load, it should be unique for forms and for user session. I think that's how CSRF protection works in Symfony, though I don't know internal details 100%, just saying at the first sight. The idea of CSRF is not protect from double submit, but protect from malicious user sending a request on behalf of user. For this, it's enough to generate a token that is known to server and user only, even if it's not unique per page load.

    I hope it helps.

    Cheers!

  • 2019-03-13 Mike

    On the reload of the POST request, it posted the unpublished new article. (5:39 Minutes)
    But shouldnt validation fail instead, because the request was posted with the same csrf token as the request before?
    Isn't that the whole point of CSRF, to create new strings on every page reload to prevent duplicated posting and XSS (Attackers which used old CSRF Tokens)

  • 2019-02-11 weaverryan

    Hey darighteous1!

    No worries about the indentation - Disqus has a way to do code blocks - but it's honestly kind of annoying ;).

    Ok, I *think* I know your problem. I noticed that you did ->setData(['status' => 'new']) on your form when rendering it. That is totally legal and fine. However, when you do this, *all* you're doing is setting the data onto the Form object during that request. Typically the data would then be used by the form to pre-fill some of your fields (e.g. you set 'firstName' => 'Ryan' and then the first name input by has "Ryan" in it). But, in your case, it sounds like you're not rendering any fields - just a submit button. So, when your form submits... it really is submitting absolutely *no* data.

    If you want to send some sort of "flag" to the controller that's handling the submit, I'd recommend either (A) putting it in the URL - e.g. /my/custom/controller/{status} or (B) possibly as a HiddenType

  • 2019-02-08 darighteous1

    Hey, what about if we do not submit the form tho the same end point? I got this case: I have a Sonata admin class, which renders a form. That form submits to a controller. I use Symfony 3.4
    EDIT: Sorry, I can't figure out why indentations are wrong. Code is in a code block :/

    class MySuperAwesomeAdmin extends BaseAdmin
    {
    public function myForm()
    {
    $form = $this->formFactory
    ->createBuilder(MyFormType::class)
    ->setAction(
    $this->getRouteGenerator()->generate('my_route',['id' => $entity->getId()])
    )
    ->setData(['status' => 'new'])
    ->getForm()
    ;

    return $form->createView();
    }
    }

    In my controller I got:

    class TransferManagementController extends Controller
    {
    public function foo($id, Request $request)
    {
    $form = $this->createForm(FailTransferType::class);
    $form->handleRequest($request);
    if($form->isSubmitted() && $form->isValid()) {
    dump($form->getData);
    }
    }

    But data is empty. I can't figure out why. Oh, yeah, and my form has just a single submit button that POSTs to my controller. Any idea what am I missing?

  • 2018-11-05 weaverryan

    Hey cybernet2u!

    Ah! I think the problem isn't the paginator, but Doctrine (well really SensioFrameworkExtraBundle) is unable to figure out how to query for your "Advert" entity. My guess is that you have a route that includes a {advert} wildcard, right? When SensioFrameworkExtraBundle sees the Advert $advert type-hint, it tries to query for this Advert object. Unless you have extra configuration, it tries to see if there is a property on your entity called "advert". If there is not, then it can't figure out how to query for your entity object. This is why I often us {id} in my route - because the property is called "id", this "param converter" feature from SensioFrameworkExtraBundle knows how to query.

    But also... this page looks like it is listing many adverts. Did you intend/want to query for a single Advert? I see you'r using it to transform some markdown... but then you don't use that $mk variable. Let me know!

    Cheers!

  • 2018-11-03 cybernet2u

    Hi,
    How can we use knp-paginator & knp-markdown-bundle for listing ... a list ?

    i have this


    public function AdminAdvertList(Advert $advert, EntityManagerInterface $em, PaginatorInterface $paginator, Request $request, MarkdownInterface $markdown)
    {
    $q = $request->query->get('q');
    $repository = $em->getRepository(Advert::class);
    $queryBuilder = $repository->getWithSearchQueryBuilder($q);
    $mk = $markdown->transform($advert->getContent());
    $pagination = $paginator->paginate(
    $queryBuilder,
    $request->query->getInt('page', 1),
    10
    );
    return $this->render('admin/advert/index.html.twig', [
    'pagination' => $pagination,
    ]);
    }

    doesn't seem to work anymore after updating symfony

    Unable to guess how to get a Doctrine instance from the request information for parameter "advert".