Factory as a Service

Dependency Injection Containers are a great invention - when used the right way, they allow us to keep our factories and assembly logic of services outside the core business logic of our application.

By default, a service created is shared, meaning that exactly the same instance will be returned whenever service is retrieved from a container. This is a desired behaviour in most of the cases. For example, application typically use a single database, so database connection service should be instantiated only once for the entire request lifecycle.

config.php

return [
    'db' => [
        'host' => 'localhost',
        'user' => 'root',
        'password' => 'secret',
        'dbname' => 'app',
    ],
];

services.php

return [
    DbConnectionInterface::class => function(ContainerInterface $container) {
        $config = $container->get('Config');
        return new DbConnection($config['db']);
    }
];

Yet certain use cases may require services to be created conditionally during runtime, such as for example based on the value of a parameter resolved from the current request.

Imagine that your application stores data in multiple databases, and the database itself is selected based on certain criteria in your application logic. This implies that database connection service cannot be shared anymore because dbname becomes a dynamic parameter for multiple connections that can exist during the request lifecycle.

This of course is not a difficult problem to solve, but things can go wrong if not treated properly.

Anti-patterns

From my experience, there are several pitfalls I've seen developers fall into in struggle to come up with a solution for such an requirement while still adhering to the usage of a DI container for assembling application services:

  1. setter method - add a method to the service class that allows changing object's configuration on the fly:

    public function __construct(DbConnectionInterface $dbConnection)
    {
       $this->dbConnection = $dbConnection;
       $this->dbConnection->selectDatabase('some_database');
    }
    

    For this to work, database connection implementation had to be hacked to allow switching to a different database, and therefore made the class mutable for no good reason. This probably will not even be possible to achieve if you use some 3rd party libraries that were designed in a way that prevents changing object's state after its initial creation.

    Don't sacrifice immutability of your services.

  2. service location - have a DI container as a dependency and use it to build or locate appropriate service instance. Some DI container libraries, such as Zend Service Manager for example, extend PSR-11 interface with methods that allow creating discrete instances of objects, acting as factory methods:

    public function __construct(ServiceManager $serviceManager)
    {
       $this->dbConnection = $serviceManager->build([
           'dbname' => 'some_database',
       ]);
    }
    

    While this is a handy feature, passing around and having the entire DI container as a dependency causes testing difficulties and leads to a problem of hidden dependencies.

  3. static factory - give up on using DI container and introduce a static factory for creating service instances in-place:

    $dbConnection = DbConnectionFactory::create($dbConfig);
    

    Not only that this approach results in tight coupling between the consumer code and creation details of its dependency, but also makes it very difficult to test its functionality with fake database connection objects for example.

Solution

The way I handle these cases is by introducing a special type of service - one whose responsibility is to create and manage objects. I avoid using the term "factory" here, because this factory class is also a service that should have its own factory (see?) when registered in the DI container. For this particular example, I will formulate service as a Database Connection Pool, which nicely describes its purpose. It encapsulates default database configuration and creates connection objects for the database name passed as an input parameter:

interface DbConnectionPoolInterface
{
    public function get(string $dbName) : DbConnectionInterface;
}

final class DbConnectionPool implements DbConnectionPoolInterface
{
    private $dbConfig; 

    public function __construct(array $dbConfig)
    {
        $this->dbConfig = $dbConfig;
    }

    public function get(string $dbName) : DbConnectionInterface
    {
        return new DbConnection(array_merge(
            $this->dbConfig,
            [
                'dbname' => $dbName,
            ]
        ));
    }
}

You register it with your favorite DI container the same way you do in case of any other service:

return [
    DbConnectionPoolInterface::class => function(ContainerInterface $container) {
        $config = $container->get('Config');
        return new DbConnectionPool($config['db']);
    }
];

... and inject / use it where needed:

public function __construct(DbConnectionPoolInterface $dbConnections)
{
   $this->dbConnection = $dbConnections->get('some_database');
}

Additionally, you can apply Flyweight design pattern to minimize memory usage:

final class DbConnectionPool implements DbConnectionPoolInterface
{
    private $dbConnections = []; 

    public function get(string $dbName) : DbConnectionInterface
    {
        if (!isset($this->dbConnections[$dbName]) {
            $this->dbConnections[$dbName] = new DbConnection(array_merge(
                $this->dbConfig,
                [
                    'dbname' => $dbName,
                ]
            ));
        }

        return $this->dbConnections[$dbName];
    }
}

dependency-injection container service-locator factory service