testo/bridge-vcr — API
PHP-VCR integration: mark a test with #[VCR] and its HTTP interactions are recorded to a cassette on the first run and replayed offline afterwards.
Install
composer require --dev testo/bridge-vcr
#[VCR] attribute — Testo\Bridge\VCR
Targets a method or a class (class-level = default for every test in the case; a method-level attribute overrides it). It is self-wiring — the interceptor is inserted into the pipeline only for tagged tests, so no plugin registration is needed.
public function __construct(
string $name, // cassette file name (required)
?RecordMode $mode = null, // null → inherit php-vcr default (NewEpisodes)
array $match = [], // list<Matcher>; empty → php-vcr default (method + url)
)
RecordMode — Testo\Bridge\VCR\RecordMode
| Case |
php-vcr mode |
Behaviour |
NewEpisodes |
new_episodes |
Replay existing; record anything new (real call). Default. |
Once |
once |
Record on first run (no cassette file yet), replay-only after. |
None |
none |
Replay-only; a request with no match throws. Best for CI. |
Matcher — Testo\Bridge\VCR\Matcher
Which request attributes must line up for a recording to replay. Transport-agnostic (php-vcr normalizes stream_wrapper/curl/soap into one request object).
Method, Url (path + query; there is no separate path), Host, QueryString, Body, PostFields (form POSTs only), Headers, SoapOperation (SOAP envelope only).
VcrPlugin — optional
#[VCR] works on its own. Register Testo\Bridge\VCR\VcrPlugin only to point php-vcr at a non-default cassette directory:
// testo.php
use Testo\Application\Config\ApplicationConfig;
use Testo\Application\Config\SuiteConfig;
use Testo\Bridge\VCR\VcrPlugin;
return new ApplicationConfig(
plugins: [new VcrPlugin(cassettePath: __DIR__ . '/tests/fixtures')],
suites: [new SuiteConfig(name: 'Feature', location: ['tests/Feature'])],
);
VcrPlugin(?string $cassettePath = null) — null keeps php-vcr's default (tests/fixtures, relative to CWD); global in php-vcr, so the last configured suite wins; the directory must exist.
Examples
use Testo\Bridge\VCR;
use Testo\Bridge\VCR\Matcher;
use Testo\Bridge\VCR\RecordMode;
// Replay-only, default matchers (method + url):
#[VCR('github-user', mode: RecordMode::None)]
public function fetchesUser(): void
{
$json = \file_get_contents('https://api.github.com/users/roxblnfk');
// assert on $json …
}
// Match on body too (e.g. a JSON API POST):
#[VCR('checkout', mode: RecordMode::None, match: [Matcher::Method, Matcher::Url, Matcher::Body])]
public function submitsOrder(): void { /* … */ }
// Class-level default cassette for the whole case; a method may override it:
#[VCR('users-api', mode: RecordMode::None)]
final class UsersApiTest
{
public function listsUsers(): void { /* … */ } // uses users-api
public function getsOne(): void { /* … */ } // uses users-api
#[VCR('user-detail', mode: RecordMode::None)]
public function getsDetail(): void { /* … */ } // overrides → user-detail
}
Layout
Package-level attribute like plugin/retry: Testo\Bridge\VCR lives at the package root (VCR.php, loaded via composer files); everything else is under Testo\Bridge\VCR\ in src/.
Concurrency semantics
php-vcr is process-global (one active cassette per process). A #[VCR] test therefore runs as an exclusive, synchronous block: while the cassette is inserted the test does not yield to the fiber scheduler, two #[VCR] tests never overlap, and awaiting real async work inside the window is unsupported by design.
Note
Released php-vcr constrains symfony/event-dispatcher to ^4–^7, so it can't install on Symfony 8 yet. Symfony 8 support is in php-vcr #442 (unreleased). The bridge is unblocked once php-vcr tags a Symfony 8 release.
testo/bridge-vcr— APIPHP-VCR integration: mark a test with
#[VCR]and its HTTP interactions are recorded to a cassette on the first run and replayed offline afterwards.Install
#[VCR]attribute —Testo\Bridge\VCRTargets a method or a class (class-level = default for every test in the case; a method-level attribute overrides it). It is self-wiring — the interceptor is inserted into the pipeline only for tagged tests, so no plugin registration is needed.
RecordMode—Testo\Bridge\VCR\RecordModeNewEpisodesnew_episodesOnceonceNonenoneMatcher—Testo\Bridge\VCR\MatcherWhich request attributes must line up for a recording to replay. Transport-agnostic (php-vcr normalizes
stream_wrapper/curl/soapinto one request object).Method,Url(path + query; there is no separatepath),Host,QueryString,Body,PostFields(form POSTs only),Headers,SoapOperation(SOAP envelope only).VcrPlugin— optional#[VCR]works on its own. RegisterTesto\Bridge\VCR\VcrPluginonly to point php-vcr at a non-default cassette directory:VcrPlugin(?string $cassettePath = null)—nullkeeps php-vcr's default (tests/fixtures, relative to CWD); global in php-vcr, so the last configured suite wins; the directory must exist.Examples
Layout
Package-level attribute like
plugin/retry:Testo\Bridge\VCRlives at the package root (VCR.php, loaded via composerfiles); everything else is underTesto\Bridge\VCR\insrc/.Concurrency semantics
php-vcr is process-global (one active cassette per process). A
#[VCR]test therefore runs as an exclusive, synchronous block: while the cassette is inserted the test does not yield to the fiber scheduler, two#[VCR]tests never overlap, and awaiting real async work inside the window is unsupported by design.Note
Released php-vcr constrains
symfony/event-dispatcherto^4–^7, so it can't install on Symfony 8 yet. Symfony 8 support is in php-vcr #442 (unreleased). The bridge is unblocked once php-vcr tags a Symfony 8 release.