Greenlight

Documentation

Extend

On this page

Symfony applications

The Symfony bridge supplies Symfony services and built-in Greenlight harness services to test constructors.

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(static fn(): SymfonyPlugin => 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.

If a required container service is absent, the bridge reports a configuration error with a correction. It 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,
    ) {}
}

Without an explicit service source, 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.

Bridge setup and service-resolution failures throw ServiceResolutionFailed. Concrete Symfony bridge exceptions are internal.

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\Harness\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.

Select a service source

Pass source: 'app' to SymfonyPlugin to name this plugin instance. Use #[Service(source: 'app')] to request a service by type from this source. Use #[Service('mailer.transports.async', source: 'app')] to select an explicit ID in its container.

An explicit source takes precedence over global harness services. A missing service fails without a request to another source. Use the same source attribute to select this plugin’s KernelInterface harness service. See service sources for naming and resolution rules.

The kernel itself

Greenlight supplies KernelInterface as a per-worker 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, each test that requests the kernel or a container service has an error. Tests that use only other harness services do not boot the kernel.

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 does not isolate databases or other external services.

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. Its value is a stable number from 1 through the worker count. Within one run, concurrent tests use different channels. Use it in Symfony configuration to name resources:

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

parameters:
    fallback_channel: '1'

The default: processor reads the fallback_channel container parameter when GREENLIGHT_CHANNEL is absent. See the Symfony environment variable processors.

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

Separate runs and CI shards reuse channel numbers. Add a resource prefix for each concurrent run that uses the same external service.

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 crash. 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 does not replace container services with doubles. 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.

Non-goals

The current bridge does not cover: