waffle-commons / error-handler
Error-Handler component for Waffle framework.
Requires
- php: ^8.5
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/log: ^3.0
- waffle-commons/contracts: 0.1.0-beta2.1
Requires (Dev)
- carthage-software/mago: ^1.29
- cyclonedx/cyclonedx-php-composer: ^6.2
- php-mock/php-mock-phpunit: ^2.15
- phpunit/phpunit: ^12.5
- vimeo/psalm: ^6.16
This package is auto-updated.
Last update: 2026-05-30 19:46:39 UTC
README
Waffle Error Handler Component
Release:
v0.1.0-beta2ย |ยCHANGELOG.mdPSR Compliance: PSR-15 (middleware), PSR-3 (logging), RFC 7807 (application/problem+json), RFC 7231 (Allowheader on405)
The outermost middleware in every Waffle pipeline. Catches Throwable thrown deeper in the stack, logs it via the injected PSR-3 logger, and renders an RFC 7807 "Problem Details" JSON response.
๐ฆ Installation
composer require waffle-commons/error-handler
๐งฑ Surface
| Class | Role |
|---|---|
Waffle\Commons\ErrorHandler\Middleware\ErrorHandlerMiddleware |
PSR-15 middleware. Wraps $handler->handle() in try/catch(Throwable), logs, then delegates to the renderer. |
Waffle\Commons\ErrorHandler\Renderer\JsonErrorRenderer |
final readonly renderer implementing ErrorRendererInterface. Produces RFC 7807 JSON. |
๐ Wiring it up
use Waffle\Commons\ErrorHandler\Middleware\ErrorHandlerMiddleware; use Waffle\Commons\ErrorHandler\Renderer\JsonErrorRenderer; use Waffle\Commons\Http\Factory\ResponseFactory; use Waffle\Commons\Log\StreamLogger; $renderer = new JsonErrorRenderer( responseFactory: new ResponseFactory(), debug: $appDebug, // false in production ); $stack->prepend(new ErrorHandlerMiddleware($renderer, new StreamLogger()));
๐ฆ RFC 7807 payload
JsonErrorRenderer::render(Throwable $e, ServerRequestInterface $request) always emits the canonical RFC 7807 shape, encoded with JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Untrusted Host \"evil.example\".",
"instance": "/login"
}
Extensions added by Waffle:
- If the exception implements
Waffle\Commons\Contracts\Exception\Validation\ValidationExceptionInterfaceandgetField()returns non-null, afieldkey is added to the payload (RFC-011). - If
$debug = true, additionaltrace,file,linekeys are added. - In production (
$debug = false), any 5xxdetailis masked to"An internal server error occurred."to avoid leaking implementation details.
๐งฉ Status-code resolution
JsonErrorRenderer::determineStatusCode(Throwable $e) walks well-known exception interfaces (e.g. ValidationExceptionInterface โ 422, RouteNotFoundExceptionInterface โ 404, MethodNotAllowedExceptionInterface โ 405, \InvalidArgumentException โ 400) and falls back to 500 for unknown throwables. The matching is interface-based โ your application exceptions can opt in by implementing the right contract interface. For a MethodNotAllowedExceptionInterface, the renderer also emits an RFC 7231 Allow header (e.g. Allow: GET, HEAD, OPTIONS, POST).
๐ PHP 8.5 features used
final readonly class JsonErrorRendererโ the renderer holds an injectedResponseFactoryInterfaceand abool $debugflag, bothreadonly.- Strict-typed constructor + return types.
JSON_THROW_ON_ERRORfor fail-fast encoding.
๐งญ Architectural boundary (mago guard)
An active dependency perimeter is enforced on every CI run by vendor/bin/mago guard (bundled into composer mago; zero baselines). The rules live in mago.toml under [guard.perimeter] โ a forbidden use statement fails the build, not a reviewer.
Production code under Waffle\Commons\ErrorHandler may depend only on:
Waffle\Commons\ErrorHandler\**โ itselfWaffle\Commons\Contracts\**โ the shared contracts package, the only Waffle dependency permittedPsr\**โ PSR interfaces (PSR-7 / PSR-15 / PSR-17)@global+Psl\**โ PHP core and the PHP Standard Library
Test code under WaffleTests\Commons\ErrorHandler is unrestricted (@all). Structural rules are guarded too: interfaces must be named *Interface, Exception\** classes must end in *Exception, and any Enum\** namespace may hold only enum declarations.
Contract-first, component-agnostic by construction: components compose through waffle-commons/contracts, never directly through one another.
๐งช Testing
docker exec -w /waffle-commons/error-handler waffle-dev composer tests
๐ License
MIT โ see LICENSE.md.