Peter Robins, his website

Setting up this site in Symfony

Phase 1: static pages

On the previous system, I was partly using a blog package (WordPress) to maintain these, with the content stored in a database rather than HTML files. For those like me who are happy using a text editor, it's debatable whether this is worth the bother, so I'll go back to maintaining simple HTML templates. Several of the separate sections used ordinary html files, so these are easy to convert to HTML templates as well.

As the site already exists and the same style is to be maintained, converting this static content is straightforward:

  1. assets (i.e. images, css, js): the default setup created by init-project can be used by simply copying the existing assets: stylesheet into web/css/main.css; images into web/images/; js into web/js/
  2. layout: the default template apps/main/templates/layout.php can be adapted by inserting the basic layout and sidebar menu from the old site, changing the links to use the link_to() helper and the asset links to use the asset helper. The breadcrumbs at the top are adapted from Richard Steven's snippet on the Symfony site (making it compatible with xhmtl amongst other things)
Modules
The static content is divided into several sections, so we'll create modules for each of those:
symfony init-module main general/it/walking/wales
Each page will have its own template containing the content but, as there is no logic involved in generating them, the only action needed is to show it. So we'll change actions.class.php for each module, replacing the default executeIndex() with:
  public function executeShow()
  {
    return $this->getRequestParameter('page');
  }
So, instead of returning 'success' or whatever, it returns the name of the page.
Meta html
apps/main/config/view.yml sets up default metatags for the head section of the html files; we'll change these to read
  metas:
    title:        Peter Robins, his website
    description:  Peter Robins, his website
    author:     Peter Robins
However, this doesn't describe the individual pages, so we'll also define separate texts for each page by setting up a view.yml for each module. For example, apps/main/modules/walking/config/view.yml might contain:
showindex:
  metas:
    title:        Walking Intro
    description:  General description of the walking part of the website
    keywords:     walking, hiking

showblisters:
  metas:
    title:        Tips on blisters
    description:  Tips on blisters
    keywords:     walking, hiking, blisters
URL routing
The default url would now be :module/show/:page but the 'show' is not necessary, so we'll change apps/main/config/routing.yml so :module/:page can be used, with general/index as the home page:
homepage:
  url:   /
  param: { module: general, action: show, page: index }
page_show:
  url:   /:module/:page
  param: { action: show }
In addition, the default for each module should be changed to show the home page:
default_show:
  url:   /:module/
  param: { action: show, page: index }
Note that /:module/ is not the same as /:module - under the normal Unix directory structure, /xxx means show me a page called xxx; /xxx/ means show me the contents of the directory xxx. In this case, we don't want a page called :module, but the default action of the module, which in our case is 'show', and the default page, 'home'.
Finally, we'll add '.html' to each page name so they look like ordinary static pages to search engines, by adding:
    suffix:         .html
to the 'all' settings in apps/main/config/settings.yml.

This structure maintains backwards-compatibility with the normal url structure that was used previously - /directory/page.html - but it does mean that symfony's default /:module/:action won't work. When we come to set up the routing for those pages that have an action other than 'show', we have to remember to include them all in routing.yml. Of course, this increases the size of the file, and the work that the routing process has to do for each request.

Templates
Now the main task is to enter the text, as a template show{pagename}.php for each page, which can mainly be transfered from the existing pages. The default indexSuccess.php files are not needed, so can be deleted. An example link would be
<?php echo link_to('static HTML','it/symfony-static') ?>
and an example image tag
<?php echo image_tag('wales/map', 'alt=map size=611x389') ?>
The email in the menu uses the mail_to helper to create a different address depending on which section of the site the user is looking at: mail_to('web_'.$sf_params->get('module').'@peterrobins.co.uk', 'Email me')
Javascript

Those javascript functions in the html head section can be put in separate js files in web/js/ and loaded by adding them to the javascript section for the page in the module's config/view.yml, such as:

  javascripts: [positionfinder, http://maps.google.co.uk/maps?file=api&v=2&key=googlekey]

Any javascript in the body can be left there, but some pages, like those using Google Maps for example, need an onload() method on the body tag. How to do this in Symfony? No doubt there are several ways, but I did it with a slot. This requires a change to:

  1. the page itself, to create the slot; conceptually this is logic, so belongs in the action, but I couldn't be bothered creating an action just for that so added it to the template:
    <?php slot('body_onload') ?>onunload="GUnload()" onload="load();"<?php end_slot() ?>
  2. apps/main/templates/layout.php, to put the slot in the body tag:
    <body <?php if (has_slot('body_onload')) echo get_slot('body_onload'); ?>>

One problem I encountered was with older javascript written for non-strict html pages. These may not work in xhtml which expects css style, for example, to conform fully with the standard. Browsers do not always provide useful error messages for this. I have often found in IT projects that you sail through the main tasks but then get held up for hours on really trivial issues. This project was no exception: the most time by far was spent with the floating Pooh page which uses a javascript script called moveobjectslib. However, once I realised what the problem was, it was quickly fixed.

Error page

Something which is often forgotten about in websites is a default error page: programmers are so busy testing for possible errors in their programs that they forget that it's very easy for people to mistype urls, or click on an ancient link on another website to a long-dead page. A 404 error page is easy to set up in Symfony:

  • uncomment the error404 lines in apps/main/config/settings.yml, changing them to, for example, general/error (you might as well change the default action while you're at it, for example, to general/index). Beware, though! This is not a url but a module/action, in other words, it expects there to be an error action in the general module
  • so, add the function to apps/main/modules/general/actions/actions.class.php
      public function executeError()
      {
        return '!';
      }
  • and create the template apps/main/modules/general/templates/error!.php, with a text like "You have tried to access a page that doesn't exist. Please select an option from the menu opposite." (I find the concept of a successful error rather illogical, so I return '!' to get the template error!.php, instead of returning the default 'Success')

Now, trying to get a non-existent module like web/grunt or web/ir should give you this error page rather than Symfony's default.

However, what if the user gets the module name right, but mistypes the page name, for example, 'general/avout', or simply tries a non-existent page like general/grant? Because we set up the routing to allow any name after 'general/', Symfony will not raise a 404 error; it will find a valid routing for the module 'general', but will then fail to find the template 'showavout.php' or 'showgrant.php'. This will raise a 500 internal server error 'template not found'. To catch these, we must set up an error500.php file in web/errors/ to override Symfony's default one. A simple way would be to have http forward to /error, a non-existent module: <meta http-equiv="refresh" content="0;URL=/error">. However, this is not a very good solution, as it assumes that all 500 errors are due to mistyped urls, and there should really be a proper 500 error page to display; create one afterwards.

A better solution is to revisit our actions and check whether the template file actually exists before passing control to the view; so change actions.class.php for all the modules:

  public function executeShow()
  {
    if (is_readable(sfConfig::get('sf_app_module_dir').DIRECTORY_SEPARATOR.$this->getRequestParameter('module').DIRECTORY_SEPARATOR.'templates'.DIRECTORY_SEPARATOR.$this->getRequestParameter('action').$this->getRequestParameter('page').'.php'))
      return $this->getRequestParameter('page');
    else
      $this->forward404();
  }

Of course, this assumes that the filenames follow the conventions. If the template file is not readable, it's an error, so we can ship it off to the 'page not found' error we created above. An alternative way to handle these erroneous urls would be to go back to the beginning of this page, and put all the page texts in a database instead of in separate templates; then, if the action finds the text, it uses it, else it's an error.

Phew! What a lot of work just to get an error report! However, better to give the user some meaningful feedback rather than an uninformative default file.

Other assets

I mentioned above the publicly available files that Symfony puts by default in web/. Any other publicly available files you have can be put here; for example, I put the source code for the Mojavi tutorial in web/downloads/.

February 2008