lezhnev74/psr-recording-middleware
Composer 安装命令:
composer require --dev lezhnev74/psr-recording-middleware
包简介
A recording middleware for any PSR-7 HTTP client that records every request/response exchange and can replay them, turning recorded traffic into a predictable fixture for integration tests. Built on PSR-7, PSR-17 and PSR-3.
README 文档
README
A recording middleware for any PSR-7 HTTP client that records every request and response passing through it, and can switch to replay mode to serve the recorded responses back - turning real HTTP traffic into a predictable fixture for integration tests.
It is built on PSR-7 (messages), PSR-17 (message factories) and PSR-3 (logging), so it stays implementation-agnostic: it works with any PSR-7 implementation, none of which is a hard dependency. Guzzle is the primary test target, not a runtime dependency.
Requirements
- PHP 8.1 - 8.5
- Any PSR-7 / PSR-17 implementation installed in your app (e.g.
guzzlehttp/psr7ornyholm/psr7) - discovered automatically via php-http/discovery.
Installation
composer require lezhnev74/psr-recording-middleware
Usage
Configure a run, open a named session, and attach it to your client - via the Guzzle-style handler middleware or the PSR-18 decorator:
use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Promise\Create; use Lezhnev74\PsrRecordingMiddleware\{HandlerMiddleware, Mode, RecordingClient, RecordingRun, RunConfig}; // The run's default mode seeds every session it opens. $run = RecordingRun::fromConfig(RunConfig::create('/path/to/store', Mode::Replay)); $session = $run->openSession('example.com'); // inherits Replay // Guzzle handler stack: $stack = HandlerStack::create(); $stack->push(HandlerMiddleware::for($session, Create::promiseFor(...))); $client = new Client(['handler' => $stack]); // ...or wrap any PSR-18 client: $client = new RecordingClient($psr18Client, $session); // Replay (the default): requests are served from the store, network untouched. // Requests are matched byte-for-byte, in recorded order; a mismatch throws. $client->get('https://example.com/'); // Mode lives on the session, so one session can be flipped without touching // the run's default or any sibling session - either seed it when opening: $recording = $run->openSession('example.com', Mode::Record); // ...or flip an already-open session (chains off openSession): $run->openSession('example.com')->setMode(Mode::Record); // A Record session hits the network and saves every turn to the store.
Full working examples live in the tests:
tests/KnownHostRecordReplayTest.php
(end-to-end record/replay with a real Guzzle client),
tests/HandlerMiddlewareTest.php and
tests/RecordingClientTest.php.
PHPUnit integration
Drive the mode from an env var, keep the fixture store in a committed
directory, and give each test its own session. Tests then need no
record/replay branching - run once with HTTP_RECORD=1 against the real
network, commit tests/fixtures/http/, and every ordinary run replays
offline. In your base TestCase:
protected function httpSession(): Session { $run = RecordingRun::fromConfig(RunConfig::create( __DIR__ . '/fixtures/http', getenv('HTTP_RECORD') !== false ? Mode::Record : Mode::Replay, $this->masker(), // see below )); // One fixture dir per test; session names may not contain "\". return $run->openSession(str_replace('\\', '.', static::class) . '.' . $this->name()); }
Attach the session to the client your code under test uses (handler stack or
RecordingClient, as above). For integration tests, do it at the DI seam,
e.g. in Laravel: $this->app->extend(ClientInterface::class, fn ($inner) => new RecordingClient($inner, $this->httpSession())).
To re-prove a single test still works against the live network without
re-recording the whole suite, flip only that test's session:
$this->httpSession()->setMode(Mode::Record).
Masking volatile fields
Replay matches requests byte-for-byte, so any field that changes between runs
(rotating tokens, a default User-Agent embedding PHP/Guzzle versions) breaks
committed fixtures - and secrets must not land in git. Mask them: masking
applies identically when recording and when matching, so a masked field is
stored as ***, never mismatches, and never leaks.
private function masker(): Masker { return RecordingMasker::create(MaskingConfig::create( ['Authorization', 'User-Agent'], // headers ['token'], // query args ['password'], // JSON/form body fields )); }
The full runnable pattern - env-driven mode, session per test, masking
proven against changed tokens and User-Agent - is
tests/PhpUnitIntegrationExampleTest.php.
Local development
All QA tooling runs inside a PHP 8.5 Docker container (with Xdebug) via the
dev wrapper script - no PHP is needed on the host. Each command tunnels to the
matching Composer script inside the container, so
./dev <script> is just composer <script> run in Docker.
cp .env.dist .env # set DOCKER_USER_ID / DOCKER_GROUP_ID (see `id -u` / `id -g`) ./dev install # composer install ./dev test # run the test suite ./dev check # code style + static analysis + tests ./dev cs-fix # auto-fix code style ./dev debug-test # run tests under Xdebug (step-debugging)
Anything unrecognized is passed to composer, so ./dev require <pkg>,
./dev stan, ./dev rector etc. all work. Use ./dev php ... for a raw PHP
call, ./dev sh for a shell, and ./dev compose ... for raw docker compose.
Contributing
A few conventions, kept light:
- TDD. Every new branch needs a covering test; the recording/replay suite must stay green against both Guzzle and Nyholm PSR-7 impls.
- Conventional Commits (spec) for
messages, e.g.
feat(recording): ...,fix: ...,test: .... - Semantic versioning. Tags are
vMAJOR.MINOR.PATCH; the commit type drives the bump (fix→ patch,feat→ minor, a!/BREAKING CHANGE→ major). - Run
./dev check(style + static analysis + tests) before opening a PR.
License
MIT - see LICENSE.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 3
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-08