I want to figure out how to do it right.

Now in my code, the error handler (ErrorHandler) handles both internal script errors and exceptions that I create myself. This looks like this:

class ErrorHandler { public static function register() { ini_set('display_errors', 'on'); error_reporting(E_ALL); set_error_handler(get_class() . "::showError"); register_shutdown_function(get_class() . "::catch_fatal_error"); ob_start(); } /** * @param int $errno * @param string $errstr * @param string $file * @param int $line * @param string $header */ public static function showError($errno, $errstr, $file, $line, $header = "HTTP/1.0 404 Not Found") {/*здСсь ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡŽ ΠΎΡ‚Π²Π΅Ρ‚ ΠΎΠ± ошибкС*/} public static function catch_fatal_error() { /*здСсь ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡŽ ΠΎΡ‚Π²Π΅Ρ‚ ΠΎ критичСской ошибкС ΠΏΡ€ΠΈ ΠΏΠΎΠΌΠΎΡ‰ΠΈ всС Ρ‚ΠΎΠ³ΠΎ ΠΆΠ΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Π° self::showError*/ } } 

All application code is enclosed in a try {} block and the exception is processed using the same method:

 try{ //Π—Π΄Π΅ΡΡŒ вСсь ΠΊΠΎΠ΄ ΠΌΠΎΠ΅Π³ΠΎ прилоТСния //Π² Π½Π΅ΠΌ Π²ΡΡ‚Ρ€Π΅Ρ‡Π°ΡŽΡ‚ΡΡ Ρ‚Π°ΠΊΠΈΠ΅ выбросы ошибок: throw new \Exception("Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅ ΠΎΠ± ошибкС."); } catch(\Exception $e){ //обрабатываСтся ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΏΡ€ΠΈ ΠΏΠΎΠΌΠΎΡ‰ΠΈ всС Ρ‚ΠΎΠ³ΠΎ ΠΆΠ΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Π° класса ErrorHandler ErrorHandler::showError($e->getCode(), $e->getMessage(), $e->getFile(), $e->getLine()); } 

Questions:

  1. Is it possible to do that?

  2. What is the most acceptable way to work with errors and exceptions, if my is incorrect?

The question arose because my friend, whom I consider to be more advanced than me, says that since I cannot do it. But I can not understand why. What's wrong?

Some important points that I realized:

  1. When a fatal error occurs, it automatically falls into the output buffer. This means that we can immediately delete it and NOT create it manually beforehand with ob_start(); if, of course, we want to handle fatal errors too. What is not recommended to do.
  2. Directive ini_set('display_errors', 'on'); in my code is redundant, because I myself decide what to do with errors and I do not need to tell php what to do with them. I will decide myself. And let the rest of the basic configuration of the server.
  3. Sending a response to the user inside the error handler is valid. Because other response mechanisms provided by the application may be unavailable precisely because of these errors. The built-in handler does just that - it interrupts the program flow at the error location and sends the response to the user.

    2 answers 2

    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.

    • Do we obviously have to hand-pass 404 answer in the header? Simply in different frameworks different reaction to the not found page. Someone calmly returns the status of 200. But it seems to me that it is somehow wrong to return 200 if the page is not found, otherwise it will turn out that the search engine indexes it for some reason. I'm right? Or you can still give the status of 200 when the page is not found? - Razzwan
    • @Razzwan: Complete the answer. - romeo
    • Thank you very much for the comprehensive answer. I hope it will be useful to the community. - Razzwan
    • @Razzwan: You're welcome. I wanted to add about the local interception of exceptions (those in the depth of the code). Exceptions can be of two types: systemic and user-defined (in Ipatiev, this is clause 2.2). I am personally not a supporter of writing custom exceptions, and therefore, the interception of all exceptions occurs only in index.php with the output of a stub or stack trace for the test environment. But there are some non-critical services, for example, sending mail using the PHPMailer library. This library has its own set of phpmailerException exceptions. - romeo
    • In case of a connection error with the mail server or some other error, it is quite appropriate to intercept it locally at the time of sending the letter, add it to the log, and display the user with a dummy over the form, saying "I cannot send the letter yet ..." And, for example, for such a service as a DBMS, this behavior is critical, so let the exception β€œrun” further up to index.php , where our basic interceptor will already expect it. - romeo

    In general, normal, only two comments.

    1. Enclosing all code in a try catch is pointless and harmful. Harmful because try, in principle, should never be used to display an error on the screen - PHP can do it very well. And it makes no sense, because even without this operator, the error is perfectly understood.
    2. In principle, there should be no unconditional showerror . The user should never see system errors at all. That is, the display of an error on the screen can occur only in two cases:
      1. The only user of the site is the programmer himself - that is, on the development server. In this case, errors can and should be displayed on the screen. For this, the only setting directive should be responsible - the one that determines whether we are on the battle server or not.
      2. If an exception was thrown by a programmer, and the error text is specifically intended to be shown to the user. To do this, you can create a separate exception class, and show the error message only if the exception belongs to this class.

    In all other cases, no output to the screen, except for standard apologies and requests to come in later, should not be.

    The question about the HTTP header is not directly related to the handling of errors and exceptions, but the answer is obvious: the HTTP status should always reflect the current status of the server. If this is a server error, then the status should be 5xx. If the page is not found, then 404. If access is denied, then 403. And so on.

    • Do we obviously have to hand-pass 404 answer in the header? Simply in different frameworks different reaction to the not found page. Someone calmly returns the status of 200. But it seems to me that it is somehow wrong to return 200 if the page is not found, otherwise it will turn out that the search engine indexes it for some reason. I'm right? Or you can still give the status of 200 when the page is not found? - Razzwan
    • The answer is certainly useful, but less complete than the next. - Razzwan