Mastering PHP IoC: Build a Singleton Container from Scratch
This tutorial explains how to create a lightweight Inversion of Control (IoC) container in PHP, covering the singleton pattern, binding services, PSR‑11 compliance, retrieving and injecting dependencies, and implementing class and method resolvers with practical code examples.
An Inversion of Control (IoC) container centralises the creation and injection of class dependencies in PHP projects. The implementation below follows the PSR‑11 container interface and uses reflection to resolve constructor and method parameters automatically.
Singleton Container Pattern
The container is a singleton so that a single instance holds all bindings.
class Container {
/**
* Get the singleton instance of Container
*/
public static function instance(): ?Container {
static $instance = null;
if ($instance === null) {
$instance = new static();
}
return $instance;
}
}
$container = Container::instance(); // same instance on every callBinding Services
The container stores a bindings array that can map an identifier to either a class name (namespace mapping) or an already‑created object (singleton mapping).
class Container {
protected array $bindings = [];
/** Bind an identifier to a class name */
public function bind(string $id, string $namespace): Container {
$this->bindings[$id] = $namespace;
return $this;
}
/** Bind an identifier to a concrete instance (singleton) */
public function singleton(string $id, object $instance): Container {
$this->bindings[$id] = $instance;
return $this;
}
}PSR‑11 Compatibility
Implementing Psr\Container\ContainerInterface requires get() and has(). This makes the container interchangeable with other PSR‑11 compliant libraries.
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface {
public function get($id) {
if ($this->has($id)) {
return $this->bindings[$id];
}
throw new Exception("Container entry not found for: {$id}");
}
public function has($id): bool {
return array_key_exists($id, $this->bindings);
}
}Resolving Services
Bindings can be retrieved, overridden, or registered as singletons at runtime.
$container->bind(ConfigInterface::class, PHPConfig::class);
$container->get(ConfigInterface::class); // PHPConfig::class
$container->bind(ConfigInterface::class, YAMLConfig::class);
$container->get(ConfigInterface::class); // YAMLConfig::class
$container->singleton(PHPConfig::class, new PHPConfig());
$container->get(PHPConfig::class); // same PHPConfig instance each callDependency Injection
When the container resolves a class or calls a method, it automatically injects the required dependencies.
class App {
public ConfigInterface $config;
public ConfigInterface $methodConfig;
public function __construct(ConfigInterface $config) {
$this->config = $config;
}
public function handle(ConfigInterface $config) {
$this->methodConfig = $config;
}
}
$container->bind(ConfigInterface::class, PHPConfig::class);
$instance = $container->resolve(App::class); // injects PHPConfig into __construct
$container->resolveMethod($instance, 'handle'); // injects PHPConfig into handle()Core Resolver Classes
ClassResolver
Uses ReflectionClass to inspect a class, resolve its constructor parameters via ParametersResolver, and instantiate the object.
class ClassResolver {
protected ContainerInterface $container;
protected string $namespace;
protected array $args = [];
public function __construct(ContainerInterface $container, string $namespace, array $args = []) {
$this->container = $container;
$this->namespace = $namespace;
$this->args = $args;
}
public function getInstance(): object {
if ($this->container->has($this->namespace)) {
$binding = $this->container->get($this->namespace);
if (is_object($binding)) {
return $binding; // singleton instance
}
$this->namespace = $binding; // namespace alias
}
$refClass = new ReflectionClass($this->namespace);
$constructor = $refClass->getConstructor();
if ($constructor && $constructor->isPublic()) {
if (count($constructor->getParameters()) > 0) {
$resolver = new ParametersResolver(
$this->container,
$constructor->getParameters(),
$this->args
);
$this->args = $resolver->getArguments();
}
return $refClass->newInstanceArgs($this->args);
}
return $refClass->newInstanceWithoutConstructor();
}
}ParametersResolver
Iterates over reflected parameters. If a name exists in the extra arguments array, that value is used. For class‑type parameters the resolver recursively resolves the class via the container; otherwise it returns the default value.
class ParametersResolver {
protected ContainerInterface $container;
protected array $parameters;
protected array $args = [];
public function __construct(ContainerInterface $container, array $parameters, array $args = []) {
$this->container = $container;
$this->parameters = $parameters;
$this->args = $args;
}
public function getArguments(): array {
return array_map(function (ReflectionParameter $param) {
if (array_key_exists($param->getName(), $this->args)) {
return $this->args[$param->getName()];
}
return $param->getType() && !$param->getType()->isBuiltin()
? $this->getClassInstance($param->getType()->getName())
: $param->getDefaultValue();
}, $this->parameters);
}
protected function getClassInstance(string $namespace): object {
return (new ClassResolver($this->container, $namespace))->getInstance();
}
}MethodResolver
Reflects a method, resolves its parameters with ParametersResolver, and invokes the method with the resolved arguments.
class MethodResolver {
protected ContainerInterface $container;
protected object $instance;
protected string $method;
protected array $args = [];
public function __construct(ContainerInterface $container, object $instance, string $method, array $args = []) {
$this->container = $container;
$this->instance = $instance;
$this->method = $method;
$this->args = $args;
}
public function getValue(): mixed {
$refMethod = new ReflectionMethod($this->instance, $this->method);
$resolver = new ParametersResolver(
$this->container,
$refMethod->getParameters(),
$this->args
);
return $refMethod->invokeArgs($this->instance, $resolver->getArguments());
}
}Container API for Resolution
class Container implements ContainerInterface {
// ... bind / singleton / get / has as shown earlier ...
/** Resolve a class, optionally passing scalar arguments */
public function resolve(string $namespace, array $args = []): object {
return (new ClassResolver($this, $namespace, $args))->getInstance();
}
/** Resolve and invoke a method on an existing object */
public function resolveMethod(object $instance, string $method, array $args = []): mixed {
return (new MethodResolver($this, $instance, $method, $args))->getValue();
}
}Usage Example
Install the package via Composer: composer require tinywan/ioc Below is a minimal script (index.php) that demonstrates binding, overriding, singleton registration, and both constructor and method injection.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use tinywan\ioc\Container;
interface ConfigInterface {}
class PHPConfig implements ConfigInterface {}
class YAMLConfig implements ConfigInterface {}
$container = Container::instance();
// Bind interface to a concrete class
$container->bind(ConfigInterface::class, PHPConfig::class);
if ($container->get(ConfigInterface::class) !== PHPConfig::class) {
throw new Exception('Binding failed');
}
// Override the binding
$container->bind(ConfigInterface::class, YAMLConfig::class);
if ($container->get(ConfigInterface::class) !== YAMLConfig::class) {
throw new Exception('Override failed');
}
// Register a singleton instance
$configSingleton = new PHPConfig();
$container->singleton(PHPConfig::class, $configSingleton);
if ($container->get(PHPConfig::class) !== $configSingleton) {
throw new Exception('Singleton failed');
}
// Class with constructor and method injection
class App1 {
public ConfigInterface $config;
public ConfigInterface $methodConfig;
public function __construct(ConfigInterface $config) { $this->config = $config; }
public function handle(ConfigInterface $config) { $this->methodConfig = $config; }
}
$container->bind(ConfigInterface::class, PHPConfig::class);
$app1 = $container->resolve(App1::class);
if (get_class($app1->config) !== PHPConfig::class) { throw new Exception('Constructor injection failed'); }
$container->resolveMethod($app1, 'handle');
if (get_class($app1->methodConfig) !== PHPConfig::class) { throw new Exception('Method injection failed'); }
// Class that also requires scalar arguments
class App2 {
public ConfigInterface $config;
public string $arg1;
public string $arg2;
public function __construct(ConfigInterface $config, string $arg1, string $arg2) {
$this->config = $config;
$this->arg1 = $arg1;
$this->arg2 = $arg2;
}
}
$app2 = $container->resolve(App2::class, ['arg1' => 'value1', 'arg2' => 'value2']);
if ($app2->arg1 !== 'value1' || $app2->arg2 !== 'value2') {
throw new Exception('Scalar argument injection failed');
}This implementation provides a lightweight, PSR‑11 compliant IoC container that can be integrated into any PHP project requiring dynamic dependency resolution.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
