CoiSA / Error Handler Source repository

2.x API · PHP 7.4+

Error Handler 2.x

Compose diagnostic reporting, native PHP callbacks and HTTP responses with explicit application boundaries.

The library supplies handlers and PSR adapters. Your application chooses reporters, PSR implementations, callback installation and HTTP emission.

This guide describes the 2.x API. Check your installed version and matching source.

New to the library? Follow installation, run direct reporting, then choose process callbacks or an explicit HTTP response policy. Add container integration when the application needs service lookup.

Installation

composer require 'coisa/error-handler:^2.4'

This common baseline covers ordinary reporting and legacy integration. JSON/Whoops adapters and Interop providers were published in 2.5.0; select coisa/error-handler:^2.5 for those routes and verify the installed source.

The package declares PHP ^7.4 || ^8.0. Composer installs PSR interfaces; applications supply concrete dispatchers, loggers and HTTP factories when those integrations are used.

For the runnable recipes, use the cookbook's explicit application dependencies. Resolve them with the actual PHP runtime and retain the application's lockfile.

Composer 2 and the application's vendor/autoload.php are the starting point. Direct reporting needs no container or HTTP implementation. Nyholm, Phly and Laminas are used only by their respective recipes; install compatible versions for the selected integration. The bounded verifier also needs the documented Symfony Process development dependency.

Start with direct reporting

handleThrowable() delegates to your reporter without installing process callbacks. If the reporter returns, execution continues; reporter failures propagate.

Excerpt from examples/direct.php · imports and bootstrap in the complete source
$errors = new ErrorHandler(new CallableThrowableHandler(
    /**
     * Print only the synthetic message used by this executable example.
     *
     * @param Throwable $error the synthetic failure to report
     */
    static function (Throwable $error): void {
        echo 'reported: ' . $error->getMessage() . PHP_EOL;
    },
));

$errors->handleThrowable(new RuntimeException('Example failure'));

echo 'continued' . PHP_EOL;

This synthetic reporter prints so its behavior can be verified. Use an appropriate private diagnostic destination in a real application.

After setup, run php examples/direct.php from the source checkout. Expect reported: Example failure, then continued. A reporter delivers diagnostics; the core connects it to explicit calls or PHP callbacks. See the API guide for responsibilities and application extension contracts.

Report directly to an application logger

LogThrowableHandler is an explicit opt-in 2.x adapter. Verify that your checkout or release contains it; older installed versions may not provide it. Supply your own PSR-3 logger, optional string level (ERROR by default) and context.

Excerpt from examples/logging.php · the complete source supplies an in-memory logger, imports and bootstrap
$context = ['component' => 'worker', 'exception' => new LogicException('Caller context')];
$failure = new RuntimeException('Synthetic worker failure');
$reporter = new LogThrowableHandler($logger, LogLevel::CRITICAL, $context);

$process = new ErrorHandler($reporter);

$process->handleThrowable($failure);

Each call logs exactly getMessage(), including empty, and places the original throwable under exception over any caller value. Other context is preserved without coercion; key order is not a contract. The logger decides supported levels and its failures propagate unchanged, without adapter retries or output.

No automatic discovery or callback registration is added. A private logger is not a public body renderer: keep process logging and the existing HTTP capture/fallback boundary explicit. Read process composition and the complete contract.

Choose the process lifetime

For a whole process, register one handler at bootstrap and leave it active for uncaught exceptions. For a caught operation, report inside its catch and restore callbacks in finally.

Scoped-warning excerpt from examples/process.php · uses its configured handler
    $errors->register();

    try {
        trigger_error('Scoped warning', E_USER_WARNING);
    } finally {
        $errors->unregister();
    }

Callbacks are process-wide. Use distinct handlers for nested scopes and unregister in reverse order. An unwinding finally runs before the uncaught-exception callback; unregistering there can prevent that handler from seeing the exception.

PHP cannot remove queued shutdown functions. Repeated register/unregister cycles enqueue callbacks. A successfully reported uncaught exception currently exits 0; native compile-fatal reporting observes exit 255 and does not recover execution.

Supply PHP-error policy explicitly

CallablePhpErrorHandler($callback) adapts four unchanged values: code, message, filename and line. Inject the wrapper as the second ErrorHandler argument. The existing constructor remains typed to interfaces; this adapter adds no registration, mask check, conversion, output or automatic discovery.

Application mask/conversion policy from examples/php-errors.php · explicit reporting installs no callbacks
        if (error_reporting() & $code) {
            throw ErrorException::fromPhpError($message, $code, $code, $filename, $line);
        }

Callback failures propagate unchanged. The interface returns void: false/null results are ignored and do not restore native PHP fall-through. No fifth error-context argument is supplied. Verify that the selected source contains this additive API, then read the full API contract.

Compose adapters deliberately

AdapterExisting behavior
CallableThrowableHandlerInvokes one configured reporter.
CallablePhpErrorHandlerInvokes one four-argument application callback; ignores its return and leaves mask/conversion policy explicit.
LogThrowableHandlerLogs the exact message and original exception once through an explicit application PSR-3 logger.
ThrowableHandlerAggregateCalls reporters in order; a failure stops later deliveries and propagates.
BestEffortThrowableHandlerAggregateOpt-in adapter: attempts every reporter slot, then exposes the original and all ordered failures through one aggregate exception.
ThrowErrorExceptionPhpErrorHandlerConverts enabled PHP errors; honors the reporting mask and suppression.
CallableShutdownHandler
PhpLastErrorShutdownHandler
Adapts an application shutdown callback or forwards a matching last fatal/compile/core error.
DispatchThrowableHandler
DispatchErrorEventThrowableHandler
Dispatches a raw throwable or an ErrorEvent through application-owned PSR-14.

Both listener providers and callable listeners are demonstrated in the event example. LogErrorEventListener passes the event string to an application PSR-3 logger, at ERROR by default. Configuring raw and wrapped routes creates separate deliveries.

Select diagnostic branches explicitly

FilteringThrowableHandler($reporter, $predicate) evaluates a required boolean predicate once. A match forwards the original throwable once; a nonmatch skips only that delegate. Predicate/delegate failures propagate without retry; non-boolean results raise TypeError. Invert policy in the application's predicate.

Severity selection from examples/filtering.php · the example also retains a general diagnostic branch
    static fn(Throwable $error): bool => $error instanceof ErrorException && E_USER_DEPRECATED === $error->getSeverity(),

This opt-in adapter adds no discovery, output, callback registration, recursion guard or response policy. A nonmatch is not recovery or redaction: built-in empty capture still falls back to diagnostic text. See the API guide and HTTP composition.

Attempt every reporter and retain every failure

BestEffortThrowableHandlerAggregate takes a fixed reporter list. Each invocation attempts every slot once, in order, with the same original throwable. Duplicate slots are separate attempts. Zero/all-success passes return normally; any failure, including partial success, produces one AggregateReportingException after all attempts.

Terminal handling from examples/best-effort.php · synthetic reporters and imports appear in the complete source
$reporter = new BestEffortThrowableHandlerAggregate($audit, $archive, $backup);
$recordedFailure = null;

try {
    $reporter->handleThrowable($original);
} catch (AggregateReportingException $failure) {
    $recordedFailure = [
        'original' => $failure->getOriginalThrowable(),
        'failures' => $failure->getFailures(),
    ];
}

The original also remains the previous cause; reporter failures retain their order and object identities. The fixed summary does not make the cause, failures or exception string public-safe. Applications own terminal handling and durable delivery. This opt-in adapter changes no fail-fast default, factory, callback lifecycle or HTTP response policy and adds no retry or reentry guard. Read the full contract and explicit composition.

Keep the existing container boundary

ErrorHandlerContainer accepts an optional application PSR-11 parent. The parent's known services take priority; otherwise library aliases and factories resolve services. The default reporter aggregate is empty and silent, so supply a reporter explicitly.

Resolution excerpt from examples/container.php · application services are configured earlier
$legacy = new ErrorHandlerContainer($application);

$process = $legacy->get(ErrorHandler::class);

ConfigProvider retains aliases, factories and Laminas discovery metadata. ServiceManager is an optional application integration, with a version compatible with that application's PHP.

Services are cached. Calling ErrorHandlerFactory explicitly creates a new core handler; a fresh middleware wrapper can still resolve the cached handler. Discovery and construction do not call register().

The three service aliases select the throwable aggregate, captured-output stream factory and throwable response factory. They redirect container IDs; they do not create PHP class aliases or global singletons. Parent services take priority and retain the parent's lifetime. The container guide maps every alias and factory, explains required/optional overrides and provides service-resolution troubleshooting.

The container-cause repair preserves the original throwable through factory/alias wrapper chains. The two public exception factories copy integer codes and use 0 for string codes, retaining the original SQLSTATE on the cause. Check the selected source/release and the container guide before relying on this repair.

Select an optional response provider

The optional Interop providers are available from published 2.5.0. Select coisa/error-handler:^2.5; an application using PHP 8.3+ can install fast-forward/container:^1.9 as its own runtime dependency. Supply PSR-17 response/stream factories and a private ThrowableHandlerInterface reporter, then choose JSON explicitly:

Public JSON selection from examples/provider-json.php · the complete recipe provides application services and imports
    $services = container(
        JsonErrorHandlerServiceProvider::class,
        ErrorHandlerServiceProvider::class,
        $application,
    );

Each provider declares only its own services. Register JSON before the separate base provider so its response alias wins over legacy capture. The JSON profile composes one private report before pure rendering for the existing 2.x middleware. Public JSON is the default. A constructor bool, passed from trusted Config values before container construction, enables development details deliberately; no request header or environment name enables them. Use strict typing and real bool values rather than casting the string "false".

Explicit development HTML selection from examples/provider-whoops.php · application services and page configuration appear in the full source
$services = container(new AggregateServiceProvider(
    new ErrorHandlerServiceProvider(),
    new WhoopsErrorHandlerServiceProvider(),
    $application,
));

The Whoops profile has no constructor arguments and declares only its page and HTML/reporting services. The application composes the independent maps in one aggregate: base, Whoops, application. Later definitions select HTML and application page overrides; the base supplies fresh scopes. Install optional filp/whoops:^2.18.7 wherever development HTML is selected. The default PrettyPageHandler service is lazy and cached. The configured recipe reads Config and binds a prepared page through ServiceFactory. Sequential renders retain configuration and replace current diagnostic data; the Run, response and stream are fresh. Selected key hiding is not complete redaction. Read the container failure boundaries: concrete factory errors may trigger autowiring fallback, and separate providers without Whoops may report an original twice through legacy capture. This choice does not infer debug mode or negotiate Accept.

Fresh scope from the same executable recipe · factory service under ServiceProvider\Factory
$middlewareFactory = $services->get(ErrorHandlerMiddlewareFactory::class);

$middleware = $middlewareFactory($services);

Each factory invocation creates a distinct core handler and middleware. The process alias and directly resolved middleware remain cached; lookup does not register callbacks. The base ErrorHandlerServiceProvider retains legacy capture defaults and requires the application's reporter. The core still supports PHP 7.4 and the legacy container/ConfigProvider. Fresh objects do not remove PHP's existing shutdown-queue limits.

For a required replacement of a library-defined service, combine base, JSON and application definitions in that order in one AggregateServiceProvider; the last definition wins. Separate providers prioritize the first successful resolution. Read the provider guide for complete maps, Config, runtime installation and override semantics. Applications own emission and any reporter output.

Separate HTTP responsibilities

The PSR-15 middleware registers its lifecycle handler, invokes downstream, delegates caught throwables to a response factory and unregisters in finally. It does not independently report before rendering.

The built-in stream factory reports while capturing output. Empty output and the string "0" fall back to the throwable's diagnostic string. A global ThrowableHandlerInterface binding also selects this renderer's reporter: private logging or a filtering nonmatch can leave empty output and trigger that fallback. Keep process reporting and public response construction explicit. The built-in response factory also uses a valid exception message as its reason phrase; a silent or filtered reporter does not create a safe public response.

If the reporter closes capture and returns, the stream factory uses the same diagnostic fallback without reading or closing caller-owned buffers at or below its entry depth or invoking their output callbacks. Missing capture with no caller buffer also reaches fallback. This boundary cannot undo the reporter's own writes, flushes or buffer changes; reporters must balance their buffers.

In a checkout/release containing the reason-phrase repair, CR, LF, NUL, DEL and other forbidden control bytes omit the reason argument so the application's PSR-17 implementation can select its default. Valid horizontal tabs, visible ASCII and opaque high bytes remain unchanged. Empty messages are still passed explicitly, retaining the existing implementation-specific behavior. This prevents an invalid status line; the original throwable, reporting count and diagnostic body fallback remain unchanged.

Application safe-renderer excerpt from examples/http.php · this is an explicit application adapter
            $this->reporter->handleThrowable($error);

            return $this->messages->createResponse(500)->withBody(
                $this->messages->createStream('Internal Server Error'),
            );

The application supplies PSR-17 factories, private diagnostic delivery and its emitter. The legacy status policy accepts integer codes from 400 through 500 inclusive; other integers and all string codes, including SQLSTATE, become 500. Use application-owned mapping for domain errors.

For a fixed public response, choose the complete recipe's safe mode: its application response factory reports privately once, then creates the response. The middleware does not add that report. If the reporter or PSR-17 factory throws, response creation fails after callback restoration; the application owns the terminal policy.

Opt-in JSON composition from examples/http-json.php · imports and the private reporter appear in the complete source
$json = new JsonThrowableResponseFactory($messages, $messages, $debug);

$responses = new ReportingThrowableResponseFactory($reporter, $json);

$middleware = new ErrorHandlerMiddleware($errors, $responses);

Available from published 2.5.0 with coisa/error-handler:^2.5, the pure JSON factory returns application/json with {"error":"Request failed"}, preserves integer codes 400–599 and otherwise uses 500. It does not report or capture output. The explicit decorator reports the original once before rendering; reporter failure prevents rendering, and failures propagate after middleware restores callbacks. Use a private reporter that does not print. Defaults remain unchanged.

The optional third boolean defaults to false. An explicit trusted development configuration can enable class/message/file/line and trace metadata, without arguments or objects; debug sets Cache-Control: no-store and replaces invalid UTF-8. Messages and paths still contain diagnostic details. No environment/request autodetection enables it, and the recipe uses a private reporter without output. PHP 7.4 debug use needs the optional JSON extension, included by PHP 8.

Shared reporting composition from examples/http-whoops.php · a trusted development choice selects the pure HTML renderer earlier
$responses = new ReportingThrowableResponseFactory($reporter, $renderer);

$errors = new ErrorHandler($reporter);

$middleware = new ErrorHandlerMiddleware($errors, $responses);

The optional WhoopsThrowableResponseFactory takes two PSR-17 factories and a callable creating a fresh configured PrettyPageHandler. Install filp/whoops:^2.18.7 only where development HTML is used. Each render owns a new, unregistered Run, returns HTML without emission/exit and sets text/html; charset=UTF-8 plus no-store. It preserves integer codes 400–599 and delegates the reason to PSR-17. Core/container defaults remain unchanged.

Applications select this renderer through trusted development configuration and keep the private reporting decorator for 2.x. Default Whoops diagnostic pages can expose sensitive globals; hiding selected keys does not redact messages, traces, source or every secret. No request/Accept negotiation or automatic debug activation is supplied. Verify the selected checkout/release contains this additive API.

See the HTTP guide for success/failure restoration, capture limitations and the complete application composition.

Run the complete recipes

Every excerpt above is checked against its executable source. Read all fourteen complete sources together in the 2.x cookbook, with imports, readable spacing and every executable branch. Both the complete cookbook and the excerpts are checked against source. The recipes use synthetic input and keep their application dependencies explicit.

  • direct.phpExplicit reporting without installing native callbacks.
  • logging.phpDirect PSR-3 delivery, exact/empty messages and original exception context.
  • process.phpWarnings, scoped restoration, uncaught behavior and custom shutdown.
  • handlers.phpCallable/aggregate order, failure propagation, conversion and suppression.
  • php-errors.phpExplicit four-argument callbacks and application-owned PHP-error policy.
  • filtering.phpExplicit severity selection with the same original throwable and a general diagnostic branch.
  • best-effort.phpOrdered attempts through partial success, preserving the original and every reporter failure.
  • events.phpRaw and wrapped dispatch, providers/listeners and PSR-3 logging.
  • container.phpParent services, all seven factories, ConfigProvider and cached/fresh cores.
  • http.phpCapture/empty/zero fallback, safe application responses and callback restoration.
  • http-json.phpGeneric/development JSON, HTTP error statuses and one explicit private report.
  • http-whoops.phpTrusted development HTML, fresh configured Whoops pages and a generic public path.
  • provider-json.phpOptional Interop/FastForward JSON profile, explicit Config bool and fresh middleware scope; PHP 8.3+.
  • provider-whoops.phpOptional Whoops profile, lazy default/configured pages, private reporting and two fresh middleware scopes; PHP 8.3+.

Run process/fatal/HTTP scenarios through php examples/verify.php from the source checkout. Add --with-fast-forward on PHP 8.3+ with FastForward and Whoops installed to include JSON and HTML provider modes. Its bounded child runner checks actual exit, output and callback contracts. Read the verifier guide before running failure experiments.

Choose recipes by their application prerequisites in the cookbook. The verifier reports the passed scenarios and actual PHP version; recording reporters and returned response objects do not establish real transport delivery.

Continue in the source guides