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

CLI Reference

vendor/bin/http-vcr <command> [options]

The mtk3d/laravel-http-vcr package exposes the same seven commands as Artisan commands prefixed with vcr: (stale → vcr:stale, tests → vcr:tests, and so on) — Artisan commands share one flat namespace across the whole framework and every installed package, so they need a prefix to stay collision-free and easy to find in artisan list; a standalone single-purpose binary like vendor/bin/http-vcr doesn’t have that problem, so its commands stay bare.

stale

vendor/bin/http-vcr stale

Lists interactions that have crossed staleAfter — meant as a separate, non-blocking CI step, not a build gate. The per-cassette threshold is read from #[UseCassette(staleAfter: ...)] via the same AST scan tests uses (not by running any tests) — a cassette no test declares a threshold for is simply skipped, and conflicting thresholds declared for the same cassette name are reported rather than silently resolved.

With scoped cassettes, one attribute names a base cassette that exists on disk as several scope files. The declared threshold applies to all of them, and the report names the specific scope file, not just the base name.

providers

vendor/bin/http-vcr providers

Prints each provider configured in http-vcr.php — its host patterns, its requiresEnv, and how many cassettes and interactions belong to it — then the hosts running on implicit providers, i.e. those no configuration has claimed:

shopify        *.myshopify.com          SHOPIFY_API_KEY     4 cassettes, 11 interactions
zendesk-a      account-a.zendesk.com    ZENDESK_A_API_KEY   2 cassettes, 3 interactions

Implicit (addressable by host, no requiresEnv):
  api.stripe.com          2 cassettes, 5 interactions

A host on an implicit provider works fine — VCR_ERASE_TAPE=@api.stripe.com targets it like any other — it just has no requiresEnv and no shorter name. This section is the shortlist of integrations worth declaring in http-vcr.php.

tests

vendor/bin/http-vcr tests --provider=shopify
vendor/bin/http-vcr tests --provider=shopify --filter-only

Lists the tests that touch a given provider, plus a ready-made regex for PHPUnit’s --filter. --filter-only prints just the regex, for dropping into a shell substitution:

SHOPIFY_API_KEY=xxx VCR_ERASE_TAPE=@shopify \
  vendor/bin/phpunit --filter "$(vendor/bin/http-vcr tests --provider=shopify --filter-only)"

This is a speed optimization, never a safety requirement — what gets erased and re-recorded is decided by the VCR_ERASE_TAPE selector, so the same command without any filter produces the same cassettes, just slower.

It answers the question by combining two sources, neither sufficient alone: an AST scan of #[UseCassette] (which test opens which cassette) and the contents of the cassettes (which hosts actually appear in them). The second is what makes it work for a test whose cassette name gives away nothing about it talking to Shopify.

The regex ends a test’s name where PHPUnit does, so the data sets of a test with a data provider (…::testFoo#0) are covered by it, and a test whose name merely starts the same way is not. With no match at all it prints a regex that matches nothing, since an empty --filter would run the whole suite.

One limitation that falls straight out of that: a test whose cassette doesn’t exist yet won’t be listed, because there’s nothing to scan. Record it the first time with an unfiltered run.

Loading the test classes to read their attributes by reflection would mean every one of them needing a correctly configured environment — the exact thing the CLI is supposed to avoid — so it parses the syntax instead. What the scan can resolve statically: scalar literals and arrays of them, enum cases (RecordMode::RecordIfAbsent), and new DateInterval('P7D'), which is what staleAfter almost always looks like since PHP 8.1 allows new in attribute arguments. Anything else (staleAfter: self::INTERVAL, a computed cassette name) is reported as “couldn’t be fully analyzed” rather than guessed at.

scan-secrets

vendor/bin/http-vcr scan-secrets

The full, manual pass of the same scanner that runs automatically after every recording — a heuristic sweep of every cassette’s contents for Bearer tokens, AWS-style AKIA[0-9A-Z]{16} keys, long hex/base64 strings in fields that look like tokens, and Authorization/Cookie/Set-Cookie values that don’t look like placeholders.

Two things this adds over the automatic check: it covers every cassette — the configured directory plus any a test class keeps beside itself with #[CassetteDirectory] — not just what a run happened to record, and it can be made blocking:

vendor/bin/http-vcr scan-secrets --fail-on-findings

Without that flag a finding is reported and the command still exits 0.

The test is what the value looks like, not which redact() rules exist: this command doesn’t run the test suite, so it can’t know about rules registered imperatively in a setUp(). Anything in the <...> convention counts as a placeholder — both the built-in <REDACTED-*> values and your own <API_KEY>. Rules declared in http-vcr.php are read too, since that file can be loaded without running anything.

Narrowing the sweep

vendor/bin/http-vcr scan-secrets shopify/get-product     # one cassette, its scope files included
vendor/bin/http-vcr scan-secrets --provider=stripe       # only interactions belonging to one API

A --provider resolves the way VCR_ERASE_TAPE=@name does: a name configured in http-vcr.php, or a host, which is a provider of its own without being declared. A name nothing recorded answers to fails the command rather than reporting a clean sweep — a selector that selects nothing looks exactly like a project with no secrets in it. A cassette name that is not on disk fails the same way.

Hiding a value that is already recorded

vendor/bin/http-vcr scan-secrets --redact

Walks the findings one at a time, asks about each, and rewrites the cassettes with what was confirmed:

tests/Cassettes/billing/charge.yaml
  #1 response.body (/api_key) — "sk_live_…" (32 chars)
     Redact it? [y/N] y
     Placeholder [<REDACTED-API-KEY>]
  redacted 1 value — one-way: the original values are not in this file any more.

Every occurrence of the value goes, not only the one that was found — a credential repeated in three places must not survive in two. The offered placeholder follows the field’s name, and anything can be typed over it.

One way. http-vcr has nothing to put back: unlike a redact() rule with a value provider, this is an edit to the file. For a response body that changes nothing about replay, since nothing matches on it. A value in the request is matched on, so replacing it here means a live request no longer lines up with the recording unless http-vcr.php redacts the same field — the command says so under any such finding.

It refuses to run without a terminal to ask on, so --redact in a pipeline redacts nothing rather than quietly rewriting cassettes.

Sidecar files are scanned as well. They hold the largest payloads in the project and go through redaction like any inline body, so leaving them out would be the exact gap this command exists to close. Sidecars whose content isn’t text (an image, an archive) are skipped, to avoid false positives from arbitrary byte sequences.

migrate

vendor/bin/http-vcr migrate --to=yaml

Rewrites every cassette in the project in the other format — --to=yaml or --to=json — and removes the file it replaced. The two formats hold the same schema, so nothing about a cassette changes except how it is written. Sidecar files are named after the cassette without its format extension and are left where they are.

This is what makes a format switch a step rather than an accident: a cassette is only ever looked for under the extension its serializer owns, so files in the old format are invisible to the next run, not migrated by it.

vendor/bin/http-vcr migrate --to=yaml --dry-run

The sweep covers the configured cassette directory plus any a test class keeps beside itself with #[CassetteDirectory]. A cassette that already exists under the target name is left alone and reported, as is one that cannot be parsed; either one exits non-zero, since both leave the project holding two formats at once.

lock / unlock

vendor/bin/http-vcr lock shopify/checkout --interaction=2
vendor/bin/http-vcr unlock shopify/checkout --interaction=2

Sets or clears "locked": true on a specific interaction. Without --interaction, applies to every interaction in the file at once. With scoped cassettes, a bare name covers every scope file for that cassette; --scope=2024-01 narrows it to one. See Locked Interactions.

Exit codes

A finding is not a failure. stale reports interactions past their threshold and still exits 0 — crossing a threshold is a fact about the clock, and the same commit run an hour later would answer differently — and scan-secrets exits 0 unless asked for --fail-on-findings. What does exit non-zero, in every command that reads cassettes, is a cassette that cannot be parsed: that is a defect in the file rather than a verdict about its contents. tests also exits non-zero when --provider names something that is neither a configured provider nor a host in any cassette, and lists both sets. migrate exits non-zero when it left any cassette behind — a name already taken in the target format, or a file it could not read.

Color

Output is colored on a terminal and plain everywhere else, with three spans carrying it: a cassette path, a finding — the credential-shaped value in scan-secrets, how far past its threshold an interaction is in stale — and a table heading. --no-ansi turns it off and --ansi forces it on, and both also settle it for any warning a command’s own run prints, so one run never comes out half-colored.

Running from inside a consuming project

Composer links the binary into the consuming project’s vendor/bin, and the script resolves the host project’s vendor/autoload.php and http-vcr.php, not its own package directory — so it works the same whether it’s run from a project that depends on http-vcr or from http-vcr’s own test suite. For an unusual layout, point it at a config explicitly:

vendor/bin/http-vcr providers --config=path/to/http-vcr.php

The CLI is built on symfony/console and reads attributes with nikic/php-parser; both are regular dependencies of the package rather than optional extras, so these commands work immediately after composer require --dev mtk3d/http-vcr. Neither reaches a production autoloader, since the package itself is a dev dependency. The record/replay core doesn’t touch either one.