[EXPERIMENTAL] Promoting container interoperability through standard service providers
PHP
Latest commit 11438df Feb 15, 2017 @mnapoli mnapoli committed on GitHub Merge pull request #37 from PrisisForks/master
Update container namespace in readme
Permalink
Failed to load latest commit information.
src Rename interface for consistency Feb 15, 2017
.gitignore First commit Feb 3, 2016
README.md Update container namespace in readme Feb 15, 2017
composer.json use psr-11 Feb 14, 2017
puli.json Adding Puli binding type Feb 23, 2016

README.md

Standard service providers

This project tries to find a solution for cross-framework modules (aka bundles) through standard container configuration. It is part of the container-interop group.

Work in progress: the project is currently experimental and is being tried in frameworks, containers and modules until considered viable. Until a 1.0.0 release the code in this repository is not stable. Expect changes breaking backward compatibility between minor versions (0.1.x -> 0.2.x).

Gitter chat

Background

Three main alternatives were identified to standardize container configuration:

  • standard PHP objects/interfaces representing container definitions
  • standard container configuration format (e.g. XML, …)
  • standard service providers

The first solution that container-interop members tried to implement was a set of standard PHP interfaces for container definitions. While this solution is working, it has a few limitations and it is complicated to explain, understand and use.

There were then discussions about a standard configuration format (for example in XML), which has the advantage of being slightly easier to understand and use for module developers. This work has not be formalized yet because of the amount of work needed. This approach would also suffers from a few of the limitations identified in the first approach. It would also requires the inclusion in the standard of many specific features: the standard must define many different ways for how objects can be created and dependencies injected. That makes the standard complex to define, and would force all containers (even simple ones) to support all the features.

This repository contains a proposition for standard service providers (service providers are PHP components that provide container entries). This approach has turned out to be simpler on many level:

  • the standard is much simpler, which means it is easier to explain and understand
  • it is easier to use as it relies on plain old PHP code
  • it is easier to implement support in containers

Usage

To declare a service provider, simply implement the ServiceProviderInterface interface.

use Interop\Container\ServiceProviderInterface;

class MyServiceProvider implements ServiceProviderInterface
{
    public function getServices()
    {
        return [
            'my_service' => function(ContainerInterface $container, callable $getPrevious = null) {
                $dependency = $container->get('my_other_service');
                return new MyService($dependency);
            }
        ];
    }
}

The getServices() method must return a list of all container entries the service provider wishes to register:

  • the key is the entry name
  • the value is a callable that will return the entry, aka the factory

Factories have the following signature:

function(ContainerInterface $container, callable $getPrevious = null)

Factories accept the following parameters:

  • the container (instance of Psr\Container\ContainerInterface)
  • a callable that returns the previous entry if overriding a previous entry, or null if not

The only difference between defining an entry from scratch or overriding/extending a previous entry is that the $getPrevious parameter will be either a callable or null. Factories are free to use it or ignore it if it's not null.

If you know you will not be using the $container parameter or the $getPrevious parameter, you can omit them:

    function() {
        return new MyService();
    }

Each factory is responsible for returning a given entry of the container. Nothing should be cached by service providers, this is the responsibility of the container.

Consuming service providers

Service providers are typically consumed by containers.

For containers implementing container-interop/container-interop or PSR-11:

  • A call to get on an entry defined in a service-provider MUST always return the same value.
  • The container MUST cache the result returned by the factory and return the cached entry.

Values (aka parameters)

A service provider can provide PHP objects (services) as well as any value. Simply return the value you wish from factory methods.

Aliases

To alias a container entry to another, you can get the aliased entry from the container and return it:

class MyServiceProvider implements ServiceProviderInterface
{
    public function getServices()
    {
        return [
            'my_service' => [ MyServiceProvider::class, 'createMyService' ],
            'alias' => [ MyServiceProvider::class, 'resolveAlias' ],
        ];
    }

    // ...

    public static function resolveAlias(ContainerInterface $container)
    {
        return $container->get('my_service');
    }
}

Entry overriding

Overriding an entry defined in another service provider is as easy as defining it again.

Module A:

class A implements ServiceProviderInterface
{
    public function getServices()
    {
        return [
            'foo' => [ A::class,  'getFoo' ],
        ];
    }

    public static function getFoo()
    {
        return 'abc';
    }
}

Module B:

class B implements ServiceProviderInterface
{
    public function getServices()
    {
        return [
            'foo' => [ B::class, 'getFoo' ],
        ];
    }

    public static function getFoo()
    {
        return 'def';
    }
}

If you register the service providers in the correct order in your container (A first, then B), then the entry foo will be 'def' because B's definition will override A's.

Entry extension

Extending an entry before it is returned by the container is very similar to overriding it.

Module A:

class A implements ServiceProviderInterface
{
    public function getServices()
    {
        return [
            'logger' => [ A::class, 'getLogger' ],
        ];
    }

    public static function getLogger()
    {
        return new Logger;
    }
}

Module B:

class B implements ServiceProviderInterface
{
    public function getServices()
    {
        return [
            'logger' => [ B::class, 'getLogger' ],
        ];
    }

    public static function getLogger(ContainerInterface $container, callable $getPrevious = null)
    {
        // Get the previous entry
        $previous = $getPrevious();

        // Register a new log handler
        $previous->addHandler(new SyslogHandler());

        // Return the object that we modified
        return $previous;
    }
}

If you register the service providers in the correct order in your container (A first, then B), the logger will be first created by A then a new handler will be registered on it by B.

Compatible projects

Projects consuming service providers

Packages providing service providers

Best practices

Managing configuration

The service created by a factory should only depend on the input parameters of the factory ($container and $getPrevious). If the factory needs to fetch parameters, those should be fetched from the container directly.

class MyServiceProvider implements ServiceProviderInterface
{
    public function getServices()
    {
        return [
            'logger' => [ MyServiceProvider::class, 'createLogger' ],
        ];
    }

    public static function createLogger(ContainerInterface $container)
    {
        // The path to the log file is fetched from the container, not from the service provider state.
        return new FileLogger($this->container->get('logFilePath'));
    }
}

FAQ

Why inject a callable instead of the previous entry directly in factories?

In a first version, service provider factories received the previous entry directly as a second parameter:

    public static function getMyService(ContainerInterface $container, $previous = null)
    {
        // ...
    }

That caused 2 problems:

  • it was inefficient since it caused the container to resolve all the previous entries that might exist, even when they were overridden by another service provider
  • when the entry name was a class name, autowiring containers would try to resolve the previous entry using autowiring: when some parameters could not be resolved by the container, there would be exceptions

By injecting a callable that returns the previous entry, that makes it lazily loaded. That is both more efficient and avoids most problems with autowiring containers.

For a more detailed explanation you can read the full discussion in the issue #9.

Why does the service provider not configure the container instead of returning entries?

Service providers usually take a container and configure it (e.g. in Pimple). The problem is that it requires the container to expose methods for configuration. That's an impossible requirement in a standard because all containers have a different API for configuration and they could never be made to implement the same.

These service providers provide factories for each container entry it provides. They do not require configuration methods on containers, so they can be made compatible with all/most of them. Each container entry is, in the end, just a callable to invoke, which most containers can do.

If everything is standardized is there a point to having many container implementations anymore?

The goal of container-interop/container-interop is to decouple frameworks (or sometimes libraries) from containers, so it is meant to be used mainly by frameworks. End users (i.e. developers) can still choose their favorite containers and make use of all their specific features.

The goal of this package (standard configuration) is to decouple modules from containers, so it is meant to be used by developers writing modules. End users (i.e. developers) can still choose their favorite containers and make use of all their specific features.

Developers are encouraged to continue using their containers like before. However modules can now become reusable accross frameworks by using this standard configuration format.

Puli integration

The Puli integration is completely optional and not required to use this standard. It is only here to facilitate usage with Puli.

This package provides a Puli binding type: container-interop/service-provider. Modules using Puli and implementing this standard can register service providers (fully qualified class names) through this binding type.

This way, frameworks or applications based on Puli can discover service providers automatically.

To register your service provider, simply use Puli's bind command:

puli bind --class Acme\\Foo\\MyServiceProvider container-interop/service-provider