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.
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).
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.
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.
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.
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.
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.
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.
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.
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.