Recently I decided to create a new organization which is dedicated to providing decoupled CMS components: CMS components which are framework agnostic[1]. Each package is provided in a separate github repository in order that they can be semantically versioned.

Some of the components require either integrations or drivers for 3rd parties. For one example, the "content type" component requires drivers for Doctrine ORM and Doctrine PHPCR-ODM, the "description component" could have drivers for the ODM, Sonata Admin, Sylius - and just about anything else with a metadata system.

The problem is how to package these "satellite" dependencies. But wait, why would you want to package them? Why not put them in the component?

[1] Similar to the Symfony CMF but experimenting with a different approach.

Unbounded Scaling

Of course, you can package these drivers in the main libraries repository. The issues start when you want to test the package.

For each "integration" you will need to require the thing that it is integrating, for example Sonata Admin, Sylius and "Big Admin X".

Each dependency you require in the dev section of composer.json:

  1. Increases build time.
  2. Risks two packages conflicting with each-other.
  3. Increases maintenance overhead.

These factors multiply each time you add an integration. How many integrations will there be? Given that we have tacitly decided to support everything in our main library, in theory there is no upper-bound on the number of integrations and we will hit a wall sooner or later with the build time, but more important than that, there is a large chance that package X will conflict with package B and the build will not even start.

We could make the build more complicated and do a separate composer install for each integration, but this would make problem 1 worse.

The burden of maintenance of these integrations lies with the main package developers In some cases the integration will have been provided by a 3rd party - but once it is merged into the main repository they are no longer obliged to maintain it. In some cases the main package developers will not even know anything about the system the integration integrates with and the integration will rot.

All of the above factors mean that the library has potentially unbounded scaling issues.

Satellite Dependencies

So the solution here is to keep the main library clean of these troublesome 3rd party integrations, in particular when they introduce new dependencies, by creating a new package for each integration.

            /----\
      /     | S1 |      \
            \----/

         /----------\
/----\   |          |   /----\
| S3 |   | main lib |   | S2 |
\----/   |          |   \----/
         \----------/


            /----\     
     \      | S4 |     /
            \----/

A good example is the PHP League's Flysystem[1] which packages some drivers which require no dependencies (e.g. local filesystem driver) but the documentation offers links to a multitude of other drivers, each with a separate repository/package and some of which are maintained by 3rd parties and reside in a different Github organization.

The separate packages means distributed maintenance and also more control over inverse dependencies (e.g. flysystem-phpcr can depend on flysystem versions ^1.2 explicitly).

[1] I will continue using this as an example, but consider the example to be unrelated to reality.

Framework Coupling

So far so good. We have a main package and a number of satellite packages containing our 3rd party integrations.

But now we need to integrate these packages with our framework, each integration will need to be wired into a dependency injection container, and may also require some configuration.

Some options:

  1. Add the framework-integrations to the main library.
  2. Create a single "bundle/provider/module" for each framework.
  3. Create a single "bundle/provider/module" for each integration.

The first approach is to add the framework integrations to the main library, this approach is blocked to us if we accept the arguments from the previous section. We would need to include all of the things that we are integrating in the dev build in order to test the framework integrations.

The second is to create a single bundle for our main package, for example, FlysystemBundle. This bundle would wire-up all of the available integrations. This is the solution commonly taken, for example, in the Symfony CMF and to a lesser extent in Symfony itself (think forms / validation).

I dislike this because we suffer from the scaling issues, we suffer more for maintain once as we are making decisions about the configuration of the integrations and it gives special privilege to packages that "we" make and as opposed to those users create.

The third approach is to create a single bundle for each integration - for example FlysystemPhpcrBundle. This is technically the least of all evils, but it is impractical. It means that:

nb. repos = 1 + (nb. integrations + (nb. integrations * nb. frameworks)).

So imagine that Flysystem has 10 integrations and supports 3 frameworks that means that we have 41 packages and each package requires lots of boiler plate code and manual configuration (github, packagist, styleci, travis). So this idea is truly terrible for people who are pressed for time.

Bundle as Facilitator

So there is actually another solution: do not provide framework integrations for 3rd party integrations.

Here we create a single bundle, e.g. FlystemBundle which has the simple task of instantiating core flysystem services and then (in the case of Symfony) pulling in tagged driver definitions. It is agnostic to third parties.

It is then the users responsibility to create and tag the service definitions of 3rd party integrations as they require them.

For example:

services:
    foo.description.enhancer.phpcr_odm:
        class: "Foo\\Component\\Description\\Enhancer\\Doctrine\\PhpcrOdmEnhancer"
        tags:
            - { name: foo_description.enhancer, alias: doctrine_phpcr_odm }

The bundle (in this case DescriptionBundle) will then simply pull in this user defined service by its tag name.

This has the following advantages:

  • No scaling issues.
  • No more maintenance overhead.
  • User has full freedom of configuration.

The disadvantage is that, while some integrations will be single DI definitions, more complicated ones (f.e. the PHPCR-ODM driver for the "content type" component) require non-trivial and multiple DI definition entries.

But this disadvantage can also be seen as an advantage when you consider that the user has a better idea of how things are actually working and can tailor the integration to their own needs, also it can be mitigated by providing documented examples of configuration required for different frameworks.

Going Further in the Future

So the main problem presented in this post is how to handle framework integrations. This basically comes down to DI configuration.

Wouldn't it be great to have a single, standardized DI configuration format that we could package in our main library?

This was proposed in PHP-FIG but was deemed to complicated a task but if I understand, it would be difficult to accommodate all of the features provided by different frameworks. Instead it was opted to attempt to standardize an interface for accessing services.

The container-interop library provides a standardized ContainerInterface with a get method, and an interesting feature called "delegate lookup". This allows containers to access services from other containers - this would allow us to package a container-interop compatible container in our library and have it included by the "consuming" container (e.g. the Symfony DIC, Zend DIC, whatever).

But this also has some issues - it means that we depend on services being named exactly as we expect them to be (e.g. doctrine_phpcr.orm.manager or whatever) and we sacrifice any features the framework may provide (e.g. configuration definitions in Symfony).

Conclusion

Ultimately I think the best solution is to simply let users define the service definitions themselves and provide a bundle only for the core business of plumbing these definitions into the system, but I am still undecided. What do you think?

Posted on: 2016-01-23 00:00:00

Comments

Recent Posts

Psi Grid Component (a data grid)

I spent the last three months or more working arou...

Bundles: Service Providers, Definition Factories and Tags.

In this post I will talk about how we have employe...

PHPBench 0.11

PHPBench 0.11 (Dornbirn) has been released, and it...
Twatter