Symfony Odyssey

A lesson in debugging and a reference that hopefully never gets used

Every developer probably knows the pain of debugging software he didn't write, in a framework he doesn't know with a bug that is defying all logic. How to solve this kind of problem? Clearly with a very systematic approach and a sprinkle of creativity and intuition.

Yesterday, I ran into this kind of situation with one of the websites I'm working on. The framework in question is Symfony 1.4 which has long been eclipsed by a newer version but unfortunately isn't upgradeable without a rewrite. This means for the moment, we are stuck with it.

It has bugged me for a long time, that we didn't have a proper 404 page. The site even showed the stack trace which isn't exactly user friendly nor particularly good for security. So that had to change.

Stack traces make our users feel uneasy

Symfony has some proper mechanisms for handling 404's so normally, this shouldn't be too difficult. Normally.

In the end, it took almost a day. And as usual, in the end, the solution was simple and came in a sudden moment of inspiration. So here, without further ado the little walkthrough, of Symfony 1.4's 404 error handling.

Starting in settings.yml. This is the file, where a custom 404 page is normally defined.

We have to configure the module and the action for the 404 like this (for the production environment):


prod:
  .actions:
    .error_404_module:   tools
    .error_404_action:   show404
…

This means, we now have to create a tools module which contains an executeShow404 function. This looks like this:


// actions.class.php

class toolsActions extends sfActions
{
  public function executeShow404(sfWebRequest $request)
  {
  }
}

Leaving the function empty is OK in our case. This just means that Symfony is going to display the show404Success.php template. The folder structure looks like this for the complete module:

This is The Standard Way™ to do this in Symfony 1.4 and that's where the trouble started. Because the page just wouldn't show up. What a great occasion to learn the inner workings of an obsolete technology. It feels a bit like archeology.

So, let's first be sure, that we are really in the production environment. Just echo the proper variable in one of the working templates like this:


<?php
    echo sfConfig::get('sf_environment');
?>

This showed prod. OK.

Next step, let's see where the error is thrown and what happens within the framework for such cases:

Let's have a look at the last "non-framework"-function that was implied BaseaTools.class.php look suspicious as it is part of a plugin.


// BaseaTools.class.php

 static public function validatePageAccess(sfAction $action, $page)
    $action->forward404Unless($page);
…

Nothing all too special. The stack trace shows that the $page variable is null which would be normal when an invalid URL is called. Also, forward404Unless() seems to be a standard symphony method. So it had to be something with the configuration (as we can reasonably presume that bugs in symfony are seldom). Let's have a look at the symfony inner workings to understand the configurations that are considered in the process:


// sfAction.class.php

  public function forward404Unless($condition, $message = null)
  {
    if (!$condition)
    {
      throw new sfError404Exception($this->get404Message($message));
    }
  }

Throwing the regular 404 exception as the condition false (respectively null) when there is no page. Perfectly sensible. But it's still not clear where the configuration finally gets into play and why it isn't applied properly. So on we go to sfError404Exception which is found in the exception folder:


// sfError404Exception

  public function printStackTrace()
  {
    $exception = null === $this->wrappedException ? $this : $this->wrappedException;

    if (sfConfig::get('sf_debug'))
    {
      $response = sfContext::getInstance()->getResponse();
      if (null === $response)
      {
        $response = new sfWebResponse(sfContext::getInstance()->getEventDispatcher());
        sfContext::getInstance()->setResponse($response);
      }

      $response->setStatusCode(404);

      return parent::printStackTrace();
    }
    else
    {
      // log all exceptions in php log
      if (!sfConfig::get('sf_test'))
      {
        error_log($this->getMessage());
      }

      sfContext::getInstance()->getController()->forward(sfConfig::get('sf_error_404_module'), sfConfig::get('sf_error_404_action'));
    }
  }
}

Finally, we find our good friends, the sf_error_404_module and sf_error_404_action. But they are wrapped in an else clause. Having a look at the corresponding if reveals another configuration option: sf_debug. This options seems to be set to true and looks like a likely culprit.

It seems, that all the configuration variables are prefixed with sf_ when used in PHP. With some clever deduction, we can guess that we should be able to set the debug option in settings.yml which finally looks like this:


prod:
  .actions:
    .error_404_module:   tools
    .error_404_action:   show404
  .settings
    .debug: false
…

Sure enough, we get a beautifully customized 404-page:

Naturally, the odyssey was a bit more complicated than that with lots of Googling around and dead ends. So who knows, maybe this is helpful for someone and helps them avoid some dead ends and Google requests.