The configuration of instances via factories is not uniform, uses different ways to retrieve and check the needed options and it's not clear which configuration options needs the factory to create the instance. See how the interop-config PHP library solves this problem.

The goal set by the interop-config library is to standardize how factories uses a configuration to create instances, support for auto discovery of needed configuration, to reduce boilerplate code and to make it more readable and easier to understand. interop-config validates the configuration structure depending on the implemented interfaces. If you use Zend Expressive, you should also look at the following article.

Concrete implementation

Specifications are good, but it's easier to understand with a concrete implementation. The interop-config library has a PHP trait. This is great, because it reduces boilerplate code and returns the needed configuration options depending on the implemented interfaces.

You should have coding conventions and you should have config conventions. If not, you should think about that. The config keys should have the following structure vendor.package.container_id. A common configuration looks like that:

Whatever configuration structure you use, interop-config can handle it. You can use a three-dimensional array with vendor.package.id or you don't care of it and organize your configuration by behavior or nature (db, cache, ... or sale, admin).

// interop config example
return [
    // vendor name
    'doctrine' => [
        // package name
        'connection' => [
            // container id
            'orm_default' => [
                // mandatory options
                'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver',
                'params' => [
                    'host'     => 'localhost',
                    'port'     => '3306',
                    'user'     => 'username',
                    'password' => 'password',
                    'dbname'   => 'database',
                ],
            ],
        ],
    ],
];

Now let's create an Interop-Container compatible factory which creates the connection. In this example, the configuartion is retrieved from the Container. It's independent where the PHP configuration comes from, because you must provide them to the options method. You see that the container id is orm_default.

use Interop\Config\ConfigurationTrait;
use Interop\Config\RequiresMandatoryOptions;
use Interop\Config\RequiresConfigId;
use Interop\Container\ContainerInterface;

class MyDBALConnectionFactory implements RequiresConfigId, RequiresMandatoryOptions
{
    // this trait handles to retrieve the options
    use ConfigurationTrait;

    public function __invoke(ContainerInterface $container)
    {
        // get options for doctrine.connection.orm_default
        // you can also use a static factory, see documentation for more details
        $options = $this->options($container->get('config'), 'orm_default');

        // mandatory options check is automatically done by RequiresMandatoryOptions

        $driverClass = $options['driverClass'];
        $params = $options['params'];

        // create your instance and set options

        return $instance;
    }

    /**
     * Is used to retrieve options from the configuration array ['doctrine' => ['connection' => [...]]].
     *
     * @return []
     */
    public function dimensions()
    {
        return ['doctrine', 'connection'];
    }

    /**
     * Returns a list of mandatory options which must be available
     *
     * @return string[] List with mandatory options
     */
    public function mandatoryOptions()
    {
        return ['driverClass', 'params'];
    }
}

For more examples, please look at the latest online documentation.

Auto discovery of the configuration

Starting in 2.1.0, interop-config began shipping with console tools

Another interesting feature is to auto discover the PHP configuration and create config files depending on the factory. Now it's possible to analyze the factory to get the available, default and mandatory, option keys. The ProvidesDefaultOptions interface returns a list of default options. These options are merged with the provided options or can also be used for the configuration file. The same applies for the RequiresMandatoryOptions interface which returns a list of mandatory options.

Conclusion

The interop-config interfaces can be combined to fit your needs. If you want to not allow multiple configurations of an instance, don't implement the RequiresConfigId interface. You see which configuration needs the factory to create an instance and you can use the factory to auto discover needed and optional options or to generate a configuration file. Is this great, isn't?

If you have any suggestions please open an issue on GitHub or leave a comment to share your experience of configuration of factories.