Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Save, Redirect, setFlash (and Dance)

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

We already have the finished Genus object. So what do we do now? Whatever we want!

Probably... we want to save this to the database. Add $genus = $form->getData(). Get the entity manager with $em = this->getDoctrine()->getManager(). Then, the classic $em->persist($genus) and $em->flush():

... lines 1 - 12
class GenusAdminController extends Controller
{
... lines 15 - 31
public function newAction(Request $request)
{
... lines 34 - 37
if ($form->isSubmitted() && $form->isValid()) {
$genus = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($genus);
$em->flush();
... lines 44 - 45
}
... lines 47 - 50
}
}

Always Redirect!

Next, we always redirect after a successful form submit - ya know, to make sure that the user can't just refresh and re-post that data. That'd be lame.

To do that, return $this->redirectToRoute(). Hmm, generate a URL to the admin_genus_list route - that's the main admin page I created before the course:

... lines 1 - 12
class GenusAdminController extends Controller
{
... lines 15 - 31
public function newAction(Request $request)
{
... lines 34 - 37
if ($form->isSubmitted() && $form->isValid()) {
... lines 39 - 44
return $this->redirectToRoute('admin_genus_list');
}
... lines 47 - 50
}
}

Because redirectToRoute() returns a RedirectResponse, we're done!

Time to try it out. I'll be lazy and refresh the POST. We should get a brand new "Sea Monster" genus. There it is! Awesome!

Adding a Super Friendly (Flash) Message

Now, it worked... but it lack some spirit! There was no "Success! You're amazing! You created a new genus!" message.

And I want to build a friendly site, so let's add that message. Back in newAction(), add some code right before the redirect: $this->addFlash('success') - you'll see where that key is used in a minute - then Genus created - you are amazing!:

... lines 1 - 12
class GenusAdminController extends Controller
{
... lines 15 - 31
public function newAction(Request $request)
{
... lines 34 - 37
if ($form->isSubmitted() && $form->isValid()) {
... lines 39 - 44
$this->addFlash('success', 'Genus created!');
return $this->redirectToRoute('admin_genus_list');
}
... lines 49 - 52
}
}

It's good to encourage users.

But let's be curious and see what this does behind the scenes. Hold command and click into the addFlash() method:

... lines 1 - 38
abstract class Controller implements ContainerAwareInterface
{
... lines 41 - 102
/**
* Adds a flash message to the current session for type.
*
* @param string $type The type
* @param string $message The message
*
* @throws \LogicException
*/
protected function addFlash($type, $message)
{
if (!$this->container->has('session')) {
throw new \LogicException('You can not use the addFlash method if sessions are disabled.');
}
$this->container->get('session')->getFlashBag()->add($type, $message);
}
... lines 119 - 396
}

Okay, cool: it uses the session service, fetches something called a "flash bag" and adds our message to it. So the flash bag is a special part of this session where you can store messages that will automatically disappear after one redirect. If we store a message here and then redirect, on the next page, we can read those messages from the flash bag, and print them on the page. And because the message automatically disappears after one redirect, we won't accidentally show it to the user more than once.

And actually, it's even a little bit cooler than that. A message will actually stay in the flash bag until you ask for it. Then it's removed. This is good because if you - for some reason - redirect twice before rendering the message, no problem! It'll stay in there and wait for you.

Rendering the Flash Message

All we need to do now is render the flash message. And the best place for this is in your base template. Because then, you can set a flash message, redirect to any other page, and it'll always show up.

Right above the body block, add for msg in app.session - the shortcut to get the session service - .flashbag.get() and then the success key. Add the endfor:

<!DOCTYPE html>
<html>
... lines 3 - 13
<body>
... lines 15 - 28
<div class="main-content">
{% for msg in app.session.flashBag.get('success') %}
... lines 31 - 33
{% endfor %}
... lines 35 - 36
</div>
... lines 38 - 46
</body>
</html>

Why success? Because that's what we used in the controller - but this string is arbitrary:

... lines 1 - 12
class GenusAdminController extends Controller
{
... lines 15 - 31
public function newAction(Request $request)
{
... lines 34 - 37
if ($form->isSubmitted() && $form->isValid()) {
... lines 39 - 44
$this->addFlash('success', 'Genus created!');
... lines 46 - 47
}
... lines 49 - 52
}
}

Usually I have one for success that I style green and happy, and on called error that style to be red and scary.

I'll make this happy with the alert-success from bootstrap and then render msg:

<!DOCTYPE html>
<html>
... lines 3 - 13
<body>
... lines 15 - 28
<div class="main-content">
{% for msg in app.session.flashBag.get('success') %}
<div class="alert alert-success">
{{ msg }}
</div>
{% endfor %}
... lines 35 - 36
</div>
... lines 38 - 46
</body>
</html>

Cool! Go back and create Sea Monster2. Change its subfamily, give it a species count and save that sea creature! Ocean conservation has never been so easy.

And, I'm feeling the warm and fuzzy from our message.

Next, let's really start to control how the fields are rendered.

Leave a comment!

39
Login or Register to join the conversation
Peter-K Avatar
Peter-K Avatar Peter-K | posted 5 years ago

Is there any plan to create tutorial for dynamically generating form based on user data. I found documentation but real struggle is when form is invalid to keep submited values and filtered options.

Basically example of each of these: https://symfony.com/doc/cur...

Reply

Hey Peter,

We don't have any plans to make a separate tutorial about form events, but we do have a few related screencasts that explain this concept, check it out:
- https://knpuniversity.com/s...
- https://knpuniversity.com/s...

But if you stuck on some step with it, we'd be glad to help you.

Cheers!

Reply
Default user avatar
Default user avatar Blueblazer172 | posted 5 years ago

when i refresh the page on the /new page it shows up my message twice. Why is that so?
can i prevent that ?

Reply

Hey Blueblazer172 ,

Where do you call your addFlash() method? It should be right before redirect to the other page, then when you successfully save your entity - you will be redirected to the other page and then reloading page do nothing.

Cheers!

Reply
Default user avatar
Default user avatar Blueblazer172 | Victor | posted 5 years ago

Yeah that works :) Thanks

Reply
Claire S. Avatar
Claire S. Avatar Claire S. | posted 5 years ago | edited

Hi,

I'm having an issue with setting a flash message. After submitting a form, I redirect to a page and the flash message isn't shown. However if I navigate back to the form I can see it.


       if ($feedbackForm->isSubmitted() && $feedbackForm->isValid()){

            $feedback = $feedbackForm->getData();

            $em = $this->getDoctrine()->getManager();
            $em->persist($feedback);
            $em->flush();

            $this->addFlash('success', 'This is a test, thank you');

            return $this->redirectToRoute('www_news');

        }

any pointers?

thank you

Reply

Hi Claire S.!

Hmm, tricky! So, I *do* have one idea :). When you set a flash message, it "sticks" in the session *until* something reads it. So, in theory, it could stick in the session for 100 requests... only to display on the 101st request (if that was the first page that *actually* tried to render them).

So, your situation makes me wonder: does the www_news page possibly *not* contain the flash-rendering code? If that's the case, then it (obviously) won't render on that page, but it *would* render later on any other page that contains the flash-rendering code (e.g. the form page). The best recommendation is to put the flash message in your base layout, so that it shows everywhere.

But, if I'm wrong, let us know! In that case, we'll want to see the templates to in order to spot the issue. Your controller code looks great!

Cheers!

Reply
Trafficmanagertech Avatar
Trafficmanagertech Avatar Trafficmanagertech | posted 5 years ago | edited

Is the $genus = $form->getData() needed?
I see no difference in using it or no, the object is updated by handleRequest

Reply

Hey Trafficmanagertech,

Actually, it depends. If you pass $genus object to the createForm() as a second argument - then you don't need to call $genus = $form->getData():


$genus = new Genus;
$form = $this->createForm(GenusFormType::class, $genus);
// the $genus object will be updated after handleRequest() call

Cheers!

1 Reply
Trafficmanagertech Avatar
Trafficmanagertech Avatar Trafficmanagertech | Victor | posted 5 years ago

Thank you!

Reply
Default user avatar
Default user avatar Szymon Chomej | posted 5 years ago

Is there easy way to do all of this with Ajax?
...i mean send form by post and use doctrine to save data to database.

Reply

Hey Szymon,

Yes, you can totally do it via AJAX. Just render the form as always, but then add an event listener in JS to trigger when you press the submit button. All you need to do in it is just e.preventDefault() to prevent submitting form, then if you're using jQuery: $("form :input").serialize() - which will serialize all the data in form and you can pass it to data attribute to $.ajax({}), e.g.:


$("input[type='submit']").click(function(e) {
    e.preventDefault();

    $.ajax({
        url: $("form").attr('action'),
        data: $("form : input").serialize(),
        // other data if needed...
    });
});

Feel free to make more complex changes to my simple example ;) Then, you don't need to change anything on the backend side, everything should work the same.

Cheers!

1 Reply
Default user avatar
Default user avatar Szymon Chomej | Victor | posted 5 years ago | edited

Hi:)
Thx for answer.

I was trying this way but i messed up with data parameter :).

In your way i had syntax error in here:

$("form : input").serialize()```

Error was: "Unrecognized input type"
i did like that:

$("form").serialize()`

and now its ok.

Unfortunatelly now in form object i dont have "clickedButton" property.
It seems that its not send with other form parameter:(

I really need it because i use few buttons in my form, each for diffrent action.
Do you know how to fix it?

I was trying send another data in Ajax.data parameter but i have syntax error no matter how i do it :(

What is the corect syntax? How to send submit button name?

...and now with ajax, flash massages dont show up.
Do you know way?

Reply

Hey Szymon,

Ah, sorry, my bad! I meant "$("form :input").serialize()", so there should not be any spaces between ":" and "input". Actually, see ":input selector" in jQuery docs: https://api.jquery.com/input-selector/ . But yeah, "$("form").serialize()" should do the trick.

OK, ok, if you have a few buttons and check for specific button click on the server side - you need a bit more work. You can inject the proper button data yourself into the serialized data like:


var serializedData = $("form :input").serialize(),
serializedData.your_submit_button_name = null;

$.ajax({
    data: serializedData,
    // other data if needed...
});

You an hardcode it because you know exactly what button was clicked since you added listeners to those buttons. If you're not sure about how the button should be added to the serializedData, try to do "console.log(serializedData);" to see how your serialized object looks like. And I think it should work.

But you know what? Probably instead of adding listeners for submit buttons you better add one listener for form submit event, see https://api.jquery.com/submit/ and probably this way you'll have the clicked button in serializedData out-of-the-box, but I'm not sure here. But in anyway, injecting button name into the serializedData manually should work well.

Cheers!

Reply
Default user avatar
Default user avatar Szymon Chomej | Victor | posted 5 years ago | edited

Hi Victor, ...its me again:)

Another problem is that each form element has name where last characters are inside square bracet like that: "dodaj_dane_procesu_suszeniaform[zapisz]" - i dont know why, it must be symfony think.

So i have error: "referenceError: zapisz is not defined at HTMLButtonElement.<anonymous>" in this line
"serializedData.your_submit_button_name..."

I think it will be easily to add listener for each submitt buttons like that:

 $(document).on('click', 'button.id', function(){ //here puting  $.ajax({}) }```


And on the server side makes seperate routes (which render the same twig ) for each action.  

What do you think? Is it good solution? Is it good programing practice in symfony?
Reply

Hey Szymon,

Ah, sorry for misleading you, I somehow though that .serialize() return JSON, but it returns string! So what you need is just to concatenate button's name to it via ampersand ("&"):


var serializedData = $("form :input").serialize(),
serializedData += '&your_submit_button_name';

But yeah, since Symfony uses field names as "form_name" + "[property_name]", you need to escape those square brackets, so in your case for "dodaj_dane_procesu_suszeniaform[zapisz]":


var serializedData = $("form :input").serialize(),
serializedData += '&dodaj_dane_procesu_suszenia_form_%5Bzapisz%5D';

You can console.log(serializedData) to see the result. So try this way.

Cheers!

Reply
Default user avatar
Default user avatar Szymon Chomej | Victor | posted 5 years ago | edited

Hey Victor.
Thank you for help.
I ended up with this code:


<script>
    //Akcja do wykonania
    var zapisz = "";
    var edytuj = "";
    var usun = "";
    
        jQuery('#dodaj_dane_procesu_suszenia_form_zapisz').click(function () {
            zapisz = true;
        });

        jQuery('#dodaj_dane_procesu_suszenia_form_edytuj').click(function () {
            edytuj = true;
        });

        jQuery('#dodaj_dane_procesu_suszenia_form_usun').click(function () {
            usun = true;
        });

    $("button[type='submit']").click(function(e) {
    e.preventDefault();
    
    var serializedData = "";
    serializedData = $("form").serialize();

        if (zapisz) {
           serializedData += '&dodaj_dane_procesu_suszenia_form%5Bzapisz%5D';
        }
         if (edytuj) {
           serializedData += '&dodaj_dane_procesu_suszenia_form%5Bedytuj%5D';
        }

        if (usun) {
           serializedData += '&dodaj_dane_procesu_suszenia_form%5Busun%5D';
        }

    $.ajax({
        url: $("form").attr('action'),
        type: "POST",
        data: serializedData,
        async: true,
                success: function (data)
                {
                    
                    if (zapisz) 
                    { document.getElementById("info").innerHTML = "<div class='alert alert-success alert-dismissable'><button type='button' class='close' data-dismiss='alert'>&times;</button><b>Sukces!!!</b>&nbsp Dane zostały zapisane.</div>"; }

                     if (edytuj) 
                    { document.getElementById("info").innerHTML = "<div class='alert alert-success alert-dismissable'><button type='button' class='close' data-dismiss='alert'>&times;</button><b>Sukces!!!</b>&nbsp Wybrane dane zostały edytowane. </div>"; }

                     if (usun) 
                    { document.getElementById("info").innerHTML = "<div class='alert alert-success alert-dismissable'><button type='button' class='close' data-dismiss='alert'>&times;</button><b>Sukces!!!</b>&nbsp Wybrane dane zostały usunięte. </div>"; }

                    zapisz = "";
                    edytuj = "";
                    usun = "";
                    
                    console.log(data)

                    $('div#raport').html(data.output);
                }
    });
});

</script>

It works like i wanted:).
Maybe it will be useful for somebody.

Cheers!

Reply

Hey Szymon,

Looks legitimate for me, well done! And thanks for sharing it with others.

P.S. Since you have jQuery, you can replace document.getElementById("info").innerHTML with jQuery('#info').html('<div>...</div>') to be consistent. ;)

Cheers!

Reply
Default user avatar
Default user avatar Szymon Chomej | Victor | posted 5 years ago

Hey Victor, I'm back :)

Now i have second form in my aplication and i also send it by Ajax.
Unfortunately Symfony keep saying me that "Token CSRF is not invalid. Please send form again".
The first form works fine.

Do you know what may cause this problem?

...I have already read everything on Stackoverflow about it and try what they say but it doesn't work in my case:(

Reply

Hey Szymon Chomej

You may be missing rendering or passing the CSRF field to your endpoint. Compare the inputs that you send in your first form.
Btw, you can disable CSRF protection if you need to. If you are already in a secured area, removing the CSRF protection may save you some headaches

Cheers!

Reply
Default user avatar
Default user avatar Szymon Chomej | MolloKhan | posted 5 years ago | edited

Hi.
The CSRF input field is on it's place, it was first what i checked.
It turns out that this form it's not submiting and i don't know why.
The code is exactly the same like in my first form:

<script>
     $("#dodaj_info_dodatkowe_form_dodajInfoDodatkowe").click(function(e) {
    e.preventDefault();

    jQuery('#info').html('<div id="loader_wraper"> <img id="loader" src="{{ asset('images/loader.gif') }}" /> </div>') 
        
        var serializedData = "";
        serializedData = "{{ path('tworzenieRaportuSuszenia')|escape('js') }}";
        serializedData += '&dodaj_info_dodatkowe_form%5BdodajInfoDodatkowe%5D';
        console.log(serializedData)

       $.ajax({
        url: "{{ path('dodajInfoDodatkowe')|escape('js') }}",
        type: "POST",
        data: serializedData,
        async: true,
            beforeSend:function(data)
            {
                 var formularz = $('#dane_dodatkowe_form').serializeArray();
                 console.log(formularz)
                     $.each(formularz, function(i, field)
                     {
                        if(formularz[i].value == "")
                        {
                           var dlugosc = formularz[i].name.length-1;

                          jQuery('#info').html('<div class="alert alert-warning alert-dismissable"><button type="button" class="close" data-dismiss="alert">&times;</button><b>Uwaga!!!</b>&nbsp Wypełnij puste pole: <b>'+formularz[i].name.substring(26,dlugosc)+'</b>.</div>')

                          xhr.abort();   
                        }
                    }
                    );
            },
            error: function (data)
            {
                 jQuery('#info').html('<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert">&times;</button><b>Uwaga!!!</b>&nbsp Wymiana danych z serwerem nie powiodła się. Błąd połączenia AJAX. Zgłoś problemy z aplikacją do jej administratora. </div>') 
            },
            success: function (data)
            {
                code
            },
            complete: function()
            {
                 $("#loader_wraper").hide();
                 
            }
    });


});
</script>```


And this is in my controller:
    $form_info_dodatkowe = $this->createForm(dodajInfoDodatkoweFormType::class);
    $form_info_dodatkowe->handleRequest($request);

    if ($form_info_dodatkowe->isSubmitted() && $form_info_dodatkowe->isValid())
      {

         $wiadomosc = 'Form dziala';
         $info2 = ['info2' => $wiadomosc];
         return new JsonResponse($info2); 
     }
      if ($form_info_dodatkowe->isSubmitted() && !$form_info_dodatkowe->isValid())
        {   
            $wiadomosc = 'Zle dane';
            $info = ['info' => $wiadomosc];
            return new JsonResponse($info);      
        }


I see all data in console but it mising in $form_info_dodatkowe object so it wasn't send by Ajax :(

Do you have any idea why this doesn't work?
Reply

Try serializing the data like this:

serializedData = $("form").serialize()```

So you don't lose any form field

Cheers!
Reply
Default user avatar
Default user avatar Szymon Chomej | MolloKhan | posted 5 years ago

Yes, that was it :)
...i don't know why i have not noticed it, probably i look to much on it :)

Thx for help.

Cheers!

Reply

No worries, for next time you already know where to look ;)

Reply
Default user avatar
Default user avatar Juan Nicolás | posted 5 years ago

Hello,
And if I don't want to save the data into my DB, how can I pass the data into another controller?

For example, I have "number1 = 1" and "number2 = 2", on my next controller I need to sum them and show the result. Nothing with a DB.

What is the correct process to do something like that? Thanks again.

Reply

Hey Juan Nicolás

In that case you can pass them as *query* parameters of the URL (e.g. site.com/your/route?number1...
If that doesn't work as you want, there are other options, like saving it in the filesystem or in the Session, just like the flash message

I hope it helps you :)

Cheers!

Reply
Default user avatar
Default user avatar Juan Nicolás | MolloKhan | posted 5 years ago

Thanks for answer me Diego (at two questions :) I did can't sleep thinking about Symfony. Too much to learn.

Have a nice day!

Reply

haha, it have happened to me too (Symfony punching your brain while sleeping) :)

Reply
Default user avatar
Default user avatar Terry Caliendo | posted 5 years ago

Towards the end you you do a for loop on the flashbag in twig on the "success". Does this mean you can have multiple calls in your controller to submit to the same "success" key without overwriting the last?

Reply

Very astute Terry! That's exactly it: each "key" in the flash bag is actually an array that can hold many values. Most of the time, you don't need this, but it's built with that flexibility. The only time I've ever relied on this is a few times when I've used the flash bag to send custom analytic information. When the user takes an action, I would record it in a special place on the flashbag. Then, the next time a full HTML page was loaded, we would read those flash messages and render some custom JS to send that info to the analytics service. In this case, it was possible that the user would take *multiple* actions, before the next full page load... and that was totally fine :).

Cheers!

Reply
Default user avatar
Default user avatar Terry Caliendo | weaverryan | posted 5 years ago

Got it. Thanks.

Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 5 years ago

Everything works and is AMAZING! But I don't get autocompletion in the for loop at twig.

if I type "app". <- "app" will be autocompleted
but everything after app. nothing is shown, I get the "no suggestion" message

Do you know why this could be happening?
In the video every option of app.session.flashBag.get('success') could be autocompleted.

Reply
Mike P. Avatar

One more note:
In the video, "app" gets autocompleted by Symfony\Bundle\FrameworkBundle\Templa.. but in my case app gets autcompleted by \Symfony\Bridge\Twig\AppVariables

And of course Ive installed the symfony plugin, on every video before this one autocompletion worked as expected

Reply

Hey Mike P.

Which version oh PHPStorm are you using ? You might need to upgrade it, aswell the Symfony plugin
Have you checked your settings for the Symfony plugin ? settings->language&frameworks->symfony - make sure all the checkboxes related to twig are checked

Cheers!

Reply
Mike P. Avatar

Hey Diego, I've reviewed everything and even deleted the cache of the project, doesn't seem to work.

Here are all infos for you:

Repository from me:

https://github.com/emovere/...

Pictures of PHPStorm Settings you requested:

http://imgur.com/a/LPrMV

I really hope you can help to get the autocomplete working for TWIG app.

I assist you with everything you need.

Reply

Hmm, look's like your configuration is correct
Try clearing index in symfony plugin details and exclude from your project "var/cache" folder
Are you on Windows ?

Reply
Mike P. Avatar

Even after a click on "clear index" and wait until it is completed, the same behavior. app.$x doesn't autocomplete. (Well it does kind of but not the way it should be, see the pictures: http://imgur.com/a/LPrMV )

Its strange because things like error.$x does get autocompleted.
Iam on macOS 10.12.6.

Reply

Yo Mike P.!

Hmm, interesting. So, in general, auto-completion in Twig is not rock solid. Usually, it's because you're using some variable in a template... and since PhpStorm doesn't 100% know which controller is rendering that template (it tries to guess), sometimes it may not auto-complete. And even when it does know what variables are available, it doesn't always know what type those variables are (this goes back to adding good @return statements, etc).

But your situation is more mysterious... which is never a good thing ;). I can see that when it's auto-completing the global App variable, it knows that this is an AppVariable object! And so therefore, it should be smart enough to know that any get* methods on that class (e.g. getSession) can be accessed via the app.session key. And actually, I just tested with the latest versions of everything, and I also don't get this auto-completion :/. It seems like it's some bug in storm or the plugin. I don't know if that will make you feel better or worse ;). Hopefully it will resolve itself.

Cheers!

Reply
Mike P. Avatar

It makes me feel better, because I know I haven't overseen anything! Many thanks for your AWESOME LIGHNING FAST comments / help, iam really feeling more dangerous and have just started 2 weeks ago. You're awesome!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice