sergix44/container
A lightweight PSR-11 compatible dependency injection container for PHP, with bindings, singletons, aliases, and automatic constructor resolution. It keeps service wiring simple and testable for small apps and libraries, without requiring a full framework.
composer require sergix44/container. It publishes no service provider, config file, or facade — that is intentional, and it is the first thing to internalise.composer.json for the PSR-11 requirement, then src/ for the Container class. The README is short; the class itself, and the PSR-11 get() / has() surface, are the real documentation. Check the tests directory for canonical binding/resolution examples before writing your own.Psr\Container\ContainerInterface (both get(string $id) and has(string $id)), since that is your interoperability contract and your best debugging hook.First use case — bind an interface to an implementation and resolve it:
use Sergix44\Container\Container;
$container = new Container();
// transient: fresh instance each get()
$container->set(LoggerInterface::class, FileLogger::class);
// singleton/shared: same instance every get()
$container->set(Database::class, fn () => new Database(config('db')));
$db = $container->get(Database::class); // shared
$logger = $container->get(LoggerInterface::class); // new each time
Treat this as a micro-container for a framework-agnostic slice of your app (a package, a domain module, a CLI worker), not as a replacement for Laravel's container.
Scope it deliberately. The strongest daily pattern in a Laravel app is to isolate a subsystem — a pricing engine, an importer, a queue worker — behind a PSR-11 container you build yourself. Laravel's own container stays untouched for HTTP/framework wiring; this container lives inside the module and can later be lifted into a standalone package with zero Laravel coupling.
Register in a single bootstrap point. Mirror Laravel's service-provider idea without the framework: one bindings.php or a ModuleContainerFactory that does all set() calls, so you never wonder where a binding came from. Keep it deterministic and free of side effects.
final class PricingContainer
{
public static function make(): Container
{
$c = new Container();
$c->set(TaxRateProvider::class, fn () => new EuTaxRates::fromConfig(config('tax')));
$c->set(PriceCalculator::class, PriceCalculator::class);
return $c;
}
}
Use closure factories for anything with config or env dependencies. Closures are lazy — nothing is constructed until get() fires, so a singleton closure that reads config() won't blow up during boot/console commands that never touch it. This is the cleanest way to inject Laravel config or env() values without importing app() into the module.
Lean on automatic constructor injection for plain services. Let the container resolve type-hinted constructor params for you; only register bindings where an interface needs a concrete choice or a value needs constructing. Fewer bindings means fewer places to drift.
Bridge into Laravel once, not everywhere. If a Laravel controller needs the module's service, resolve it through Laravel:
$this->app->singleton(PriceCalculator::class,
fn () => PricingContainer::make()->get(PriceCalculator::class));
Now Laravel owns the lifecycle and the micro-container stays internal. Avoid passing the PSR-11 container itself around as a service locator.
Interop with PSR-11 consumers. Because it is compliant, you can hand this container to any third-party library that type-hints ContainerInterface (e.g. a router or a middleware stack in a CLI tool). That is where the "framework-agnostic" claim earns its keep.
app() from a class resolved by the micro-container, or vice versa, silently couples them and defeats the purpose. The module should only ever receive values, not the Laravel container.queue:work daemons, a "singleton" in a container you rebuilt per request is fine — but a container cached in a static or a long-lived worker will return stale instances holding stale config. Rebuild the container per job/request, or be certain the shared state is safe.string $dsn) cannot be auto-wired — bind those explicitly with closures. If resolution fails, the exception is your signal, not a mystery.NotFoundExceptionInterface; construction failures must surface as ContainerExceptionInterface. When wrapping third-party code, catch those interfaces, not concrete classes, so swapping containers later stays cheap.set() without an explicit lifecycle.dump() helper or assert $container->has(Id::class) before get() in a test. Since there's no debug toolbar, tests are the observability layer — assert that each binding resolves and returns the expected concrete type.src/ for the exact factory signature before relying on fn (Container $c) => ...; getting this wrong is a common silent TypeError.^ constraint on a pre-1.0 release can pull breaking changes; pin to the 2024-06-18-compatible line you tested against.How can I help you explore Laravel packages today?