Hyperf applications
The Hyperf bridge runs each Greenlight test attempt in a Hyperf coroutine.
The bridge supports Hyperf 3.2 with Swoole 5 or later. It does not support the Swow engine.
Setup
Install the Hyperf framework and dependency injector in the application:
composer require hyperf/framework:~3.2.0 hyperf/di:~3.2.0
Enable the Swoole and pcntl extensions for the PHP command that runs Greenlight.
Register the plugin in greenlight.php:
use Greenlight\Config\GreenlightConfig;
use Greenlight\Hyperf\HyperfPlugin;
return GreenlightConfig::create()
->paths(['tests'])
->plugins(static fn(): HyperfPlugin => new HyperfPlugin(__DIR__));
Pass the application root directory to the plugin. Include the standard
config/container.php file in this directory.
Return a Psr\Container\ContainerInterface instance from the container file.
For the test-attempt container lifetime, create a new instance in this file.
The standard Hyperf container file does this.
Container lifetime
The default ContainerLifetime::Worker mode uses one application container for
each worker. Greenlight reuses the container for all test attempts in that
worker.
use Greenlight\Hyperf\ContainerLifetime;
use Greenlight\Hyperf\HyperfPlugin;
new HyperfPlugin(
__DIR__,
containerLifetime: ContainerLifetime::Worker,
);
Container singleton state can reach later tests in this mode. Request data in a worker service, global variable, or static property can also reach later tests.
Use ContainerLifetime::TestAttempt to create an application container for
each test attempt:
new HyperfPlugin(
__DIR__,
containerLifetime: ContainerLifetime::TestAttempt,
);
This mode uses the fresh-container strategy from Hyperf test helpers. It does not test if container services are safe across requests in a long-running worker. Static properties and global variables remain in the Greenlight worker process.
Worker bootstrap
Each Greenlight worker calls Hyperf\Di\ClassLoader::init() once. This call
loads scan configuration and generates AOP proxy classes.
The bridge locks runtime/container/greenlight.scan.lock during this call.
The lock prevents concurrent writes to the Hyperf scan cache.
Keep scan_cacheable disabled during development. If you enable
scan_cacheable, generate current scan caches before you run tests.
Greenlight initializes the class loader before it loads test classes. Composer can then load generated classes for application services.
In ContainerLifetime::Worker mode, bootstrap also loads
config/container.php. It resolves Hyperf\Contract\ApplicationInterface to
boot the application once for the worker.
Worker-container lifecycle
The default mode uses this lifecycle:
- Boot one application container during Greenlight worker bootstrap.
- Start one root Swoole coroutine for the worker.
- Start one child coroutine for each test attempt.
- Construct the test and run all hooks, plugins, and the test method.
- Close the Greenlight test service scope.
- Call the configured reset callback.
- End the child coroutine and release its
Hyperf\Context\Contextdata. - Reuse the application container for the next test attempt.
- When the worker exits, call the disposal callback inside the root coroutine.
- Clear Swoole timers and resume Hyperf’s worker-exit coordinator.
- End the root coroutine.
The root coroutine contains all assignments that Greenlight sends to one worker. Worker replacement closes this coroutine and creates a new application container.
Test-attempt container lifecycle
ContainerLifetime::TestAttempt uses this lifecycle for each attempt:
- Start one root Swoole coroutine.
- Load
config/container.phpand activate its new container. - Resolve
Hyperf\Contract\ApplicationInterfaceto boot the application. - Construct the test and run all hooks, plugins, and the test method.
- Close the Greenlight test service scope.
- Call the reset callback, then the disposal callback, even if reset fails.
- Remove access to the discarded application container.
- Clear Swoole timers and resume Hyperf’s worker-exit coordinator.
- End the coroutine.
In both modes, the complete test attempt runs inside its own coroutine. This
includes constructor injection, hooks, test-scope disposal, and afterTest()
plugins. Swoole destroys the coroutine context when the attempt ends.
Container services
Declare a service dependency by type:
final readonly class RegistrationTest
{
public function __construct(private RegistrationHandler $handler) {}
}
Without an explicit service source, Greenlight first checks its harness services. It then asks the active Hyperf container.
Use #[Service] when the parameter type does not select the necessary ID:
use Greenlight\Harness\Service;
public function __construct(
#[Service('payments.client')] private PaymentClient $client,
) {}
The bridge checks the returned service type. A different type causes a test error.
Bridge setup and service-resolution failures throw ServiceResolutionFailed.
Concrete Hyperf bridge exceptions are internal.
A test can also receive Psr\Container\ContainerInterface. This service is
available only during the current test attempt.
Select a service source
Pass source: 'app' to HyperfPlugin to name this plugin instance. Use
#[Service(source: 'app')] to request a service by type from this source.
Use #[Service('payments.client', 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 ContainerInterface harness service. See
service sources for naming and resolution rules.
Reset and disposal
Hyperf does not supply one reset operation for all application services. The application keeps request state in coroutine context or resets that state after each attempt. See Hyperf’s coroutine guidance.
Use reset: to reset project state after each attempt. Use
dispose: for resources that belong to the selected container lifetime:
use Psr\Container\ContainerInterface;
new HyperfPlugin(
__DIR__,
reset: static function (ContainerInterface $container): void {
$container->get(RequestStateProbe::class)->reset();
},
dispose: static function (ContainerInterface $container): void {
$container->get(TestResourceRegistry::class)->close();
},
);
The reset callback runs after Greenlight closes its per-test service scope. It runs inside the test coroutine in both modes.
The disposal callback runs before Greenlight discards its container. In worker mode, it runs once after the worker finishes its assignments. A disposal failure stops the worker runtime and fails the run.
In test-attempt mode, the reset callback runs first. Greenlight calls disposal even if reset throws. A failure from either callback gives the attempt an error. A reset failure in worker mode also gives the attempt an error.
The first throwable remains the cause. At the end of the container lifetime, Greenlight still removes container access, clears the coroutine runtime, and ends the root coroutine.
The bridge does not reset application static properties or global variables.
Reset these values in an #[After] hook or the reset: callback.
Parallel resources
Workers run tests at the same time. Use GREENLIGHT_CHANNEL in Hyperf
configuration to assign separate external resources to each worker.
Add the channel to database names, cache prefixes, queue names, and temporary directories. The channel remains stable for the worker slot.
If workers cannot use separate external resources, use #[RequiresResource].
See configuration for concurrency limits.
Supported features
The bridge provides:
- Hyperf class scan and AOP proxy generation
- One root Swoole coroutine for each worker container
- Application boot for the selected container lifetime
- One coroutine context for each complete test attempt
- Persistent or isolated container singleton state
- Project reset and disposal callbacks
- Hyperf timer and coordinator cleanup at the container lifetime boundary
- Type-based and explicit-ID service injection
The bridge does not provide:
- A live Swoole HTTP server or server worker events
- Concurrent test attempts inside one Greenlight worker
- Swow coroutine execution