PHPackages                             lezhnev74/psr-recording-middleware - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. lezhnev74/psr-recording-middleware

ActiveLibrary[Testing &amp; Quality](/categories/testing)

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.

1.0.2(1mo ago)017↓80%MITPHPPHP ^8.1CI passing

Since Jul 7Pushed 1mo agoCompare

[ Source](https://github.com/lezhnev74/psr-recording-middleware)[ Packagist](https://packagist.org/packages/lezhnev74/psr-recording-middleware)[ RSS](/packages/lezhnev74-psr-recording-middleware/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (17)Versions (4)Used By (0)

PSR-compatible Recording Middleware
===================================

[](#psr-compatible-recording-middleware)

[![tests](https://github.com/lezhnev74/psr-recording-middleware/actions/workflows/tests.yml/badge.svg)](https://github.com/lezhnev74/psr-recording-middleware/actions/workflows/tests.yml)[![codecov](https://camo.githubusercontent.com/b1a52011644f9780051e82bb08c4f9b27ce617b7343e6d4910d3babd4cc37edf/68747470733a2f2f636f6465636f762e696f2f67682f6c657a686e657637342f7073722d7265636f7264696e672d6d6964646c65776172652f6272616e63682f6d61696e2f67726170682f62616467652e737667)](https://codecov.io/gh/lezhnev74/psr-recording-middleware)[![PHPStan](https://camo.githubusercontent.com/14995ff65edea59395c224e37e4fc66f91c1e601c1a58311e3c6f38c4fe37feb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c2532306d61782d627269676874677265656e)](https://phpstan.org/)

A recording middleware for any [PSR-7](https://www.php-fig.org/psr/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](https://www.php-fig.org/psr/psr-7/) (messages), [PSR-17](https://www.php-fig.org/psr/psr-17/) (message factories) and [PSR-3](https://www.php-fig.org/psr/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
------------

[](#requirements)

- PHP 8.1 - 8.5
- Any PSR-7 / PSR-17 implementation installed in your app (e.g. `guzzlehttp/psr7` or `nyholm/psr7`) - discovered automatically via [php-http/discovery](https://docs.php-http.org/en/latest/discovery.html).

Installation
------------

[](#installation)

```
composer require lezhnev74/psr-recording-middleware
```

Usage
-----

[](#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`](tests/KnownHostRecordReplayTest.php)(end-to-end record/replay with a real Guzzle client), [`tests/HandlerMiddlewareTest.php`](tests/HandlerMiddlewareTest.php) and [`tests/RecordingClientTest.php`](tests/RecordingClientTest.php).

### PHPUnit integration

[](#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

[](#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`](tests/PhpUnitIntegrationExampleTest.php).

### Committing recordings to git

[](#committing-recordings-to-git)

Recordings capture raw request/response bytes, including binary bodies. Tell git to treat the store as binary so it never normalizes line endings or mangles a payload, and to keep the noisy diffs out of code review. Add a `.gitattributes`next to your fixture store:

```
tests/fixtures/http/** binary
```

`binary` implies `-text -diff`, so recordings are stored verbatim and shown as `Binary files differ` rather than a line-by-line diff.

Local development
-----------------

[](#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](composer.json) inside the container, so `./dev ` is just `composer ` 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 `, `./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
------------

[](#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](https://www.conventionalcommits.org/)) 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
-------

[](#license)

MIT - see [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance90

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

3

Last Release

49d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/cb00f0fc4db7dc06ce8ef99d6fd623780a3d52608c61c45fbaed0f8cf1ab5810?d=identicon)[lezhnev74](/maintainers/lezhnev74)

---

Top Contributors

[![lezhnev74](https://avatars.githubusercontent.com/u/10206110?v=4)](https://github.com/lezhnev74 "lezhnev74 (9 commits)")

---

Tags

httppsrpsr-7psr-3middlewaretestingpsr-17replayrecording

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/lezhnev74-psr-recording-middleware/health.svg)

```
[![Health](https://phpackages.com/badges/lezhnev74-psr-recording-middleware/health.svg)](https://phpackages.com/packages/lezhnev74-psr-recording-middleware)
```

###  Alternatives

[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

86337.5k](/packages/flow-php-flow)[tempest/framework

The PHP framework that gets out of your way.

2.3k37.6k21](/packages/tempest-framework)[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

36826.2k2](/packages/telnyx-telnyx-php)[cakephp/cakephp

The CakePHP framework

8.9k20.0M1.9k](/packages/cakephp-cakephp)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k6.0M780](/packages/sylius-sylius)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
