Skip to main content

Getting Started

One-liner: install php-vcr, turn it on as early as possible, insert a cassette, make requests — first run records, every run after that replays.

On this page: Requirements · Install · Turn it on — early · Record, then replay · Next steps

Requirements

The short version: PHP 8, ext-curl. Full compatibility matrix (including which HTTP libraries are covered and when ext-soap/ext-xml matter) lives in Requirements.

Install

composer require --dev php-vcr/php-vcr

Turn it on — early

⚠️ Warning — this is the part everyone gets wrong once. VCR::turnOn() must run before any file that calls curl_* or instantiates SoapClient is loaded — ideally right after Composer's autoloader, in your test bootstrap. The curl and soap hooks work by rewriting source code as PHP includes/requires it; code that's already loaded when turnOn() runs cannot be rewritten anymore. See How VCR works for the full mechanism — it also explains a sharp edge: those two hooks only ever rewrite code loaded via include/require, not the top-level script PHP was invoked with. In a real test suite this is a non-issue (PHPUnit loads your test classes via the autoloader), but a raw script with curl_exec() written directly at the top level will silently bypass interception.

// tests/bootstrap.php
require __DIR__ . '/../vendor/autoload.php';

\VCR\VCR::turnOn();
\VCR\VCR::turnOff();

That turnOn()/turnOff() pair looks pointless but isn't: turnOn() is what registers the curl/soap source rewriting — a one-time, permanent registration for the rest of the process. turnOff() right after just flips the hooks back to passthrough; it doesn't undo the registration. So this pattern gets the registration done as early as possible without leaving hooks live for your whole test run. Each individual test then calls turnOn() again — cheaply, since the registration already happened — only when it actually wants a cassette, and turnOff() when it's done (see the example below).

The stream_wrapper hook (used by fopen(), file_get_contents(), …) doesn't have this restriction — it replaces the http/https stream wrapper globally, so it works no matter where the call is written.

Record, then replay

use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
public function testFetchesExampleDotCom(): void
{
\VCR\VCR::turnOn();
\VCR\VCR::insertCassette('example');

// First test run: no recording exists yet -> a real HTTP request is made and recorded.
// Every run after that: the cassette has a match -> the real request is never sent.
$result = file_get_contents('http://example.com');

$this->assertNotEmpty($result);

\VCR\VCR::eject();
\VCR\VCR::turnOff();
}
}

💡 Tip: if your request contains something that changes on every call — a timestamp, a nonce, a generated idempotency key — the default configuration (all matchers enabled) will never replay, since the exact body/query string never matches again. Narrow the enabled matchers to ignore that part, e.g. VCR::configure()->enableRequestMatchers(['method', 'url', 'host']);. See Request Matching.

Cassettes land in the configured cassette path (default tests/fixtures, see Configuration) as a file named exactly example — php-vcr does not append .yml/.json automatically. If the cassette name contains a path separator ('api/example'), the subfolder is created for you.

Delete the cassette file and re-run the test to force a fresh recording — that's the entire "re-record" workflow for new_episodes (the default mode). For other strategies, see Record Modes.

Next steps


Documentation home · Next: How VCR works