Composer: Replace, Conflict & Forks Explained

Recently there has been an increase of cases in which Composer installs a fork of a package instead of the package the user expects. Most frequently these are forks of packages using a “replace” statement in their composer.json. These forks are usually meant for private use only but are still published on Packagist.

Developers: composer update. Automated systems: composer install.

First of all, this behavior is not a security issue in Composer. While the behavior is unintuitive, it will not result in malicious code being used if you use Composer correctly. Most importantly you should only ever run composer update yourself manually on your development machine. You should read its output, verify it installed and updated packages as expected (use --dry-run to check what would happen without installing anything). You should then commit the generated composer.lock file to your version control system. Continous integration, deployment tools and other automated systems should always run composer install. A composer install run will always use the exact packages that were installed by composer update which was used to generate the lock file – no surprises possible.

Replace identifies which original versions a fork is API compliant with

Secondly it is not wrong to publish forks on Packagist. While we discourage the publication of forks intended solely for private use, you are encouraged to publish forks of open source projects which you intend to maintain for others on Packagist. You may use the replace key to specify which versions of the original package your fork is an API compliant replacement for. You should avoid using too generic version constraints (e.g. * or >=1.0.0) with replacements as with all other dependencies.

Dependency Resolution with Replace, Conflict & Provide

After downloading the list of available packages Composer tries to find a set of packages that satisfies all version constraints specified by all dependencies and their dependencies recursively. Package dependencies are specified as a tuple of name and version constraint. Only such packages will be considered that match the name and version constraint, or replace or provide the specified name with a version constraint matching the given version constraint, and do not match a name and version constraint of a conflict specified in any other package about to be installed. This means that a conflict resulting in a package being excluded, alternatives which replace or provide that name will be considered.

Debugging unexpected fork installations

If your dependencies lead to a conflict with a package Composer may decide to install a fork instead which does not have the same conflict. If you notice that an unexpected fork is installed when running composer update you can debug the dependency problem that lead to the fork installation. Use the conflict key in your composer.json to blacklist the fork. Its structure matches that of the require key. If you run composer update again you will now receive an error message explaining the problem, as the fork is no longer available as an alternative solution to the dependency issue.

{
    "require": {
        "my-favorite/framework": "1.0.0",
        "cool/library-which-conflicts": "0.0.1"
    }
    "conflict": {
        "fork-of/my-favorite-framework": "*"
    }
}

Planned changes to improve usability

We understand that Composer’s behaviour regarding forks and package replacements is unintuitive. So I’ve proposed a number of changes yet to be implemented to the handling of replace & provide on our issue tracker at https://github.com/composer/composer/issues/2690.

TL;DR

Replace is not a bug. Don’t run composer update in automated systems. Forks are allowed on Packagist. Don’t be an idiot when publishing a fork. Got an unexpected fork on update? Your dependencies conflict with the original package. Use conflict (syntax like require) in your composer.json to blacklist the fork and see an explanation of the dependency issue.

Need Composer Consulting? Contact me at composer-consulting@forumatic.com.

9 comments

  1. I’m sorry, but this puts the onus on the end-user consumer of a package, and requires that they know obscure details of composer configuration. I consider myself an advanced user of Composer (I maintain multiple satis repositories, publish dozens of packages, and have given talks on it), and had not run into this particular setting.

    For end-user consumers of a package, this becomes a nightmare; they suddenly get a different package than what they expect, and have no idea how or why. Most will not find this post, or the relevant documentation, and either give up on Composer, or take their risks with the forked code, and hope it’s not malicious or out-of-date.

    This is not a feature, it’s a security issue, plain and simple. To treat it as anything less is negligence.

    My proposal:

    – Packagist should either not honor “provides” and/or “replaces”, OR
    – Packagist should only honor “provides” and/or “replaces” from the same _vendor_.

    If developers want to use alternate packages, they would need to add them explicitly in their own project configuration.

    1. I agree that currently the user is required to know obscure details. The very purpose of this article is to explain these, so that users have a chance to understand them until this behaviour is changed.

      We do intend to change how this feature works, hence the section “Planned changes to improve usability”. Please read my proposal at https://github.com/composer/composer/issues/2690 – We will keep the very useful provide and replace features but they will no longer lead to this kind of confusion. As I explained in this article there are legitimite uses for provide and replace and these will remain. Limiting alternatives using provide and forks using replace to custom repositories would restrict Packagist far too much, and unfairly disadvantage forks over original packages.

      Restricting provide and replace to the same vendor is absurd, as it is essentially the same as removing the feature altogether. The very purpose of provide and replace is for one vendor to offer alternative but compatible packages to another.

      1. Everything you say can, and SHOULD, be done at the project level. These should be features developers opt-in to, not enabled by default. Making them opt-in means that the developer understands the risk, and specifically wants that functionality; doing otherwise is going to make issues like this keep cropping up. As it is, I’ve seen time and time again developers wondering why they are not getting the latest release of a package, only to find out that a competing package from an alternate vendor was installed. This sets up a situation where each project has to police packagist regularly for forks that conflict with what they provide. This is untenable.

        “Secure by default” should be the mantra.

          1. If you say so. 🙂 My remark to that is to make the “Proposed Solution” section demonstrate a composer.json, what the current result might be (in terms of installed packages), what the new result would be (in terms of resolved packages), and how the composer.json could be modified to force installation of alternate packages. The verbiage currently is ambiguous, because as a consumer, I don’t know what changes I may need to make for specific circumstances.

            Also, a timeline for getting these changes into Composer is very necessary at this point, as the current situation is making people reconsider the tool. Composer has revolutionized how PHP developers manage dependencies, and sparked a renaissance in sharing of code — but this will be for naught if people lose trust in it. Communicate clearly how the changes will make the tool more secure, and when developers may expect to see the changes in place. If you need help, reach out to the various communities using/encouraging the tool so that we can assist in either the fixes or testing.

            1. I’m surprised for this. I thought that past year we had fixed this issue. Never any package can be a replacement for a version which don’t exist in the replaced package.

Leave a reply to naderman Cancel reply