test.ical.ly | getting the web by the balls

Sep/10

10

symfony Workshop – How to handle a form from a component in an action

In yesterdays post I boldly proclaimed saving symfony forms in dedicated actions rather than in components as a best practice.

A comment by Francisco then pointed out that this way the validation errors go missing when redisplaying the form.

So I had to get my hands dirty and show some code.

First of all lets see what we want to achieve.

  • There must be a component displaying a form that can be included in any template.
  • After submitting and handling the form the original page must be loaded again.
  • The form input must be handled (validation and saving) in an action to avoid processing not handling related code.
  • The data input by the user must be saved if validated.
  • The form must show the errors if the validation failed.
  • After successful saving a page reload must not trigger another saving attempt.

I think that’s all. So lets see how to achieve that.

I start with a very simple schema definition in order to have a form generated with a validation that can easily be failed during testing.

Next lets have a look at the component that renders the form.

In this component you see that first we create a form instance and then set a variable with the current URL unless it is set on the request already (you will see in a moment what this is for).

In case the current request is a POST request we bind the form values from the current request to the form instance (line 11-14). This is necessary for the form instance to know about any validation errors.

The result of this component is the following partial template.

There is not much to explain on this template except that its action points to the route that calls the saving action and that we manually add a GET parameter redirect_to to it containing the URL of the originating page (This is quite useful to keep form data and redirect URL separate).

When the user submits this form it is sent to the following action.

The code on the lines 9-18 should be fairly familiar to you. The form instance is created, values are bound to it and if it is valid the form will be saved (I could have checked for the current request being a POST request but I’m assuming this is added as a requirement to the route definition).

When the form is successfully saved a redirect to the passes redirect_to URL is issued loosing all POST data and sending the originating page to the users browser.

If the form did not validate a redirect would not help as then all validation errors would be lost (see Franciscos comment). So instead we want to do a forward keeping the current POST data.

The forward is a bit more tricky than a redirect as it does not accept a URL but only a module/action pair.

So in line 20 we strip any scriptname (i.e. /frontend_dev.php) from the redirect_to URL. In line 21 we get a route definition matching this URL and in line 23 we do the forward.

Update: And on line 22 we add the originating pages route parameters to the current request so that the page has all information it needs and previously had.

In my experiment I used nothing but the default routes (/:module and /:module/:action) and still the forward went to the correct module/action. So no worries.

And that’s it. All the above requirements are met. Now you can include the above form component in any template you like and it will (fingers crossed) just work. :)

· · · ·



  • Pingback: • Why you should not handle your symfony form inside a component | test.ical.ly

  • http://www.fizyk.net.pl fizyk

    So… the line 23 on the last gist will take care of all route parameters we want to forward to?

  • http://test.ical.ly Christian

    @fizyk When you do a forward instead of a redirect then there is no more bootstrap and no new sfWebRequest instance. It’s almost as if only calling another action.
    So by adding the route parameters of the redirect_to url to the current sfWebRequest instance they will be available for the forwarded action.

  • http://www.creatio.com.au Daniel

    This is a really useful post, thanks for this! I’ve always done it the “contained in component” way. I knew there was a performance overhead, but it felt easier to maintain all being in the one spot and there was a bit of laziness on my part in not looking for a proper solution. This looks like it will work well.

  • http://test.ical.ly Christian

    @Daniel actuaally it’s not only a performance issue. Think about code that performs write actions like view counters and things like that. These would change your data with no need distorting it.

  • bender

    Christian, I applaud you for trying to solve this problem and it would be absolutely excellent if it worked but after trying to recreate what you did here precisely to the T, it did not work correctly.

    When the form was invalid I found that the error messages were not saved, a csrf error occured and the following page is realoaded without a csrf token so you cannot “fix” the error and resubmit. You actually have to go back to the homepage and try again.

    FYI: In the 3rd Gist instead of:
    url_for(‘petition_save’);
    I think you meant:
    url_for(‘petition/save’);

    Is there any way we can get this to work?

    Thanks for your efforts.

  • http://test.ical.ly Christian

    @bender I just emailed you a simple sf project where I do not experience the issues you describe. Can you give feedback on this please?

  • http://test.ical.ly Christian

    @bender and no I meant petition_save assuming that a route of that name exists. ;)

  • Pat

    Hello,
    Thanks for this tutorial!
    I seem to be having the same problem that Bender was. Was this ever solved?

  • ceieneka

    Same for me

  • http://www.freethinkingdesign.co.uk Dan Crack

    Christian: Thanks for the post, very useful.

    @bender: I came across the same csrf issue but fixed it by re-appending the form parameters to the request after it was cleared… (NB I’m unsure if the clear is required but it seems sensible)

    Code I replaced the last 5 lines with used below:

    $redirectTo = str_replace($request->getScriptName(), ”, $redirectTo);
    $route = $this->getContext()->getRouting()->findRoute($redirectTo);
    $formParams = $request->getParameter($this->form->getName());
    $request->getParameterHolder()->clear();
    $request->addRequestParameters($route['parameters']);
    $request->addRequestParameters(array($this->form->getName() => $formParams));
    $this->forward($route['parameters']['module'], $route['parameters']['action']);

  • http://www.freethinkingdesign.co.uk Dan Crack

    Also just as a random aside, Christian your text is HUGE in my browser even on the lowest ‘A’ setting…

    Might be worth providing a smaller size by default?

  • http://test.ical.ly Christian

    @Dan what browser do you use? For me it’s working just fine in Chrome, Firefox, IE and Safari under both Windows and Mac..?

  • Michael

    What’s the purpose of this code inside the form action found inside the component? Since components can’t be accessed from outside I can’t see what this piece of code does.

    if($request->isMethod(‘post’))
    { $this->form->bind($request->getParameter($this->form->getName()), $request->getFiles($this->form->getName()));
    }

    Thanks for the article by the way. It was really helpful.

  • http://test.ical.ly Christian

    @Michael this code is responsible for the whole form handling which only makes sense if there is POST data.

  • Michael

    Sorry. I hadn’t looked at the code quite as thoroughly. The problem in my case is that I have a search form loaded as a component as part of the layout. Whatever page you’re visiting this form is always there. So let’s say I visit my login page where there is a login form which is validated when you send a post request to the same url that loads it.

    So after typing the login url I essentially have two forms one from the layout and the other as part of the login view. So the problem arises when I fill out the search form (the one part of the layout) and click submit. Both forms end up being validated as ultimately the page i’m on will be loaded with a post request.

    Another problem that could arise is when I load a url with a route that has a requirement of “sf_method: get”. After loading a url with such a route if I use the search form then it won’t work because it’ll try to load the page I’m on with a post method.

    What do you think? Is there a workaround to this problem?

  • http://test.ical.ly Christian

    @Michael technically speaking you would need to find a way to disinguish between the forms that got submitted. I.e. you could do that by different values on your submit button so the controllers can say if it’s not mine I’ll ignore it.
    BUT a search form will always end up with a search result list right? So you should post to a dedicated route anyway.
    If that’s not possible because the results have to end up on the exact same page they got searched for you should post to a dedicated route anyway passing the URL to which your action should redirect after form processing.

  • Michael

    Thanks for the quick reply.

    Thanks for the suggestion pertaining to distinguishing between different forms. That solves part of the problem.

    “BUT a search form will always end up with a search result list right? ”

    This doesn’t always happen in my case. The search form may fail validation sometimes. In that case I don’t want to redirect. That’s the source of my problems. I can’t seem to find a way to give validation error feedback if the URL the user is currently on works only with a get method since the page has to be loaded with a post method for the validation to work. I can create a dedicated route like you said for the search results showing “No Results” as a response when validation does fail, but I only want to do that as a last resort.

    What do you think?

  • http://test.ical.ly Christian

    @Michael do the redirect in that case and set your validation error as a flash variable?

  • Michael

    Thanks for all the help

  • Kejml

    Wonderful, I just googled this and it solved all my problems, thanks!

<<

>>

Theme Design by devolux.nh2.me