Remember the times when we copied PHP “libraries” into our project folder, or we copy and pasted code from some random site into our project? Those times are over. Composer and Packagist are the modern way to manage PHP dependencies. They are great. Almost as good as The Maven repos and their build tools in the Java world. However, while Composer is really good at managing the dependencies of a single project, i.e. one composer.json file, it does not play well if you want to plug different projects together at runtime. And by “does not play well” I mean it simply doesn’t work if you have two or more composer.json files. This quick post demonstrates a way around this limitation. Quick and dirty. Just like the foundations of PHP :-)
Content
1. Scenario: Multiple optional components
Imagine you have multiple components of a project, and each of them has their own composer.json file. Maybe something like this:
project-core/
composer.json
vendor/
project-ui/
composer.json
vendor/
project-addon-1/
composer.json
vendor/
project-addon-2/
composer.json
vendor/
...
Each of the components has its own dependencies. Normally you would just put them on your own Packagist server and then use Composer’s require
statement to let them depend on each other. But what if some dependencies are optional? Like the add-ons above. Or you want to be able to have different user interfaces and you don’t want to distribute the entire project as a whole? What if your components are Debian packages?
project-core.deb
project-ui.deb
project-addon-1.deb
project-addon-2.deb
...
You won’t have any luck with Composer. Because Composer only loads and initializes dependencies at build time. It has no ability to discover components or dependencies at runtime and then load them, unfortunately. So even if we do a composer install on all the subprojects/components, there is no easy way to plugin them together dynamically.
To achieve this, we have to be a little creative …
2. “Solution 1”: Merge at build time
Wait? Didn’t I say runtime? Yes, I said runtime. However, if you know at build time what components will be shipped, or you can pull them down from Git (as submodule or subtree), you can use a very neat little plugin called the composer-merge-plugin. This plugin can extend a composer.json file by including others. However, be aware that it creates only one vendor folder. It does not use multiple composer.json files and vendor directories at runtime!
3. Solution 2: Merge at runtime
If the option above is not really an option for you, you can write your own little autoloader instead, or reuse the composer autoloaders of the projects that you want to include. Be aware that while this works if you have no conflicting libraries, things might blow up if you have:
1 2 3 4 5 6 7 8 9 10 |
<?php /** @var Composer\Autoload\ClassLoader $loader */ $loader = require_once __DIR__ . '/../vendor/autoload.php'; $map = require_once __DIR__ . '/../../core/vendor/composer/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->addPsr4($namespace, $path); } |
The snippet above loads the autoloader of the current project (../vendor/autoload.php) and adds the PSR-4 namespaced classes of the ‘core’ project to it (../../core/vendor/composer/autoload_psr4.php). The autoload_psr4.php file returns an associative array of namespace-to-folder mappings, which then will be used by the composer autoloader to discover the class file.
I realize that this is not the most elegant solution, but it worked for me. Let me know if any of you have a better solution.
A. About this post
I’m trying a new section for my blog. I call it Code Snippets. It’ll be very short, code-focused posts of things I recently discovered or find fascinating or helpful. I hope this helps.
Brilliant – thx!