coisa/error-handler uses FastForward Container's Interop service-provider
contract under the CoiSA\ErrorHandler namespace. It requires PHP ^8.3;
CI targets PHP 8.3–8.5.
The library requires fast-forward/container:^1.10 and the
container-interop/service-provider:^0.4 interface package at runtime.
Composer installs them for consumers. The provider reuses FastForward's
AliasFactory::get(), InvokableFactory and CallableFactory; it does not ship a second container.
To install the container explicitly in an application:
composer require fast-forward/container:^1.10FastForward 1.10 requires Config ^1.6; these lines no longer use PHP-DI or
DotAccessData. Record the actual PHP and locked versions rather than treating a
successful Composer resolution as a complete runtime check. See
test evidence.
The development checkout includes it in require. After installing
the checkout dependencies, run the complete example:
php examples/fast-forward.phpIt reports one warning and restores the prior PHP callbacks. Pass the library
provider class and application provider directly to container(). This example
adds a callable reporter for the library's optional reporter discovery. Its
bootstrap.php selects the checkout or application autoloader as described in
example setup:
<?php
declare(strict_types=1);
use CoiSA\ErrorHandler\ErrorHandlerInterface;
use CoiSA\ErrorHandler\Handler\CallableThrowableHandler;
use CoiSA\ErrorHandler\ServiceProvider\ErrorHandlerServiceProvider;
use Interop\Container\ServiceProviderInterface;
use function FastForward\Container\container;
require __DIR__ . '/bootstrap.php';
$application = new class implements ServiceProviderInterface {
/**
* Supplies application service definitions for the integration fixture.
*
* @return array<class-string, callable> application service factories
*/
public function getFactories(): array
{
return [
CallableThrowableHandler::class => static fn(): CallableThrowableHandler => new CallableThrowableHandler(
/**
* Prints only this CLI recipe's synthetic diagnostic.
*
* @param Throwable $error the converted warning supplied for reporting
*/
static function (Throwable $error): void {
printf("Reported: %s\n", $error->getMessage());
},
),
];
}
/**
* Declares no application service extensions.
*
* @return array<string, callable> an empty extension map
*/
public function getExtensions(): array
{
return [];
}
};
$services = container($application, ErrorHandlerServiceProvider::class);
/**
* @var ErrorHandlerInterface $errors
*/
$errors = $services->get(ErrorHandlerInterface::class);
$errors->register();
try {
trigger_error('FastForward provider warning', E_USER_WARNING);
} finally {
$errors->unregister();
}For a process-wide handler, keep it registered through termination. For a scoped
operation, use try/finally and unregister; the caller then owns any propagated
exception. See lifecycle before combining these patterns.
getFactories() declares lazy services; getExtensions() returns an empty
array. Neither method, service construction nor has() installs PHP callbacks.
There is no provider boot hook. The application decides when to register.
| Service | Provider behavior |
|---|---|
ErrorHandlerInterface / ErrorHandler |
The same cached process handler; initially unregistered |
ThrowableHandlerInterface |
Default aggregate of available callable and PSR-14 reporters |
DispatchThrowableHandler / DispatchErrorEventThrowableHandler |
Existing PSR-14 adapters; require an application dispatcher |
ThrowableStreamFactoryInterface |
Pure generic fallback stream; requires a PSR-17 stream factory |
ThrowableResponseFactoryInterface |
Request-aware negotiation among generic, JSON and Symfony HTML responses |
HtmlThrowableResponseFactory / JsonThrowableResponseFactory / WhoopsThrowableResponseFactory |
Pure concrete renderers; only Whoops requires its optional package |
PrettyPageHandler |
Lazy cached page, customizable through an application service |
ErrorHandlerMiddleware |
Cached middleware with a handler separate from the process handler |
ServiceProvider\Factory\ErrorHandlerFactory |
Reusable callable that creates a fresh handler on each invocation |
ServiceProvider\Factory\ErrorHandlerMiddlewareFactory |
Reusable callable that creates fresh middleware and a fresh handler |
The single provider declares lifecycle, response negotiation and renderer services.
Dispatch handlers and simple generic stream/response adapters use FastForward
constructor autowiring. Interface aliases remain explicit, and application definitions
can override either an interface or its concrete implementation when their
maps are merged as shown under required overrides. Explicit library factories encode optional-handler defaults, reporter composition,
fresh scopes and lazy renderer selection; autowiring alone does not replace those
contracts. The factory-object services use FastForward's InvokableFactory.
Bind a required custom renderer through ThrowableStreamFactoryInterface or
ThrowableResponseFactoryInterface. FastForward can discard a concrete
class definition's PSR-11 construction failure and fall back to autowiring that
class. The same failure on an explicitly bound interface prevents construction,
because the interface has no autowire fallback. Successful concrete overrides
work, but they do not provide the same failure contract. The integration suite
checks both successful concrete overrides and required-interface failures.
Names in the table are relative to CoiSA\ErrorHandler; reporter classes are
under Handler, HTTP adapters under Http\Message, and middleware under
Http\Middleware.
Bind ThrowableHandlerInterface in the application provider for an explicit
reporter. Alternatively provide CallableThrowableHandler or
Psr\EventDispatcher\EventDispatcherInterface to populate the aggregate. The
PSR-14 path dispatches both the original throwable and an ErrorEvent wrapper;
subscribe intentionally to avoid duplicate application reporting. An aggregate
with no configured reporters is silent. No logger is silently installed.
The compatible 2.x LogThrowableHandler and FilteringThrowableHandler remain explicit application composition. Construct the chosen reporter and bind it under ThrowableHandlerInterface using the merged-provider pattern below. Neither adapter is added to the optional discovery list. A logger binding alone does not opt into direct logging, and a filter does not change the separate public HTTP renderer.
For an explicit PHP-error policy, bind an interface instance or callable under
PhpErrorHandlerInterface. ErrorHandlerFactory passes the configured value into
process and fresh middleware handlers, preserving four arguments, ignored callback
results and the callback's own mask/conversion policy. The same choice is available
under ThrowableHandlerInterface and ShutdownHandlerInterface. Bind callbacks as
values with ServiceFactory($callback); a raw callable in a factory map describes
service construction instead. Required overrides use the aggregate below.
The explicit ErrorHandlerFactory remains responsible for optional defaults and fresh scopes; this does not require a container to autowire arbitrary union types. Interface instances take precedence over their callable capability. No callback is executed or registered during construction. See adapter contracts and the logging, filtering and PHP-error examples.
Some autowiring containers advertise CallableThrowableHandler through has()
even when its required callable cannot be constructed. Optional discovery skips
a PSR-11 not-found result or FastForward's structural AutowiringException only
for that callable lookup. This lets an application dispatcher receive both event
routes without a dummy callable. Other callable-construction failures and all
dispatch-adapter failures propagate from the aggregate factory.
FastForward 1.10.0 wraps an executing provider factory's failure in
ContainerException, retaining its cause; this is not skipped by the optional
lookup. The final container can nevertheless recover from failed concrete
aggregate construction by autowiring an empty ThrowableHandlerAggregate and
caching it. Merging maps does not disable that native fallback. Use an explicit
ThrowableHandlerInterface binding for a required reporter and the merged-provider
composition below; verify delivery and failures in the actual application graph.
The short bootstrap above adds optional reporters and external dependencies.
FastForward 1.10 searches separate providers in the supplied order;
they do not merge their factory maps. The normal
container($application, ErrorHandlerServiceProvider::class) bootstrap gives
application definitions priority. If an application factory throws a PSR-11
construction failure, resolution can still fall through to the silent library
aggregate. This distinction matters only when failure must prevent construction.
When replacing an existing service ID, merge the definitions explicitly, with the application last:
use FastForward\Container\ServiceProvider\AggregateServiceProvider;
$services = container(
new AggregateServiceProvider(
new ErrorHandlerServiceProvider(),
$application
)
);Here $application binds the required reporter or renderer interface.
AggregateServiceProvider uses its last definition for each service ID. This
replaces the library default and makes required-reporter failures fail
construction of both process handler and middleware. FastForward normalizes
construction failures into PSR-11 exceptions; its aggregate retains the first
construction failure when no source succeeds. The supported contract is failed
construction after reaching the required factory, without silent fallback to the
library's default reporter. Do not depend on a particular wrapper type or message;
inspect the available cause chain when diagnosing a failed resolution.
Container resolution may attempt a failing construction more than once while
trying its delegates and autowiring. Application factories must tolerate those
attempts; this differs from the handler's no-retry contract after reporting fails.
Optional PhpErrorHandlerInterface and ShutdownHandlerInterface bindings are
passed to fresh handlers too. The provider reuses the package's PSR-11 factories
and introduces no separate configuration format or automatic event subscriptions.
The Interop service provider is the only supported container integration.
ConfigProvider, its Laminas discovery metadata, ErrorHandlerContainer and the
constructor-only factories have been removed. Provider factories all live under
ServiceProvider\Factory; see migration. Provider consumers must
support constructor autowiring, as FastForward's container() does.
The usage cookbook exposes complete examples for every built-in adapter: PSR-14 dispatch and PSR-3 logging, private HTTP reporting, controlled JSON rendering, custom PHP/shutdown handlers, direct reporter composition and nested scopes. The cookbook includes each complete program. The documentation site links the same sources and displays checked excerpts.
Supply Psr\Http\Message\ResponseFactoryInterface and
Psr\Http\Message\StreamFactoryInterface, for example using an application's
Nyholm PSR-7 factory. The normal middleware service can process sequential
requests and restores an already registered process handler after each one.
The middleware factory deliberately does not resolve the cached ErrorHandler
process service. Its finally must unregister only its own handler; sharing the
process instance would deactivate the application's bootstrap registration.
ErrorHandlerFactory also applies optional PHP/shutdown bindings consistently.
Autowiring selects constructor dependencies, but does not establish a fresh
lifecycle or make a cached instance independent.
FastForward caches get() results. For nested scopes, resolve a factory object
and invoke it for each middleware instance:
use CoiSA\ErrorHandler\ServiceProvider\Factory\ErrorHandlerMiddlewareFactory;
$makeMiddleware = $services->get(ErrorHandlerMiddlewareFactory::class);
$outer = $makeMiddleware($services);
$inner = $makeMiddleware($services);Likewise invoke the ServiceProvider\Factory\ErrorHandlerFactory service to create
fresh direct handlers. Avoid sharing the process handler with middleware and
do not pre-register the middleware's own handler. The reusable factory pattern
does not provide concurrent-request isolation: PHP callback stacks remain
process-wide. Reuse instances for sequential work when practical because PHP
retains registered shutdown callbacks until process exit.
The integration tests use the real released FastForward container, including provider-class bootstrap, explicit and optional reporters, custom PHP/shutdown handlers, event dispatch, fresh factories, nested HTTP scopes and restoration after renderer failure. The full test command retains the existing bounded fatal-error fixtures under PHPUnit Process tests. CI also installs without development dependencies and resolves a process handler through the provider, proving that FastForward's factory classes and autowiring are available in production installations.
The full FastForward framework is not required. The Composer package is
coisa/error-handler, under the CoiSA\ErrorHandler namespace.
Sources: FastForward Container 1.10.0, provider contract, cached provider services.
If the built-in handler's reporter fails during a request warning, middleware propagates that failure and restores callbacks without rendering it through the reporter again. Ordinary downstream failures still use the configured response factory. See failure boundaries.
Middleware requires one scoped instance implementing both ErrorHandlerInterface
and ThrowableHandlerInterface. It reports downstream failures before invoking
the response factory with the original Throwable and current request. The fallback
ThrowableStreamFactoryInterface alias
selects Http\Message\GenericThrowableStreamFactory, which uses PSR-17 directly
and produces a controlled body without invoking reporters or output buffering.
A lifecycle-only custom implementation must add reporting to use the middleware.
Reporters keep their void contract and must write HTTP diagnostics privately.
Use the request-aware responses below or implement a custom
stream/response factory; see the complete
renderer example. The retained ThrowableStreamFactory
captures a separate public renderer only when explicitly selected. Binding the
old concrete capture class no longer changes the default alias. Bind the
interface for required custom rendering, and do not inject the diagnostic reporter
into a capture adapter after middleware has already reported it.
new ErrorHandlerServiceProvider(cli: true) appends SymfonyCliThrowableHandler
to the default reporter aggregate after its optional reporters. With the flag false,
terminal formatting is not selected automatically. A configured
ThrowableHandlerInterface override replaces the aggregate as usual.
The optional OutputInterface application service selects a Console destination;
without one the handler writes to stderr. The CLI flag does not enable HTTP debug
or install callbacks. See terminal integration and Console ownership.
ErrorHandlerServiceProvider(bool $debug = false, bool $cli = false) is the only library provider.
Its response alias selects NegotiatingThrowableResponseFactory. The current
request's Accept header chooses from generic text/plain, application/json or application/problem+json
and Symfony text/html. Trusted debug enables diagnostics in the same Symfony
renderer. Negotiated responses include Accept in Vary and preserve existing Vary fields. Installed Whoops never changes this default.
Missing Accept or no supported positive match uses the generic fallback.
Debug never forces HTML; JSON-only requests do not resolve Whoops.
See quality values and fallbacks.
The application supplies its own PSR-17 implementations and private reporter:
$services = container($application, ErrorHandlerServiceProvider::class);For trusted development configuration, read a real boolean before bootstrap:
$settings = config(['error_handler' => ['debug' => true]]);
$services = container(
$application,
new ErrorHandlerServiceProvider(debug: $settings->get('error_handler.debug', false)),
);The Config key belongs to this application example; the library reads no
environment variable or configuration key automatically. Avoid casting the
string "false" to bool, which enables PHP truthiness. The
JSON provider recipe
shows public/debug modes and Accept: application/json with one private report.
The provider also exposes the concrete JSON renderer for explicit application
composition; selecting a concrete renderer directly bypasses negotiation. JSON uses
SerializerInterface, supplied by the provider as a Symfony Serializer with
ProblemNormalizer and JsonEncoder. Both accepted JSON media types use Problem
Details; direct JSON rendering returns application/problem+json. Override the
serializer service explicitly only with a collaborator supporting that same contract.
The negotiator is cached, while the request is supplied for each rendering call. Its lazy renderer map resolves only the selected response service. Use the middleware factory for fresh scopes; repeated container lookups remain cached. Reporter and renderer failures retain the HTTP boundaries.
Symfony HTML is the default in public and debug modes. To select Whoops instead,
install optional filp/whoops:^2.18.7 in the application. The public/default/configured recipe
uses the same provider with Accept: text/html and an explicit application
override of the negotiating HTML candidate in its development modes. Public mode retains Symfony HTML. The default provider never
auto-selects Whoops based on package availability.
The provider binds PrettyPageHandler through FastForward's InvokableFactory.
The HTML renderer's lazy callback resolves that service only during selected HTML
rendering. Discovery, JSON-only requests and middleware construction do not create
a page or register native callbacks. The concrete Whoops renderer remains usable
directly, where the application owns development authorization.
Customize a page before container lookup, apply trusted Config values and bind it
with ServiceFactory in the application map. Application-first composition gives
that page priority. The container caches it; sequential renders replace diagnostic
data while retaining configuration. Every render owns a fresh unregistered Whoops
run, response and stream. This is not concurrent-request isolation.
FastForward can recover a failing concrete page factory by autowiring a default page. A merged provider fixes map precedence but does not disable that concrete fallback. Prepare required page configuration before lookup when configuration failures must be observed immediately. Wrong-type pages and rendering failures propagate through their actual runtime boundaries; there is no second resolver, page cloning or warning suppression.
The manual renderer's callable remains available for explicit composition.
The provider never selects ReportingThrowableResponseFactory: middleware already
reported the original. Debug output can expose source, globals and traces; selected
key hiding is not complete redaction. See diagnostic HTML.
| Symptom | Check |
|---|---|
| A required reporter is silent | Bind ThrowableHandlerInterface and merge library then application maps in one aggregate. A separate application provider can be shadowed or skipped after a construction failure. |
| A request deactivates the process handler | Give middleware a distinct handler. Resolve and invoke its factory for each independent scope; do not pre-register that handler. |
| A page override appears to be ignored | Configure the page before lookup and bind the instance with ServiceFactory. A failed concrete factory can fall back to an autowired default page. |
| Whoops classes are unavailable | Install optional filp/whoops:^2.18.7 in the consuming application and confirm that the selected autoloader includes it. |
For reporter echo, duplicate reports and diagnostic exposure, follow HTTP boundaries. Container failure normalization does not make an invalid required binding optional.