In general, your error catching mechanic is correct, because I do not really understand the criticism of your friend. It would be great to look at his decision.
Now essentially. Actually, the class error handler:
class ErrorHandler { protected $format = '{{message}} {{class}}::{{method}} {{file}} on line {{line}}'; /** * @var HandlerInterface */ protected $displayHandler; /** * @var integer the size of the reserved memory. A portion of memory is pre-allocated so that * when an out-of-memory issue occurs, the error handler is able to handle the error with * the help of this reserved memory. If you set this value to be 0, no memory will be reserved. * Defaults to 256KB. */ protected $memoryReserveSize = 262144; /** * @var string Used to reserve memory for fatal error handler. */ private $_memoryReserve; /** * Register this error handler. */ public function register() { // Catch errors set_error_handler([$this, 'handleError']); if ($this->memoryReserveSize > 0) { $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize); } // Start buffer ob_start(); // Catch fatal errors register_shutdown_function([$this, 'handleShutdown']); } /** * Unregisters this error handler by restoring the PHP error handlers. */ public function unregister() { restore_error_handler(); } /** * Error handler. * * @param int $code * @param string $msg * @param string $file * @param int $line * @return bool * @throws \ErrorException */ public function handleError($code, $msg, $file, $line) { if (~error_reporting() & $code) { return false; } switch ($code) { case E_USER_WARNING: case E_WARNING: $exception = new \ErrorException("[E_WARNING] {$msg}", Log::WARNING, $code, $file, $line); break; case E_USER_NOTICE: case E_NOTICE: case E_STRICT: $exception = new \ErrorException("[E_NOTICE] {$msg}", Log::NOTICE, $code, $file, $line); break; case E_RECOVERABLE_ERROR: $exception = new \ErrorException("[E_CATCHABLE] {$msg}", Log::ERROR, $code, $file, $line); break; default: $exception = new \ErrorException("[E_UNKNOWN] {$msg}", Log::CRITICAL, $code, $file, $line); } throw $exception; } /** * Fatal handler. * * @return void */ public function handleShutdown() { unset($this->_memoryReserve); $error = error_get_last(); if ( isset($error['type']) && ($error['type'] == E_ERROR || $error['type'] == E_PARSE || $error['type'] == E_COMPILE_ERROR || $error['type'] == E_CORE_ERROR) ) { $type = ""; switch ($error['type']) { case E_ERROR: $type = '[E_ERROR]'; break; case E_PARSE: $type = '[E_PARSE]'; break; case E_COMPILE_ERROR: $type = '[E_COMPILE_ERROR]'; break; case E_CORE_ERROR: $type = '[E_CORE_ERROR]'; break; } $exception = new \ErrorException("$type {$error['message']}", Log::CRITICAL, $error['type'], $error['file'], $error['line']); if (APP_LOG) { Log::log(Log::CRITICAL, $this->convertExceptionToString($exception)); } $this->display($exception); } else { if (ob_get_length() !== false) { // Display buffer, complete work buffer ob_end_flush(); } } } /** * Sets a display handler. * @param HandlerInterface $handler */ public function setDisplayHandler(HandlerInterface $handler) { $this->displayHandler = $handler; } /** * Sets a format message log. * @param string $format */ public function setFormat($format) { $this->format = $format; } /** * Sets a size memory. * @param int $size */ public function setMemoryReserve($size) { $this->memoryReserveSize = $size; } /** * @param \Exception $exception */ public function display(\Exception $exception) { // display Whoops if (APP_DEBUG === true) { if (!isset($this->displayHandler)) { $this->displayHandler = new PrettyPageHandler(); } $run = new Run(); $run->pushHandler($this->displayHandler); $run->handleException($exception); return; } die('This site is temporarily unavailable. Please, visit the page later.'); } /** * Converts an exception into a simple string. * * @param \Exception $exception the exception being converted * @return string the string representation of the exception. */ public function convertExceptionToString(\Exception $exception) { $trace = $exception->getTrace(); $placeholders = [ '{{class}}' => isset($trace[0]['class']) ? $trace[0]['class'] : '', '{{method}}' => isset($trace[0]['function']) ? $trace[0]['function'] : '', '{{message}}' => $exception->getMessage(), '{{file}}' => $exception->getFile(), '{{line}}' => $exception->getLine(), ]; return strtr($this->format, $placeholders); } }
The class can be static (i.e. contain static methods and properties). Irrelevant.
The well-known Whoops library is responsible for displaying exceptions and errors in a beautiful interface, and logging is done using Monolog (here is a light wrapper above it)
There are various nuances, for example, for catching a Fatal associated with memory overflow ( Allowed memory size of... ), you need to reserve a certain amount. Empirically, the volume was calculated in 256KB. I learned about this at a conference from Alexander samdark Makarova (evangelist and mainteyner of the yii framework). Actually, there is a similar hack in the framework handler. If you analyze the code of the yii2 handler, you can substitute other nuances associated, for example, with the handling of error / exception HHVM.
In a single entry point to your application ( index.php ) we specify the following:
defined('APP_DEBUG') or define('APP_DEBUG', true); defined('APP_LOG') or define('APP_LOG', true); $errorHandler = new ErrorHandler; $errorHandler->register(); try { // ... bootstrap Π²Π°ΡΠ΅Π³ΠΎ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡ } catch (\Exception $e) { if (APP_LOG) { $msg = $errorHandler->convertExceptionToString($e); Log::log($e->getCode() ? : Log::CRITICAL, $msg); } $errorHandler->display($e); }
In the application itself, you can catch local exceptions and do anything with them. For example, catch and immediately log:
try { // ... Π½Π΅ΠΊΠ°Ρ Π»ΠΎΠΊΠ°Π»ΡΠ½Π°Ρ Π»ΠΎΠ³ΠΈΠΊΠ° ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡ } catch (\Exception $e) { Log::log(Log::ERROR, (new ErrorHandler())->convertExceptionToString($e)); }
Anyway, any exceptions thrown will be caught in index.php and, depending on the given constants ( APP_DEBUG and APP_LOG ), are presented in a nice interface and added to the log. Naturally in production, you should definitely turn off debug mode.
As for the Whoops handlers:
Knowing in what format ( content type ) you need to give the data to the user, you can choose the appropriate handler.
For example, checking whether a request is an ajax request
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { $errorHandler->setDisplayHandler(new JsonResponseHandler()); }
Debag-information will be presented in the form of json-a, which is convenient for viewing through the browser console.
Ideally, you need to have an HTTP interlayer (Response / Request classes) and detect all this at the routing or filter / behavior level of the controller - ContentNegotiator or something like that.
PS Watch the holivar video about the error-cancellation operator @ with devconf. Very entertaining.
UPDATE
I personally in projects try to return the correct http statuses. No one bothers you to give the user a specially designed 404 page with the same status.
if (headers_sent()) { // ΠΏΡΠΎΠ²Π΅ΡΠΊΠ°: Π½Π΅ ΠΎΡΠΏΡΠ°Π²Π»Π΅Π½Ρ Π»ΠΈ ΡΠΆΠ΅ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊΠΈ return; } $version = isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0' ? '1.0' : '1.1' $statusCode = 404; $statusText = 'Not Found'; header("HTTP/$version $statusCode $statusText");
As I noted earlier, it is best to use the Response interlayer class over native functions.
With routing, this is achieved quite simply:
$route = new Route(); $route->get('/items/{id:\d+}/', ['\namespace\ItemsController', 'actionOne']); $route->post('/items/', ['\namespace\ItemsController', 'actionCreate']); //... Π΄ΡΡΠ³ΠΈΠ΅ ΠΏΡΠ°Π²ΠΈΠ»Π° $route->any('*', ['\namespace\ItemsController', 'actionNotFound']); // Ρ.Π΅. Π΅ΡΠ»ΠΈ Π²ΡΡΠ΅ΠΏΡΠΈΠ²Π΅Π΄Π΅Π½Π½ΡΠ΅ ΠΏΡΠ°Π²ΠΈΠ»Π° Π½Π΅ Π²ΡΠΏΠΎΠ»Π½ΠΈΠ»ΠΈΡΡ, ΡΠΎ Π²ΡΠΏΠΎΠ»Π½ΠΈΡΡΡ ΠΏΠΎΡΠ»Π΅Π΄Π½Π΅Π΅ ΠΏΡΠ°Π²ΠΈΠ»ΠΎ.
For an ajax request, it suffices to specify a header without body in the response:
$route->any('*', function(Route $route) { $route->response->status404(); return $route->response; });
403 - if access to a resource (at some URL) is denied for an unauthorized user or a user with a different role / rights, for example, not having administrator rights. See RBAC . In yii, a similar mechanism is implemented through behaviors / filters to controller actions.
201 - resource successfully created. For example, you can issue when a user is registered, a comment or post is created.
204 - the query to the database completed successfully, but for some reason there is no data. For example, a new section has been opened on the site, but articles have not yet been written for it.
I use 422 if data validation, for example a form, has not passed. In this case, through $statusText (see above), an explanation can be given: Validation failure .
429 is a classic rate limiter. Most often used for REST API. Upon reaching a certain limit on the number of requests to issue this status.
302 - with redirect. For example, if the data received from the form is correct and the record in the database is successfully made, then a redirect to a third-party page will be generated, or the current page will be updated with the given status.
500 - if you could notice, then ErrorHandler has a die('This site is temporarily unavailable. Please, visit the page later.'); . Instead of this "fresh" record, you can give the user a static page-stub by specifying the http status 500.
die(file_get_contents('/path/to/stub.html'));
To implement your RESTful API, specifying the correct http-methods, statuses, headers (read about HATEOAS ) is mandatory.
Full list of statuses
Thus, in debug mode, you will see Whoops with a stack trace, and a production stub will be displayed to the user (production server). Log or not errors / exceptions at your discretion. For example, in many frameworks, by default, information logging (level Log::INFO ) is performed, i.e. each connection to the database, the success of transactions, user authorization, etc., that your drive on the server can do so well. It is necessary to increase the level of error logging to Log::WARNING or Log::ERROR , as well as use regular utilities for log rotation. For example, in Linux, this is logrotate .
If I touched on a lot of topics here, then let me advertise my open source libraries:
- Rock Route - routing with flexible rules, grouping (dividing rules into spaces / modules for, for example, ajax, backend / admin, etc., as in laravel) and REST support. The algorithm for implementing lightweight regexp-patterns was taken from the Nikita Popov's FastRoute library (included in the core team of PHP). Detailed documentation is available.
- Rock Response - fork yii2 response, which is completely separated from the framework. Unfortunately, there is no documentation , but there is official documentation yii . True, too, while short.
As we know, yii2 is a monolithic framework.
- Rock Request is a similar fork, but with my filtering library this one .
All of these libraries are without any extra dependencies - everything is only the most necessary for their work.