Skip to main content
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:
src/Runner.php
$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:
src/Workflow.php
if (is_string($item->action())) {
    $this->dependencies = $this->dependencies
        ->withClass($item->action());
}
When using Chevere’s Container, dependencies are auto-injected:
src/Run.php
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

1

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
) {}
2

Register dependencies early

Configure your container before creating workflows to catch missing dependencies early.
3

Use type hints

Always type-hint constructor parameters so the container knows what to inject.
4

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());
    }
}

Build docs developers (and LLMs) love