slince/di
A lightweight, PSR-11 compatible dependency injection container for PHP. Slince DI supports constructor and setter injection, service providers, aliases, lazy loading, and shared instances, giving you a simple, flexible foundation for managing object dependencies in small to medium applications.
composer require slince/di
slince/di is a small PSR-11 container (Slince\Di\Container implements Psr\Container\ContainerInterface). The fastest way to get value is to stop hand-wiring object graphs in your bootstrap/front-controller and let the container build them.
The three files worth reading first:
src/Container.php — set(), get(), has(), delegation, parameter handling.src/Definition.php — the object that describes how to build a service (class, arguments, method calls, shared flag).src/Reference.php + src/Exception/ — how one service points at another, and the PSR-11 exception types.Minimal first use case:
use Slince\Di\Container;
$container = new Container();
// Register by class name; constructor dependencies are autowired via reflection.
$container->set(UserRepository::class);
$repo = $container->get(UserRepository::class);
That is the whole pitch: for simple classes with type-hinted constructor parameters, set(Foo::class) is often optional — get() will auto-resolve a concrete, non-abstract class if your version has autowiring enabled by default. Start by replacing one new Foo(new Bar()) chain in a bootstrap file, run your test suite, and expand outward. Do not attempt to convert the whole application on day one; a container adopted late is a composition-root tool, not a rewrite.
Four registration styles, one for each situation.
// 1. Class name — autowired
$container->set(Mailer::class);
// 2. Closure — when construction needs logic or reads config
$container->set(PDO::class, function (Container $c) {
return new PDO($c->getParameter('db.dsn'));
});
// 3. Definition object — explicit arguments, setters, sharing
use Slince\Di\Definition;
use Slince\Di\Reference;
$definition = (new Definition(UserRepository::class))
->setArguments([new Reference(PDO::class)])
->addMethodCall('setLogger', [new Reference(Logger::class)])
->setShared(true);
$container->set(UserRepository::class, $definition);
// 4. A ready-made object — useful for mocks, config objects, PDO handles
$container->set('clock', new SystemClock());
Interface → implementation aliasing. Register the concrete class, then point the interface at it with a Reference. Consumers type-hint the interface; only the bootstrap knows the concrete name. This is the single highest-leverage pattern in the package — it is what makes the container worth having in a legacy codebase.
Shared vs. prototype. Services are new instances per get() unless marked shared. Repositories, loggers, and connection objects should be shared; value objects and request-scoped objects should not. Pick this deliberately per service rather than defaulting everything to shared, because a shared service that caches state between requests will bite you in long-running workers.
Group definitions into providers. Rather than one 300-line bootstrap, write small functions or classes that accept the container and register one concern (registerDatabase($c), registerHttp($c)). This keeps the wiring readable and lets tests compose only the providers they need.
Parameters as a config layer. Use setParameter() / getParameter() for DSNs, feature flags, and paths, and reference them from Definition arguments instead of hardcoding. Keep the raw %placeholder% strings out of closure bodies — inside a closure, read getParameter() directly so the failure mode is a clear exception rather than a literal string being injected into a service.
Testing pattern. In setUp(), build a fresh container, register the real graph, then overwrite the leaves with fakes: $container->set(Mailer::class, $fakeMailer). Because get() resolves lazily, one override replaces the dependency everywhere without touching constructors. This is the day-to-day payoff — assert against the container-built graph, not against manually assembled objects.
Integration point. Instantiate the container once in the front controller (or in a framework service provider), pass it to the router/console kernel, and resolve controllers/handlers through it. If the framework already has its own container, use slince/di as the delegation target for the parts of the app you control and bridge only at the boundary — running two containers side by side with interleaved get() calls is a debugging tax.
Definition with arguments, or it will fail at get() time — usually deep in a request, not at boot. Smoke-test every top-level service in CI (foreach ($ids as $id) { $container->get($id); }).addMethodCall) or by injecting a factory closure instead of the service itself.has() is cheap, get() is not. Use has() for optional integrations and get() only when you are sure the service exists. PSR-11 requires has() not to throw, and get() on a missing ID to throw NotFoundExceptionInterface — catch the interfaces, not the concrete exception classes, so container swaps in tests keep working.Foo::class and 'Foo' and 'foo' are different IDs unless your version normalizes them. Pick FQCNs as the canonical ID and alias anything else; mixed casing produces "class not found" errors that look like autoloading bugs.set('x', new Foo()) returns the same instance forever and is not autowired — don't use it as a shortcut for a service that should be prototype-scoped.set() signature varies by version. In some releases the third argument is a shared flag; in others sharing lives only on Definition. Check Container::set() in your installed tag before relying on it, and pin the version in composer.json — this package moves and the README lags.Definitions or build the graph once at boot and cache the container construction. Profile before optimizing, but know the cost exists.get() in your bootstrap with a log line of the requested ID, or subclass Container and override the instance-creation step to trace resolution order. Subclassing is also the supported extension point for custom instantiation (e.g., proxying, decorators).Container into domain classes and calling get() inside business logic trades constructor injection for invisible dependencies. Keep get() calls in bootstrap, controllers, and console commands only.How can I help you explore Laravel packages today?