flightphp/container
A lightweight PSR-11 dependency injection container built for the Flight PHP micro-framework, offering simple service binding, lazy resolution, and constructor injection without the overhead of heavier container libraries.
This is flightphp/container — the PSR-11 container that ships with the Flight micro-framework, not an Illuminate component. Treat it as a small, standalone DI container you can drop into CLI tools, queue workers, package internals, or legacy code that doesn't need Laravel's full container. v1.2.0 is purely additive (singletons + fluent registration), so it's safe to pull in alongside a Laravel app.
composer require flightphp/container
Where to look first, in order:
src/Container.php — the whole API is set(), get(), has(), and now singleton(). It's a short read; skim it before trusting the README.tests/ — the new singleton tests confirm identity semantics (===) and how objects are keyed.v1.1.0...v1.2.0) — the only behavioural change is that set()/singleton() now return $this instead of void.First use case — one shared connection instead of six:
use flight\Container;
$container = new Container;
$container->singleton(PDO::class, fn() => new PDO('sqlite::memory:'));
$container->get(PDO::class) === $container->get(PDO::class); // true
If you're in a Laravel app and only need "one PDO / one HTTP client / one logger across a package", this is lighter than reaching for a full service provider + singleton() on the app container.
Class-string singletons (autowired). Passing only a class name tells the container to resolve it once and cache it. This works cleanly for zero-arg or fully-resolvable constructors like DateTimeImmutable:
$container->singleton(DateTimeImmutable::class);
$container->get(DateTimeImmutable::class); // same instance every time
Object singletons for pre-built config. Register an already-configured instance; the container keys it by its class.
$logger = new Logger('app');
$container->singleton($logger);
$container->get(Logger::class) === $logger; // true
This is the pattern to reach for when construction needs runtime values (paths, DSNs, API keys) you don't want to re-derive inside a factory.
Fluent bootstrap. Since registration methods now return $this, the whole container can be built in one expression — handy in a bootstrap.php, a service provider's register(), or a package's setUp():
$container
->set(Cache::class, fn() => new ArrayCache())
->singleton(PDO::class, fn() => new PDO('sqlite::memory:'))
->singleton(HttpClient::class, fn() => new Client(['timeout' => 5]));
Keep it as one chain per layer (infrastructure → services) rather than one giant chain, so diffs stay readable.
Lazy shared factories. singleton($id, $closure) is the workhorse: the closure is deferred until first get(), then the result is memoised. Use this for connections you don't want opened on every request/command.
Bridging into Laravel. Two viable approaches:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(\flight\Container::class, function () {
return (new \flight\Container)
->singleton(\PDO::class, fn() => DB::connection()->getPdo());
});
$this->app->bind(\Psr\Container\ContainerInterface::class, \flight\Container::class);
}
Or use it only inside a package you're writing, and expose the resolved services to Laravel via normal bindings. Don't hand flight\Container around as if it were app().
Test swapping. Because every registration is a closure slot, tests can re-register a fake before exercising the subject:
$container->set(Mailer::class, fn() => new FakeMailer);
singleton(Foo::class) is not magic. It still has to construct Foo once. If the constructor needs arguments, you get a resolution failure — supply a closure instead: singleton(Foo::class, fn() => new Foo($bar)). The DateTimeImmutable example works precisely because it's zero-arg.get_class(), not by interfaces or arbitrary ids. singleton($pdo) only satisfies get(PDO::class). If callers ask for ConnectionInterface, register explicitly with set(ConnectionInterface::class, fn() => $pdo).set() and singleton() on the same id silently overwrites; there's no "already registered" guard. Audit chains when a service mysteriously behaves statelessly.get() before singleton() is a silent trap. If something resolved the id earlier via set(), your later singleton() doesn't retroactively make that instance shared. Register singletons during bootstrap, not lazily mid-request.singleton($object) capture state at registration time. If that object reads config in its constructor, config changes after registration won't be picked up — register late in the boot sequence.set() with a non-closure-shaped factory returning a fresh object each call gives you a new instance per get(). That's the correct behaviour, but it's the usual cause of "why isn't my container caching?" — switch to singleton().tagged(), no method injection into controllers, no app() global. Don't expect resolve()-style autowiring of Laravel contracts; anything you get() must have been set() or be trivially constructible.get() on an unregistered id throws the PSR-11 not-found exception; use has() for feature detection rather than try/catch.set(Foo::class, function () use ($c) { logger('building Foo'); return new Foo; }). If you see two lines, something is bypassing the singleton registration.set(Foo::class, fn($c) => new DecoratedFoo($c->get(InnerFoo::class))) — so keep ids stable and avoid circular decoration.set()/singleton() from void to self. Non-breaking for callers, but if you extend Container and override those methods with void, update your signatures.How can I help you explore Laravel packages today?