The container internals — Container.php bindings array structure
Concept
Illuminate\Container\Container is the engine that powers every resolution in a Laravel application. Understanding its internal data structures is essential for debugging resolution failures, writing package service providers, and building your own IoC container. The class lives at vendor/laravel/framework/src/Illuminate/Container/Container.php and is roughly 1,000 lines of carefully layered PHP.
The container's state is held in a set of protected arrays. The most important are:
$bindings — the primary registry. It is a string → array map where each entry has the shape ['concrete' => Closure|string, 'shared' => bool]. bind() populates this with 'shared' => false; singleton() uses 'shared' => true. The concrete value is either a Closure factory or a class name string. When make() resolves a binding, it calls $this->getConcrete($abstract), which looks up $this->bindings[$abstract]['concrete'] or falls back to $abstract itself (auto-resolution).
$instances — the singleton cache. Once a shared binding is resolved for the first time (or when instance() is called directly), the result is stored here as string → object. Every subsequent make() call returns the cached value directly without calling build() again. This is the most frequently read array in the container during a request.
$aliases — maps string aliases to their canonical abstract names. alias('cache', \Illuminate\Contracts\Cache\Repository::class) means that resolving 'cache' returns the same object as resolving the contract. Aliases are resolved via getAlias() which walks the chain recursively.
$abstractAliases — the reverse: canonicalName → alias[]. Used to flush all aliases when a binding is rebound.
$resolved — a string → bool set tracking which abstracts have been resolved at least once. Used by resolved() and by rebound() to know whether a rebind should fire the rebound callbacks immediately.
$resolvingCallbacks / $afterResolvingCallbacks — string → Closure[] maps for container event hooks.
$contextual — a nested array for contextual bindings: $contextual[WhenClass][NeedsAbstract] = giveConcrete.
The build(Closure|string $concrete) method is where auto-resolution happens. If $concrete is a Closure, it calls it directly. If it is a class string, it calls new ReflectionClass($concrete), checks that the class is instantiable (throws if abstract or interface), then calls resolveDependencies($constructor->getParameters()). Each parameter is resolved by resolveClass() → make() recursively, or by resolveNonClass() for primitives (which checks contextual bindings and default values).
| Array | Purpose | Populated by |
|---|---|---|
$bindings | All bind()/singleton() registrations | bind(), singleton(), scoped() |
$instances | Resolved singletons + manual instances | first resolution of shared binding, instance() |
$aliases | Alias → canonical name | alias() |
$resolved | Tracks first-time resolution | make() after build |
$contextual | Contextual binding rules | when()->needs()->give() |
Code Example
<?php
declare(strict_types=1);
// --- Exploring the container's internal arrays ---
use Illuminate\Container\Container;
$container = new Container();
// 1. Inspect $bindings after registering types
$container->bind(\App\Contracts\Mailer::class, \App\Services\SmtpMailer::class);
$container->singleton(\App\Contracts\Cache::class, fn(Container $c) => new \App\Services\RedisCache(
$c->make('config')['cache.redis']
));
// $bindings structure:
// [
// 'App\Contracts\Mailer' => ['concrete' => 'App\Services\SmtpMailer', 'shared' => false],
// 'App\Contracts\Cache' => ['concrete' => Closure, 'shared' => true],
// ]
$bindings = $container->getBindings(); // returns protected $bindings
// 2. After first resolution, $instances is populated for shared bindings
$cache = $container->make(\App\Contracts\Cache::class);
// Now: $instances['App\Contracts\Cache'] = $cache object
// 3. Non-shared: a new instance is built every time
$mailer1 = $container->make(\App\Contracts\Mailer::class);
$mailer2 = $container->make(\App\Contracts\Mailer::class);
assert($mailer1 !== $mailer2); // different instances
$cache2 = $container->make(\App\Contracts\Cache::class);
assert($cache === $cache2); // same singleton
// 4. Aliases — 'cache' string resolves to the Cache contract
$container->alias(\App\Contracts\Cache::class, 'cache');
$same = $container->make('cache'); // walks getAlias() chain
// 5. Observing the resolved array
assert($container->resolved(\App\Contracts\Cache::class) === true);
assert($container->resolved(\App\Contracts\Mailer::class) === true);
// 6. Manual instance() call — bypasses build() entirely, stores in $instances
$fakeMailer = new \App\Services\NullMailer();
$container->instance(\App\Contracts\Mailer::class, $fakeMailer);
assert($container->make(\App\Contracts\Mailer::class) === $fakeMailer);
// 7. Rebound callbacks — fire when a binding that was already resolved is overwritten
$container->rebinding(\App\Contracts\Cache::class, function (Container $c, $newInstance): void {
// Update any class that already holds a reference to the old cache
$c->make(\App\Services\SessionManager::class)->setCache($newInstance);
});Interview Q&A
Q: Walk through what happens inside Container::make() from the moment you call app(SomeService::class) to the moment you receive the object.
make() first calls getAlias($abstract) to resolve any registered alias. It then checks $this->instances[$abstract] — if a cached singleton exists, it is returned immediately. Otherwise getConcrete($abstract) retrieves the concrete from $this->bindings[$abstract]['concrete'], defaulting to $abstract itself for auto-resolution. build($concrete) is then called: if $concrete is a Closure, it is invoked with the container as argument. If it is a class string, ReflectionClass is used to inspect the constructor; each constructor parameter is resolved recursively via make(). After build() returns, fireResolvingCallbacks() runs registered resolving hooks, and if the binding is shared, the result is stored in $this->instances[$abstract]. Finally fireAfterResolvingCallbacks() runs, and the instance is returned.
Q: What is the difference between the $bindings array and the $instances array, and why does that distinction matter for singletons?
$bindings holds the recipe — a factory closure or class name, plus a shared flag. $instances holds the result of running that recipe for shared bindings. The distinction is that $bindings is consulted by getConcrete() to know how to build something, while $instances is consulted first by make() to short-circuit building entirely. A singleton binding appears in both: $bindings holds the factory so the container knows how to build it the first time, and $instances caches the result for all subsequent calls. Calling instance($abstract, $object) writes directly to $instances without touching $bindings at all — that is how you inject pre-built objects (like in tests) without registering a factory.
Q: What does the $resolved array track, and how does it relate to the rebinding() method?
$resolved is a boolean map recording every abstract that has been resolved through make() at least once. Its primary role is to support rebinding(): when you call bind() or singleton() for an abstract that $resolved[$abstract] is true (meaning some code already has a reference to the old instance), the container knows it should fire any registered rebound callbacks immediately. This lets service providers tell dependent services "the implementation changed, update your cached reference." Without $resolved, the container would not know whether overwriting a binding affects live instances — it would just update $bindings silently. The resolved() public method exposes this check for testing and debugging purposes.