David NĂ©grier CTO

A few weeks ago, I started comparing 2 different approaches regarding the "service provider PSR". The goal was to spur some discussion about the scope we might want for the service provider PSR. We have had a lot of feedback and it appears clear to me we now need to fix this scope once and for all.

In this article, I will present my ideal scope. Please feel free to share your ideal scope.

Context

The PHP-FIG (framework interoperability group) is about increasing interoperability between frameworks. By defining a common set of interfaces (LoggerInterface, CacheInterface...), the PHP-FIG allows a developer to write a class once, and have it consumed by many frameworks.

Let's assume you wrote a new logger class, that respects the PSR-3 (the LoggerInterface). If you want to easily use it in Symfony, you will have to write a "bundle" for it. If you want to use it in Laravel, you will have to write a "service provider". If you want to use it in Drupal 8, you will have to write a Drupal plugin....

Stated goal

Each framework has it's own custom package format. What these package formats are doing is essentially always the same. They are used to put things in a container.

If the PHP-FIG could come up with a unique package format that could be supported by all frameworks, package developers could truly write classes that can be used in any framework more easily.

Hence, the stated goal of this PSR (let's call it PSR-X since it does not have a number yet) is to find a common package format that could be supported by all frameworks.

In the rest of this article, I'll highlight a list of "abilities" that this package format should have.

1: Ability to put entries in a container

The very first thing our package format should be able to do is to put things in a container.

Use case:

If I wrote a new "logger" package, with a MyLogger class in it, I want to register an entry in the container containing a MyLogger instance.

In other words, the PSR should be able to feed the container with a factory that can create the requested service.

2: Ability to claim a default implementation for an interface

The package should be able to declare that the service it provides is the default implementation for a given interface (this is important for auto-wiring containers).

Use case:

My MyLogger class implements PSR-3. I want a way to tell the container explicitly that if a service has a dependency on the Psr\Log\LoggerInterface, the entry I provide is a good candidate and can be used.

Note: in almost all containers I've seen, this is done by relying on convention and "aliases". It seems there is an agreement that to "claim" ownership of an interface, your identifier for the entry should be the fully qualified name of the interface.

"Psr\Log\LoggerInterface" => your logger instance

Since a service can implement many interfaces, the best practice here seems to be using aliases.

"MyLogger" => your logger instance
"Psr\Log\LoggerInterface" => alias to "MyLogger"

3: Ability to "tag", with or without priorities

The package can register a provided service as part of a "group" of services that share the same purpose.

Those services might need to be ordered inside the tag.

Use case:

My package is providing a PSR-15 middleware in charge of transforming exceptions into HTTP 500 pages. I don't know who will consume it (it might be Zend Stratigility or any other PSR-15 consumer). I want my package to instruct the container that:

  • my middleware should be part of the middleware pipe (i.e. it should be "tagged" as belonging to a middleware)
  • my middleware should be the first in the "middleware pipe".

Note: PSR-11 does not introduce the notion of tags. Indeed, all containers do not have the notion of tag built-in. But PSR-11 specifies that a container entry can be anything (it is not limited to objects only). Therefore, a "tag" can be seen as a container entry that contains an array of services.

4: Ability to alter a service

A package should be able to alter/modify a service stored in the container, either by calling methods on it or by "decorating" it.

Use case:

My package is providing a Twig extension. I want my package to:

  • Create a new container entry called `MyExtension`
  • Alter the `Twig_Engine` instance in the container to call: $twig->register($myExtension);.

5: Auto-discovery

When you install a Symfony Bundle in Symfony 4 (via composer require), Symfony Flex takes care of registering the bundle for you. Same thing with Laravel Service Providers since Laravel 5.5 or with Zend Framework. Those frameworks are actually coming with a Composer plugin that triggers after a package is installed and that is looking for some configuration in the package (either a manifest.json file or a special "extra" section in the composer.json file).

If we want our package format to be as easy to use as the framework specific package formats, we need to have a way for underlying frameworks to perform auto-discovery of service providers.

Use case:

I'm performing a composer require to install a new package. Some composer plugin specific to my framework is triggered. The framework registers the service provider of my package automatically.

6: Ability to expose requirements (external required services and configuration)

If a package has requirements (in the form of dependencies that need to be available in the container or in the form of required configuration), it should be able to publish those programmatically to the underlying framework.

Use case 1:

I'm installing a database abstraction library (Doctrine DBAL for instance). The package creates a default database connection instance in the container. This instance requires a number of parameters (database driver, host, name, user, password, etc...). These parameters might not be available in my application yet. My framework is clever enough to notice this at install time (maybe via a Composer plugin) and asks me to fill the parameters.

Use case 2:

I'm installing a library that performs heavy computation. This library absolutely needs a PSR-16 CacheInterface to run. I don't have such an entry yet in my container. My framework notifies me that my container is in an inconsistent state and that I need to install another package to provide this PSR-16 CacheInterface entry (bonus point if the framework can propose a package that does this).

7: Ability to provide different services based on configuration

Based on configuration, a package should be able to register or not a set of entries.

Use case:

I'm installing a database abstraction library (Doctrine DBAL for instance). For my application, I don't have one but 2 databases. I can add an array of connections in my configuration and the service provider can create in the container 2 separate connection entries.

Give us some feedback

That's it! That's my dream list of requirements for this PSR.

Actually, this might span over several PSRs (auto-discovery might be part of a different PSR for instance) And of course, there are other topics related to containers that need to be addressed in other PSRs (I'm thinking about the ability to do some introspection on existing containers).

Anyway, if we manage to agree upon such a cross-platform package format, I'm absolutely sure this might be absolutely beneficial to the PHP ecosystem.

What about you? Any requirement that are important for you and that I missed?

I cross-posted this article on the PHP-FIG mailing list. Comments can go directly on the mailing list.

In my next blog post, I'll be comparing what we have so for been able to achieve with container-interop/service-provider to the list of requirements above.

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.