Pular para conteúdo

Advanced: Custom Handlers

Erlenmeyer allows you to fully customize logging and exception handling behavior.
This includes:

  • Creating custom loggers by implementing LoggerInterface;
  • Registering custom exception handlers using App::setExceptionHandler().

These features are useful when integrating Erlenmeyer with external systems (such as Sentry, Logstash, or Graylog), or when defining precise responses for specific error types.


1. Creating a Custom Logger

All loggers in Erlenmeyer implement the LoggerInterface:

interface LoggerInterface
{
    public function log(LogLevel $level, string $message): void;
    public function logException(Exception $e, ?Request $request = null): void;
}

To create your own logger, simply implement this interface.

Example: JSON-based Logger

use AdaiasMagdiel\Erlenmeyer\Logging\LoggerInterface;
use AdaiasMagdiel\Erlenmeyer\Logging\LogLevel;
use AdaiasMagdiel\Erlenmeyer\Request;
use Exception;

class JsonLogger implements LoggerInterface
{
    private string $file;

    public function __construct(string $file = __DIR__ . '/app.log')
    {
        $this->file = $file;
    }

    public function log(LogLevel $level, string $message): void
    {
        $entry = [
            'timestamp' => date('c'),
            'level' => $level->value,
            'message' => $message
        ];

        file_put_contents($this->file, json_encode($entry) . PHP_EOL, FILE_APPEND);
    }

    public function logException(Exception $e, ?Request $request = null): void
    {
        $entry = [
            'timestamp' => date('c'),
            'level' => LogLevel::ERROR->value,
            'message' => $e->getMessage(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
            'trace' => explode("\n", $e->getTraceAsString()),
            'request' => $request ? [
                'method' => $request->getMethod(),
                'uri' => $request->getUri(),
            ] : null,
        ];

        file_put_contents($this->file, json_encode($entry) . PHP_EOL, FILE_APPEND);
    }
}

Then inject it into your app:

use AdaiasMagdiel\Erlenmeyer\App;

$app = new App(null, new JsonLogger(__DIR__ . '/logs/app.jsonl'));

Each log entry is stored as a separate JSON line — ideal for structured logging and observability tools.


2. Registering Custom Exception Handlers

The method setExceptionHandler() lets you define specific behaviors for particular exception types.

$app->setExceptionHandler(TypeError::class, function ($req, $res, $e) {
    $res->setStatusCode(400)
        ->withJson([
            'error' => 'Invalid type',
            'message' => $e->getMessage(),
        ])
        ->send();
});

You can also handle your own custom exception classes:

class ValidationException extends Exception {}

$app->setExceptionHandler(ValidationException::class, function ($req, $res, $e) {
    $res->setStatusCode(422)
        ->withJson(['error' => $e->getMessage()])
        ->send();
});

When an exception is thrown, Erlenmeyer traverses the exception’s class hierarchy to find the most specific registered handler, falling back to the generic Throwable handler if none matches.


3. Global (Fallback) Exception Handler

By default, Erlenmeyer defines a generic 500 handler:

$app->setExceptionHandler(Throwable::class, function ($req, $res, $e) {
    $res->setStatusCode(500)
        ->withHtml("<h1>500 Internal Server Error</h1><p>Error: {$e->getMessage()}</p>")
        ->send();
});

You can override it to return a consistent JSON response instead:

$app->setExceptionHandler(Throwable::class, function ($req, $res, $e) {
    $res->setStatusCode(500)
        ->withJson([
            'status' => 'error',
            'message' => $e->getMessage(),
        ])
        ->send();
});

4. Combining Loggers and Handlers

It’s common to use a logger inside a custom exception handler for better traceability:

use AdaiasMagdiel\Erlenmeyer\Logging\LogLevel;

$app->setExceptionHandler(RuntimeException::class, function ($req, $res, $e) use ($app) {
    // Log details
    $logger = new JsonLogger(__DIR__ . '/logs/errors.jsonl');
    $logger->logException($e, $req);

    // Send friendly response
    $res->setStatusCode(500)
        ->withJson(['error' => 'Unexpected server error'])
        ->send();
});

5. Full Example

use AdaiasMagdiel\Erlenmeyer\App;
use AdaiasMagdiel\Erlenmeyer\Logging\FileLogger;

require 'vendor/autoload.php';

$logger = new FileLogger(__DIR__ . '/logs');
$app = new App(null, $logger);

// Handler for validation exceptions
$app->setExceptionHandler(ValidationException::class, function ($req, $res, $e) use ($logger) {
    $logger->logException($e, $req);
    $res->setStatusCode(422)->withJson(['error' => $e->getMessage()])->send();
});

// Global fallback handler
$app->setExceptionHandler(Throwable::class, function ($req, $res, $e) use ($logger) {
    $logger->logException($e, $req);
    $res->setStatusCode(500)->withText('Internal Server Error')->send();
});

$app->get('/test', function ($req, $res) {
    throw new ValidationException('Invalid input data');
});

$app->run();

6. Best Practices

Catch specific exception types first (e.g. ValidationException, TypeError). ✅ Use loggers for technical detail, and handlers for user-facing messages. ✅ Avoid exposing sensitive data in production error responses. ✅ Combine with global middlewares to normalize errors consistently.


Summary

Feature Purpose
LoggerInterface Defines the common logging contract
FileLogger / ConsoleLogger Default logger implementations
setExceptionHandler() Associates exception types with custom responses
Custom Logger Integrate with external tools (Sentry, Logstash, etc.)

With these tools, you can build professional-grade error handling and observability pipelines inside Erlenmeyer.


Next: 📘 Testing with ErlenClient →