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.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
Mastering PHP IoC: Build a Singleton Container from Scratch

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 call

Binding 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 call

Dependency 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

IoCcontainerPHPDependency InjectionSingletonPSR-11
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.