PHP

Test composer dependencies with prefer-lowest

This 4 year old post already blogged about it.
Surprisingly many libraries still don’t use it in their CI system yet.

The --prefer-lowest option has been added around that time.
Composer documentation says: "Useful for testing minimal versions of requirements".

Why and when is it useful?

The above article states:

To give your users the smoothest experience possible, it’s, therefore, best to try to nail down the oldest possible dependencies your package can still work with, and only increase that minimum version if you’re depending on the latest introduced feature, or to get around a major bug.

In other words:
It is possible that you declare dependencies to other libraries, but you only test the latest versions and therefore you accidentally use new functionality that breaks your code in previous versions.
In a pull request, you ideally would like to know about these things before you merge to master.

If your library, framework, framework plugin etc has no dependencies in "require", then you can skip this kind of check in your CI matrix.

A practical case

I was merging changes (via PR) to a plugin for the CakePHP framework.
The plugin was using cakephp/cakephp:"^3.6". At the time 3.7 already came out.
At some point then, one of the require-dev dependencies silently made the prefer-lowest run download 3.7 now, no one noticed that 3.6, as declared as minimal version, is not tested anymore.
Now the PR and the introduced changes used a method that was only available since 3.7. Tests are all still green here for the plugin.
All projects that are still on 3.6 download this new release and everything breaks at this point for them.
At this point, the plugin either should have been written in a compatible way (using existing methods of 3.6), or being 3.7+ and not downloadable by this project.
But now it is too late and you released sth that must be reverted as hotfix and then re-introduced as minor with proper constraints.

This could easily have been prevented if during the PR creation it would have been obvious that this will break 3.6 compatibility due to those new methods being used.

Composer prefer lowest validation

Now comes in handy a small validation script that analyzes the composer.json and composer.lock files, compares them and lets you know if the declared minimal versions cannot be tested (anymore).
In that case, you need to then see if you can either downgrade some of the require/require-dev dependencies that prevent going back here or if you simply raise the minimum here for the impossible constraint.
Usually, the latter is fine, as people at that time already are on a newer version.

This is useful for all libraries that want to make sure

  • the defined minimum of each dependency is actually still being tested
  • no silent regressions (like using too new methods of depending libraries) sneaked in

This has been built after Composer didn’t have the motivation for it to include it more natively.

A total must-have for

  • frameworks
  • framework plugins/addons (and testing against the framework minors)
  • custom libraries to be used by apps/projects which have at least one dependency on other libraries

It is somewhat important for the involved packages to follow semver here. Otherwise, some of the comparisons might be problematic.

This is not so useful for projects, as here there is no need to test against anything than the latest versions already in use.
Also, if your library has no dependencies, you can skip prefer-lowest checks as well as this validation.

Prefer-stable

Usually composer update --prefer-lowest suffices.
Make sure you have "prefer-stable": true in your composer.json for this to work.
Otherwise, you might have to use the longer version as outlined above.

In general, it is best to just use all flags for your CI script:

composer update --prefer-lowest --prefer-dist --prefer-stable --no-interaction

You do not want to test all the alpha and beta versions here, only actually stable releases 🙂

PHP version

In general: Use the minimum PHP version for prefer-lowest as defined in your composer.json.

At this point, with it being EOL already, you can and should not use any PHP version below 5.6 anyway, or provide support for it.

It is advised to also raise your composer.json entry for the min PHP version here. Use 5.6 or higher:

    "require": {
        "php": ">=5.6",

Many libraries already dropped 5.x support and directly went 7.1+ in 2018.

Travis CI setup

As an example, lets set up Travis for this:

php:
  - 5.6
  - 7.3

env:
  global:
    - DEFAULT=1

matrix:
  include:
    - php: 5.6
      env: PREFER_LOWEST=1

before_script:
  - if [[ $PREFER_LOWEST != 1 ]]; then composer install --prefer-source --no-interaction; fi
  - if [[ $PREFER_LOWEST == 1 ]]; then composer update --prefer-lowest --prefer-dist --prefer-stable --no-interaction; fi
  - if [[ $PREFER_LOWEST == 1 ]]; then composer require --dev dereuromark/composer-prefer-lowest; fi

script:
  - if [[ $DEFAULT == 1 ]]; then vendor/bin/phpunit; fi
  - if [[ $PREFER_LOWEST == 1 ]]; then vendor/bin/validate-prefer-lowest; fi

The validate-prefer-lowest will warn about the declared minimums not being reachable.

You can, of course, also directly include dereuromark/composer-prefer-lowest into require-dev. But since this is only needed for one check, it can be simpler to do it this way.
This way it will also not be affected by the prefer-dist option, as it shouldn’t and doesn’t need to be.

Be reasonable

Of course, you could also test all PHP versions and all combinations with prefer-lowest. But to what end?
Best to not waste too many resources here in terms of Travis jobs run, as the benefit of others than the above is 0.
A pragmatic approach focuses on the limits.

This simplified example aboce shows you a basic test for min/max PHP and prefer-lowest on top:

  • 5.6 (min)
  • 7.3 (max)
  • 5.6 + prefer-lowest (min and lowest dependencies)

It will – depending on your library – usually add quite a few more runs anyway:

  • DBs – each at least one job
  • checks (phpcs, phpstan, phan, psalm, …) – ideally as one job
  • coverage (if it can’t be included in an existing one)

I usually run coverage on one of the PHP versions not explicitly tested, e.g. 7.2 here. This way you do that "for free" for this PHP version.

Tips

CI cronjobs

In Travis CI, for example, you can add a daily cronjob trigger for the tests to run (if not run otherwise already).
You should enable this to get notified early about possible regressions. In these cases, it is less about the minimal version, though, and more about new versions of dependencies. But doesn’t hurt.

Composer require-dev

Since require-dev dependencies do not count as "real" dependencies when including this library (only require dependencies are pulled), you can here go higher. Don’t try to support older versions here.
You only make your life more difficult usually. A few patches or minors back here usually is fine.

Outlook

There is an open Composer issue that would enable prefer-lowest to only count for actual require dependencies.
This would further ease the process, as you can develop normally, but you can test against the relevant dependencies for the minimum.
Upvote it or implement a PR here if you would like.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.