The Death Star Version Constraint

PHPUnit 6, a new major version of the de-facto standard for testing PHP-based software, was recently released. Soon after, the test suites of developers who did not yet want to upgrade to the new version stopped working. What had happened?

In a nutshell, these PHP developers were caught off guard by the consequences of using the "Death Star" version constraint in their composer.json file, the * operator.

You see, neither * nor unbounded ranges such as >= 1.0 can be considered version constraints, really.

If you use

{
    "require-dev": {
        "phpunit/phpunit": "*"
    }
}

or

{
    "require-dev": {
        "phpunit/phpunit": ">= 4.8"
    }
}

then you are asking for trouble as you instruct Composer to install the newest version of PHPUnit. This could be a major version, as was the case with PHPUnit 6.0.0, that is not backward compatible and will not be able to run your test suite without a migration effort.

Semantic Versioning

Most of the components that you can depend on using Composer use semantic versioning. In a nutshell, this means that you can rely on the following three simple rules for interpreting their version numbers:

  • The major version is incremented when there are incompatible changes, for instance when the public API changes. PHPUnit, for instance, also increments the major version when it drops support for a version of PHP that was previously supported.
  • The minor version is incremented when new functionality is added in a backwards-compatible manner.
  • The patch version is incremented when bugs are fixed in a backwards-compatible manner.

Composer supports the idea of semantic versioning through its caret operator (^):

{
    "require-dev": {
        "phpunit/phpunit": "^5.7"
    }
}

Using the version constraint shown above, ^5.7, you instruct Composer to install the latest version of PHPUnit that is backward compatible with PHPUnit 5.7.

Keep Dependencies Fresh

Back in 2013 I wrote:

"It makes a lot of sense to use third-party components in the development of your software. However, when you use a component that is not (sufficiently) tested you run the same risk as the cook who uses an off-the-shelf sauce from the supermarket. You do not know exactly what the code of the component does. It may work fine for the "happy path". But what about the edge cases? Even worse is using a third-party component that is no longer maintained. It may work fine today. But will it still work when a new version of PHP, for instance, is released?"

What I wrote in 2013 is still valid. But there is a point that I did not make back then: keep your dependencies fresh. When the active support for PHP 5 ended I wrote:

"Upgrading the version of PHP you use must not be a rare event you are afraid of. You must not think of upgrading your PHP stack as a "special project". You need to make upgrading the PHP version you use part of your normal operational procedure and align the upgrade cycle of your PHP stack with the release cycle of the PHP project."

The same is true for any third-party component you use: keep them fresh to keep your software healthy.

If you use Composer to manage the dependencies of your project then you should use

{
    "require-dev": {
        "phpunit/phpunit": "^6.0"
    }
}

With this configuration, Composer will always install the latest version of PHPUnit that is compatible with PHPUnit 6.0.

This makes sure that you "stay fresh" as long as PHPUnit 6 is the current stable version of PHPUnit and includes new minor versions such as PHPUnit 6.1. And when the time comes and PHPUnit 7 is released then Composer will not automatically and unexpectedly install it.

Upgrading a dependency to a new major version must be a conscious decision that is part of a defined process. This process should at least include the reading of the ChangeLog.

Über den Autor

Sebastian Bergmann
Sebastian Bergmann
Twitter LinkedIn Xing
Artikel teilen
Migrating to PHPUnit 6 Testen hält mich von der Arbeit ab