There's no such thing as an optional dependency
Matthias Noback
On several occasions I have tried to explain my opinion about “optional dependencies” (also known as “suggested dependencies” or “dev requirements”) and I’m doing it again:
There’s no such thing as an optional dependency.
I’m talking about PHP packages here and specifically those defined by a composer.json
file.
What is a dependency?
First let’s make sure we all agree about what a dependency is. Take a look at the following piece of code:
namespace Gaufrette\Adapter;
use Gaufrette\Adapter;
use \MongoGridFS;
class GridFS implements Adapter
{
private $gridFS;
public function __construct(MongoGridFS $gridFS)
{
$this->gridFS = $gridFS;
}
public function read($key)
{
$file = $this->find($key);
return ($file) ? $file->getBytes() : false;
}
}
This GridFS
class is part of the Gaufrette filesystem abstraction library, though I heavily modified it.
To determine all the dependencies of this code we can ask the following question:
What is needed to run this code?
You need to think of several things:
-
Which PHP version is needed to run the code without getting a syntax error? Maybe you even need a specific patch version (like 5.3.6) because of a bug in older 5.3 versions that could interfere with your code.
-
Which PHP extensions should be installed?
-
Which PEAR libraries should be installed?
-
Which other packages should be installed?
In the case of the GridFS
class the PHP version should be at least PHP 5.3, because of the use of namespace
. Also the \MongoGridFS
class should be available. This class is part of the mongo
PECL extension for PHP. The \MongoGridFS
class is only available since version 0.9.0 of that PHP extension, so we have to make sure that we explicitly mention this version constraint. Finally, it appears there are no other packages needed to be able to use the GridFS
class. So when we would create a composer.json
file for a package that contains the GridFS
file, it would look like this:
{
...,
"require": {
"php": ">=5.3",
"ext-mongo": ">=0.9.0"
}
..
}
Now this is an exhaustive list of the dependencies of package that contains the GridFS
class: when these dependencies are installed, nothing stands in the way of using this class in your application.
The actual list of dependencies of knplabs/gaufrette
As I already mentioned the GridFS
class is part of the Gaufrette library which provides a filesystem abstraction layer so you can store files on different types of filesystems without worrying about the details of those filesystems. Let’s take a look at the composer.json
file of this library:
{
"name": "knplabs/gaufrette",
"require": {
"php": ">=5.3.2"
},
"require-dev": {
...
},
"suggest": {
...
"amazonwebservices/aws-sdk-for-php": "to use the legacy Amazon S3 adapters",
"phpseclib/phpseclib": "to use the SFTP",
"doctrine/dbal": "to use the Doctrine DBAL adapter",
"microsoft/windowsazure": "to use Microsoft Azure Blob Storage adapter",
"ext-zip": "to use the Zip adapter",
"ext-apc": "to use the APC adapter",
"ext-curl": "*",
"ext-mbstring": "*",
"ext-mongo": "*",
"ext-fileinfo": "*"
},
...
}
After what we’ve discussed above, this is quite a surprise: the library says it has only one actual dependency: a PHP version that is at least 5.3.2. Everything else is either a “dev” requirement or a “suggested” requirement.
Of course people who use Composer and Packagist for some time now (like myself) have become quite used to this way of advertising the dependencies of a package. But it is just wrong. As we concluded earlier, ext-mongo
is a true dependency of the GridFS
class, yet looking at the composer.json
file it is only a suggested dependency.
This means that if I want to use the class in my project, it is not sufficient to require just the knplabs/gaufrette
package. I also have to add ext-mongo
as a requirement to my own project. Which is semantically wrong: it is not my project that needs the mongo
extension, it is the knplabs/gaufrette
package that actually needs it. Besides, how do I know which version of ext-mongo
I have to choose? Dependencies listed under the suggest
key in composer.json
don’t come with version constraints, so I have to figure them out myself.
Not just this package
knplabs/gaufrette
is not the only package out there that advertises actual, required dependencies as “suggested” dependencies. It is a convenient way for package maintainers to put a lot of different classes in a package that may or may not be needed by users. Since using those classes is optional, their dependencies are made optional too. But package maintainers forget that dependencies never are optional. They are always required, since the code would not be executable without them.
The solution
What package maintainers should do is split their packages. In the case of knplabs/gaufrette
this means there should be a knplabs/gaufrette
package containing all the generic code for filesystem abstraction. Then each specific adapter, like the GridFS
class, should live in its own package, e.g. knplabs/gaufrette-mongo-gridfs
. This package itself has the following dependencies:
{
...,
"require": {
"php": ">=5.3",
"knplabs/gaufrette": "~0.1"
"ext-mongo": ">=0.9.0"
}
}
No hidden dependencies there: everything is truly required for using the code in this package.
On the other hand the knplabs/gaufrette
package has almost no dependencies anymore, and the “adapter” packages are listed under the suggested
key:
{
"require": {
"php": ">=5.3.2"
},
"suggested": {
"knplabs/gaufrette-mongo-gridfs": "For storing files using Mongo GridFS",
...
}
}
This approach has many advantages:
-
The main package will be very stable. There are almost no reasons for it to change anymore, since all the moving parts are inside the “adapter” packages.
-
The adapter packages can have different specialists as maintainers, for instance the
knplabs/gaufrette-mongo-gridfs
can be maintained by someone who knows all about MongoDB. -
Users don’t have to keep track of available updates for parts of the library they don’t use.
-
Users don’t have to manually add extra dependencies to their projects (which means they don’t have to worry about version constraints for them).
So keep in mind, next time you are tempted to add a suggested dependency to your package: is it an actual dependency of (part of) the code in this package? Then split the package and reinstate that dependency as a true requirement. If all the code in the package works perfectly well without that suggested dependency, then you are indeed allowed to advertise it as a suggested dependency.
Want to know more?
I’m working on a book about package design principles, based on the work of Robert Martin. You may register yourself as an “interested reader” and receive a considerable discount when the first part of the book becomes available next week.
You may also want to read some of the articles about package coupling by Paul Jones (Frameworks are good, components are awesome!, Symfony components: sometimes decoupled, sometimes not). He maintains the Aura framework and components and does a great job when it comes to package coupling.