Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changelog/whoops-native-source-frames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
category: fixed
type: patch
---

Require optional Whoops `^2.18.7` for development HTML, adopting its upstream fix
for native STDIN and `php -r` source inspection. Reject older Whoops versions
through Composer without making the optional package mandatory. Preserve original exception
frames, one original report, legitimate application warnings and the existing
2.x response and lifecycle policies. Align installation guidance and the bundled
consumer skill, with bounded native source regressions alongside the renderer's
unit tests.
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"suggest": {
"ext-json": "Required only for debug JSON responses on PHP 7.4; included in PHP 8.",
"fast-forward/container": "^1.9",
"filp/whoops": "Install ^2.18.4 for optional development HTML responses.",
"filp/whoops": "Install ^2.18.7 for optional development HTML responses.",
"llm/skills": "Optionally synchronize the bundled agent skill in consumer projects."
},
"require": {
Expand All @@ -41,6 +41,9 @@
"psr/event-dispatcher": "^1.0",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"conflict": {
"filp/whoops": "<2.18.7"
},
"autoload": {
"psr-4": {
"CoiSA\\ErrorHandler\\": "src/"
Expand All @@ -58,7 +61,7 @@
"require-dev": {
"coisa/php-cs-fixer": "^3.0",
"fast-forward/container": "^1.9",
"filp/whoops": "^2.18.4",
"filp/whoops": "^2.18.7",
"jangregor/phpstan-prophecy": "^1.0",
"laminas/laminas-servicemanager": "^4.3",
"llm/skills": "^1.15",
Expand Down
4 changes: 2 additions & 2 deletions docs/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ Use `declare(strict_types=1)` in the application entrypoint, as the complete rec

### Development HTML profile

Select `WhoopsErrorHandlerServiceProvider` only after the application's trusted development and HTML-format decision. Install optional `filp/whoops:^2.18.4` where that renderer is used. With the same application PSR-17 and private reporter definitions, the default form is:
Select `WhoopsErrorHandlerServiceProvider` only after the application's trusted development and HTML-format decision. Install optional `filp/whoops:^2.18.7` where that renderer is used. With the same application PSR-17 and private reporter definitions, the default form is:

```php
use CoiSA\ErrorHandler\ServiceProvider\ErrorHandlerServiceProvider;
Expand Down Expand Up @@ -234,7 +234,7 @@ Applications decide when a resolved process handler calls `register()`. For HTTP

## Optional development HTML composition

For manual or legacy-container composition, supply application PSR-17 factories and a callable supplying `Whoops\Handler\PrettyPageHandler`, then compose private reporting with `ReportingThrowableResponseFactory` in 2.x. The renderer adds no legacy aliases or ConfigProvider bindings; the separate opt-in [Whoops Interop profile](#development-html-profile) supplies its own map. The renderer can be cached with configuration; do not capture a current request. A manual callback may create a fresh page or reuse one for sequential rendering under the state limits above. Construct it only after the application's trusted development/format decision. Core/container installation does not require Whoops; choosing the adapter without installing `filp/whoops:^2.18.4` raises `LogicException`. See [the HTML guide](http.md#opt-in-development-html-with-whoops) and [manual recipe](../examples/http-whoops.php). Existing container/discovery/middleware defaults remain unchanged.
For manual or legacy-container composition, supply application PSR-17 factories and a callable supplying `Whoops\Handler\PrettyPageHandler`, then compose private reporting with `ReportingThrowableResponseFactory` in 2.x. The renderer adds no legacy aliases or ConfigProvider bindings; the separate opt-in [Whoops Interop profile](#development-html-profile) supplies its own map. The renderer can be cached with configuration; do not capture a current request. A manual callback may create a fresh page or reuse one for sequential rendering under the state limits above. Construct it only after the application's trusted development/format decision. Core/container installation does not require Whoops; choosing the adapter without installing `filp/whoops:^2.18.7` raises `LogicException`. See [the HTML guide](http.md#opt-in-development-html-with-whoops) and [manual recipe](../examples/http-whoops.php). Existing container/discovery/middleware defaults remain unchanged.

## Troubleshoot service resolution

Expand Down
2 changes: 1 addition & 1 deletion docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -1404,7 +1404,7 @@ echo 'HTTP Whoops ' . $mode . ': explicit renderer selection and one private rep

## Optional Interop Whoops profile

With PHP 8.3+, install FastForward as an application runtime dependency and optional Whoops ^2.18.4 as described in the [provider guide](container.md#development-html-profile). Trusted application configuration selects this diagnostic HTML profile. Compose the independent providers in one aggregate ordered base, Whoops, application. The provider has no constructor arguments; its default page service is lazy and cached. The configured mode reads Config and binds a prepared `PrettyPageHandler` through `ServiceFactory`. Both use private reporting and two distinct middleware scopes, without negotiation or emission. Sequential renders retain page configuration while replacing the current diagnostic. The guide records container fallback and missing-package limits.
With PHP 8.3+, install FastForward as an application runtime dependency and optional Whoops ^2.18.7 as described in the [provider guide](container.md#development-html-profile). Trusted application configuration selects this diagnostic HTML profile. Compose the independent providers in one aggregate ordered base, Whoops, application. The provider has no constructor arguments; its default page service is lazy and cached. The configured mode reads Config and binds a prepared `PrettyPageHandler` through `ServiceFactory`. Both use private reporting and two distinct middleware scopes, without negotiation or emission. Sequential renders retain page configuration while replacing the current diagnostic. The guide records container fallback and missing-package limits.

Source: [examples/provider-whoops.php](https://github.com/coisa/error-handler/blob/master/examples/provider-whoops.php).

Expand Down
8 changes: 4 additions & 4 deletions docs/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,17 @@ Read the [provider dependencies, Config values and override precedence](containe

## Opt-in development HTML with Whoops

`Http\Message\WhoopsThrowableResponseFactory` was published in 2.5.0. Applications selecting this development HTML route need `coisa/error-handler:^2.5`. Install optional `filp/whoops:^2.18.4` in that application's development dependencies. The library suggests Whoops and uses it for development tests; core/container installations do not require it. Loading the adapter class without Whoops works; constructing it without the dependency raises `LogicException` with installation guidance.
`Http\Message\WhoopsThrowableResponseFactory` was published in 2.5.0. Applications selecting this development HTML route need `coisa/error-handler:^2.5`. Install optional `filp/whoops:^2.18.7` in that application's development dependencies. The library suggests Whoops and uses it for development tests; core/container installations do not require it. Loading the adapter class without Whoops works; constructing it without the dependency raises `LogicException` with installation guidance.

The minimum Whoops 2.18.4 includes the upstream PHP 8.5 deprecation fix. Older Whoops versions are outside this adapter's supported integration; the minimum keeps that native deprecation from adding a PHP error event during rendering. This optional integration requirement does not add Whoops to core runtime dependencies.
The minimum Whoops 2.18.7 includes the upstream PHP 8.5 deprecation fix and native virtual-source inspection repair. Whoops preserves frames created through STDIN (`Standard input code`) and `php -r` (`Command line code`) without trying to read those names as files. Composer rejects older Whoops versions through an optional conflict constraint; it does not install Whoops unless the application requests it. This requirement neither adds Whoops to core runtime dependencies nor suppresses legitimate application warnings.

The constructor takes `ResponseFactoryInterface`, `StreamFactoryInterface` and a required `callable(): Whoops\Handler\PrettyPageHandler`. The callable supplies the configured page for each render and is not invoked during construction. It may create a new page or return a configured page reused for sequential rendering. Whoops replaces the page's current exception, inspector and generated template data; configuration stays in place. A reused page retains its latest diagnostic until the next render. This is not a concurrency or arbitrary-subclass guarantee. The adapter creates its own fresh Run, sets `handleUnconditionally(true)`, `allowQuit(false)`, `writeToOutput(false)` and `sendHttpCode(false)`, and never calls `register()`.

HTML comes from Whoops' public string-returning path. Its internal buffering belongs to Whoops; CoiSA adds no reporter-output capture. The adapter returns a new PSR body with `text/html; charset=UTF-8` and `Cache-Control: no-store`. It preserves integer codes 400–599, otherwise uses 500, and omits the reason argument so PSR-17 supplies its default. The original diagnostic code shown by Whoops is not a separate HTTP status promise; the Throwable is neither wrapped nor changed.

Reuse `ReportingThrowableResponseFactory($reporter, $html)` in the 2.x middleware. The private reporter runs once before rendering; its failure prevents page lookup. Failures escaping the manual page callback, Whoops or HTTP collaborators propagate without adapter retry, fallback or another report. Returning an incompatible page produces `TypeError`. These controls do not suppress arbitrary output, callbacks or process termination deliberately performed by application callbacks/subclasses.

One integration limit is reproduced on PHP 8.5.11 with Whoops 2.18.6: rendering a Throwable created through STDIN makes Whoops try to read `Standard input code` as a source file. Its resulting `file_get_contents` warning reaches the active PHP-error handler as a separate event. The original is still reported once; a reporter that throws on that additional warning can interrupt rendering. This also occurs in the manual composition, independently of the provider, and is tracked in [issue 99](https://github.com/coisa/error-handler/issues/99). The executable recipes use real source files; no warning suppression or trace rewriting is supplied.
The renderer leaves the original Throwable and its trace unchanged. The 2.x reporting decorator still reports the original once before rendering, and enabled application PHP warnings still reach their configured reporter. No warning suppression, fabricated filename or trace rewriting is supplied. The [native source regression](testing.md#optional-whoops-html-adapter) checks STDIN, `php -r` and real files separately.

The [complete HTML recipe](https://github.com/coisa/error-handler/blob/master/examples/http-whoops.php) starts with generic JSON. Its trusted `development` mode selects Whoops in an explicitly chosen HTML pipeline; `public` mode never constructs a page and can run without Whoops. The mode represents application configuration, not a request parameter. There is no additional debug flag on the HTML class: choosing this diagnostic renderer is the explicit opt-in.

Expand All @@ -165,7 +165,7 @@ This factory receives only Throwable. It does not negotiate Accept, infer a brow

### Whoops through an Interop service provider

With application PHP 8.3+, `fast-forward/container:^1.9` installed at runtime and optional `filp/whoops:^2.18.4`, choose Whoops after the trusted development/HTML decision. Compose the independent providers in one `AggregateServiceProvider`, ordered `ErrorHandlerServiceProvider`, `WhoopsErrorHandlerServiceProvider`, application. Its last definition wins, retaining HTML and application page overrides. Supply the same application PSR-17 factories and private reporter used by the JSON profile. The Whoops map selects the 2.x reporting decorator; the base supplies `ServiceProvider\Factory\ErrorHandlerMiddlewareFactory` for a fresh HTTP scope.
With application PHP 8.3+, `fast-forward/container:^1.9` installed at runtime and optional `filp/whoops:^2.18.7`, choose Whoops after the trusted development/HTML decision. Compose the independent providers in one `AggregateServiceProvider`, ordered `ErrorHandlerServiceProvider`, `WhoopsErrorHandlerServiceProvider`, application. Its last definition wins, retaining HTML and application page overrides. Supply the same application PSR-17 factories and private reporter used by the JSON profile. The Whoops map selects the 2.x reporting decorator; the base supplies `ServiceProvider\Factory\ErrorHandlerMiddlewareFactory` for a fresh HTTP scope.

The profile has no constructor arguments. It defines `PrettyPageHandler::class` through FastForward's `InvokableFactory` and resolves that cached page lazily when rendering starts. Neither provider discovery, renderer resolution nor middleware scope construction creates the default page. The [complete provider recipe](../examples/provider-whoops.php) also shows application Config preparing a customized page before composition, bound through `ServiceFactory`. Sequential renders reuse page configuration while replacing the current diagnostic, with a new Run, response and stream each time. There is no request negotiation or automatic development detection. Selected key hiding remains a limited diagnostic policy, not comprehensive redaction.

Expand Down
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ <h2 id="interop-title">Select an optional response provider</h2>
$application,
));</code></pre>
</figure>
<p>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 <code>filp/whoops:^2.18.4</code> wherever development HTML is selected. The default <code>PrettyPageHandler</code> service is lazy and cached. The configured recipe reads Config and binds a prepared page through <code>ServiceFactory</code>. 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 <a href="https://github.com/coisa/error-handler/blob/master/docs/container.md">container failure boundaries</a>: 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.</p>
<p>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 <code>filp/whoops:^2.18.7</code> wherever development HTML is selected. The default <code>PrettyPageHandler</code> service is lazy and cached. The configured recipe reads Config and binds a prepared page through <code>ServiceFactory</code>. 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 <a href="https://github.com/coisa/error-handler/blob/master/docs/container.md">container failure boundaries</a>: 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.</p>
<figure>
<figcaption>Fresh scope from <a href="https://github.com/coisa/error-handler/blob/master/examples/provider-json.php">the same executable recipe</a> · factory service under <code>ServiceProvider\Factory</code></figcaption>
<pre tabindex="0" aria-label="Fresh Interop middleware scope PHP source"><code data-source="examples/provider-json.php">$middlewareFactory = $services-&gt;get(ErrorHandlerMiddlewareFactory::class);
Expand Down Expand Up @@ -326,7 +326,7 @@ <h2 id="http-title">Separate HTTP responsibilities</h2>

$middleware = new ErrorHandlerMiddleware($errors, $responses);</code></pre>
</figure>
<p>The optional <code>WhoopsThrowableResponseFactory</code> takes two PSR-17 factories and a callable creating a fresh configured <code>PrettyPageHandler</code>. Install <code>filp/whoops:^2.18.4</code> only where development HTML is used. Each render owns a new, unregistered Run, returns HTML without emission/exit and sets <code>text/html; charset=UTF-8</code> plus <code>no-store</code>. It preserves integer codes 400–599 and delegates the reason to PSR-17. Core/container defaults remain unchanged.</p>
<p>The optional <code>WhoopsThrowableResponseFactory</code> takes two PSR-17 factories and a callable creating a fresh configured <code>PrettyPageHandler</code>. Install <code>filp/whoops:^2.18.7</code> only where development HTML is used. Each render owns a new, unregistered Run, returns HTML without emission/exit and sets <code>text/html; charset=UTF-8</code> plus <code>no-store</code>. It preserves integer codes 400–599 and delegates the reason to PSR-17. Core/container defaults remain unchanged.</p>
<p>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.</p>
<p>See <a href="https://github.com/coisa/error-handler/blob/master/docs/http.md">the HTTP guide</a> for success/failure restoration, capture limitations and the complete application composition.</p>
</section>
Expand Down
Loading
Loading