PHPackages                             eyond/laravel-http-replay - 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. eyond/laravel-http-replay

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

eyond/laravel-http-replay
=========================

Record and replay HTTP responses in your Laravel/Pest tests

v0.2.0(1mo ago)164↓50%[2 PRs](https://github.com/EYOND/laravel-http-replay/pulls)MITPHPPHP ^8.4CI passing

Since Feb 20Pushed 1mo agoCompare

[ Source](https://github.com/EYOND/laravel-http-replay)[ Packagist](https://packagist.org/packages/eyond/laravel-http-replay)[ Docs](https://github.com/eyond/laravel-http-replay)[ GitHub Sponsors](https://github.com/eyond)[ RSS](/packages/eyond-laravel-http-replay/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (4)Dependencies (26)Versions (8)Used By (0)

Laravel Http Replay
===================

[](#laravel-http-replay)

[![Latest Version on Packagist](https://camo.githubusercontent.com/4faf051314847d7b79564be672381321ad4d3db971ebc21149b9fc0c16b639b0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f65796f6e642f6c61726176656c2d687474702d7265706c61792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/eyond/laravel-http-replay)[![GitHub Tests Action Status](https://camo.githubusercontent.com/f2fc5ee67b25cab9bfecb7defa45a4a3381c4c4e62fb448f5196f541b155c3e9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f65796f6e642f6c61726176656c2d687474702d7265706c61792f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/eyond/laravel-http-replay/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/f5e01785154e16aa5b050d94b07335fcc4e2a697356230ab0d6e1d8898fea927/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f65796f6e642f6c61726176656c2d687474702d7265706c61792f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/eyond/laravel-http-replay/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/a5a412942c09c9cc8ea43beb0deea0dbf928b4a74d4f4a5b982b9f8eb8f16089/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f65796f6e642f6c61726176656c2d687474702d7265706c61792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/eyond/laravel-http-replay)

Record and replay HTTP responses in your Laravel/Pest tests. Like snapshot testing, but for HTTP calls — responses are recorded on the first run and replayed automatically on subsequent runs.

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

[](#installation)

```
composer require eyond/laravel-http-replay --dev
```

Optionally publish the config file:

```
php artisan vendor:publish --tag="laravel-http-replay-config"
```

Quick Start
-----------

[](#quick-start)

Add `Http::replay()` to your test. The first run makes real HTTP calls and stores the responses. Every subsequent run replays the stored responses — no network needed.

```
it('fetches products', function () {
    Http::replay();

    $products = app(ShopifyService::class)->getProducts();

    expect($products)->toHaveCount(10);
});
```

Stored responses are saved as JSON in `tests/.laravel-http-replay/`, organized by test file and test name:

```
tests/.laravel-http-replay/
└── Feature/
    └── ShopifyTest/
        └── it_fetches_products/
            └── GET_shopify_com_api_products.json

```

Usage
-----

[](#usage)

### Basic Replay

[](#basic-replay)

```
it('fetches products', function () {
    Http::replay();

    $response = Http::get('https://api.example.com/products');

    expect($response->json('products'))->toHaveCount(10);
});
```

### Same-URL Disambiguation (GraphQL etc.)

[](#same-url-disambiguation-graphql-etc)

When multiple requests go to the same URL (e.g. GraphQL endpoints), you need to disambiguate them. There are several approaches:

#### Via `withAttributes`

[](#via-withattributes)

The `replay` attribute is a **reserved key** that always takes priority over all matchers — no `matchBy` configuration needed:

```
it('fetches products and orders via GraphQL', function () {
    Http::replay();

    $products = Http::withAttributes(['replay' => 'products'])
        ->post('https://shopify.com/graphql', ['query' => '{products{...}}']);

    $orders = Http::withAttributes(['replay' => 'orders'])
        ->post('https://shopify.com/graphql', ['query' => '{orders{...}}']);
});
```

This stores the responses as `products.json` and `orders.json`.

For custom attribute keys, use `matchBy('attribute:key')`:

```
it('uses a custom attribute for naming', function () {
    Http::replay()->matchBy('method', 'attribute:operation');

    Http::withAttributes(['operation' => 'getProducts'])
        ->post('https://shopify.com/graphql', ['query' => '{products{...}}']);
});
```

#### Via `matchBy` with Body Hash

[](#via-matchby-with-body-hash)

Automatically distinguish requests by including the request body hash in the filename:

```
it('auto-disambiguates by body', function () {
    Http::replay()->matchBy('url', 'body_hash');

    Http::post('https://shopify.com/graphql', ['query' => '{products{...}}']);
    Http::post('https://shopify.com/graphql', ['query' => '{orders{...}}']);
});
```

#### Via Closure Matcher

[](#via-closure-matcher)

Use a closure for custom filename generation. The closure may return a `string`, `int`, `array`, or `Collection` — multiple parts are joined with `_`, empty parts are filtered out:

```
Http::replay()->matchBy(
    'method',
    fn(Request $r) => $r->data()['operationName'] ?? 'unknown',
);

// Or return multiple parts as array or Collection:
Http::replay()->matchBy(
    fn(Request $r) => ['graphql', $r->data()['operationName'] ?? 'unknown'],
);
```

### Composable Matchers

[](#composable-matchers)

The `matchBy()` method accepts any combination of built-in matchers:

MatcherConfig StringAliasExample OutputHTTP Method`method``http_method``GET`URL (host + path)`url``shop_myshopify_com_api_products`Host only`host``shop_myshopify_com`Domain (host without subdomain)`domain``myshopify_com`Subdomain`subdomain``shop`Path only`path``api/v1/products`HTTP Attribute`attribute:key``http_attribute:key`Value of `$request->attributes()['key']`Body Hash`body_hash``a1b2c3` (6-char hash of entire body)Body Hash (keys)`body_hash:query,variables.id`Hash of specific body fieldsBody Field`body_field:path`Value of JSON body field (dot notation)Query Hash`query_hash``a1b2c3` (6-char hash of all query params)Query Hash (keys)`query_hash:page,limit`Hash of specific query paramsQuery Param`query:key`Value of a specific query parameterHeader`header:key`Value of a specific request headerClosure`fn(\Illuminate\Http\Client\Request $r) => ...`Returns `string`, `int`, `array`, or `Collection`Default: `['method', 'url']`

### Per-URL Configuration

[](#per-url-configuration)

Configure different matchers for different URL patterns:

```
Http::replay()
    ->for('myshopify.com/*')->matchBy('url', 'attribute:request_name')
    ->for('reybex.com/*')->matchBy('method', 'url');
```

The `for()` method returns a proxy object — you must call `matchBy()` directly on it. This prevents accidental state leaks.

### Global Configuration (`Replay::configure()`)

[](#global-configuration-replayconfigure)

Use `Replay::configure()` to set up matchers globally (e.g. in `tests/Pest.php`) without activating replay. This stores configuration only — no fake callback or event listener is registered. When `Http::replay()` is called in a test, it inherits the stored config automatically.

```
// tests/Pest.php — configures, does NOT activate
use EYOND\LaravelHttpReplay\Facades\Replay;

Replay::configure()
    ->for('myshopify.com/*')->matchBy('url', 'attribute:request_name')
    ->for('reybex.com/*')->matchBy('method', 'url');
```

```
// Test — activates and inherits config
it('replays shopify', function () {
    Http::replay();

    app(ShopifyService::class)->getProducts();
});

// Test — overrides config for this test
it('special test', function () {
    Http::replay()
        ->for('myshopify.com/*')->matchBy('method', 'url');

    // Uses method + url instead of url + attribute:request_name
});
```

`Replay::configure()` supports:

MethodDescription`matchBy(string|Closure ...$fields)`Set global default matchers (overrides config file default)`for(string $pattern)->matchBy(...)`Set per-URL matchersPer-test overrides in `Http::replay()` always take precedence over `Replay::configure()` for the same pattern.

### Shared Fakes

[](#shared-fakes)

Record responses once and reuse them across multiple tests.

**Record to a shared location (read + write):**

```
it('records shared shopify fakes', function () {
    Http::replay()->useShared('shopify');

    app(ShopifyService::class)->getProducts();
});
```

**Read from shared, write to test-local:**

```
it('uses shared shopify fakes', function () {
    Http::replay()->readFrom('shopify');

    $products = app(ShopifyService::class)->getProducts();

    expect($products)->toHaveCount(10);
});
```

**Read from multiple shared locations (first wins):**

```
Http::replay()->readFrom('shopify', 'shopify-fallback');
```

**Write to shared, read from test-local:**

```
Http::replay()->writeTo('shopify');
```

**Combine read + write explicitly:**

```
Http::replay()->readFrom('shopify')->writeTo('shopify-v2');
```

**Use shared fakes for an entire file:**

```
beforeEach(function () {
    Http::replay()->readFrom('shopify');
});

it('test one', function () {
    // Uses shared shopify fakes
});

it('test two', function () {
    // Uses shared shopify fakes
});
```

MethodReads fromWrites to`readFrom('a', 'b')`shared/a, shared/b (first wins)test-specific`writeTo('x')`test-specificshared/x`useShared('name')`shared/nameshared/name`readFrom('a')->writeTo('x')`shared/ashared/x**Load a single shared fake in `Http::fake()`:**

```
use EYOND\LaravelHttpReplay\Facades\Replay;

Http::fake([
    'foo.com/posts/*' => Replay::getShared('fresh-test/GET_jsonplaceholder_typicode_com_posts_3.json'),
]);
```

Shared fakes are stored in `tests/.laravel-http-replay/_shared/{name}/`.

### Mix: Recorded + Static Fakes

[](#mix-recorded--static-fakes)

Combine replay recording with static `Http::fake()` stubs. Use `only()` to limit which URLs are recorded:

```
it('mixes recorded and static fakes', function () {
    Http::replay()
        ->only(['shopify.com/*'])
        ->alsoFake([
            'api.stripe.com/*' => Http::response(['ok' => true]),
            'sentry.io/*' => Http::response([], 200),
        ]);

    // Shopify calls are recorded/replayed
    $products = Http::get('https://shopify.com/api/products');

    // Stripe and Sentry use static fakes
    $charge = Http::get('https://api.stripe.com/charges');
});
```

### Renewal / Re-Recording

[](#renewal--re-recording)

#### Fluent API

[](#fluent-api)

```
// Re-record everything for this test
Http::replay()->fresh();

// Re-record only matching URLs
Http::replay()->fresh('shopify.com/*');

// Auto-expire after 7 days (re-records expired responses)
Http::replay()->expireAfter(days: 7);

// Auto-expire after 1 month (accepts DateInterval)
Http::replay()->expireAfter(new DateInterval('P1M'));

// Re-record shared fakes
Http::replay()->readFrom('shopify')->fresh();
```

#### Artisan Command

[](#artisan-command)

```
# Delete all stored replays
php artisan replay:prune

# Delete replays for a specific test
php artisan replay:prune --test="it fetches products"

# Delete replays for a specific test file
php artisan replay:prune --file=tests/Feature/ShopifyTest.php

# Delete replays matching a URL pattern
php artisan replay:prune --url="shopify.com/*"

# Delete specific shared fakes
php artisan replay:prune --shared=shopify
```

#### Pest Flag

[](#pest-flag)

```
# Re-record all fakes
vendor/bin/pest --replay-fresh
```

#### Environment Variable

[](#environment-variable)

```
REPLAY_FRESH=true vendor/bin/pest
```

Or set it in your app config:

```
// config/http-replay.php
'fresh' => env('REPLAY_FRESH', false),
```

### Bail on CI

[](#bail-on-ci)

Prevent tests from accidentally recording new fakes in CI by enabling bail mode. When active, tests will **fail** if Replay attempts to write a new file.

```
// Per-test or in beforeEach
Http::replay()->bail();

// Per-test with other options
Http::replay()->readFrom('shopify')->bail();
```

```
# Pest flag (recommended for CI)
vendor/bin/pest --replay-bail

# Or via environment variable
REPLAY_BAIL=true vendor/bin/pest
```

You can also set it permanently in your config:

```
// config/http-replay.php
'bail' => env('REPLAY_BAIL', false),
```

### Incomplete Test Marking

[](#incomplete-test-marking)

When Replay records a new response during a test, the test is automatically marked as **incomplete** (yellow) — just like Pest's snapshot testing. This makes it clear which tests recorded new data and need a re-run to verify.

### Complex Scenario

[](#complex-scenario)

```
it('complex shopify sync', function () {
    Http::replay()
        ->only(['shopify.com/*'])
        ->for('shopify.com/graphql')->matchBy('url', 'body_hash')
        ->expireAfter(days: 7)
        ->alsoFake([
            'api.stripe.com/*' => Http::response(['ok' => true]),
        ]);

    $products = Http::withAttributes(['replay' => 'products'])
        ->post('https://shopify.com/graphql', ['query' => '{products{...}}']);

    $charge = Http::get('https://api.stripe.com/charges');

    expect($products->json())->toHaveKey('data.products');
});
```

File Storage Format
-------------------

[](#file-storage-format)

Each stored response is a JSON file containing the response data and metadata:

```
{
    "status": 200,
    "headers": {
        "Content-Type": ["application/json"]
    },
    "body": {
        "products": []
    },
    "recorded_at": "2026-02-12T14:30:00+00:00",
    "request": {
        "method": "GET",
        "url": "https://shopify.com/api/products",
        "attributes": {}
    }
}
```

### Directory Structure

[](#directory-structure)

```
tests/.laravel-http-replay/
├── _shared/                                    # Shared fakes (via useShared/readFrom/writeTo)
│   └── shopify/
│       └── GET_shopify_com_api_products.json
├── Feature/
│   └── ShopifyTest/
│       └── it_fetches_products/                # Auto-named from Pest test
│           ├── GET_shopify_com_api_products.json
│           ├── products.json                   # Via withAttributes(['replay' => 'products'])
│           └── POST_shopify_com_graphql_a1b2c3.json  # Via matchBy('url', 'body_hash')

```

### Filename Conventions

[](#filename-conventions)

ScenarioFilenameDefault`GET_api_example_com_products.json``withAttributes(['replay' => 'products'])``products.json``matchBy('url', 'body_hash')``shopify_com_graphql_a1b2c3.json`Duplicate URL (sequential calls)`GET_api_example_com_products__2.json`Configuration
-------------

[](#configuration)

```
// config/http-replay.php
return [
    // Directory for stored replays
    // Relative paths are resolved from base_path() (your project root)
    // Absolute paths (starting with /) are used as-is
    'storage_path' => 'tests/.laravel-http-replay',

    // Default matchers for filename generation
    // Short forms: 'method', 'attribute:key'
    // Aliases: 'http_method', 'http_attribute:key'
    'match_by' => ['method', 'url'],

    // Auto-expire after N days (null = never)
    'expire_after' => null,

    // Force re-recording of all replays
    'fresh' => false, // Use env('REPLAY_FRESH', false) in your app

    // Fail tests if Replay attempts to write
    'bail' => false, // Use env('REPLAY_BAIL', false) in your app
];
```

API Reference
-------------

[](#api-reference)

### `Http::replay()`

[](#httpreplay)

Returns a `ReplayBuilder` instance with the following fluent methods:

MethodDescription`matchBy(string|Closure ...$fields)`Matchers for filename generation`for(string $pattern)`Set URL pattern for per-URL matcher config (returns proxy, must chain `matchBy()`)`only(array $patterns)`Only record/replay URLs matching these patterns`alsoFake(array $stubs)`Additional static fakes for non-replayed URLs`readFrom(string ...$names)`Load stored fakes from shared location(s), first wins`writeTo(string $name)`Save recorded fakes to a shared location`useShared(string $name)`Read + write from a shared location`fresh(?string $pattern)`Delete stored fakes and re-record (optionally filtered by URL pattern)`bail()`Fail if Replay attempts to record a new fake (no stored response found)`expireAfter(int|DateInterval $days)`Auto-expire stored fakes after N days or a DateInterval### `Replay::configure()`

[](#replayconfigure)

Returns a `ReplayConfig` instance for global configuration without activating replay. Inherits into every `Http::replay()` call.

MethodDescription`matchBy(string|Closure ...$fields)`Set global default matchers`for(string $pattern)`Set per-URL matchers (returns proxy, must chain `matchBy()`)### `Replay::getShared(string $path)`

[](#replaygetsharedstring-path)

Load a single shared replay file for use in `Http::fake()`. Returns a `PromiseInterface`.

### `php artisan replay:prune`

[](#php-artisan-replayprune)

OptionDescription`--test="name"`Delete fakes for a specific test description`--file=path`Delete fakes for a specific test file`--url="pattern"`Delete fakes matching a URL pattern`--shared=name`Delete shared fakes by name*(no options)*Delete all stored replaysHow It Works
------------

[](#how-it-works)

This package uses **only public Laravel APIs** — no internal hacks, no monkey-patching, no overriding core classes. Everything is built on top of two official extension points:

1. **`Http::fake()` with a callback** — Laravel's HTTP client supports passing a closure to `Http::fake()`. This closure receives each outgoing request and can return a response or `null` (to allow the real request). Http Replay registers a single callback that checks for stored responses and either serves them or lets the request through.
2. **`ResponseReceived` event** — Laravel dispatches this event after every HTTP response. Http Replay listens for it to capture real responses and save them to disk.

The flow:

```
Http::replay()
    │
    ├─ Registers Http::fake(callback) via Factory::macro()
    └─ Registers ResponseReceived event listener

Request comes in:
    │
    ├─ Stored response exists? → Return it (no network call)
    └─ No stored response? → Return null → Real HTTP call happens
                                                │
                                                └─ ResponseReceived event fires
                                                    → Serialize & store to disk

```

The `Http::replay()` macro itself is registered on `Illuminate\Http\Client\Factory` via Laravel's standard `macro()` method in the service provider. No classes are extended or replaced.

Requirements
------------

[](#requirements)

- PHP 8.4+
- Laravel 13
- Pest PHP 4

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

This package is built with **Vibe Coding** — designed and developed in collaboration with Claude Code. Despite that, the codebase follows strict quality standards: PHPStan level 5, full test coverage across PHP 8.4-8.5 and Laravel 13, and consistent code formatting via Pint.

**Bug fixes** — PRs with a failing test and fix are welcome.

**New features** — Please don't submit a traditional code PR. Instead, open an issue or PR that:

1. Describes the problem or use case
2. Includes a **Claude Code prompt** or a **Claude Code plan** (`.md` file) that I can use to implement the feature myself

This keeps the codebase consistent and lets me iterate on the implementation with the same AI-assisted workflow used to build the package.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

This package is an opinionated version of the original idea by [Michael Ruf](https://github.com/michiruf) in [laravel-http-automock](https://github.com/michiruf/laravel-http-automock).

- [Patrick Korber](https://github.com/pikant)
- [Michael Ruf](https://github.com/michiruf) — original idea
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

42

—

FairBetter than 90% of packages

Maintenance91

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

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 ~7 days

Total

5

Last Release

54d ago

PHP version history (2 changes)v0.1.0PHP ^8.3

v0.2.0PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/00fe43308a0e871cde1306c35abb92ca38b3ee960ab43ab39e1fa76913c74131?d=identicon)[pikant](/maintainers/pikant)

---

Top Contributors

[![pikant](https://avatars.githubusercontent.com/u/24542171?v=4)](https://github.com/pikant "pikant (43 commits)")

---

Tags

testinglaraveleyondlaravel-http-replay

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/eyond-laravel-http-replay/health.svg)

```
[![Health](https://phpackages.com/badges/eyond-laravel-http-replay/health.svg)](https://phpackages.com/packages/eyond-laravel-http-replay)
```

###  Alternatives

[timacdonald/log-fake

A drop in fake logger for testing with the Laravel framework.

4235.9M56](/packages/timacdonald-log-fake)[sti3bas/laravel-scout-array-driver

Array driver for Laravel Scout

971.5M3](/packages/sti3bas-laravel-scout-array-driver)[vormkracht10/laravel-mails

Laravel Mails can collect everything you might want to track about the mails that has been sent by your Laravel app.

24149.7k](/packages/vormkracht10-laravel-mails)[spatie/laravel-visit

Quickly visit any route of your Laravel app

15614.6k](/packages/spatie-laravel-visit)[marvinrabe/laravel-graphql-test

Provides you with a simple GraphQL testing trait.

58329.7k](/packages/marvinrabe-laravel-graphql-test)[michiruf/laravel-http-automock

Automatically mock http requests when testing

161.0k](/packages/michiruf-laravel-http-automock)

PHPackages © 2026

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