Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Containers

Level 2 opens up the test process. “Container” in C4 normally means a deployable unit — a service, a database, an app. For a library the useful reading is a thing with its own lifecycle: the PHP process the tests run in, the files on disk that outlive it, and the CLI you run separately.

graph TB
    dev["<b>Developer</b><br/><span>Person</span>"]

    subgraph proc["Test process — one PHPUnit run"]
        sut["<b>Code Under Test</b><br/><span>PHP</span><br/><br/>Your service class, an SDK,<br/>anything that sends requests"]
        vcr["<b>VcrClient</b><br/><span>PHP — this library</span><br/><br/>PSR-18 decorator. Records on<br/>a miss, replays on a hit"]
        client["<b>Real HTTP Client</b><br/><span>Guzzle · Symfony · php-http</span><br/><br/>Any PSR-18 implementation.<br/>Only called while recording"]
        bridge["<b>PHPUnit Bridge</b><br/><span>PHP</span><br/><br/>#[UseCassette] opens and closes<br/>the session around each test"]
    end

    subgraph disk["Working tree"]
        cass[("<b>Cassette Files</b><br/><span>JSON · YAML</span><br/><br/>tests/Cassettes/**.json<br/>Committed to the repository")]
        side[("<b>Sidecar Bodies</b><br/><span>Binary files</span><br/><br/>Bodies past 1 MiB, stored<br/>beside the cassette")]
        conf["<b>http-vcr.php</b><br/><span>PHP config file</span><br/><br/>Project-wide defaults,<br/>providers, redaction rules"]
    end

    cli["<b>http-vcr CLI</b><br/><span>Symfony Console</span><br/><br/>stale · tests · providers<br/>scan-secrets · lock · unlock"]
    api["<b>Third-party HTTP API</b><br/><span>External System</span>"]

    dev -->|runs| proc
    dev -->|runs| cli
    bridge -->|"opens the session,<br/>closes it after the test"| vcr
    sut -->|"sendRequest()<br/><span>PSR-18</span>"| vcr
    vcr -->|"only on a cassette miss<br/>with recording allowed"| client
    client -->|HTTPS| api
    vcr -->|"read · append · lock"| cass
    vcr -->|"large bodies by reference"| side
    vcr -->|"read once, frozen<br/>at first request"| conf
    cli -->|"scans #[UseCassette],<br/>reads and edits"| cass
    cli -->|reads| conf

    classDef person fill:#08427b,stroke:#052e56,color:#ffffff
    classDef container fill:#438dd5,stroke:#2e6295,color:#ffffff
    classDef store fill:#438dd5,stroke:#2e6295,color:#ffffff
    classDef external fill:#999999,stroke:#6b6b6b,color:#ffffff
    classDef boundary fill:none,stroke:#444444,stroke-dasharray:5 5,color:#888888

    class dev person
    class sut,vcr,client,bridge,cli container
    class cass,side,conf store
    class api external
    class proc,disk boundary

The one arrow that matters

VcrClient → Real HTTP Client is the only path to the network, and it is conditional. On a cassette hit it is never taken; the response is rebuilt from the recorded snapshot through a PSR-17 factory. That is what makes the suite deterministic — not a mock the test has to set up, but a decorator that answers from a file.

Because the decorator sits above the real client, everything the client does — retries, middleware, connection pooling — is on the far side of the recording. What lands in the cassette is what the client returned, not what the wire carried.

Lifecycles

The three boxes have genuinely different lifetimes, and most of the library’s design follows from that:

ContainerLives forConsequence
VcrClientOne instance, possibly many per testCheap to construct; holds no cross-test state
Cassette sessionOne testOwns the lock, the consumption counters, the strict-mode verdict
Cassette fileThe repository’s lifetimeMust stay diffable and free of secrets

The middle row is the subtle one. State that looks like it belongs on the client actually belongs to the session, because the Guzzle bridge produces a fresh VcrClient per request and hooks registered on one of them have to apply to all of them. See ADR-0006 for why configuration freezes at the session boundary rather than the object boundary.

The CLI is a separate reader

bin/http-vcr runs outside the test process entirely. It never constructs a VcrClient; it reads cassettes through the same serializer and finds #[UseCassette] declarations by parsing test files with nikic/php-parser rather than by loading them. Static analysis rather than reflection, so a test file that would fatal on load can still be inventoried.

See the CLI Reference for the commands themselves.