From 3573832cb458e177b3d5bc23da3beaf69e5280ab Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Fri, 4 Sep 2026 22:46:56 +0100 Subject: [PATCH] Add CI/coverage badges and integration examples CI: new 'coverage' job (pcov + Codecov upload, fail_ci_if_error: false so CI never blocks on it before the Codecov app is enabled for this repo). README: Tests and codecov badges. examples/: basic.php (a PHP fatal that is not a Throwable, caught via the shutdown handler), psr3-logger.php (a PSR-3 logger writing to stdout, 12-factor style), sse-interception.php (a fatal mid-stream on an active SSE response, answered as one more SSE event instead of corrupting the stream), slim.php (where this boundary sits relative to Slim's own error middleware). Each snippet other than slim.php was run and its output verified; none add a dependency to the package itself. --- .github/workflows/tests.yml | 27 ++++++++++++++++++++ README.md | 15 ++++++++++++ examples/basic.php | 31 +++++++++++++++++++++++ examples/psr3-logger.php | 38 +++++++++++++++++++++++++++++ examples/slim.php | 41 +++++++++++++++++++++++++++++++ examples/sse-interception.php | 46 +++++++++++++++++++++++++++++++++++ 6 files changed, 198 insertions(+) create mode 100644 examples/basic.php create mode 100644 examples/psr3-logger.php create mode 100644 examples/slim.php create mode 100644 examples/sse-interception.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5595fd6..dfd32a0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,6 +42,33 @@ jobs: - name: Run tests run: vendor/bin/phpunit + coverage: + name: Coverage + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: pcov + + - name: Install dependencies + uses: ramsey/composer-install@4.0.0 + + - name: Run tests with coverage + run: vendor/bin/phpunit --coverage-clover=coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + static-analysis: name: Static Analysis runs-on: ubuntu-latest diff --git a/README.md b/README.md index e236223..c2a03fa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # PHP Error Boundary +[![Tests](https://github.com/CleatSquad/php-error-boundary/actions/workflows/tests.yml/badge.svg)](https://github.com/CleatSquad/php-error-boundary/actions/workflows/tests.yml) +[![codecov](https://codecov.io/gh/CleatSquad/php-error-boundary/branch/main/graph/badge.svg)](https://codecov.io/gh/CleatSquad/php-error-boundary) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![PHP Version](https://img.shields.io/badge/php-%3E%3D8.2-777bb4.svg)](composer.json) @@ -89,6 +91,19 @@ ErrorBoundary::setShutdownInterception(function (array $error): bool { }); ``` +## Examples + +Full, runnable-style snippets for common setups live in +[`examples/`](examples/): + +- [`examples/basic.php`](examples/basic.php) — installing at the entry + point of a plain PHP script. +- [`examples/psr3-logger.php`](examples/psr3-logger.php) — wiring a PSR-3 + logger (Monolog, or any other implementation). +- [`examples/slim.php`](examples/slim.php) — a Slim 4 application. +- [`examples/sse-interception.php`](examples/sse-interception.php) — + intercepting a fatal mid-stream on an active Server-Sent Events response. + ## Design notes **No stack trace, no file path, ever reaches the client.** The default diff --git a/examples/basic.php b/examples/basic.php new file mode 100644 index 0000000..9715c10 --- /dev/null +++ b/examples/basic.php @@ -0,0 +1,31 @@ + $user->getName()]; +} + +echo json_encode(handleRequest()); diff --git a/examples/psr3-logger.php b/examples/psr3-logger.php new file mode 100644 index 0000000..2b76c82 --- /dev/null +++ b/examples/psr3-logger.php @@ -0,0 +1,38 @@ +pushHandler(new StreamHandler('php://stdout')); + +ErrorBoundary::install(null, $logger); + +header('Content-Type: application/json'); + +function handleRequest(): array +{ + throw new RuntimeException('Upstream payment provider is unreachable.'); +} + +echo json_encode(handleRequest()); + +// stdout now carries a structured log line for this exception, in addition +// to the JSON envelope the client received — one event, two destinations. diff --git a/examples/slim.php b/examples/slim.php new file mode 100644 index 0000000..6a1f106 --- /dev/null +++ b/examples/slim.php @@ -0,0 +1,41 @@ +get('/status', function ($request, $response) { + $response->getBody()->write(json_encode(['status' => 'ok'])); + + return $response->withHeader('Content-Type', 'application/json'); +}); + +$app->get('/crash', function () { + // A memory_limit exhaustion here (not simulated — genuinely uncatchable) + // is exactly the case Slim's own error middleware cannot see coming: + // it is a PHP fatal, not a Throwable. ErrorBoundary's shutdown handler + // answers with the same JSON envelope regardless. + throw new RuntimeException('Unreachable in this example — illustrates where the boundary sits.'); +}); + +$app->run(); diff --git a/examples/sse-interception.php b/examples/sse-interception.php new file mode 100644 index 0000000..2083470 --- /dev/null +++ b/examples/sse-interception.php @@ -0,0 +1,46 @@ + 'The stream ended unexpectedly.']) . "\n\n"; + + return true; // Skip the boundary's standard JSON emit — already answered. +}); + +function streamEvents(): void +{ + echo "event: message\n"; + echo 'data: ' . json_encode(['step' => 1]) . "\n\n"; + flush(); + + // Simulate a bug partway through the stream. + $state = null; + echo 'data: ' . json_encode(['step' => $state->next()]) . "\n\n"; +} + +streamEvents();