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.
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.