waffle-commons/error-handler

Error-Handler component for Waffle framework.

Maintainers

Package info

github.com/waffle-commons/error-handler

pkg:composer/waffle-commons/error-handler

Statistics

Installs: 20

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

0.1.0-beta2.1 2026-05-30 18:58 UTC

README

Discord PHP Version Require PHP CI codecov Latest Stable Version Latest Unstable Version Total Downloads Packagist License

Waffle Error Handler Component

Release: v0.1.0-beta2 ย |ย  CHANGELOG.md PSR Compliance: PSR-15 (middleware), PSR-3 (logging), RFC 7807 (application/problem+json), RFC 7231 (Allow header on 405)

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\ValidationExceptionInterface and getField() returns non-null, a field key is added to the payload (RFC-011).
  • If $debug = true, additional trace, file, line keys are added.
  • In production ($debug = false), any 5xx detail is 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 injected ResponseFactoryInterface and a bool $debug flag, both readonly.
  • Strict-typed constructor + return types.
  • JSON_THROW_ON_ERROR for 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\** โ€” itself
  • Waffle\Commons\Contracts\** โ€” the shared contracts package, the only Waffle dependency permitted
  • Psr\** โ€” 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.