Dependency Injection Containers (DIC) manage the instantiation and lifecycle of objects in an application, allowing developers to define how dependencies should be resolved. By leveraging a DIC, you can easily swap implementations, making your code more flexible and easier to maintain.

Understanding Dependency Injection

Before we dive into the implementation, let’s clarify the concept of Dependency Injection. Essentially, DI allows an object to receive its dependencies from an external source rather than creating them itself. This is typically achieved using constructor injection, setter injection, or interface injection.

Types of Dependency Injection

TypeDescription
Constructor InjectionDependencies are provided through the class constructor.
Setter InjectionDependencies are set through setter methods after instantiation.
Interface InjectionDependencies are provided through an interface that the class implements.

Building the Dependency Injection Container

Step 1: Create the Container Class

We will start by creating a Container class that will hold our services and manage their instantiation.

<?php

class Container
{
    private $services = [];

    public function set($name, $service)
    {
        $this->services[$name] = $service;
    }

    public function get($name)
    {
        if (!isset($this->services[$name])) {
            throw new Exception("Service not found: " . $name);
        }

        return $this->services[$name];
    }
}

Step 2: Registering Services

Next, we need a way to register services with our container. Services can be defined as closures that return an instance of the required class.

<?php

class Container
{
    private $services = [];

    public function set($name, callable $service)
    {
        $this->services[$name] = $service;
    }

    public function get($name)
    {
        if (!isset($this->services[$name])) {
            throw new Exception("Service not found: " . $name);
        }

        return $this->services[$name]($this);
    }
}

Step 3: Example Usage

Let’s create a simple service and see how we can use our container to manage its dependencies.

<?php

class Database
{
    public function connect()
    {
        return "Connected to the database.";
    }
}

class UserService
{
    private $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function getUser()
    {
        return $this->db->connect() . " Fetching user data.";
    }
}

$container = new Container();

// Register services
$container->set('database', function () {
    return new Database();
});

$container->set('user_service', function ($c) {
    return new UserService($c->get('database'));
});

// Usage
$userService = $container->get('user_service');
echo $userService->getUser();

Step 4: Adding Singleton Support

To enhance our container, we can implement singleton support, ensuring that only one instance of a service is created.

<?php

class Container
{
    private $services = [];
    private $instances = [];

    public function set($name, callable $service, $singleton = false)
    {
        $this->services[$name] = $service;
        if ($singleton) {
            $this->instances[$name] = null;
        }
    }

    public function get($name)
    {
        if (!isset($this->services[$name])) {
            throw new Exception("Service not found: " . $name);
        }

        if (isset($this->instances[$name])) {
            return $this->instances[$name];
        }

        $this->instances[$name] = $this->services[$name]($this);
        return $this->instances[$name];
    }
}

Step 5: Registering Singleton Services

Now, we can register services as singletons by passing true as the third argument.

$container->set('database', function () {
    return new Database();
}, true);

Conclusion

Building a custom Dependency Injection Container in PHP allows for better organization and management of your application's dependencies. By following the principles of DI, you can create more maintainable and testable code. This tutorial covered the basics of creating a DIC, registering services, and implementing singleton support.

Learn more with useful resources: