On a PHP webapp using the Zend 1 framework, if the code throws an exception, I get a nice error page with my branding etc.
If the code encounters a PHP fatal error (e.g. "method call on non-object" when an object reference is unexpectedly null), then I just get a bare Apache 500 error page.
How can I get a nice error page in the latter case?
Things I have tried:
- If the PHP ini "display_errors" is set, then I get just the fatal error message as plain text
- If "display_errors" is not set, then I get the Apache default 500 error page
- The Apache "ErrorDocument" directive seems to be ignored in this case
In case this helps anyone I was able, via a bit of hackery, to get the full Zend error page (with application layout) to display in the case of fatal errors.
Hopefully there is a much easier way to do this. If so, please add an answer with your alternative.
Add this to the bootstrap:
/**
* Sets up a register_shutdown_function hook to give a nice error page when a
* PHP fatal error is encountered.
*/
protected function _initFatalErrorCatcher()
{
register_shutdown_function(array($this, 'onApplicationShutdown'));
}
public function onApplicationShutdown()
{
$error = error_get_last();
$wasFatal = ($error && ($error['type'] === E_ERROR) || ($error['type'] === E_USER_ERROR));
if ($wasFatal)
{
$frontController = Zend_Controller_Front::getInstance();
$errorHandler = $frontController->getPlugin('Zend_Controller_Plugin_ErrorHandler');
$request = $frontController->getRequest();
$response = $frontController->getResponse();
// Add the fatal exception to the response in a format that ErrorHandler will understand
$response->setException(new Exception(
"Fatal error: $error[message] at $error[file]:$error[line]",
$error['type']));
// Call ErrorHandler->_handleError which will forward to the Error controller
$handleErrorMethod = new ReflectionMethod('Zend_Controller_Plugin_ErrorHandler', '_handleError');
$handleErrorMethod->setAccessible(true);
$handleErrorMethod->invoke($errorHandler, $request);
// Discard any view output from before the fatal
ob_end_clean();
// Now display the error controller:
$frontController->dispatch($request, $response);
}
}
You need a custom router class to help:
class My_Router_Rewrite extends Zend_Controller_Router_Rewrite
{
public function route(Zend_Controller_Request_Abstract $request)
{
// If the ErrorHandler plugin has stashed the error in a request param, then
// it will have already dealt with routing (see Bootstrap::onApplicationShutdown())
// (Note that this param cannot be spoofed, since any user-supplied params
// will be strings, not objects)
if (is_object($request->getParam('error_handler'))) {
return $request;
} else {
return parent::route($request);
}
}
}
(make sure that this class is registered as your router in the bootstrap)
A solution similar to previous but without needing of rewrite router.
In Bootstrap.php:
public function __construct($application) {
parent::__construct($application);
// handle PHP errors
Core_Error_Handler::set();
}
The Core/Error/Handler.php class:
class Core_Error_Handler {
public static function handle() {
if(!$error = error_get_last()) {
return;
}
$frontController = Zend_Controller_Front::getInstance();
$request = $frontController->getRequest();
$response = $frontController->getResponse();
$response->setException(new Exception("Fatal error: $error[message] at $error[file]:$error[line]",$error['type']));
// this clean what is before the error
ob_end_clean();
$frontController->dispatch($request, $response);
}
public static function set(){
register_shutdown_function(array(__CLASS__, 'handle'));
}
}