AthenaeumAthenaeum
Packages
  • next
  • current
  • v9.x
  • v8.x
  • v7.x
  • v6.x
  • v5.x
  • v4.x
  • v3.x
  • v2.x
  • v1.x
Changelog
GitHub
Packages
  • next
  • current
  • v9.x
  • v8.x
  • v7.x
  • v6.x
  • v5.x
  • v4.x
  • v3.x
  • v2.x
  • v1.x
Changelog
GitHub
  • Version 6.x

    • Release Notes
    • Upgrade Guide
    • New to this...
    • Contribution Guide
    • Security Policy
    • Code of Conduct
    • Origin
  • ACL

    • Introduction
    • How to install
    • Setup
    • Permissions
    • Roles
    • Users
    • Cached Permissions
  • Audit

    • Audit
    • How to install
    • Setup
    • Recording
    • Events
  • Circuits

    • Circuits
    • How to install
    • Setup
    • Usage
    • Events
  • Collections

    • Collections
    • How to install
    • Summation

      • Summation Collection
      • Items Processor
  • Config

    • Configuration Loader
    • How to install
    • Setup
    • Load Configuration Files
    • Custom File Parsers
  • Console

    • Command and Schedule Registration
    • How to install
    • Setup
    • Commands
    • Schedules
  • Container

    • IoC Service Container
    • How to install
    • Container
    • List Resolver
  • Core

    • Athenaeum Core Application
    • How to install
    • Setup
    • Usage

      • Configuration
      • Service Providers
      • Service Container
      • Events
      • Caching
      • Logging
      • Console
      • Task Scheduling
      • Exception Handling
      • Extending Core Application
      • Testing
  • Database

    • Introduction
    • How to install
    • Models

      • Instantiatable
      • Sluggable
    • Query

      • Criteria (Query Filter)
  • Dto

    • Data Transfer Object (DTO)
    • How to install
    • Create Interface
    • Implement DTO
    • How to use
    • Populate
    • Export
    • Json
    • Serialization
    • Nested DTOs
    • Array DTO
  • ETags

    • ETags
    • How to install
    • Setup
    • Usage
    • Generators

      • Default Generator
      • Custom Generator
    • Eloquent Models
    • Macros
  • Events

    • Register Listeners and Subscribers
    • How to install
    • Setup
    • Listeners
    • Subscribers
  • Filters

    • Search Filter Utilities
    • Prerequisites
    • How to install
    • Setup
    • Processor
    • Filters Builder
    • Predefined Resources

      • Search Processor
      • Sorting Processor
      • Constraints Processor
      • Matching Processor
    • Tip: Create a base builder
  • Flysystem

    • Introduction
    • Database Adapter

      • Introduction
      • How to install
      • Setup
      • Data Deduplication
      • MIME-Type Detection
  • Http

    • Api

      • Http API
      • How to install
      • Setup
      • Resources

        • Introduction
        • Timestamps
        • Self-Link
        • Relations
        • Registrar
      • Middleware

        • Introduction
        • Request Must Be Json
        • Capture Fields To Select
    • Clients

      • Http Clients
      • How to install
      • Setup
      • Basic Usage
      • Available Methods

        • Fluent Api
        • Protocol Version
        • Base Uri
        • Http Method and Uri
        • Headers
        • Accept & Content-Type
        • Authentication
        • Http Query
        • Payload Format
        • Payload
        • Attachments
        • Cookies
        • Response Expectations
        • Middleware
        • Conditions
        • Criteria
        • Redirects
        • Timeout
        • Debugging
        • Logging
        • Driver Options
        • Driver
      • Http Query Builder

        • Introduction
        • Select
        • Where
        • Dates
        • Include
        • Pagination
        • Sorting
        • Raw Expressions
        • Custom Grammar
    • Cookies

      • Http Cookies
      • How to install
      • Usage
    • Messages

      • Http Messages
      • How to install
      • Serializers
  • Maintenance

    • Modes

      • Maintenance Modes
      • How to install
      • Setup
      • Basic Usage
      • Available Drivers
  • Mime Types

    • MIME-Types
    • How to install
    • Setup
    • Usage
    • Drivers

      • Available Drivers
      • File Info
  • Properties

    • Properties Overload
    • How to install
    • Usage
    • Naming Convention
    • Properties Visibility
  • Redmine

    • Redmine Api Client
    • How to install
    • Setup
    • General Usage

      • Supported Operations
      • Fetch list of resources
      • Find
      • Fetch
      • Create new record
      • Update existing record
      • Delete existing record
      • Relations
    • Available Resources

      • Predefined Resources
      • Attachments
      • Enumerations
      • Issue Relations
      • Users
      • User Groups
      • Roles
      • Project Memberships
      • Versions (Milestones)
      • Issue Categories
      • Trackers
  • Service

    • Service Registrar
    • How to install
    • How to use
  • Streams

    • Streams
    • How to install
    • Setup
    • How to use

      • Introduction
      • Open and Close
      • Raw Resource
      • Seeking
      • Reading
      • Writing
      • Size
      • Truncate
      • Flush
      • Hash
      • MIME-Type
      • Output
      • Locking
      • Transactions
      • Meta
      • Misc
  • Support

    • Introduction
    • How to install
    • Laravel Aware-of Helpers

      • How to use
      • Enforce Via Interface
      • Custom Default
      • Pros and Cons
      • Available Helpers
    • Aware-of Properties

      • Generator
      • Available Aware-of Helpers
    • Live Templates
  • Testing

    • Introduction
    • How to install
    • Test Cases
    • Testing Aware-of Helpers
  • Utils

    • Introduction
    • How to install
    • Array
    • Duration
    • Json
    • Math
    • Memory
    • Method Helper
    • Invoker
    • Populatable
    • String
    • Version
  • Validation

    • Introduction
    • How to install
    • Setup
    • Rules

      • Alpha-Dash-Dot
      • Semantic Version
You are viewing documentation for an outdated version. It is no longer supported!

Exception Handling

Caution

For legacy reasons, exception handling in Athenaeum Core Application is disabled, by default. You are encouraged to read carefully through this chapter and enable appropriate exception handling for your custom application.

Within this context, error, exception and shutdown handling will be referred to as "exception handling".

  • Laravel's Exception Handling?
  • How it Works
    • Error & Shutdown Handling
  • Prerequisite
  • Enabling Exception Handling
  • "Last Resort" Handler
    • Register Your Exception Handler
  • Reporting
    • Don't Report
  • Handler In Action
    • Application didn't Terminate
  • Graceful Shutdown
    • Encapsulate logic via run()
    • Avoid using terminating()?
    • Use Handlers to Cleanup

Laravel's Exception Handling?

In the limitations section, it has been mentioned that the Core Application does not offer Http Request / Response Handling. Since Laravel's Error & Exception Handling mechanism depends on a Http Request and Response, it cannot be used directly with this application. More specifically, the render() method, in Laravel's Exception Handler, requires a request and must return a response. Such cannot be satisfied by the Core Application. Therefore, a different mechanism is offered - it is still inspired by that of Laravel!

How it Works

The offered exception handling mechanism uses a pseudo Composite Pattern, where a captured exception is passed through a series of "leaf" exception handlers. The first handler to return true, will stop the process and the exception is considered handled.

Behind the scene, a CompositeExceptionHandler is responsible for reporting (e.g. logging) and passing captured exceptions to the registered exception handlers.

        Captured Exception
                +
+-----------+   |
| Handler A |   |
+-----------+   |
| Handler B |   |
+-----------+   |
| Handler C |   |
+-----------+   |
  ...           |
+-----------+   v
| Handler X |
+-----------+

This kind of approach allows you to split your application's exception handling, into multiple smaller sections of logic ("leaf" exception handlers). Each of these handlers will, ideally, only be responsible for dealing with a few exceptions, in contrast to a single large and complex exception handler.

Error & Shutdown Handling

Whenever a PHP error has been captured, it will be wrapped into an ErrorException and thrown. The exception handling mechanism will then capture and process that exception. Similar logic is applied during PHP's shutdown, in case that an error was encountered.

Prerequisite

This exception handling mechanism depends on Laravel's Log package, as means of default reporting. See Logging chapter for how to install it.

Enabling Exception Handling

To enable exception handling, edit your .env and set the EXCEPTION_HANDLING_ENABLED to true.

# ... previous not shown ...

# Exception Handling
EXCEPTION_HANDLING_ENABLED=true

"Last Resort" Handler

The first "leaf" exception handler that you SHOULD create, is a "Last resort" exception handler; a handler that deals with any kind of exceptions - aka. your fallback exception handler.

To create a "lead" exception handler, extend the BaseExceptionHandler abstraction. In the following example, a very simplified "last resort" exception handler is shown.

<?php

namespace Acme\Exceptions\Handlers;

use Aedart\Core\Exceptions\Handlers\BaseExceptionHandler;
use Throwable;

class LastResortExceptionHandler extends BaseExceptionHandler
{
    public function handle(Throwable $exception): bool
    {
        // If custom application is handling web / Http request...
        if( ! $this->runningInConsole()){
            http_response_code(500);

            echo '<h1>Sorry... but it seems we have some trouble in our end.</h1>';

            return true;
        }

        // When running in console, just output entire exception
        echo (string) $exception;

        return true;
    }
}

If your custom application is handling web content / Http requests, then you should you should avoid sending output directly via your exception handlers. Consider assigning your desired Http output to a response handler, if such is possible for you.

Register Your Exception Handler

Once your "leaf" exception handler has been completed, add it's class path in the handlers array, which is located in the /config/exceptions.php file.

<?php
return [

    // ... previous not shown ...

    'handlers' => [
        Acme\Exceptions\Handlers\EditorExceptions::class,
        Acme\Exceptions\Handlers\ShoppingExceptions::class,
        Acme\Exceptions\Handlers\NavigationExceptions::class,
        Acme\Exceptions\Handlers\DbExceptions::class,
        Acme\Exceptions\Handlers\LastResortExceptionHandler::class
    ]
];

Tips

Your "last resort" exception handler SHOULD be placed last, in the handlers array.

Reporting

By default, an exception is "reported" by the CompositeExceptionHandler, before it is passed through to the registered "leaf" exception handlers. Within this context, the "reporting" means logging exceptions.

Don't Report

Just like in Laravel's exception handler, if you wish to disable reporting of certain exceptions, add their class paths in the $dontReport property.

<?php

class NavigationExceptionHandler extends BaseExceptionHandler
{
    /**
     * List of exceptions not to be reported
     * 
     * @var string[] 
     */
    protected array $dontReport = [
        \Acme\Routing\Exceptions\RouteNotFoundException::class,
        \Acme\Routing\Exceptions\FileDoesNotExistException::class,
    ];

    public function handle(Throwable $exception): bool
    {
        // ... not shown ...
    }
}

Handler In Action

Imagine that the following entry point encounters a condition, where it must throw an exception.

<?php
require_once __DIR__ . '/../vendor/autoload.php';

$app = require_once __DIR__ . '/../bootstrap/app.php';
$app->run();

// ... your custom application's logic

if( ! $user->hasSignedIn()){
    throw new \RuntimeException('User is not authenticated');
}

// ... etc

$app->terminate();
$app->destroy();

The thrown exception is captured and passed through the list of registered "lead" exception handlers, until a handler returns true. Your "last resort" exception handler SHOULD handle any exception, which isn't handled by other registered handlers.

Application didn't Terminate

When exceptions are thrown and PHP's native exception handling mechanism kicks in, remaining code is not executed. In other words, in the above shown example, the last two lines are never reached, should an exception be thrown. As a consequence of this, any registered termination logic is not executed, by the terminate() method.

// ... previous not shown ...

// Never executed, in case of exception thrown...
$app->terminate();
$app->destroy();  

Ensuring clean and graceful application shutdown, is a common problem for many applications. In the next section, a possible solution is explored.

Graceful Shutdown

Depending upon your registered service providers, or application's overall logic, it may require termination and shutdown logic. For instance, you may require logic that ensures all open database transactions are committed, or perhaps rolled back in case of exceptions. Or perhaps you may require logic, that closes the current session, file points, or other resources. A possible solution is to utilise the terminating() method. It registers callback methods that will be executed, when terminate() is invoked.

// Register callback
$app->terminating(function(){
    $session = IoCFacade::tryMake(Session::class);
    $session->close();
});

// ... later in your application ...
$app->terminate(); // Triggers the registered "terminating" callback method

Unfortunately, if an exception is thrown, the terminate() method might never be reached. All of your registered callbacks are therefore not invoked. This could prove problematic, if your application depends on being able to perform "graceful shutdown" logic.

Encapsulate logic via run()

A different solution could be, to encapsulate your custom application's logic via the run() method. It accepts a single callback. If the callback should fail, e.g. an exception is thrown, it will be captured by the run() method and passed on to the exception handling mechanism. Once the exception has been handled, code execution is resumed and the terminate() method is triggered.

<?php
require_once __DIR__ . '/../vendor/autoload.php';

$app = require_once __DIR__ . '/../bootstrap/app.php';

$app->run(function($app){

    // E.g. include your custom application's entry point    
    include 'my_legacy_app_index.php';

});

$app->terminate();
$app->destroy();

If the above shown approach is possible for you to implement, then it could contribute towards allowing graceful shutdown.

Avoid using terminating()?

One could argue that you should avoid registering callbacks, via the terminating() method. But this might not always be possible. Imagine that for every request, if all goes well, your application needs to commit open database transactions, before closing it's connection gracefully. It makes sense to use the terminate(), in order to achieve such.

$app->terminating(function(){
    $db = IoCFacade::tryMake(Db::class);

    if($db->hasOpenTransations()){
        $db->commit();
    }
    
    $db->close();
});

// ... later in your application ...
$app->terminate();

Tips

Terminating callbacks can also be registered in your Service Provider's boot method.

// In your service provider
public function boot(Application $app)
{
    $app->terminating(function($app){
        $db = $app->make(Db::class);
    
        if($db->hasOpenTransations()){
            $db->commit();
        }
        
        $db->close();
    });
}

Use Handlers to Cleanup

A slightly different approach to handle "cleanup" routines, if by creating a special kind of exception handler, which is intended to perform "cleanup" - but NOT handle the captured exception! Such a handler, will simply pass the exception on to the next registered handler.

<?php

namespace Acme\Db\Cleanup;

use Aedart\Core\Exceptions\Handlers\CleanupHandler;

class RollsBackTransactions extends CleanupHandler
{
    public function cleanup(Throwable $exception): void
    {
        $db = IoCFacade::tryMake(Db::class);
    
        if($db->hasOpenTransations()){
            $db->rollback();
        }
        
        $db->close();
    }
}

Should you make use of such "cleanup handlers", then you are recommended to register them before other exception handlers, in your config/exceptions.php. Consider the following example:

<?php
return [

    // ... inside config/exceptions.php...
    
    // ... previous not shown ...

    'handlers' => [
        // Cleanup         
        Acme\Db\Cleanup\RollsBackTransactions::class,
        Acme\Session\Cleanup\ClosesSessions::class,
        Acme\Storage\Cleanup\RemovesFileLock::class,

        // Handles exceptions
        Acme\Exceptions\Handlers\EditorExceptions::class,
        Acme\Exceptions\Handlers\ShoppingExceptions::class,
        Acme\Exceptions\Handlers\NavigationExceptions::class,
        Acme\Exceptions\Handlers\DbExceptions::class,

        // Last resort - if all else fails...
        Acme\Exceptions\Handlers\LastResortExceptionHandler::class
    ]
];

Ultimately, ensuring graceful shutdown falls upon your shoulders. How you go about it, is entirely up to you. The above illustrated examples is nothing more than a possible solution.

Edit page
Last Updated: 16/02/2023, 09:10
Contributors: Alin Eugen Deac, alin
Prev
Task Scheduling
Next
Extending Core Application