Chevere Workflow supports dependency injection, allowing you to provide external dependencies to your actions through a PSR-11 container. This is particularly useful when using action class names instead of instances.
Basic usage
When you pass an action class name (instead of an instance) to a job, the workflow will automatically resolve its dependencies from the container:
use Chevere\Container\Container;
use function Chevere\Workflow\run;
use function Chevere\Workflow\sync;
use function Chevere\Workflow\workflow;
$workflow = workflow(
job1: sync(MyActionWithDependencies::class),
);
$container = new Container(
logger: new Logger(),
database: new Database(),
);
$run = run($workflow, $container);
How it works
The workflow system extracts dependencies during execution:
$action = $job->action();
if (is_string($action)) {
$dependencies = $this->run->workflow()->dependencies()->extract(
$action,
$this->run->container()
);
/** @var ActionInterface $action */
$action = new $action(...$dependencies);
}
Dependency injection only works when passing action class names. If you instantiate the action yourself, you must provide dependencies manually.
Action with dependencies
Here’s an example action that requires dependencies:
use Chevere\Action\Action;
use Psr\Log\LoggerInterface;
class ProcessData extends Action
{
public function __construct(
private LoggerInterface $logger,
private DatabaseInterface $database,
) {
}
public function __invoke(string $data): array
{
$this->logger->info('Processing data: ' . $data);
$result = $this->database->query(
'INSERT INTO data VALUES (?)',
[$data]
);
return ['id' => $result->lastInsertId()];
}
}
Using this action in a workflow:
use Chevere\Container\Container;
$workflow = workflow(
process: sync(ProcessData::class, data: variable('input')),
);
$container = new Container(
logger: new FileLogger('/var/log/app.log'),
database: new PDODatabase($dsn),
);
$run = run(
$workflow,
$container,
input: 'test data'
);
Auto-injection
The workflow automatically registers all action dependencies it discovers:
if (is_string($item->action())) {
$this->dependencies = $this->dependencies
->withClass($item->action());
}
When using Chevere’s Container, dependencies are auto-injected:
if ($this->container instanceof Container) {
$this->container = $this->container
->withAutoInject($this->workflow()->dependencies());
}
$this->workflow()->dependencies()->assert($this->container);
Container requirements
The container must implement PSR-11’s ContainerInterface:
use Psr\Container\ContainerInterface;
function run(
WorkflowInterface $workflow,
ContainerInterface $container = new Container(),
mixed ...$variable,
): RunInterface {
$run = new Run($workflow, $container, ...$variable);
$runner = new Runner($run);
return $runner->withRun()->run();
}
Nested dependencies
Dependencies can have their own dependencies. The container resolves the entire dependency tree:
tests/src/TestActionDependsNestedNoParams.php
use Chevere\Action\Action;
class TestActionDependsNestedNoParams extends Action
{
public function __construct(
private TestActionDependsNoParams $nestedDependency
) {
}
public function __invoke(): array
{
return $this->nestedDependency->__invoke();
}
}
The container automatically resolves TestActionDependsNoParams and its dependencies.
Error handling
If required dependencies are missing, the container throws an exception:
use Chevere\Container\Exceptions\ContainerException;
$workflow = workflow(
job1: sync(ActionWithDependency::class),
);
try {
run($workflow); // No container provided
} catch (ContainerException $e) {
// Failed to resolve dependencies for `ActionWithDependency`:
// Missing required argument(s): `dependency`
}
Always ensure all required dependencies are registered in your container before running the workflow.
Using custom containers
You can use any PSR-11 compliant container:
use Psr\Container\ContainerInterface;
class MyContainer implements ContainerInterface
{
private array $services = [];
public function get(string $id): mixed
{
if (!$this->has($id)) {
throw new NotFoundException($id);
}
return $this->services[$id];
}
public function has(string $id): bool
{
return isset($this->services[$id]);
}
public function set(string $id, mixed $service): void
{
$this->services[$id] = $service;
}
}
$container = new MyContainer();
$container->set(LoggerInterface::class, new Logger());
$run = run($workflow, $container);
Best practices
Use interfaces for dependencies
Depend on interfaces rather than concrete implementations to maintain flexibility.public function __construct(
private LoggerInterface $logger, // Good
private FileLogger $logger, // Avoid
) {}
Register dependencies early
Configure your container before creating workflows to catch missing dependencies early.
Use type hints
Always type-hint constructor parameters so the container knows what to inject.
Consider action instances for simple cases
If an action doesn’t need external dependencies, instantiating it directly is simpler:job1: sync(new SimpleAction()) // No DI needed
Accessing the container in jobs
The container is available in the run instance:
$run = run($workflow, $container, input: 'data');
$container = $run->container();
// Access services directly
$logger = $container->get(LoggerInterface::class);
Testing with dependency injection
DI makes testing easier by allowing you to inject mock dependencies:
use PHPUnit\Framework\TestCase;
class WorkflowTest extends TestCase
{
public function testProcessData(): void
{
$mockLogger = $this->createMock(LoggerInterface::class);
$mockLogger->expects($this->once())
->method('info');
$container = new Container(
logger: $mockLogger,
database: new InMemoryDatabase(),
);
$workflow = workflow(
process: sync(ProcessData::class, data: 'test'),
);
$run = run($workflow, $container);
$this->assertArrayHasKey('id', $run->response('process')->array());
}
}