PHP & Symfony About PHP and Symfony2 development

There's no such thing as an optional dependency

Posted on by 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:

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

  2. Which PHP extensions should be installed?

  3. Which PEAR libraries should be installed?

  4. 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:

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

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

  3. Users don't have to keep track of available updates for parts of the library they don't use.

  4. 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?

Cover of Principles of PHP Package Design 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.

Categories: Book PHP

Tags: Composer coupling package design

Comments: Comments