Skip to content

feat(bridge-vcr): PHP-VCR bridge#267

Open
roxblnfk wants to merge 10 commits into
1.xfrom
feature/bridge-vcr
Open

feat(bridge-vcr): PHP-VCR bridge#267
roxblnfk wants to merge 10 commits into
1.xfrom
feature/bridge-vcr

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Jul 14, 2026

Copy link
Copy Markdown
Member

PHP-VCR bridge (testo/bridge-vcr)

Adds a PHP-VCR bridge: mark a test with #[VCR] and its HTTP interactions are recorded to a cassette on the first run and replayed offline afterwards.

Closes #266. Full API + examples live in that issue body.

What's here

  • #[VCR] attribute (Testo\Bridge\VCR) — name, mode, match; on a method or a whole class (class-level = default, method overrides via ConflictPolicy::Last). Self-wiring: it is Interceptable with a #[FallbackInterceptor], so the interceptor is inserted (at its own order) only for tagged tests, receiving the resolved attribute via the constructor — no global registration, no per-test reflection.
  • RecordMode enum — NewEpisodes / Once / None (we deliberately omit php-vcr's all).
  • Matcher enum — Method/Url/Host/QueryString/Body/PostFields/Headers/SoapOperation. Transport-agnostic (php-vcr normalizes stream/curl/soap into one request), so it's one attribute, not one per transport.
  • VcrPlugin(cassettePath:) — optional; only points php-vcr at a non-default cassette dir. #[VCR] works without it.
  • Concurrency — php-vcr is process-global, so a #[VCR] test runs as an exclusive, non-yielding window (contained in its own fiber and driven to completion) plus a process lock. Fiber-aware for the future; synchronous today.
  • Release wiring — split-publish trigger, release-please config, resources/version.json seed, path-repo dev-alias; mirror repo php-testo/bridge-vcr created.
  • Root build — the bridge is added to require-dev and its suites are registered in testo.php, so its tests run in CI with the rest.

Tests

Acceptance / Self / Feature suites (offline replay, class-level default + method override, status mapping) — 7/7 green, validated locally against php-vcr's Symfony 8 branch.

⚠️ CI is red until php-vcr ships Symfony 8 — re-run after that lands

Released php-vcr constrains symfony/event-dispatcher to ^4–^7, so on this Symfony 8 repo composer can only satisfy it by downgrading Symfony to 7.x (console → 8.0.x, event-dispatcher → 7.4). Symfony 8 support sits in php-vcr #442 (CI green across PHP 8.0–8.5, mergeable but awaiting maintainer approval — asked for a timeline).

Follow-up once php-vcr tags a Symfony 8 release: bump the bridge's php-vcr/php-vcr lower bound and just re-run CI here for a clean Symfony 8 run.

roxblnfk added 5 commits July 15, 2026 00:21
Starter layout for testo/bridge-vcr: the `\Testo\Bridge\VCR` attribute
(cassette name only for now), `VcrPlugin`, and a minimal `VcrInterceptor`
that inserts/ejects the cassette around each tagged test. Concurrency
isolation for VCR global state is left as a documented TODO.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Register testo/bridge-vcr in the release/split pipeline so its mirror repo
(php-testo/bridge-vcr) is populated and versioned like every other bridge:

- split-publish.yml: publish on `bridge-vcr-*` tags.
- release-please config + manifest (seeded at 0.0.0 for the first release).
- root composer.json: path-repo dev-alias and test autoload namespace.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Extend the #[VCR] attribute with `mode` (RecordMode enum: NewEpisodes/Once/
None) and `match` (list<Matcher>: method/url/host/query_string/body/
post_fields/headers/soap_operation), applied per test via VCR::configure()
before the cassette is inserted. The enum is named `Matcher` because `Match`
is a reserved keyword; there is no `path` matcher (Url covers it).

Matchers are transport-agnostic — php-vcr normalizes curl/stream/soap into
one Request — so this stays one attribute, not one per transport.

The interceptor keeps ORDER_CLOSE_TO_TEST, which sits inside retry/repeat
(each attempt re-inserts the cassette) and outside lifecycle hooks. Global-
state isolation under concurrency remains a documented NOTE/TODO.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
php-vcr is process-global, so a #[VCR] test must own the single cassette
exclusively. The interceptor now contains the test in a private fiber and
drives it to completion, absorbing any suspension instead of propagating it
to the parent scheduler, so the turnOn..turnOff window never yields control
and no sibling can clobber the active cassette. A process-wide lock brackets
the window and fails loudly if two #[VCR] windows ever overlap.

Testo respects fibers but does not schedule tests concurrently itself, so
this is future-proofing; a #[VCR] test is synchronous by design. Documented
on the #[VCR] attribute.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Add a cassette-path option to VcrPlugin and the bridge's test suites:

- Acceptance: a #[VCR(mode: None)] test replays a committed cassette with no
  network access.
- Self: a class-level #[VCR] applies to every method, proving both the class
  fallback and that the exclusive window is released between sequential tests.
- Feature: status mapping via TestRunner — replay hit passes, an unrecorded
  request in None mode is Error, an untagged test passes through untouched.

Validated locally against php-vcr's Symfony 8 branch (feat/441-symfony-8);
the integrated run wires in once php-vcr tags a Symfony 8 release.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
@roxblnfk
roxblnfk requested a review from a team as a code owner July 14, 2026 22:07
roxblnfk added 2 commits July 15, 2026 02:21
Add testo/bridge-vcr (+ php-vcr) to the root require-dev and register its
suites in testo.php, so the bridge's tests run in CI alongside the others.

Until php-vcr tags a Symfony 8 release (php-vcr#442), composer resolves this
by downgrading Symfony to 7.x; re-run CI once that release lands for a clean
Symfony 8 run.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Model #[VCR] on the retry attribute: it now implements Interceptable and
declares #[FallbackInterceptor(VcrInterceptor)], so the interceptor is wired
into the pipeline (at its own order) only for tagged tests, receiving the
resolved VCR instance via the constructor — no global registration and no
per-test reflection. Class-vs-method precedence is handled by
ConflictPolicy::Last (class = default, method overrides).

VcrPlugin is now optional: it only sets a custom cassette path. #[VCR] works
without it.

Adds a method-overrides-class test (+ other.yml fixture); all 7 bridge tests
pass locally.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
bridge/vcr/src/VcrPlugin.php 0.00% 3 Missing ⚠️
bridge/vcr/VCR.php 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

roxblnfk added 3 commits July 15, 2026 22:14
Reflect the Interceptable refactor in the README: the attribute works without
registering a plugin; VcrPlugin is only for a custom cassette path.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
…idge\VCR

Follow the package-level-attribute layout used by plugin/retry: the #[VCR]
attribute lives at the package root (VCR.php) and is loaded via composer's
`files` autoload, while everything else moves under the matching namespace
Testo\Bridge\VCR\ (src/ → PSR-4 "Testo\\Bridge\\VCR\\"). So the src namespace
and the attribute FQN are consistent (VCR, not the earlier mixed-case Vcr).

Renames RecordMode/Matcher/VcrPlugin into src/ root and VcrInterceptor into
src/Internal/; updates all namespaces, use statements, test namespaces
(Tests\Bridge\VCR) and the root autoload-dev entry. All 7 bridge tests pass.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VCR bridge

1 participant