league/tactician-container
PSR-11 container plugin for League Tactician that lazily loads command handlers from your DI container. Install via Composer and integrate with Tactician to resolve handlers on demand for cleaner wiring and faster bootstrap.
## Getting Started
### Minimal Setup
1. **Install the package**:
```bash
composer require league/tactician-container
AppServiceProvider):
use League\Tactician\Container\ContainerPlugin;
use League\Tactician\Container\ContainerLocator;
public function register()
{
$this->app->bind(ContainerLocator::class, function () {
return new ContainerLocator($this->app);
});
$this->app->bind(ContainerPlugin::class, function () {
return new ContainerPlugin();
});
}
config/tactician.php):
'plugins' => [
League\Tactician\Container\ContainerPlugin::class,
],
Define a command handler as a service binding:
$this->app->bind(
\App\Commands\ProcessOrderHandler::class,
\App\Commands\ProcessOrderHandler::class
);
Tactician will now resolve it from the container when needed, avoiding eager loading.
AppServiceProvider):
$this->app->bind(
\App\Commands\SendEmailHandler::class,
\App\Commands\SendEmailHandler::class
);
$this->app->tag([\App\Commands\ProcessOrderHandler::class]);
$commandBus = $this->app->make(\League\Tactician\CommandBus::class);
$result = $commandBus->handle(new ProcessOrderCommand());
$this->app->bind(
\App\Middleware\LogCommandMiddleware::class,
\App\Middleware\LogCommandMiddleware::class
);
config/tactician.php):
'middleware' => [
\League\Tactician\Middleware\CommandHandlerMiddleware::class,
\App\Middleware\LogCommandMiddleware::class,
],
ContainerLocator to resolve handlers dynamically:
$locator = $this->app->make(ContainerLocator::class);
$handler = $locator->getHandler($command);
register()):
$this->app->tag([
\App\Commands\HandlerA::class,
\App\Commands\HandlerB::class,
]);
$this->app->bind(\App\Commands\ProcessOrderHandler::class, function ($app) {
return new \App\Commands\ProcessOrderHandler(
$app->make(\App\Services\OrderService::class)
);
});
$container = $this->createMock(\Psr\Container\ContainerInterface::class);
$container->method('get')
->with(\App\Commands\TestHandler::class)
->willReturn(new \App\Commands\TestHandler());
$locator = new ContainerLocator($container);
TacticianTestCase (if available) for boilerplate reduction.Circular Dependencies
HandlerA depends on HandlerB, which depends on HandlerA) will cause ContainerException.Handler Not Found
HandlerNotFoundException.Middleware Resolution Order
config/tactician.php under 'middleware' to take effect.Container Interface Mismatch
PSR-11 (Psr\Container\ContainerInterface). Laravel’s container implements Illuminate\Contracts\Container\Container, which is compatible, but third-party containers may not be.league/container).Singleton vs. Non-Singleton Handlers
$this->app->bind(
\App\Commands\NonSharedHandler::class
)->instantiate();
Check Bindings
$this->app->bound(\App\Commands\ProcessOrderHandler::class); // true/false
$this->app->getBindings();
Enable Debugging
$debugContainer = new class($this->app) implements \Psr\Container\ContainerInterface {
private $container;
public function __construct($container) { $this->container = $container; }
public function get($id) {
try {
return $this->container->get($id);
} catch (\Exception $e) {
throw new \RuntimeException("Failed to resolve {$id}: " . $e->getMessage());
}
}
// ... other methods
};
Log Handler Resolution
ContainerLocator to log resolutions:
$locator = new class($this->app) extends ContainerLocator {
public function getHandler($command) {
\Log::debug("Resolving handler for " . get_class($command));
return parent::getHandler($command);
}
};
Custom Locator
ContainerLocator to add logic (e.g., fallback to a static map):
class CustomLocator extends ContainerLocator {
public function getHandler($command) {
if (!$this->container->has($handlerClass = static::getHandlerClass($command))) {
return $this->fallbackLocator->getHandler($command);
}
return $this->container->get($handlerClass);
}
}
Dynamic Handler Naming
getHandlerClass() to use a custom naming convention:
class CustomLocator extends ContainerLocator {
protected static function getHandlerClass($command) {
return 'App\\Handlers\\' . class_basename($command) . 'Handler';
}
}
Middleware Binding
$this->app->when(\League\Tactician\CommandBus::class)
->needs(\App\Middleware\AuthMiddleware::class)
->give(function () {
return new \App\Middleware\AuthMiddleware($this->app->make(\App\Services\AuthService::class));
});
Integration with Laravel Events
tactician.command.before/after events to modify behavior:
\Event::listen('tactician.command.before', function ($command, $handler) {
// Pre-process command or handler
});
Avoid Over-Binding
Use instantiate() for Non-Shared Handlers
$this->app->bind(\App\Commands\TempHandler::class)->instantiate();
Cache Resolutions
$handlerCache = new \Symfony\Component\Cache\Simple\FilesystemCache();
$locator = new class($this->app, $handlerCache) extends ContainerLocator {
public function getHandler($command) {
$key = spl_object_hash($command);
if ($this->cache->has($key)) {
return $this->cache->get($key);
}
$handler = parent::getHandler($command);
$this->cache->set($key, $handler, 3600); // Cache for 1 hour
How can I help you explore Laravel packages today?