slick/di
Slick\DI is a lightweight dependency injection container for PHP that wires services through autowiring, explicit bindings, and lazy resolution. It offers a small, PSR-11 friendly API for managing object lifecycles in Laravel, Slim, and standalone applications.
Add the package with Composer and remember that slick/di is a PSR-11 container first — almost everything you do day-to-day routes through Psr\Container\ContainerInterface:
composer require slick/di
As of v2.10.0 the things worth reading first are:
Slick\Di\Container / Slick\Di\ContainerBuilder — the entry point and where you register definitions.Definition / definition-builder classes.#[Autowire] attribute (new in 2.10.0) — used on setters, applied automatically at object creation time.That attribute is the headline change: it makes setter injection a first-class, container-driven feature rather than something you wire manually.
A minimal first use case — register a service and pull it back:
use Slick\Di\ContainerBuilder;
$container = (new ContainerBuilder())->getContainer();
// Concrete classes resolve with no registration when constructors are type-hinted:
$mailer = $container->get(\App\Mail\Mailer::class);
Then wire it into the rest of your app by type-hinting ContainerInterface wherever you need lazy resolution, or — preferably — inject the services themselves and keep the container at the composition root only.
Constructor injection at the edges. Treat the container as something that only bootstraps the application. Controllers, commands, and services should receive their dependencies via constructor arguments; the container (or the framework's resolver) builds them once at the entry point.
Setter injection with #[Autowire] (2.10.0). For optional collaborators or genuinely circular graphs, mark a setter and let the container invoke it during creation:
use Slick\Di\Attribute\Autowire;
class ReportBuilder
{
private ?LoggerInterface $logger = null;
#[Autowire]
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
}
Prefer this over lazy proxies for cycles, and over a service-locator call inside the class. Keep setters idempotent — they run on creation, so anything they do should be safe to redo.
Bind interfaces to implementations. The core reason to use the container is swapping implementations. Register a definition so MailerInterface resolves to a concrete SmtpMailer, and test doubles can be swapped in tests by rebuilding the container with an override.
Scalars resolve by parameter name (2.10.0). A string argument no longer hard-fails: the container looks up a service under the parameter's name. So a constructor argument $apiKey will pick up a definition registered as apiKey. This turns parameter naming into a wiring contract — see the gotchas.
Nullable dependencies degrade to null (2.10.0). A nullable argument not present in the container is set to null rather than throwing. Use this deliberately for optional integrations; don't let it silently swallow required ones.
Singletons vs. factories. Distinguish shared instances from fresh-per-resolve factories. Register stateless services (a logger, a config bag) as shared; register anything holding per-request state (a request context, a job payload) as a factory so you don't leak state between resolutions.
Autowiring for your own classes. Let reflection resolve concrete classes with type-hinted constructors; only write explicit definitions where you need a specific implementation, a scalar config value, or a named instance. This keeps the registration list small and readable.
Definition builders for complex services. When construction is non-trivial (needs a config array, a callable, or a factory method), define the service with the builder API rather than a closure that does everything — it's easier to test and to override.
Integration in a CLI or queue worker. Because resolution is lazy, you can build the container once per process and resolve only the handlers you actually need; this matters in long-running workers where eagerly instantiating everything is wasteful.
Testing hook. Keep a helper that builds a container with your production definitions plus test overrides. Resolving from that container in tests exercises the same wiring your app uses — including #[Autowire] setters and scalar-by-name lookups — catching missing bindings early.
NotFoundExceptionInterface (usually Slick\Di\Exception\...). When you see it, check that the interface — not just the concrete class — was registered, since autowiring cannot guess an interface's implementation.string param is looked up by its name, so renaming $apiKey to $key silently changes (or breaks) what gets injected. Register scalar services under names that match your parameter names exactly, or define those arguments explicitly.null instead of failing. A typo in a binding now yields a null collaborator at runtime rather than an error at resolution. Add your own guard clauses in the constructor/setter if null is not actually acceptable.#[Autowire] setters run at creation time only. If you build the object yourself (new ReportBuilder()) instead of resolving it from the container, the setter is never called and the dependency stays null. Only resolved objects get attribute wiring.get(). A #[Autowire] setter is now the cleanest fix — prefer it over lazy proxies.null or empty arrays). Dump the resolved config early in a new project to confirm defaults rather than assuming.#[Autowire], scalar-by-name, nullable fallback) against a fresh install rather than older docs.How can I help you explore Laravel packages today?