Search by

dwiky-danov / business-error-handler

danovcode

Framework-agnostic global error handling with safe API responses and optional Kibana logging.

Package info

github.com/danovcode/laravel-error-handling

pkg:composer/dwiky-danov/business-error-handler

Statistics

Installs: 17

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v4.0.0 2026-11-06 03:07 UTC

This package is auto-updated.

Last update: 2026-11-06 03:18:21 UTC


README

A framework-agnostic PHP 8.1+ error handler that emits JSON-safe error responses, logs original exception details, supports correlation IDs, and can send structured logs to Kibana or any custom logger.

Installation

composer require dwiky-danov/business-error-handler

Laravel's Artisan integration is auto-discovered when Laravel is installed. For other frameworks, only the core package and PSR-3 are required.

Basic use

Register once during application bootstrap:

use BusinessErrorHandler\ErrorHandler;

ErrorHandler::register([
    'environment' => getenv('APP_ENV') ?: 'production',
]);

The global exception and PHP error handlers emit a JSON response with status: false, message, and correlation_id. In production, unexpected exception messages and traces are never included in the response. In development, the response also includes exception class, message, file, line, and trace.

Business rule errors are safe to expose in every environment:

use BusinessErrorHandler\BusinessValidationException;

throw new BusinessValidationException('Insufficient balance.');

You can optionally attach response data of any JSON-encodable PHP type (array, object, string, number, boolean, or null) to a business validation exception. It is included as data in the error response:

throw new BusinessValidationException(
    'Insufficient balance.',
    data: [
        'available_balance' => 50,
        'required_amount' => 75,
    ],
);

The response contains status: false, message, and the provided data. If no data argument is provided, the data key is omitted. Explicitly passing data: null includes "data": null. Values that PHP cannot encode as JSON (such as resources and closures) cannot be returned in a JSON response.

Laravel ValidationException responses expose the first validation message as message. When there are multiple validation messages, the response also includes the full field-keyed validation array:

{
    "status": false,
    "message": "username is required",
    "validation": {
        "nik": ["username is required.", "username has to be string"],
        "reservation_id": ["transaction id is required."]
    },
    "correlation_id": "..."
}

Safe custom messages

Configure exception-specific messages by class name:

ErrorHandler::register([
    'environment' => 'production',
    RuntimeException::class => ['message' => 'System processing failed.'],
    PDOException::class => ['message' => 'Database operation failed.'],
    'exception_messages' => [
        Illuminate\Database\Eloquent\ModelNotFoundException::class => [
            'message' => 'hasil Dari [Model Name] tidak di temukan',
        ],
    ],
]);

Custom messages are exposed in production. {model} is replaced with the short model class name for Laravel's ModelNotFoundException (for example, App\Models\Balance becomes Balance). {model_name}, :model, and [Model Name] are also supported. Unrecognized exceptions use Internal Server Error; recognized exception types use safe built-in defaults. See Default Exception Messages.xlsx for the full default-message reference.

When Laravel wraps a ModelNotFoundException in a NotFoundHttpException, the handler uses the underlying model-not-found exception to produce the same safe message, such as No Result for Query [Balance]..

For Laravel 11+, Laravel renders request exceptions through its own exception pipeline, so register a renderer there to use the package's response for model-not-found errors:

use BusinessErrorHandler\ErrorHandler;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Http\Request;

->withExceptions(function (Exceptions $exceptions): void {
    ErrorHandler::register(config('error-handler'));

    $exceptions->render(function (ModelNotFoundException $exception, Request $request) {
        return response()->json(ErrorHandler::render($exception), 404);
    });
})

In Laravel 10 and earlier, add the equivalent renderable callback to app/Exceptions/Handler.php. This ensures Laravel's request exception handler uses the package-formatted message rather than its default exception response.

Logging

Any BusinessErrorHandler\Logger\LoggerInterface implementation can be passed as logger. The included MonologLogger adapts a PSR-3 logger:

use BusinessErrorHandler\ErrorHandler;
use BusinessErrorHandler\Logger\MonologLogger;

ErrorHandler::register(['logger' => new MonologLogger($monolog)]);

Enable the built-in Kibana HTTP logger with kibana_logging => true. Its endpoint defaults to KIBANA_URL or http://localhost:9200/error-handler/_doc; set kibana_url, kibana_timeout, or kibana_headers to customize it. Logged payloads contain url, request_json, response_json, http_status, request_time in milliseconds, correlation_id, and private exception details.

Frameworks that already parse request bodies can supply request context without reading php://input. For long-running workers, provide request_time in milliseconds to get a per-request duration:

ErrorHandler::register([
    'logger' => $logger,
    'request_context' => static fn (): array => [
        'url' => '/api/v1/payment',
        'request_json' => request()->all(),
        'request_time' => $requestTimeInMilliseconds,
    ],
]);

The incoming X-Correlation-ID is reused when it contains only letters, numbers, ., _, or - (up to 128 characters); otherwise a UUID v4 is generated. It is included in both response and log, and sent as a response header.

Log every Laravel request and response

The built-in error logger sends a log when an exception is handled. If you also want a log for every completed Laravel request, generate middleware:

php artisan make:middleware LogRequestResponse

In the generated app/Http/Middleware/LogRequestResponse.php, use the package's logger and configuration:

<?php

namespace App\Http\Middleware;

use BusinessErrorHandler\Logger\KibanaLogger;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class LogRequestResponse
{
    public function handle(Request $request, Closure $next): Response
    {
        $startedAt = microtime(true);
        $response = $next($request);
        $body = $response->getContent();

        if (is_string($body)) {
            $decodedBody = json_decode($body, true);
            $responseJson = json_last_error() === JSON_ERROR_NONE
                ? $decodedBody
                : ['body' => $body];
        } else {
            $responseJson = null;
        }

        // Keep development exception details out of this request log.
        if (is_array($responseJson)) {
            unset($responseJson['exception']);
        }

        $payload = [
            'url' => $request->fullUrl(),
            'method' => $request->method(),
            'request_json' => $request->all(),
            'response_json' => $responseJson,
            'http_status' => $response->getStatusCode(),
            'request_time' => (int) round((microtime(true) - $startedAt) * 1000),
            'correlation_id' => $response->headers->get('X-Correlation-ID'),
        ];

        try {
            if (config('error-handler.kibana_logging')) {
                (new KibanaLogger(
                    (string) config('error-handler.kibana_url'),
                    (int) config('error-handler.kibana_timeout', 2),
                    (array) config('error-handler.kibana_headers', []),
                ))->log($payload);
            } else {
                Log::info('HTTP request and response', $payload);
            }
        } catch (Throwable $exception) {
            Log::warning('Request/response logging failed', [
                'url' => $request->fullUrl(),
                'method' => $request->method(),
                'error' => $exception->getMessage(),
            ]);
        }

        return $response;
    }
}

Set kibana_logging to true to send these request logs to Kibana (configure kibana_url, kibana_timeout, and kibana_headers as needed). When it is false, the middleware writes them to Laravel's default log channel. Register the middleware globally or on the routes you want to log. It logs completed responses; exceptions handled before the middleware receives a response are logged by the error handler instead. The middleware leaves the HTTP response unchanged. Since request and response bodies can contain credentials or personal data, redact sensitive fields before logging them.

Laravel Artisan generator

php artisan make:errorhandler
php artisan make:errorhandler --logkibana

The command creates config/error-handler.php. Register the handler from the application's exception/bootstrap setup with:

ErrorHandler::register(config('error-handler'));

Testing

composer install
vendor/bin/phpunit

The package requires PHP 8.1+ and psr/log 3.x. Laravel and Monolog are optional integrations.