Browse documentation

Symfony applications

The Symfony bridge supplies Symfony services to test constructors. It supplies these services together with the built-in Greenlight harness services.

The bridge boots the application kernel once for each worker. Register the plugin to activate the bridge. The bridge uses the Symfony packages that the application provides. Greenlight does not declare a runtime dependency on Symfony.

Setup

Register the plugin in greenlight.php with your kernel class:

use Greenlight\Config\GreenlightConfig;
use Greenlight\Symfony\SymfonyPlugin;

return GreenlightConfig::create()
    ->paths(['tests'])
    ->plugins(new SymfonyPlugin(App\Kernel::class, env: 'test', debug: false));

Use a closure when the kernel needs custom construction:

new SymfonyPlugin(static fn(): KernelInterface => new App\Kernel('test', false));

The kernel boots when a test first requests a container service. It remains active for the worker lifetime. A worker does not boot Symfony when its tests do not use the container.

Greenlight tests the bridge with symfony/framework-bundle 6.4, 7.x, and 8.x. At boot, the bridge requires the kernel container to expose test.service_container. When service resets are active, it also requires services_resetter. A standard FrameworkBundle test environment provides the test container when framework.test is active.

At boot, the bridge checks that the container supports the features it needs. The bridge reports a configuration error if the Symfony test container is not present. It also reports a configuration error if an active service reset has no services_resetter. The error message contains a correction. The bridge reports an error and does not use weaker isolation.

Container services

Declare the dependency by type:

final class RegistrationTest
{
    public function __construct(
        private readonly UserRepository $users,
        private readonly RegistrationHandler $handler,
    ) {}
}

Greenlight first resolves constructor parameters from its harness. It then uses the Symfony container. Thus, Doubles, TestChannel, and provider services take precedence over container services.

When neither side can resolve a type, the test fails and reports both misses.

The normal Symfony test-container rules still apply. The container must reference a private service to retain it during compilation. The Symfony compiler can remove an unused service. Greenlight cannot inject a removed service.

Services without a usable type

Type alone cannot select some services. Examples include string-ID-only services, interfaces with multiple implementations, and decorated services. Use #[Service] to name the service explicitly:

use Greenlight\Symfony\Service;

public function __construct(
    #[Service('mailer.transports.async')] private readonly TransportInterface $transport,
) {}

Greenlight still checks the parameter type. If the named service is not an instance of the declared type, the test fails and does not receive the object.

The kernel itself

Greenlight supplies KernelInterface as a per-run harness service. Tests can use it to inspect boot parameters or the container directly:

public function __construct(private readonly KernelInterface $kernel) {}

State between tests

The bridge keeps the kernel active between tests.

After each test, the bridge calls Symfony’s services_resetter, the same reset mechanism Symfony uses between requests. The resetter resets services with the kernel.reset tag. This includes services that Symfony configures from ResetInterface. It also includes common Symfony services such as Doctrine’s ManagerRegistry, cache pools, and the profiler. Implement ResetInterface for each stateful service that must keep tests isolated.

The bridge captures and checks the resetter when the kernel boots. If service resets are active without a container resetter, every test fails. No test runs with shared state that has not had a reset.

For a container that has no stateful services, pass resetBetweenTests: false to the plugin. This value disables the resetter requirement. Do not use this value with services that keep state. Tests on the same worker will share those service instances.

The bridge boots and resets the Symfony kernel. Isolation for databases and other external services remains the test suite’s responsibility.

Parallel resources

Workers run tests at the same time. Split shared external resources for each worker. Alternatively, protect them with a concurrency limit.

Greenlight sets GREENLIGHT_CHANNEL in every worker process. It is a stable number from 1 through the worker count, and no two concurrent tests use the same channel. Use it in normal Symfony configuration to key shared resources:

# config/packages/test/doctrine.yaml
doctrine:
    dbal:
        dbname: 'app_test_%env(default:fallback_channel:GREENLIGHT_CHANNEL)%'

parameters:
    env(fallback_channel): '1'

The same pattern works for cache directories, upload paths, message transport names, and similar resources.

The application must create and migrate databases for each channel. Use a loop in the test bootstrap, a Makefile target, or another project-level setup step. Channel numbers remain stable after a worker recycle. Thus, these schemas can remain for the complete test run.

If you cannot split a service per channel, mark the classes that use it with #[RequiresResource]. Configure its safe concurrency:

#[RequiresResource('payments-sandbox')]
final class PaymentGatewayTest { ... }
return GreenlightConfig::create()
    ->resourceLimit('payments-sandbox', 2);

The limit controls how many assignments that require this resource can run. It does not choose a service instance, and it does not coordinate another Greenlight process or CI shard. See configuration for the complete resource rules.

Doubles and the container

The bridge injects only real Symfony container services.

There is no API that replaces a container service with a double. If a test needs a doubled collaborator, get the double through Doubles. Then construct the test subject directly. Greenlight then controls the double lifecycle and verification.

Container-level replacement remains a possible future addition. Its design must prevent uncertain service replacement.

Non-goals

The current bridge does not cover:

The bridge has a deliberately small scope. It provides the kernel, container services, service resets, and worker channels.