Lately, I’ve been playing quite a lot with the notion of container definition interoperability.
Container definition interoperability? Why?
The problem we are trying to solve is the problem of packages interoperability between frameworks. Have a look at this packagist search for the "Glide" library.
See? There is the code for Glide, and then, there are a million of small libraries that provide Glide services for each framework out there (laravel-glide, glide-symfony, yii2-glide, cakephp-glide, lumen-glide...) It doesn't make any sense! Package writers are loosing precious time writing spaghetti code for each framework out there. This needs to be fixed.
But each framework out there has its own format (or uses PHP directly), so it will be hard to have everybody agree on a single file format for services, right?
That's why we are proposing a common set of interfaces that describe a container entry.
The final goal is the ability for any PHP package (on Composer) to provide the container of your application with a set a container definitions. The container of your application can then take these definitions and turn them into actual container entries.
You can have a look at the proposed interfaces on the definition-interop project (work in progress)
Objects implementing the definition-interop interfaces are meant to be consumed by a container (or even better, by a compiler that generates an optimized container).
Actually, in definition-interop, containers are consuming definition providers which are objects providing a set of definitions.
Wanna have a look at a definition provider written in pure PHP? Have a look at this provide for the Glide library. It is written using the Assembly library that provides definitions implementing definition-interop.
But I don't want to describe my definitions using PHP... I want JSON/YAML or XML definition files!
Keep cool! That's not an issue! You don't have to use Assembly to write your definitions. Assembly is only one possible library implementing definition-interop. Actually, definitions could come from everywhere, in any format. They could be stored in a file, in a cache, in database... It's up to your imagination.
The most likely source of definitions will be service files (like YML files, JSON files, ...) So package writers can choose between any format they like, and pick the appropriate "loader" that will transform their configuration file into a set of definition objects.
As a matter of facts, there is already one loader available for definition-interop: a YML loader that accepts a format very similar to Symfony services YML files. You can have a look at the YML loader here.
Yes but how will my framework discover these definitions?
At this point, there are really 2 possible strategies.
Strategy 1: we do what most frameworks do. A
DefinitionProvider is a kind of "bundle" or "module". So it would be completely ok to ask users to add each package's
DefinitionProvider to the list of all providers consumed by your container (just like a bundle must be manually added in Symfony or a module must be manually added in ZF2).
Strategy 2: we go the extra mile and provide a discovery system that can automatically discover new definitions when a Composer package is added/updated.
Here, we are getting out of the scope of definition-interop that should only focus on interfaces. Everything below this line is not endorsed (yet :) ) by definition-interop and is only pure research I'm doing. So far, I'm calling these harmony packages (packages with definition-interop + automatic discovery).
Discovery... you said "discovery"?
Package writers must be able to provide a DefinitionProvider to the container.
On the other end, the container must be able to consume DefinitionProviders.
Actually, there is a cool PHP tool out there that specializes in resources discovery. Its name is Puli. Using Puli, a package can provide (i.e. publish) a class (or a file). These classes can be consumed by containers.
So the idea is simple: packages providing DefinitionProvider (i.e. mostly loaders) can provide this definition provider to Puli. And containers will consume those definition providers. Actually, it is just slightly more difficult. DefinitionProviders are instances of objects implementing the DefinitionProviderInterface. But in its current version, Puli can only provide classes, not instances. So Puli will instead provide a factory that can create a definition provider using a static method. The factory must of course implement a DefinitionProviderFactory interface.
See? A container factory can use Puli to discover all definition provider factories. Using those factories, it can get all definition providers, add them to the container it is building and return the container. Also, please note how Puli's usage is restricted to factories. Containers and definition providers know nothing about Puli. So if some day a more shiny discovery library comes out there, there are no strong ties between definition-interop and Puli. Only the factories need to be refactored.
But wait, it's not over yet, it gets even better! Loaders can use Puli (if they want) to discover the definition files in packages. For instance, the YAML loader can discover YML files in packages declaring YML service files.
Seems overly complex? Believe me, it is not!
There are different pieces in this architecture. Each piece has a clear and limited responsibility. Those pieces are also quite simple to implement. In the end, what really matters is that usage must be simple for the package writers and for the container developers.
For package writers, it IS simple. Declaring services is a 3 step process:
- Choose the format you want to write your services in (choose a loader, or go with pure PHP) and add the appropriate loader to your
- Write your service provider file (in whatever format you chose)
- Use Puli CLI to declare your service file (two lines of shell)
For container writers, there are really 2 choices:
- Directly consume definition-interop. This is what PHP-DI is doing. We are also working on a Symfony bundle that integrates definition-interop definitions into the Symfony container.
- Use a container or compiler compatible with definition-interop. For instance, Yaco is a compiler that accepts definition providers and that generates a compiled container, compatible with container-interop. container-interop compatibility means that Yaco can be plugged into any container-interop container out there! This is great news, because there are a ton of containers compatible with container-interop out there! Using Yaco, these containers could benefit directly from definition-interop, without any change needed. And since Yaco provides a compiled container, they will have a great performance.
Yeah... but that's only theory...
No, it's not. Here is a proof of concept I've been working on.
The proof of concept is a sample app requiring an Harmony package: doctrine-cache-harmony.
As the name states, this is a simple wrapper package that defines a "doctrine.cache" entry, using a YML file. Therefore, this harmony package requires the yaml-definition-loader package.
The package also contains a Yaco compiler that will automatically detect the YAML loader (using Puli discovery) and generate a container.
Finally, the entry is fetched from the container...
... and guess what? It works!
Actually, the proof of concept is even going a step further by automatically discovering the Yaco container and aggregating it into a Composite container aggregating PSR-11 compatible containers, but that's completely optional.
To sum this up, we have:
- Great performance (thanks to a compiled container)
- Maximum flexibility (no file format imposed for services, no container forced into users)
- Easy to create packages
Of course, there remains a ton of work to do, especially in defining the complete scope of definition-interop. We must still work on service extensions (for instance, how to register a TwigExtension in the Twig main bundle, or how to add a PSR-7 router to the list of all available routers, etc...) There is a lot of work to do on discovery too. And Puli is not even stable yet, so this discovery part certainly needs some work. But one thing is sure: interoperable PHP packages are possible! It's just a matter of time and effort to get there.
About the author
David is CTO and co-founder of TheCodingMachine. He is the co-editor of PSR-11, the standard that provides interoperability between dependency injection containers. David is the lead developer of Packanalyst, a website that references all PHP classes/interfaces ever stored on Packagist. He is also the lead developper of Mouf, the only graphical dependency injection framework and currently working on another PSR, regarding standardizing service providers (more containers goodness!).