Saturday, August 23, 2008

Getting started with Symfony

I have occasionally been asked (and accepted) to make websites for some of the activities I am involved with. As they all generally based on devoted volunteers and runs on a limited budget, has they usually also been hosted on budget hosting services. This means that I have usually been restricted to coding in PHP rather than Java or other high-end languages.

The last couple of times has the projects been about putting together sign-up systems for Solarisløbet (a crazy Danish scout race). And although I had a foundation to start out from, has the request always been so late, that it was a race to get something decent put together before the deadline - although I didn't have to start from scratch. There has always been some things missing - mainly because everything was build up from the bottom. Which - together with that I don't like PHP - made it a pretty unfulfilling experience. After having delivered the last system, I decided to find a way to be able to deliver quality on time. I figured, that what I needed was a good framework. In Java I would probably have used Struts or something similar. But I didn't know any frameworks for PHP.

After a bit of digging around, I finally decided on Symfony. I suppose it doesn't offer a lot more or different than many others. But it seems well-supported and mature. And hopefully I won't have to write as much stupid PHP code! So I set out to set-up a development environment on my workstation.

Setting up Symfony

Normally I would I have tried and mimic the production environment as closely a possibly. But as I am currently without a laptop or other suitable personal computer, so I didn't really want to install linux on the box - might be lack of confidence in my linux administration skills or just because people keeps sending MS Office documents and expect the receiver to be able to read them(!). After a recommendation from a friend, I decided to try out the WAMP Server. The process is based on the installation guides, but with a couple of twists for a cleaner install. Here is a quick breakdown of the process. I will give more details below:
  1. Install WAMP
  2. Install PEAR
  3. Update your %Path% - to be able to call pear and symfony directly on the command line
  4. Install Symfony
  5. Create Symfony project
  6. Update Apache virtual hosts
  7. Go wild! ;-)

1. Installing WAMP

Should be trivial - just follow the wizard. Only catch is that you need to enable mod_rewrite yourself the first time you start the server. Just click in the tray-icon and select Apache->Apache Modules->rewrite.

2. Installing PEAR

There is a bat-file in the php-directory. Execute it and hit enter each time it ask you something. Finally it makes a reg-file to update the Windows registry.
To be sure you have all the latest versions use the command:
> pear upgrade-all

3. Update your %Path%

To be able to run pear and symfony from the command-line, they need to be in your %PATH%.
I achieve this by simply changing the variable. I really prefer this simple approach to the messy one used in the Symfony on Wamp-guide, as it a lot cleaner and thus less error-prone.
To do it, simply open Control Panel -> System (or Properties on My Computer) -> Advanced -> Environment Variables. Find the Path variable and add C:\wamp\bin\php\php5.2.6; to the end of it. If it is not already there, just add it. I prefer to have it user variable, but that is mostly a matter of taste.
You have to restart command prompts for the change to take effect.

4. Installing Symfony

Open a command prompt and type in the two commands:
> pear channel-discover pear.symfony-project.com
> pear install symfony/symfony


5. Create Symfony project

The next two steps are as described in the Symfony book, but (of course) with a little twist. But that is first in the next step.

6. Update Apache virtual hosts

I am sort of a neat-freak. So rather than adding the virtual host directly in apache/conf/httpd.conf, I prefer to do it in apache/conf/extra/httpd-vhosts.conf.
To use this configuration file uncomment the line Include conf/extra/httpd-vhosts.conf in apache/conf/httpd.conf.
Then go to conf/extra/httpd-vhosts.conf and remove the dummy VirtualHost directives and add a VirtualHost directive for your project(s). It should look something like this:
<VirtualHost>
ServerName localhost
DocumentRoot "[path]"
DirectoryIndex index.php
Alias /sf "C:\wamp\bin\php\php5.2.6\data\symfony\web\sf"
<Directory>
AllowOverride All
Allow from All
</Directory>
<Directory>
AllowOverride All
Allow from All
</Directory>
</VirtualHost>
The path to your project should be absolute.

7. Go wild! ;-)

You are now ready to start building your exciting Symfony-powered application!

This is of course only the start. Now I have to design the application and figure out how to express it within the Symfany framework. But that might be another blog-post... :-)

Saturday, July 5, 2008

Maintaining form values on browser "back" in a multi-page forms

I have lately been working on a sign-up page for Solaris. The page had to be developed in PHP, which is not my favourite language - I prefer Java. But as it is, what both Solaris' hosting provider and the university uses, I at least could develop on the university and then deploy to our hosting provider. However, I am still not very confident at programming PHP. I did however manage to overcome most of the difficulties I encountered - but one remains and it really bugs me!

I have designed the page as a multi-page form with four steps. In the two first the user enters information, then there is a review step to let the user verify that the entered information is what they intended, and finally they get a confirmation on submitting of the info.
On the back-end I use a PHP session to maintain state between the various pages. And data is submitted as POST requests.

Now, the problem arises when the user uses her browsers "back" button to get back to page 2. How it manifests itself varies a bit between browsers. Firefox will simply display the page, as it looked when you last visited - without any of the entered data. And Internet Explorer will tell you that the page is expired. In both cases can the missing data be fixed by reloading the page. Requiring the users to do that is obviously not really a solution. (Both POST and GET would work.)

Even a quite intensive Google-session did not turn up anything of any real value, but did however provide a bit of insight. For instance - I am not the only one, who is annoyed by this. One of the more common approaches I encountered was to try and disable the back button by various means. E.g. open the form in a window without tool bars and similar trickery - none of which really did anything to remedy the problem, but would encumber the web page and thus the user experience. And as a computer scientist it would really hurt my sense of professional pride.

But I suppose a little analysis is in order, as it might not be entirely obvious what is going on here.
The problem stems from the semantics of a POST request - it changes the state of the server, e.g. submitting a purchase order. This should obviously not be done more than once, unless you press the "submit" button again or similar. (There might be cases where performing the same action multiple times in a row would make sense.) So what you really would want is the page to be re-requested by GET or something similar.

This leads me to first attempt to solve this, I suppose you could call it the "POST-Redirect-GET" approach. Here the main idea is that if the submitted data is accepted (passes validation, etc.) then the browser is redirected to the next page of the form, which it should request via GET.

It took a bit to come up with this solution, but putting it to use is really straight forward.
if($_SERVER['REQUEST_METHOD'] == 'POST') {
//Handle the data - e.g.

$error = !validatePost();
if($error)
//Respond with error page - with or without Redirect
else {
header('Location: ' . getTheURL());
exit(0);
}
}
So the first thing I do, is to check whether this is a POST request. I find this a handy way to figure out when I need to handle new submitted data, and facilitate this approach.
Them comes the mandatory validation part. And finally the redirect.

That's it. There is really nothing fancy to it...

I my case was it rather pointless to send a redirect in response to invalid data, but other circumstances might warrant it.

I have afterwards found numerous sites explaining this idea. My favourite example is this piece from TheServerSide.com, which has many additional points about why this is a good design pattern.