mtk3d/laravel-http-vcr

Record and replay HTTP requests in Laravel tests — every Http:: facade call goes through a cassette, with nothing to wire up. The Laravel bridge for mtk3d/http-vcr.

Maintainers

Package info

github.com/mtk3d/laravel-http-vcr

pkg:composer/mtk3d/laravel-http-vcr

Transparency log

Statistics

Installs: 5

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 0

v0.1.2 2026-08-31 17:00 UTC

This package is auto-updated.

Last update: 2026-08-31 17:02:31 UTC


README

Every Http:: call in a test with #[UseCassette] is recorded once and replayed on every run after that, with nothing to wire up.

Tests PHP 8.2+ Laravel 12–13 License: MIT

This is the Laravel bridge for mtk3d/http-vcr. The recording and replaying lives there; this package is the auto-wiring that puts it behind Laravel's Http facade.

#[UseCassette('shopify/get-product')]
public function testGetProduct(): void
{
    $product = Http::get('https://shop.myshopify.com/admin/api/products/123.json')->json();

    $this->assertSame('T-Shirt', $product['title']);
}

No Http::fake(), no fixture to write by hand, no client to construct. The first run makes the real request and writes tests/Cassettes/shopify/get-product.yaml; every run after replays it — no network, no API key, no flakiness.

Installation

composer require --dev mtk3d/laravel-http-vcr

The service provider registers itself. One line in phpunit.xml is still needed, because PHPUnit has no auto-discovery for extensions:

<extensions>
    <bootstrap class="HttpVcr\Bridge\PHPUnit\Extension"/>
</extensions>

That is the whole setup. Without it #[UseCassette] does nothing and every call reaches the real API — so the package checks for it while booting in the testing environment and says so once per run rather than letting it pass unnoticed.

Requires Laravel 12 or newer and PHP 8.2+ (Laravel 13 needs PHP 8.3).

What it does

  • Intercepts the Http facade application-wide through Illuminate\Http\Client\Factory::globalMiddleware(), so every call is covered no matter which method on the facade made it, and no matter where in the application it came from. Middleware the application registers itself is appended to, never replaced.
  • Registers the CLI as Artisan commands: vcr:stale, vcr:providers, vcr:tests, vcr:scan-secrets, vcr:migrate, vcr:lock, vcr:unlock — the same commands as vendor/bin/http-vcr, prefixed because Artisan is one flat namespace shared with the framework and everything else installed.
  • Narrows when recording is allowed. http-vcr already refuses to record on CI. Here that is narrowed further with the application environment: with VCR_ALLOW_RECORDING unset, recording is allowed only in local or testing and where no CI was detected. The bridge only ever tightens the default — an environment check on its own would be worse than useless, since tests on CI run with APP_ENV=testing. A value set explicitly still wins over both.
  • Stays out of the way outside local and testing. The commands register anywhere; the HTTP hook does not.

A test with no #[UseCassette] behaves exactly as if this package were not installed — including Http::fake(), which is untouched.

Configuration

There is nothing Laravel-specific to configure, and no config file to publish. http-vcr reads an optional http-vcr.php in the project root, and its defaults already resolve to base_path('tests/Cassettes') and base_path('tests') in a Laravel application:

<?php

use HttpVcr\Config;
use HttpVcr\Provider;

return Config::create(
    providers: [
        'shopify' => new Provider(hosts: ['*.myshopify.com'], requiresEnv: ['SHOPIFY_API_KEY']),
    ],
    redact: ['<SHOPIFY_API_KEY>' => fn () => env('SHOPIFY_API_KEY')],
);

The root of the project rather than config/: the file returns a Config object carrying closures, and php artisan config:cache serialises every file in config/ with var_export() — a closure there turns a cached config into a fatal. Every option is in the configuration reference.

Assertions on a replayed call

Http::assertSent() works, with one line:

#[UseCassette('shopify/get-product')]
public function testItAsksForTheRightProduct(): void
{
    Http::record();

    (new Shopify)->product('123');

    Http::assertSent(fn (Request $request) => str_contains($request->url(), '/products/123'));
}

Http::record() is Laravel's own switch for the recorder behind those assertions. It is normally turned on as a side effect of Http::fake(), which a cassette replaces — so with cassettes it is asked for directly. The replayed call is reported to that recorder on purpose: assertSent() asks what the application did, and a replayed call is something the application did.

Http::preventStrayRequests()

A cassette does not fight it, and it does not have to be turned off:

#[UseCassette('shopify/get-product', mode: RecordMode::PlaybackOnly)]
public function testGetProduct(): void
{
    Http::preventStrayRequests();   // in your base TestCase, most likely

    $product = (new Shopify)->product('123');   // replayed, not stray
}

Laravel enforces the guard in its stub handler, which is pushed onto the stack after global middleware and therefore sits inside this package's — so a replayed call is answered above it and never reaches it.

The run that records is a different matter. It goes down the whole stack, so it does reach the guard, and with no stub registered Laravel refuses it before it can reach the API. That is the guard working as intended, but it means the first run of a new cassette needs Http::allowStrayRequests() — narrow it to the host you are recording, and the guard keeps covering everything else:

Http::allowStrayRequests(['https://shop.myshopify.com/*']);

The cassette format

YAML wherever symfony/yaml is installed, JSON otherwise — a Laravel application usually has it, so cassettes are usually YAML. To pin one either way, name the serializer in http-vcr.php; php artisan vcr:migrate rewrites existing recordings into the other format.

Everything else

Record modes, matchers, redaction, strict mode, staleAfter, cassette scoping, the CLI and the cassette format are the core library's, and they behave here exactly as they do anywhere else:

mtk3d.github.io/http-vcr

Not using this package

Http:: goes through Guzzle, so the middleware bridge works without any of this — at the cost of wiring the handler yourself in each test or in a testing-only service provider. The recipe is on the Laravel page of the book.

Development

composer install
vendor/bin/phpunit
vendor/bin/phpstan analyse --memory-limit=512M   # level max
vendor/bin/pint --test

CI runs PHPUnit across PHP 8.2–8.4 and Laravel 12–13, plus a leg on the lowest versions every constraint allows; PHPStan and Pint run once each, since neither answer depends on the framework release.

License

MIT — see LICENSE.