Exception hierarchy in a modern PHP application

    Publication Objective: It is easy to outline how to organize the hierarchy of exceptions and how to handle them in the application. Without reference to frameworks and specific architecture. The described method is the de facto standard in the community: it is used in many serious libraries and frameworks. Including Zend, Symfony. Despite its logic and universality, I did not find a formal description of the proposed approach in Russian. After repeated oral presentation of the concept to colleagues, the idea was born to arrange it in the form of a publication on Habrahabr.


    In PHP, starting with the 5th version, an exception mechanism is available. In the current, 7th version, this mechanism has been improved and redesigned with the aim of uniformly processing various errors using the designtry{} catch...


    In the standard library (SPL), PHP provides a ready-made set of base classes and interfaces for exceptions. In the 7th version, this set was expanded by the interface Throwable. Here is a diagram of all types available in version 7 (image - link):


    PHP7 exception type diagram


    For junior developers, it may be useful to first understand all the details of syntax, the logic of exceptions and error handling in general. I can recommend the following articles in Russian:



    Key aspects of the exclusion hierarchy of your application.


    Common interface


    Use a common interface (marker) for all exceptions defined in your application. The same rule applies to individual components, modules, packages, i.e. subspaces of the names of your code. For example \YourVendor\YourApp\Exception\ExceptionInterface, either \YourVendor\YourApp\SomeComponent\Exception\ExceptionInterface, in accordance with PSR-4.


    An example can be seen in any component of Symfony, Zend, etc.


    For each situation - its own type


    Each new exception should lead to the creation of a new type (class) of exception. The class name should semantically describe this situation. Thus, the code with the exception constructor invocation, and its throw, will be: readable, self-documenting, transparent.


    Symmetrically and the code with catching the exception will be explicit. The code inside catchshould not try to determine the situation by message or other indirect indications.


    Extend Base Types


    Base types are used to inherit from your own exception types of your application. This is clearly stated in the documentation .


    An example can be seen in any component of Symfony, or Zend.


    Forwarding and transforming according to the level of abstraction


    Because exceptions pop up throughout the call stack, there may be several places in the application where they are caught, skipped, or converted. As a simple example: it is logical to catch a standard PDOException in the DAL layer, or in the ORM, and forward your own instead DataBaseException, which in turn is already caught in the layer above, for example, the controller, where to convert to HttpException. The latter can be intercepted in the dispatcher code, at the highest level.


    Thus, the controller does not know about the existence of PDO - it works with abstract storage.


    The decision of what should be forwarded, what is converted, and what is processed, in each case lies with the programmer.


    Use the capabilities of the standard constructor


    Do not redefine the standard constructor Exception- it is ideally designed for your tasks. Just do not forget to use it for its intended purpose and call the parent with all the arguments, if overload was still required. Summing up this paragraph with the previous one, the code may look something like this:


    namespace Samizdam\HabrahabrExceptionsTutorial\DAL;
    class SomeRepository
    {
        public function save(Model $model)
        {
            // .....
            try {
                $this->connection->query($data);
            } catch (\PDOException $e) {
                throw new DataBaseException(\i18n('Error on sql query execution: ' . $e->getMessage(), $e->getCode()), $e);
            }
        }
    }
    // .....
    namespace  Samizdam\HabrahabrExceptionsTutorial\SomeModule\Controller;
    use Samizdam\HabrahabrExceptionsTutorial\Http\Exception;
    class SomeController
    {
        public function saveAction()
        {
            // .....
            try {
                $this->repository->save($model);
            } catch (DataBaseException $e) {
                throw new HttpException(\i18n('Database error. '), HttpException::INTERNAL_SERVER_ERROR, $e);
            }
        }
    }
    // .....
    namespace  Samizdam\HabrahabrExceptionsTutorial\Http;
    class Dispatcher
    {
        public function processRequest()
        {
            // .....
            try {
                $controller->{$action}();
            } catch (HttpException $e) {
                // упрощенно для примера
                http_response_code($e->getCode());
                echo $e->getMessage();
            }
        }
    }

    Summary


    Why build a whole hierarchy, with the participation of interfaces, types, subtypes and all this polymorphism for the sake of error handling? What is the meaning of this abstraction and how is its price justified?


    When designing an application, abstraction is what adds flexibility and allows you to postpone specific solutions and detailed implementation until all the nuances are known, and the requirements for the code are not yet known, or can change. This is an investment that pays off over time.


    When your exceptions have both a marker interface, an SPL supertype (like a RuntimeException), a specific type appropriate to the situation, you can fully control their handling and flexibly change its strategy in the future. Laying down these abstractions at an early stage of development, in the future, as the requirements for error handling appear and tighten, you will have at your disposal a tool that will help to implement these requirements.


    At the prototype stage, it is enough to show the inscription "creep", for this it is enough to catch any Throwable in index.php.


    In the alpha version, it will not be superfluous to distinguish between situations 401, 404 and 500.


    In beta testing, you probably want to display the traces of all previous exceptions to form bug reports.


    By the beginning of operation, you will need a single point for logging exceptional situations.


    And throughout the development of the application, you will only, as necessary, add code with processing logic, without the need to make changes to the main code , where exceptions are generated.


    Also popular now: