Skip to content

Latest commit

 

History

History
411 lines (335 loc) · 21.9 KB

fast-forward.md

File metadata and controls

411 lines (335 loc) · 21.9 KB

FastForward service provider

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.

Installation and bootstrap

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.10

FastForward 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.php

It 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.

Definitions and overrides

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.

Required overrides

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.

Runnable integration recipes

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.

HTTP scopes and fresh instances

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.

Verification and migration readiness

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.

Reporting and rendering

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.

Explicit terminal reporting

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.

Request-aware responses

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.

Development HTML configuration

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.

Troubleshooting composition

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.