David NĂ©grier CTO

In my previous blog post, I talked about the different ways to build interoperable PHP modules. It turns out we have had quite a lot of very constructive feedback from many different sources. In this blog post, I'll keep you updated with everything that was said since my last article.

Service providers are the way to go

Two months ago (see my last article), we were still unsure what was the best way to inject dependencies in a container in a cross-framework way. We explored many alternatives (common file format for describing container definitions, common interfaces for definitions, common interface for dumping definitions...) We ended up pushing forward the "universal service providers" approach. This decision has received a lot of positive feedback. To be more precise, we have had very little negative feedback, which is pretty unusual when we touch concepts like dependency injection :) . You can view discussions on the FIG mailing list, on Reddit and on Gitter.

So first news: we are now convinced that universal service-providers are the way to go to standardize the configuration of containers (i.e. how we put things in a container).

Universal service providers are moving fast

Work on universal service providers has started in the container-interop/service-provider project. A lot has been going on there, as there are many different ways to build a service provider. Right now, our proposal looks like this:

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

The code is pretty much self-explanatory. The getServices method returns an array of callables. The key is the name of the service, the value is a callable (we call it a "factory") that is in charge of creating the service. The factory method is passed 2 parameters: the container and an optional previous value for the service that will be "extended".

Interestingly enough, factory methods need to fetch dependencies from the container. Since we are writing cross-framework modules, the container could potentially be anything. The PSR-11 and the ContainerInterface are absolutely needed here to share a common way to fetch dependencies from the container.

One important feature is the ability to extend a service declared in another service provider. For instance, a Twig_Environment service might be created in one service provider, and another service provider might want to modify the service by calling the addExtension method on it to register a new Twig extension. In universal service providers, if a service is already declared, it will be passed as the second parameter to the factory method. But rather than passing the service directly, we pass a callable that will resolve to the service. Why? Because we might not want to extend this service (rather we might want to replace it). In this case, it is pointless to recreate the service. Another reason is to ease the work with autowiring containers. You can read more about it here.

What is still being discussed

Regarding the ServiceProvider interface, there are still a lot of things to be discussed.

Here is a quick overview of what we are working on:

Static VS non-static

The early versions of the interface were made of a static getServices function and static factories. Making everything static makes sense because everyting that is needed to build a service should be contained in the container. Hence, factories should not need to rely on the service provider properties (no need for $this). Also, static factories are way faster and easier to call from the container. Not surprisingly enough, most container developers working on this standard were for static methods.

However, there has been quite a strong feedback from the rest of the community weighting for non-static methods. They also have a number of advantages:

  • When registering a service provider in a container, we can type-hint on the instance (function register(ServiceProvider $provider) VS function register(string $providerClassName))
  • Being able to use closures as factories leads to shorter, more understandable code
  • We can also use classes implementing __invoke to write boilerplate services - like a service that creates an Alias or adds an element to an array (AddToArray)

The whole discussion can be seen on Github.

Service provider as-a-class VS service provider as-a-file

Service providers contain a set of services to be created (just like Zend Framework 2 PHP config files or Symfony 2 services.yml files). Hence, why not simply write a simple PHP file that returns an array of factories (without any class).

The option is currently weighted in in this Pull Request (your comments are welcome).

$getPrevious vs $previous

The signature of the factory is still in discussion. Should the second parameter be the previous entry ($previous), or a callable resolving to the previous entry ($getPrevious)? Altough it seems we will settle with $getPrevious, the way we should pass the previous instance as a parameter is still open for debate.

Arguments in favor of $getPrevious are mainly for performance reason. An issue regarding this signature is still open (even if it is not very active). Another related question is whether we should split the getServices method into 2 different methods (like getNewServices and getExtendedServices) to make it clear that some services are provided, and other services are modified.

Signature of the getServices method

Another question open for debate is the signature of the getServices method. Current definition states that it takes no arguments. This is pretty straightforward but it does not allow for a service provider to provide entries based on the entries already registered in the container. So with the current implementation, the list of services provided by a service provider are set in stone (they are really similar to a configuration file).

We could change the signature to something like getServices(ContainerInterface $container). That way, the getServices method could access already existing services/configuration and declare its own services accordingly. However, this is getting harder to implement in some containers (since the container is passed while being constructed... which is hard for compiled containers). This is issue #27 on Github.

Interface name

More trivially, the name for the interface is still open for debate. ServiceProvider vs ServiceProviderInterface? Consistency VS correctness, choose your side! :)

But we need more than an interface!

Universal service providers are a good thing, but there is quite a lot more that needs to be addressed if we want truly interoperable components. A lot of the comments we got on the PHP-FIG mailing list expressed the idea that we also need to address naming conventions, namespacing, best practices, etc...

Some of these issues are clearly to be part of the future PSR we are shaping, but not everything belongs to a PSR. For instance, discovery of service providers (the ability for a container to automatically detect and add a service provider) is important, but is clearly out of scope of the PSR (it would require a "discovery" PSR first).

Below is a table of features and whether I believe we should put them in or out of the PSR.

In the PSROut of the PSR
ServiceProvider interface
Naming conventions
Service provider discovery
Optimisation techniques
Best practices (?)

Well... that was simple! I believe that PSRs are so hard to get passed that we should really focus on the very minimum, and put everything that is not part of the "minimum viable product" in an external website (maybe managed by container-interop ?)

Let's have a look at each of these features that should be out of the PSR.

Naming convention

Service providers are going to create container entries. If we want entries to be used by other containers, there should be an agreement on the name of these entries. The obvious choice for services that are available only once in the container ("singleton-like") is to use the fully qualified class name (or the fully qualified name of the interface they are implementing). This is also a good thing because it helps autowiring containers do the wiring for these services.

But there are many other cases to consider. What names for the configuration entries? Should we put a namespace name for these entries to avoid conflicts?

Jan Tvrdik and Paul M Jones made a few very interesting comments regarding naming of services. I believe we should take into account the proposed naming conventions there. Anyway, this needs to be worked upon.

Discovery

One feature I'm pretty fond of is automatic discovery of service providers. I want to be able to create a container that can automatically detect service providers and add them. Of course, the user should have the option to opt-out, but by default, when a service provider package is added to Composer, I would like it to be automatically registered in the container. This kind of "magic" is possible with Puli discovery. Since Puli is kind of the only package out there providing this discovery feature, I'd recommend using it for service provider discovery. Of course, this should be out of the PSR, since Puli and Composer are required for this and they are not standards in any way.

Optimisation techniques

By using specially crafted invokable objects instead of factories, compiled / cached container could get a fairly important performance boost. I've started working on this idea in the "common-factories" repository. Take a look if you are interested.

Best practices

Finally, we will need to work on best practices. Here are a few I can think of, a lot of work will be needed to complete those:

  • factories should not rely on the state of the service provider (no reference to $this), they should rely on the container state instead
  • factory should (preferably) be created as "public static functions" (this way, compiled containers can call the factory without calling getServices first.)
  • ...

Conclusion

Well... that was quite a long article, but a lot has happened lately!

We are getting closer to something that is really viable. If you are interested in joining the discussion, now is a good time. Read the container-interop/service-provider README and join us on Gitter, or via the Github issues.

If you are a package developer, we would love to see an implementation of the service provider for your package (we already have several compatible containers and plugins for Laravel and Symfony 2 so we can test).

If you are a framework/container developer, jump on board and try implementing the current interface! We are looking for feedback too.

About the author

David is CTO and co-founder of TheCodingMachine and WorkAdventure. He is the co-editor of PSR-11, the standard that provides interoperability between dependency injection containers. He is also the lead developper of GraphQLite, a framework-agnostic PHP library to implement a GraphQL API easily.