Library / SDK
php-vcr/php-vcr avatar
php-vcr/php-vcr

PHP-VCR: cassettes for the HTTP calls your tests should not make

Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests.

1,218 stars214 forksPHPMIT

At a glance

What is it?
PHP-VCR is a port of the Ruby VCR library that records the HTTP interactions of a test run onto disk and replays them afterwards, with configurable request matching, four record modes and three interception hooks for the stream wrapper, curl and SOAP. The order in which you enable it matters more than it looks.
Who is it for?
PHP-VCR fits a PHP codebase whose tests talk to a third-party API that is slow, rate limited or unreliable, and whose team wants deterministic responses without hand-writing fixtures for every endpoint.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 40 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Record once, replay forever, and keep the cassette on disk

PHP-VCR is a port of the VCR Ruby library to PHP, and it does what the name suggests: it records the HTTP interactions of a test run and replays them during future test runs, for fast, deterministic and accurate tests.

The unit of recorded data is the cassette. A cassette is the set of request and response pairs from one interaction with a remote service, and the recorded requests and responses are stored on disk in a serialization format of your choice, with YAML and JSON built in. Being on disk rather than in memory is the decision that matters, because a cassette can be committed to the repository, reviewed in a diff, shared with a colleague and reused by a test that runs months later.

The feature list is short and each entry is a real problem being solved. The library automatically records and replays HTTP interactions with minimal setup or configuration code. The same request can receive different responses in different tests, by using different cassettes. And all HTTP requests that you have not explicitly allowed are disabled by setting the record mode, which is the mechanism that stops a test from quietly reaching the internet.

The library supports the common HTTP functions and extensions, and the three interception hooks are named in the documentation as `stream_wrapper`, `curl` and `soap`. All three are enabled by default when you call `VCR::turnOn()`, which is why the library works with a `file_get_contents` call as readily as with a cURL handle.

The project is MIT licensed, with the copyright line covering 2013 to 2026 and Adrian Philipp named as the author. The primary language is PHP, the default branch is `master`, the last push was on 2026-09-13, and the recent releases are 1.12.0 from 2026-08-05, 1.11.3 from 2026-08-01 and 1.11.2 from 2026-07-18. The homepage is php-vcr.github.io, and the documentation is published there in a versioned site with the raw Markdown in the `docs/` directory of the repository.

Turn it on before the code that makes requests is loaded

There is one instruction in the README that every user has to follow, and it is about ordering rather than configuration.

Turn VCR on as soon as possible, right after Composer's autoloader and before any code that calls `curl_*` or uses `SoapClient` gets loaded. That call is what registers the interception. Then turn it back off right afterwards, with `VCR::turnOn()` and `VCR::turnOff()` in your bootstrap file, if you do not want the hooks live for the whole run, because each test can cheaply call `turnOn()` again only when it actually wants a cassette.

The reason is in the mechanism. The library has to replace the behaviour of the HTTP layer before the code under test touches it, and in PHP that means replacing the stream wrapper and hooking the cURL and SOAP classes. A test that loads a class first and enables VCR afterwards has already lost the interception for that call, and the request goes to the network.

The example in the README shows the whole cycle, and it is the shape of every test that uses the library:

php
class VCRTest extends TestCase
{
    public function testShouldInterceptStreamWrapper()
    {
        \VCR\VCR::turnOn();
        \VCR\VCR::insertCassette('example');
        $result = file_get_contents('http://example.com');
        $this->assertNotEmpty($result);
        \VCR\VCR::eject();
        \VCR\VCR::turnOff();
    }
}

`turnOn` enables the interception, `insertCassette` names the recording, the request in between is recorded on the first run and replayed afterwards, `eject` stops recording, and `turnOff` restores the normal behaviour of the HTTP layer.

The README also carries a disclaimer that is worth taking seriously, because it explains the shape of the whole library. Doing this in PHP is not as easy as in programming languages which support monkey patching, and the author names Ruby directly. Everything about the design, the ordering constraint, the hooks, the need to turn the thing off again, follows from that one limitation.

A missing cassette is an exception, not a network call

The behaviour that makes the library safe to use in continuous integration is that a request with no cassette is an error rather than an accident.

Forgetting to insert a cassette throws immediately, instead of silently hitting the network, and the second example in the README shows exactly what is thrown:

php
public function testShouldThrowExceptionIfNoCasettePresent()
{
    $this->expectException(\BadMethodCallException::class);
    $this->expectExceptionMessage(
        "Invalid http request. No cassette inserted. Please make sure to insert "
        . "a cassette in your unit test using VCR::insertCassette('name');"
    );
    \VCR\VCR::turnOn();
    file_get_contents('http://example.com');
}

The alternative, hitting the network and returning whatever the service said, is the failure mode that makes a test suite slow, flaky and quietly dependent on somebody else's uptime. Throwing turns that into a failed test with a message that says what to do.

The record mode is the other half of the safety mechanism, and there are four of them. `new_episodes` is the default, and `once`, `none` and `all` complete the set. The mode controls how VCR behaves when a cassette is inserted, and the documentation has a page for the behaviour of each.

`new_episodes` as the default is the setting to look at first, because it means a request that the cassette does not contain is recorded and added to it, and that is a request that reached the network during a test run. `none` is the mode for a suite that must never touch the network, and `all` is the mode for re-recording a cassette from scratch.

One more default is worth knowing. By default PHP-VCR records identical requests separately and replays them in the same order they were made, so a test that calls the same endpoint three times gets three responses back in the same sequence, and the documentation explains how to change that.

Matching rules decide which recorded response comes back

When a request comes in during a replay, the library has to decide which recorded response it corresponds to, and that decision is configurable.

Request matching is configurable based on the HTTP method, the URI, the host, the path, the body and the headers, or you can implement a custom request matcher to handle any need. The six default dimensions are the ones a service's identity actually consists of, and a team that needs something narrower, a custom header that carries a tenant identifier for example, can write a matcher rather than lose the correspondence between request and response.

The other half of the mechanism is the cassette. The same request can receive different responses in different tests, by using different cassettes, so the cassette name is part of the test's identity. Two tests that hit the same endpoint with different fixtures get different cassette files, and the recorded data stays as legible as the test that produced it.

Storage is where the recorded data lands, and it is deliberately open. The recorded requests and responses are stored on disk in a serialization format of your choice, with YAML and JSON built in, and the documentation lists a how-to for a storage factory and a custom storage, so the format is not fixed by the library.

There is also a how-to on filtering sensitive data, and for a library that records real requests it had better have one. A cassette contains whatever the service returned, including tokens and personal data, and it is a file that is going to be committed to a repository.

The remaining documentation is organised by task. The guides cover getting started, how VCR works, cassettes, record modes and request matching. The how-tos cover PHPUnit, Codeception, filtering sensitive data, SOAP, and the storage factory. The reference covers configuration, events and the storage backends, and the events reference in particular is what a team needs when it wants to observe or modify the recording itself.

A development environment that spans six PHP versions

The repository is a modern PHP project, and the `docker-compose.yaml` at the root is the clearest statement of what it supports.

It defines one service per PHP version, from `workspace80` through `workspace85`, each built from its own directory under `resources/docker/workspace/` with the version number, each taking the `PUID` and `PGID` build arguments so that the files the container writes belong to the developer rather than to root, and each mounting the repository at `/var/www/html` and the developer's Composer directory into the home of the container user.

So the development environment covers PHP 8.0 to 8.5, and the package badge in the README says PHP `^8.0`, which is a wide range to support and explains why the compose file exists.

The static analysis and the style rules are configured in the repository as well. There is `phpstan.dist.neon` with a companion `phpstan-baseline.neon`, which is the usual way to adopt static analysis on an existing codebase, a `.php-cs-fixer.dist.php` for the code style, and `phpunit.xml.dist` for the test suite. The documentation is linted too, with `.markdownlint.jsonc` and `.markdownlint-cli2.jsonc`, and `.lycheeignore` excludes the broken link patterns that the link checker should not report, and the `website/` directory holds the published documentation site.

The contributing guidance is pointed at from the README, and it says the repository explains how it is set up, what the pre-push checks are, and why documentation is part of every change. That last point is the one that matters for a library like this, where the hardest part of using it is the ordering constraint described earlier, and where a documentation change is often the only way to record a behaviour that is easy to get wrong.

To work on the code itself the README gives the two commands, `composer install` to get the development dependencies and `composer test` to run the suite.

Where PHP-VCR is the wrong tool

The limits are visible in the disclaimer about monkey patching, and they are worth weighing before a team commits to this approach.

The first is that the interception is at the wrong level. A test double that is injected at the HTTP client boundary, where your own code asks for a client and the test supplies a fake one, has no ordering constraints, no global state and no possibility of leaking into a neighbouring test. VCR works at the transport, which is why it can record calls made by code the test does not control, from a vendor SDK three layers down, and that is also why it has to be enabled before anything is loaded and disabled afterwards. A team that controls its own HTTP boundary should inject at that boundary.

The second is the default record mode. `new_episodes` means a test run can reach the network and change the cassette, so a suite that is supposed to be hermetic is not hermetic by default, and the first CI run of a new test will make a live request. The documentation has the modes and the choice is available, but the default is the one most teams will keep.

The third is the lifecycle of the data. Cassettes are files that encode the behaviour of a service at a point in time, and services change. A cassette recorded against an old version of an API will keep returning the old answer, and the test will pass while the production code is broken, which is the same class of problem as a stale fixture but with more machinery around it. A cassette is only a good idea when the recorded behaviour is the behaviour you want to assert against.

The fourth is the sensitive data. A cassette holds real responses, and the filtering how-to exists because the default is to record everything. A team that has to filter tokens out of every recorded response has a maintenance task that no other approach imposes.

The alternative for a project with its own HTTP layer is to build the test double into that layer, which is a day of work and produces tests that are simpler to read. The difference in approach is that VCR records what happened, including the parts a test does not control, and an injected client records only what the test decided to fake, and the first is more powerful and the second is more predictable.

Editorial conclusion

PHP-VCR fits a PHP codebase whose tests talk to a third-party API that is slow, rate limited or unreliable, and whose team wants deterministic responses without hand-writing fixtures for every endpoint. It is the wrong tool for a project that already has a seam for HTTP, because a test double injected at the client boundary is easier to reason about than an interception of the transport, and PHP has no monkey patching, which the README itself names as the reason this is harder here than in Ruby. Before adopting it, read the record modes documentation, because the default of `new_episodes` will still reach the network for a request it has not seen, and decide where the cassettes live and how sensitive data is filtered before the first one is committed.

Frequently asked questions

How do I install PHP-VCR?

With `composer require --dev php-vcr/php-vcr`, as a development dependency. The library depends on PHP 8 and the curl extension, plus a few Composer packages, and the README points at a requirements page for the full list and the tested HTTP library matrix.

Which HTTP libraries does PHP-VCR intercept?

All three hooks, `stream_wrapper`, `curl` and `soap`, are enabled by default when you call `VCR::turnOn()`. The library hooks reference explains how to enable only specific hooks instead of all of them.

What happens if a test makes an HTTP request without a cassette?

It throws a BadMethodCallException immediately, with the message that no cassette is inserted and that a cassette should be inserted with VCR::insertCassette('name'). The design is deliberately to fail rather than silently hit the network.

What are the record modes in PHP-VCR?

There are four: `new_episodes`, which is the default, and `once`, `none` and `all`. The mode controls how VCR behaves when a cassette is inserted, and the documentation has a page describing the behaviour of each one.

Official sources

  1. License: MIT
  2. php-vcr/php-vcr on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/php-vcr-php-vcr.svg)](https://hysenlabs.com/projects/php-vcr-php-vcr)

Was this useful?