Testing PHP code that calls an external API has always meant choosing between two bad options. Real HTTP calls are slow and fail whenever the third-party service has a bad day. Hand-written mocks are tedious to maintain and often drift away from what the API really returns. Symfony 8.2 adds a third option to the HttpClient component: record the real exchange once, then replay it in every later test run. The feature was announced on 5 November 2026 in the official Symfony blog. Here’s how to use it.

Where Symfony 8.2 Stands Today

The first beta, Symfony 8.2.0-BETA1, came out on 4 November 2026. According to the 8.2 release page, 8.2 requires PHP 8.4.0 or higher, is planned for release in November 2026, and will be supported until July 2027. It’s still in development, so try it on a branch, not in production.

To test the beta in an existing Flex app (with your Symfony dependencies using * constraints), Symfony suggests:

composer config minimum-stability beta
composer config extra.symfony.require "8.2.*"
composer update

How the Recorder Works

The new RecorderHttpClient decorates the HTTP client transport, so it covers the default client and all your scoped clients. It’s driven by the SymfonyExtension of the PHPUnit Bridge, which the Symfony Flex recipe registers in phpunit.dist.xml. Recordings are saved as standard HAR files. HAR is human-readable, easy to review in pull requests, and supported by browser dev tools.

Step 1: Enable It for the Test Environment

# config/packages/http_client.yaml
when@test:
    http_client:
        recorder: true

If you prefer PHP config:

// config/packages/http_client.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'when@test' => [
        'http_client' => [
            'recorder' => true,
        ],
    ],
]);

Step 2: A Service That Calls an External API

Here’s a typical service that depends on HttpClientInterface. The URL is a placeholder, so swap in your real provider.

<?php
// src/Service/ExchangeRates.php
namespace App\Service;

use Symfony\Contracts\HttpClient\HttpClientInterface;

final class ExchangeRates
{
    public function __construct(private HttpClientInterface $client)
    {
    }

    public function getRate(string $from, string $to): float
    {
        $data = $this->client->request('GET', 'https://api.example.com/latest', [
            'query' => ['base' => $from, 'symbols' => $to],
        ])->toArray();

        return (float) $data['rates'][$to];
    }
}

Step 3: Mark the Test With #[UseRecord]

<?php
// tests/Service/ExchangeRatesTest.php
namespace App\Tests\Service;

use App\Service\ExchangeRates;
use Symfony\Bridge\PhpUnit\Attribute\UseRecord;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class ExchangeRatesTest extends KernelTestCase
{
    #[UseRecord]
    public function testGetRate(): void
    {
        $exchangeRates = static::getContainer()->get(ExchangeRates::class);

        $this->assertSame(1.08, $exchangeRates->getRate('EUR', 'USD'));
    }
}

Tests without #[UseRecord] aren’t affected, so you can adopt the recorder one test at a time.

Step 4: Record Once, Then Replay Forever

Run the test once with the SYMFONY_HTTP_RECORDER environment variable to make the real requests and save them:

SYMFONY_HTTP_RECORDER=missing vendor/bin/phpunit --filter testGetRate

This creates tests/Service/ExchangeRatesTest/testGetRate.har. Commit that file. From then on, every run, including CI, replays the saved responses. If a request isn’t in the file, Symfony throws a HarEntryNotFoundException instead of quietly going out to the network.

The Four Recorder Modes

The attribute only chooses the file. Whether a test can reach the network is decided at run time, so you can’t accidentally commit a test that makes live requests. SYMFONY_HTTP_RECORDER accepts:

  • replay (default): replays responses and fails when a request isn’t in the file.
  • missing: replays responses, and makes and records only the requests that aren’t in the file yet.
  • record: always makes real requests and rewrites the files for the tests that run. Use it to refresh recordings after an API change.
  • passthrough: makes real requests without reading or writing files. Use it to check your tests against the live service.

Requests are matched on method, URL and body. Repeated requests are replayed in the order they were recorded, so flows like polling a job until it finishes also work.

Handy Composer scripts

{
    "scripts": {
        "test": "phpunit",
        "test:record-missing": "SYMFONY_HTTP_RECORDER=missing phpunit",
        "test:refresh": "SYMFONY_HTTP_RECORDER=record phpunit",
        "test:live": "SYMFONY_HTTP_RECORDER=passthrough phpunit"
    }
}

On Windows, set the variable in PowerShell first ($env:SYMFONY_HTTP_RECORDER="missing") and then run vendor\bin\phpunit.

Keeping Secrets Out of Recordings

HAR files get committed to your repository, so they must never contain credentials. By default, the recorder replaces secrets in requests and responses with [REDACTED] before writing anything to disk. That covers headers such as Authorization, Cookie and X-Api-Key, and query-string, form and JSON fields such as password, api_key, client_secret and access_token.

Requests are also compared with their secrets masked, so they still match when replayed with real credentials. Use redact to mask more names and redact_except to remove names from the built-in list:

# config/packages/http_client.yaml
when@test:
    http_client:
        recorder:
            redact: ['X-Custom-Secret', 'nonce']
            redact_except: ['code']

Masking a field that changes on every request, like nonce here, also excludes it from request matching. That’s useful for avoiding false mismatches. For full control, set the redactor option to the ID of a service that implements RedactorInterface.

Watch out: replayed responses contain the masked values. If your code validates something like the signature of an id_token, you’ll need a custom redactor.

Best Practices

  • Review HAR diffs in PRs the same way you review code. They show exactly what changed in the API’s responses.
  • Run a scheduled passthrough job (for example, nightly) to catch provider changes early, while normal CI stays fast and offline.
  • Grep recordings for secrets before the first commit, even with default redaction.
  • Keep one behaviour per test so each HAR file stays small and readable.

Takeaway

The Symfony 8.2 HTTP recorder gives you the realism of live API calls with the speed and reliability of mocks, with no third-party package needed. Enable recorder: true in the test environment, add #[UseRecord], record once with SYMFONY_HTTP_RECORDER=missing, and commit the HAR file. Try it on a branch with 8.2.0-BETA1 now so you’re ready when the final release lands this month. Source: New in Symfony 8.2: HTTP Recorder.