make() — resolving from the bindings map
Concept
make() is the primary resolution method — the method you call when you need an object out of the container. While get() is the PSR-11 interface for external packages, make() is the framework's own API, and it has one additional capability that get() lacks: you can pass runtime parameters that supplement or override what the container resolves via reflection.
The resolution algorithm in make() follows a strict lookup priority: (1) check $instances for pre-built singletons, (2) check $bindings for a registered factory, (3) attempt auto-wiring if the abstract is a concrete class. This three-step cascade means you can call make(UserRepository::class) without ever calling bind() — the container will attempt to build it by reflection. This is the "convention over configuration" principle applied to dependency injection.
The runtime parameters feature (make(Foo::class, ['bar' => $value])) is essential for two scenarios: first, when a class has a primitive constructor argument (a string, int, or URL) that cannot be auto-wired by type; second, when you want to explicitly override a type-wired dependency for testing. The parameters array is merged with the reflection-resolved arguments.
Aliases are another feature of make(): the container can map short names to full class names. make('config') resolves to make(Repository::class). Laravel registers all its core services under short aliases: 'db', 'router', 'cache', 'events', etc. We will implement a simple alias() method and resolve aliases in make() before the normal lookup.
The buildStack property (a stack of abstract names currently being built) is crucial for detecting circular dependencies. If A depends on B which depends on A, the container will recurse infinitely without this guard. Every entry to build() pushes onto the stack; exit pops. If the same abstract appears twice in the stack, throw a CircularDependencyException.
Code Example
<?php
declare(strict_types=1);
namespace Lumen\Container;
/**
* Full make() implementation with alias support and circular dependency detection.
*
* Laravel equivalent: Illuminate\Container\Container::make() and resolve()
*/
class Container implements \Psr\Container\ContainerInterface
{
protected array $bindings = [];
protected array $instances = [];
protected array $aliases = [];
/** Stack of abstract names currently being built — for circular dep detection. */
protected array $buildStack = [];
// ------------------------------------------------------------------
// Registration
// ------------------------------------------------------------------
public function bind(
string $abstract,
\Closure|string|null $concrete = null,
bool $shared = false
): void {
if ($concrete === null) {
$concrete = $abstract;
}
if (!$concrete instanceof \Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
unset($this->instances[$abstract]);
}
public function singleton(string $abstract, \Closure|string|null $concrete = null): void
{
$this->bind($abstract, $concrete, shared: true);
}
public function instance(string $abstract, mixed $instance): mixed
{
unset($this->aliases[$abstract]);
return $this->instances[$abstract] = $instance;
}
/**
* Register an alias so make('router') resolves make(RouterInterface::class).
*/
public function alias(string $abstract, string $alias): void
{
if ($abstract === $alias) {
throw new \InvalidArgumentException(
"[{$abstract}] cannot be aliased to itself."
);
}
$this->aliases[$alias] = $abstract;
}
// ------------------------------------------------------------------
// Resolution
// ------------------------------------------------------------------
/**
* Resolve an abstract from the container.
*
* @param string $abstract Class name, interface name, or alias.
* @param array $parameters Extra constructor args (override auto-wiring).
*/
public function make(string $abstract, array $parameters = []): mixed
{
// Resolve alias to canonical abstract
$abstract = $this->getAlias($abstract);
// 1. Pre-built instance (singletons already resolved, or instance() calls)
if (isset($this->instances[$abstract]) && empty($parameters)) {
return $this->instances[$abstract];
}
// 2. Factory binding
if (isset($this->bindings[$abstract])) {
$binding = $this->bindings[$abstract];
$concrete = $binding['concrete'];
$shared = $binding['shared'];
$instance = $concrete($this, $parameters);
if ($shared && empty($parameters)) {
$this->instances[$abstract] = $instance;
}
return $instance;
}
// 3. Auto-wiring: if abstract is a concrete class, build it via Reflection.
// Full Reflection logic is in fw-02-005; minimal version here.
if (class_exists($abstract)) {
return $this->build($abstract, $parameters);
}
throw new NotFoundException(
"Target [{$abstract}] is not instantiable and has no binding."
);
}
/**
* Alias for make(). Mirrors Laravel's resolve() helper behaviour.
*/
public function resolve(string $abstract, array $parameters = []): mixed
{
return $this->make($abstract, $parameters);
}
// ------------------------------------------------------------------
// PSR-11
// ------------------------------------------------------------------
public function get(string $id): mixed
{
if (!$this->has($id)) {
throw new NotFoundException("No binding for [{$id}].");
}
return $this->make($id);
}
public function has(string $id): bool
{
$id = $this->getAlias($id);
return isset($this->bindings[$id])
|| isset($this->instances[$id])
|| class_exists($id);
}
// ------------------------------------------------------------------
// Private helpers
// ------------------------------------------------------------------
private function getAlias(string $abstract): string
{
// Recursively resolve alias chains: 'db' → 'database' → DatabaseManager::class
return isset($this->aliases[$abstract])
? $this->getAlias($this->aliases[$abstract])
: $abstract;
}
private function getClosure(string $abstract, string $concrete): \Closure
{
return function (Container $container, array $parameters) use ($abstract, $concrete): mixed {
if ($abstract === $concrete) {
return $container->build($concrete, $parameters);
}
return $container->make($concrete, $parameters);
};
}
protected function build(string $concrete, array $parameters = []): mixed
{
// Circular dependency guard
if (in_array($concrete, $this->buildStack, true)) {
$chain = implode(' → ', [...$this->buildStack, $concrete]);
throw new ContainerException("Circular dependency detected: {$chain}");
}
$this->buildStack[] = $concrete;
try {
// Full auto-wiring via Reflection in fw-02-005.
// For now: just instantiate with provided parameters.
return new $concrete(...array_values($parameters));
} finally {
array_pop($this->buildStack);
}
}
}Interview Q&A
Q: Why does make() skip the singleton cache when $parameters is non-empty?
When you call make(UserRepository::class, ['tenantId' => 5]), you want a fresh instance configured for tenant 5, not the cached singleton built for a different tenant. The runtime parameters override the shared nature of the binding. If we returned the cached singleton, the caller would get an instance with a different tenantId than requested. Laravel has the same behaviour: if you pass parameters to make(), it bypasses the instance cache and always builds fresh. This is why instance() (direct pre-built objects) also uses unset($this->aliases[$abstract]) — registering an instance removes any dangling alias that might interfere with the lookup.
Q: What is the purpose of the buildStack array in circular dependency detection?
When A is being built, we push 'A' onto the stack. Building A requires building B. We push 'B'. Building B requires building A — we check the stack, find 'A' already in it, and throw immediately instead of recursing infinitely until stack overflow. The finally block guarantees the stack is always popped even if construction throws — so a failed build of B does not leave 'B' permanently in the stack, poisoning future builds. Laravel uses the same approach in Illuminate\Container\Container with $this->buildStack.
Q: Why does has() return true for any existing class name, even without a binding?
Because make() will successfully auto-wire any concrete class via Reflection (fw-02-005), has() must return true for them too. The PSR-11 contract says: if has($id) is true, then get($id) must not throw NotFoundException. Since make(SomeConcreteClass::class) will succeed without a registered binding, has() must acknowledge this. This is the correct and honest answer — the container can indeed produce an instance. If we only returned true for registered bindings, callers would defensively check has() before make() and get incorrect false results for perfectly resolvable classes.