Download the PHP package mtk3d/http-vcr without Composer
On this page you can find all versions of the php package mtk3d/http-vcr. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download mtk3d/http-vcr
More information about mtk3d/http-vcr
Files in mtk3d/http-vcr
Package http-vcr
Short Description Record and replay HTTP requests in PHP tests — a decorator over any PSR-18 client, so your suite runs fast and deterministic with no network access and no API keys. Built on PSR-7/PSR-17/PSR-18.
License MIT
Informations about the package http-vcr
http-vcr — record and replay HTTP requests in PHP tests
Record real HTTP interactions once, replay them on every test run after that.
The PHP test suite for "the code that talks to Shopify/Stripe/Zendesk" becomes fast, deterministic, and runnable in CI with no network access and no API credentials.
http-vcr is a decorator over PSR-18
(Psr\Http\Client\ClientInterface). Anything that already speaks PSR-18 (Guzzle 7+,
Symfony's Psr18Client, php-http, Buzz) works unchanged, and two VcrClient instances in
one process never interfere — nothing outside the instance you construct is touched.
Recording works with no setup on a developer machine, and is refused on CI, so a missing cassette fails loudly instead of quietly reaching for a real API without credentials.
Contents
- Installation
- Quick start
- Without PHPUnit
- What lands on disk
- Record modes
- Re-recording
- Matching
- Redaction and secret scanning
- More control
- Framework integration
- CLI
- Configuration
- Documentation
- Dependencies
- Development
- License
Installation
You need PHP 8.2+ and an HTTP client to wrap — whichever one the project already uses. To
rebuild a replayed response http-vcr also needs a PSR-17 factory, and it takes that from
the client library you already have (Guzzle ships one, Symfony's client pulls one in). If
nothing usable is found it stops before the first request rather than partway through one,
and composer require --dev nyholm/psr7 settles it.
Quick start
One line in phpunit.xml registers the extension behind #[UseCassette]. PHPUnit has no
auto-discovery for extensions, so this is the one thing http-vcr can't do for you:
That's the whole setup — no bootstrap code, no config file, no cassette directory to create. Then put the attribute on the test and take the client from the trait:
$this->vcrClient() is a Psr\Http\Client\ClientInterface, so it drops into the exact
spot the real client occupied — no interface changes, no test-only branch in the code under
test.
First run — with a real key available, the call goes over the wire and is written to
tests/Cassettes/shopify/get-product.yaml:
Every run after — the cassette exists, so the same test replays it. No network, no API key, no flakiness:
Commit the cassette next to the test. Credentials in Authorization and Cookie headers
were replaced with placeholders before it hit disk.
Every field of the attribute is a VcrClient constructor parameter under the same name:
On a class it applies to every test method in it; a method-level attribute replaces a class-level one outright rather than merging with it.
Without PHPUnit
The attribute is a convenience over an ordinary object. In a script, or under a different test framework, construct it directly — everything the attribute sets is a constructor argument or a method on this object:
What lands on disk
A cassette is a plain text file, meant to be read in review and edited by hand when needed:
YAML wherever symfony/yaml is installed — which in a Laravel or Symfony project it
already is — and the same model in JSON everywhere else. Name a serializer to pin either
one, and vendor/bin/http-vcr migrate --to=json to rewrite cassettes already on disk.
Cassettes live in tests/Cassettes/ by default, with the cassette name as a path inside
it. Bodies that are binary or oversized go to sidecar files; compressed responses are
decoded before storage. HAR import/export moves traffic to and from a browser's Network
tab, Postman or a proxy.
Record modes
Three cases decide what happens when an incoming request matches nothing in the cassette:
RecordMode |
Behavior |
|---|---|
RecordIfAbsent (default) |
No cassette → record everything. Cassette exists → replay only; an unmatched request throws. |
ExtendCassette |
Replays what exists and appends unmatched requests as new recordings, leaving the rest untouched. |
PlaybackOnly |
Never records. A missing cassette or a changed request shape fails loudly. |
The declared mode never changes based on the environment. Protecting CI is a separate
switch, VCR_ALLOW_RECORDING, which sits above RecordMode and blocks the recording
branch of whichever mode is declared:
VCR_ALLOW_RECORDING |
Result |
|---|---|
1 or 0 |
exactly that — an explicit value always wins |
| unset, CI detected | recording blocked |
| unset, no CI signal | recording allowed |
CI detection is narrow and fully enumerated: a non-empty CI, CONTINUOUS_INTEGRATION,
BUILD_NUMBER, JENKINS_URL or TEAMCITY_VERSION. Setting VCR_ALLOW_RECORDING=0 in the
pipeline is one line and recommended regardless.
Re-recording
VCR_ERASE_TAPE names what to erase and record fresh. It takes a target, never a bare
1 — so the shortest thing to type is not also the one with the widest blast radius:
A cassette recorded from a test that talks to two APIs is refreshable one API at a time: interactions belonging to other providers survive the truncation and keep replaying, so the run needs credentials only for the API being refreshed.
Matching
Which recorded interaction a request corresponds to is decided by a composable list of
matchers, all of which must agree. The default set is
[MethodMatcher, UriMatcher, QueryStringMatcher].
| Matcher | Compares |
|---|---|
MethodMatcher |
HTTP method, case-insensitively |
UriMatcher |
scheme + host + path, normalized |
HostMatcher |
host only, when matching the full path is too strict |
QueryStringMatcher |
query params as an unordered set; repeated keys keep their order |
HeadersMatcher |
named headers, subset match by default, exact: true for both directions |
BodyMatcher |
raw body, exact |
BodyJsonMatcher |
semantic JSON — key order doesn't matter, types are compared strictly |
Values that legitimately change every run are handled on the matcher that sees them, since redaction can't help with a value not known in advance:
Writing your own means implementing one method — matchers compare two RecordedRequest
snapshots, not live PSR-7 objects, so no matcher can drain a stream out from under the next
one:
Redaction and secret scanning
Authorization, Proxy-Authorization, Cookie and Set-Cookie are redacted from the
first recording with nothing to configure. Anything else is opt-in:
Redaction is symmetric: the real value is replaced with the placeholder before anything touches disk, and swapped back at replay time — on the recorded request before matching, and on the recorded response before application code receives it. It covers everything stored in an interaction, including the message of a recorded transport failure.
Both are built on a general hook pipeline (beforeRecord / beforePlayback) that's open
for anything else an interaction needs done to it on the way in or out.
On top of that, every session that records anything runs the new interactions through a credential heuristic and warns:
It never fails a test and never blocks the write — the point is to put the finding in front
of you while the context is fresh, before the file is committed. For the blocking version
across every cassette, with an exit code CI can act on, run vendor/bin/http-vcr scan-secrets — and --redact on the same command asks about each finding and replaces
the confirmed ones in the file, one way.
More control
- Strict mode — a cassette that closes with an interaction nothing ever asked for
reports it by default;
StrictMode::AllPlayedturns that report into a failure, andStrictMode::InOrderrequires the recorded sequence to be replayed in order. staleAfter— flags interactions older thanStale::Week(or anyDateInterval). Informational by default (vendor/bin/http-vcr staleas a non-blocking CI step), enforced per-run withVCR_ENFORCE_STALE_CHECK=1. "Now" comes from an injectable PSR-20 clock, andFrozenClockships with the package so testing this needs no extra dependency.- Locked interactions — the write-protect tab.
vendor/bin/http-vcr lock shopify/checkout --interaction=2means that interaction never generates a real request again, aboveVCR_ERASE_TAPEandVCR_ALLOW_RECORDINGboth. For a request that charges a card or creates an order. - Scoping by URL —
RegexUrlScopeResolver('#/api/(?<scope>\d{4}-\d{2})/#')stores an API version in its own file (get-product.2024-01.yaml), so a version bump gives a clear "nothing recorded for this version" rather than a silent match against outdated data. - Transport errors — opt in with
recordTransportErrors: trueto record a timeout or connection failure as a deterministic interaction, for testing retry logic. Replay throwsVcrNetworkException/VcrRequestException, which implement the PSR-18 interfaces. - Repeatable playback — an interaction that isn't consumed when replayed, for the target of a retry loop.
Framework integration
| How | |
|---|---|
| Any PSR-18 client | new VcrClient($client, cassette: '…') — nothing else needed |
| Guzzle | VcrMiddleware on the HandlerStack, so $client->get() and friends are covered too — those bypass any decorator around the client |
| Symfony | VcrHttpClient implements HttpClientInterface, the one a Symfony app injects into services. Psr18Client needs no bridge |
| PHPUnit | #[UseCassette], #[CassetteDirectory], InteractsWithCassettes, Extension. PHPUnit 10–13 |
| Laravel | mtk3d/laravel-http-vcr — a separate package: auto-registered provider, Http facade interception, artisan vcr:*. Laravel 12+. The manual recipe covers it without the extra package |
Runnable versions of each are in examples/.
CLI
| Command | Does |
|---|---|
stale |
Lists interactions past their staleAfter |
tests |
Lists tests touching a provider, plus a ready-made --filter regex |
providers |
Every configured provider with its hosts, requiresEnv and cassette counts, then the hosts running unclaimed |
scan-secrets |
Credential scan across every cassette, with an exit code |
lock / unlock |
Sets or clears the write-protect flag on an interaction |
None of these run the test suite: #[UseCassette] is read by parsing the source, so no
command needs a correctly configured environment to answer a question about cassettes. That
also means anything not statically resolvable (staleAfter: self::INTERVAL, a computed
cassette name) is reported as "couldn't be fully analyzed" rather than guessed at.
The tests command exists to make a targeted re-record quick:
The filter is only ever a speed optimization — what gets erased is decided by
VCR_ERASE_TAPE, so the same command without it produces the same cassettes, just slower.
Configuration
Everything is optional; http-vcr works with none of it. Project-wide defaults go in an
http-vcr.php found by walking up from the working directory, no further than the
directory holding composer.json:
A provider is a name for an external API: host patterns plus the environment variables
recording it requires. Naming one buys VCR_ERASE_TAPE=@shopify and a check that fails
before anything is recorded against a missing credential — [email protected]
works against a bare hostname without any configuration either way.
The same object can be filled in from code instead, for projects that would rather configure in a PHPUnit bootstrap than add a file:
Documentation
mtk3d.github.io/http-vcr — the full book, covering every option in reference tables, plus the parts this README only names: the cassette format, the hook pipeline, storage backends and serializers, the environment-variable precedence table, and every exception with the question it answers.
The source is in docs/; mdbook serve docs reads it locally.
PLAN.md carries the design decisions, each with the alternatives that were
rejected and why. It's written in Polish; everything else is in English.
Dependencies
The record/replay core depends on psr/http-message, psr/http-client,
psr/http-factory and psr/clock, and nothing else — no Guzzle, no Symfony, no framework.
The package additionally requires symfony/console and nikic/php-parser for the CLI;
since http-vcr is installed as a dev dependency, neither reaches an application's
production autoloader.
Optional, install only if needed: guzzlehttp/guzzle (the middleware bridge),
symfony/http-client (the native HttpClientInterface bridge), symfony/yaml (makes YAML
the cassette format instead of JSON), phpunit/phpunit (the attribute and trait).
Development
All three run in CI across PHP 8.2–8.5. Conventions for working on this repository are in AGENTS.md.
License
MIT — see LICENSE.
All versions of http-vcr with dependencies
psr/clock Version ^1.0
psr/http-message Version ^2.0
psr/http-client Version ^1.0
psr/http-factory Version ^1.0
symfony/console Version ^6.4 || ^7.0 || ^8.0
nikic/php-parser Version ^5.0