I ran into the problem of using the following combinations in PHP:
- A custom classloader "ClassLoader" implemented as a singleton and registered with spl_autoload_register, which requires that require_once include classes. Nothing special, just some ways to organize classes on disk.
- The database class DB extends the mysqli class. It is created from a factory that currently knows only one instance. It does a bit more than loading the correct configuration and offers some quick access methods.
- The custom error handler "ErrorHandler", implemented as a singleton, which used the DB class to register notifications and warnings in the database.
All of this works great; classloader loads classes, the DB class correctly executes queries, and the error handler logs errors in the database.
... Except when an error occurred before all database operations were called. In this case, PHP crashes; no error messages or anything else, not even any echo or var_dump codes, not even 4xx or 5xx. The browser simply reports that nothing was received.
There are a number of fixes that I found:
- Do not extend the mysqli class in my "DB" class; this makes it non-functional, but it seems that the mysqli extension is causing the problem.
- Download your own DB class, either globally or at the top of my ErrorHandler.php file.
- Explicitly loading the "DB" class with require_once just before getting an instance inside the ErrorHandler class.
I can still autoload other classes inside ErrorHandler, but the moment I create the instance of "DB", PHP seems to crash.
From what I see, the only reasonable explanation is that something in mysqli has problems with being created for the first time inside the ErrorHandler class, since all the fixes that work seem to share the scope aspect, but this doesn't seem to make any sense.
Does anyone know what is going on here?
ClassLoader ...
class ClassLoader { private $_paths = array(); private function __construct() { // ... bunch of $this->append() calls with all paths and 3rd party libs } private static $_instance = null; public static function get() { if (self::$_instance === null) { self::$_instance = new self(); } return self::$_instance; } public function append($path, $format = '$.class.php') { if (!array_key_exists($path, $this->_paths)) { $this->_paths[$path] = $format; return true; } return false; } public function autoload($class_name) { foreach ($this->_paths as $path => $format) { $file = $path.'/'.str_replace('$', $class_name, $format); if (file_exists($file)) { require_once($file); return true; } } return false; } } $autoloader = ClassLoader::get(); $autoloader->append(dirname(__FILE__).'/classes'); spl_autoload_register(array($autoloader, 'autoload'));
DB ...
class DB extends mysqli { private static $_instances = array(); public static function get(Config $config) { $hash = md5(serialize($config)); if (!array_key_exists($hash, self::$_instances)) { self::$_instances[$hash] = new self($config); } return self::$_instances[$hash]; } private $_prefix = ''; private $_die = false; public function dieOnError($die) { $this->_die = $die; } private function __construct(Config $config) { parent::__construct( $config->host , $config->username , $config->password , $config->database ); if ($this->connect_error) { _report_error($this->connect_errno, $this->connect_error); } $this->_prefix = $config->prefix; } }
A configuration is a singleton with some public properties.
Errorhandler
class ErrorHandler extends Object { /* * Strip recursion problems in the backtrace */ private static function _filter_backtrace($array, $depth = 0) { $result = array(); foreach ($array as $name => $value) { switch (gettype($value)) { case 'object': case 'unknown type': case 'resource': break; case 'array': //$result[$name] = self::_filter_backtrace($value); break; default: //$result[$name] = $value; } } return $result; } private function _handle_db($errno, $errstr, $errfile, $errline, $backtrace) { $db = DB::get(Config::get()); $db->dieOnError(true); // prevents infinite loops in error handler // DB query here $db->dieOnError(false); // for non-dying. } private function __construct() { } private static $_instance = null; public static function get() { if (self::$_instance === null) { self::$_instance = new self(); } return self::$_instance; } public function handle($errno, $errstr, $errfile, $errline) { // No error? Return without reporting if (!($errno & error_reporting())) { return; } // Type of error switch ($errno) { case E_NOTICE: case E_USER_NOTICE: $errors = "Notice"; break; case E_WARNING: case E_USER_WARNING: $errors = "Warning"; break; case E_ERROR: case E_USER_ERROR: $errors = "Fatal Error"; break; default: $errors = "Unknown"; break; } //$backtrace = self::_filter_backtrace(array_shift(debug_backtrace())); $backtrace = array(); switch (Config::get()->error_log) { case 'db': ErrorHandler::_handle_db($errno, $errstr, $errfile, $errline, $backtrace); break; default: // Dump if (ini_get("display_errors")) { printf("<br />\n<b>%s</b>: %s in <b>%s</b> on line <b>%d</b><br /><br />\n", $errors, $errstr, $errfile, $errline); } // Log if (ini_get('log_errors')) { error_log(sprintf("PHP %s: %s in %s on line %d", $errors, $errstr, $errfile, $errline)); } break; } // Exit/return strategy switch ($errno) { case E_ERROR: case E_USER_ERROR: die(); break; } return TRUE; } }