*/ private array $bindings = []; /** @var array */ private array $instances = []; /** @var array */ private array $aliases = []; /** * The shared instance of the called class: `Application::getInstance()` is an * Application, so consumers can type properties as such. * * @return static */ public static function getInstance(): Container { if (static::$instance === null) { static::$instance = new static(); } return static::$instance; } public static function setInstance(?Container $container): void { static::$instance = $container; } /** * Register a binding. $concrete may be a closure, a class name, or null * (in which case $abstract is treated as the concrete class name). */ public function bind(string $abstract, $concrete = null, bool $shared = false): void { $this->bindings[$abstract] = [ 'concrete' => $concrete ?? $abstract, 'shared' => $shared, ]; // A re-bind invalidates any resolved singleton. unset($this->instances[$abstract]); } public function singleton(string $abstract, $concrete = null): void { $this->bind($abstract, $concrete, true); } /** * Register an already-constructed object as a shared instance. */ public function instance(string $abstract, $instance) { $this->instances[$abstract] = $instance; return $instance; } public function alias(string $alias, string $abstract): void { $this->aliases[$alias] = $abstract; } public function bound(string $abstract): bool { $abstract = $this->aliases[$abstract] ?? $abstract; return isset($this->bindings[$abstract]) || isset($this->instances[$abstract]); } /** * Resolve an entry from the container. * * @param array $parameters Extra constructor args by name. */ public function make(string $abstract, array $parameters = []) { $abstract = $this->aliases[$abstract] ?? $abstract; if (isset($this->instances[$abstract])) { return $this->instances[$abstract]; } $binding = $this->bindings[$abstract] ?? null; $concrete = $binding['concrete'] ?? $abstract; if (is_callable($concrete)) { $object = $concrete($this, $parameters); } elseif (is_string($concrete)) { $object = $this->build($concrete, $parameters); } else { $object = $concrete; } if ($binding !== null && $binding['shared']) { $this->instances[$abstract] = $object; } return $object; } /** * Instantiate a concrete class, auto-wiring type-hinted constructor deps. * * @param array $parameters */ public function build(string $concrete, array $parameters = []) { if (!class_exists($concrete)) { throw new \RuntimeException('Target class [' . esc_html($concrete) . '] does not exist.'); } $reflector = new \ReflectionClass($concrete); if (!$reflector->isInstantiable()) { throw new \RuntimeException('Target [' . esc_html($concrete) . '] is not instantiable.'); } $constructor = $reflector->getConstructor(); if ($constructor === null) { return new $concrete(); } $dependencies = []; foreach ($constructor->getParameters() as $param) { $name = $param->getName(); if (array_key_exists($name, $parameters)) { $dependencies[] = $parameters[$name]; continue; } // Only single named class types are auto-wired. Union/intersection // types (PHP 8) have no getName() and fall through to defaults. $type = $param->getType(); if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { $dependencies[] = $this->make($type->getName()); continue; } if ($param->isDefaultValueAvailable()) { $dependencies[] = $param->getDefaultValue(); continue; } if ($param->allowsNull()) { $dependencies[] = null; continue; } throw new \RuntimeException( 'Unresolvable dependency [$' . esc_html($name) . '] in ' . esc_html($concrete) . '.' ); } return $reflector->newInstanceArgs($dependencies); } /** * Call a callable, injecting container-resolved and supplied parameters. * * @param callable|array $callback * @param array $parameters */ public function call($callback, array $parameters = []) { if (is_array($callback)) { $reflector = new \ReflectionMethod($callback[0], $callback[1]); } elseif (is_string($callback) && strpos($callback, '::') !== false) { $reflector = new \ReflectionMethod($callback); } else { $reflector = new \ReflectionFunction($callback); } $dependencies = []; foreach ($reflector->getParameters() as $param) { $name = $param->getName(); if (array_key_exists($name, $parameters)) { $dependencies[] = $parameters[$name]; continue; } // Only single named class types are auto-wired. Union/intersection // types (PHP 8) have no getName() and fall through to defaults. $type = $param->getType(); if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { $dependencies[] = $this->make($type->getName()); continue; } if ($param->isDefaultValueAvailable()) { $dependencies[] = $param->getDefaultValue(); continue; } $dependencies[] = null; } return call_user_func_array($callback, $dependencies); } public function forget(string $abstract): void { $abstract = $this->aliases[$abstract] ?? $abstract; unset($this->bindings[$abstract], $this->instances[$abstract]); } public function flush(): void { $this->bindings = []; $this->instances = []; $this->aliases = []; } }