Composer 2.0 is going to be released soon. There are some breaking changes, so tooling like composer plugins need to be updated to work with Composer 2.0.

At Yii Framework we tightly integrate with Composer so we have two plugins that need to work with Composer 2.0:

  • yii2-composer is a litte tool that helps Yii to determine which extensions are installed. It also prints upgrade notes when you update Yii.
  • composer-asset-plugin brings NPM and bower packages into the PHP world and allows you to manage your Javascript and CSS dependencies together with the PHP dependencies. This is heavily used by Yii widgets, which ship PHP backend code as well as frontend code.

While making yii2-composer work with Composer 2.0 I found that it was really hard to test if it is working well. So I set to write a test suite first to see if all functions are working as expected and then go update for 2.0 and run the suite against different versions.

The problem

When running an integration test in a prepared directory (empty directory with a composer.json in it), composer will download the plugin from packagist. To make it run the version I am currently working on, I would need to release a new version every time I want to test my plugin changes.

Here is a minimal version of a composer.json that would install yiisoft/yii2-composer plugin (because it is required by yiisoft/yii2).

{
    "require": {
        "yiisoft/yii2": "~2.0.16"
    },
    "repositories": [
        {
            "type": "composer",
            "url": "https://asset-packagist.org"
        }
    ]
}

This composer.json does two things:

  1. Require yiisoft/yii2, which in turn would also install the yii2-composer plugin.
  2. Add asset-packagist to provide asset packages required by Yii. This is a drop-in replacement for the composer-asset-plugin. I could have used composer-asset-plugin but I want only one plugin involved in this test.

As already mentioned, this will fetch the latest version of yii2-composer from packagist.

Now how to make composer use the code of the plugin from my developement environment?

The solution

Composer allows to define your own repositories, which allows you to provide packages from private repos or to use your own fork.

Using my own fork

Using my own fork would work like this:

{
    "require": {
        "yiisoft/yii2": "~2.0.16",
        "yiisoft/yii2-composer": "dev-mybranch as 2.0.x-dev"
    },
    "repositories": [
        {
            "type": "composer",
            "url": "https://asset-packagist.org"
        },
        {
            "type": "vcs",
            "url": "https://github.com/cebe/yii2-composer"
        }
    ]
}

I added "yiisoft/yii2-composer": "dev-mybranch" in the require section and also provided a version alias as 2.0.x-dev to make it compatible with the requirement of yiisoft/yii2.

This is quite simple and works, but I still need to commit and push every time I want to test it. This also does not work when I want to run tests on Travis CI or Github Actions. When switching the branch I’d need to adjust the composer.json file to point to that branch.

There has to be a better way. Ideally I’d want to provide the exact code from the current directory without committing, so even pointing composer to the local git directory would not work as it would only fetch the latest commit, but not the uncommitted changes.

Providing a package ZIP file

There is a part in the Composer documentation that sounds promising:

https://getcomposer.org/doc/05-repositories.md#package-2
If you want to use a project that does not support Composer through any of the means above, you still can define the package yourself by using a package repository.

Using the package type repository I can provide a ZIP file which composer will download when installing the package. In this case it will not download it, but just copy it over from the location I provide.

Step 1: Make a ZIP file, that contains the package code

find . -type f | grep -vP '^./.git|^./tests|^./vendor' | zip "/tmp/yii2-composer.zip" -@

The line above does the following:

  • Find all files in the current directory (recursively) using find.
  • Exclude all files I do not want in the package. I’m using grep here because I know how that works, there is probably a more elegant way in find directly :)
  • passing the list of files to the zip command, which will create /tmp/yii2-composer.zip from these. -@ instructs zip to read the file list from STDIN. How do I know? I read the manpage ;)

Step 2: Add the package repository

A first try on defining a package:

{
    "require": {
        ...
        "yiisoft/yii2-composer": "dev-test as 2.0.x-dev"
    },
    "repositories": [
        {
            "type": "package",
            "package": {
                "name": "yiisoft/yii2-composer",
                "version": "dev-test",
                "dist": {
                    "url": "file:///tmp/yii2-composer.zip",
                    "type": "zip"
                }
            }
        },
        ...
    ]
}

For a package definition, we need to provide at least name, version (because we provide only one version in the zip file), and either of dist or source. This would be enough to get the package installed, however there is still something missing.

When installing a package, Composer does not look at the composer.json from the ZIP file to determine things like autoloading or in our case the plugin configuration ("type": "composer-plugin" and the "class" field in the "extra" section). It needs this metadata from the repository. So in order to make it work correctly you need to copy the content of composer.json of yii2-composer into the "package" part of the repository definition, then add "version" and "dist" to it.

The final composer.json file looks like this:

{
    "require": {
        "yiisoft/yii2": "~2.0.16",
        "yiisoft/yii2-composer": "dev-test as 2.0.x-dev"
    },
    "repositories": [
        {
            "type": "composer",
            "url": "https://asset-packagist.org"
        },
        {
            "type": "package",
            "package": {
                "name": "yiisoft/yii2-composer",
                "description": "The composer plugin for Yii extension installer",
                "keywords": [
                    ...
                ],
                "type": "composer-plugin",
                "license": "BSD-3-Clause",
                "support": {
					...
                },
                "authors": [
					...
                ],
                "require": {
                    "composer-plugin-api": "^1.0 | ^2.0"
                },
                "require-dev": {
                    "composer/composer": "^1.0 | ^2.0@dev",
                    "phpunit/phpunit": "<7"
                },
                "autoload": {
                    "psr-4": {
                        "yii\\composer\\": ""
                    }
                },
                "autoload-dev": {
                    "psr-4": {
                        "tests\\": "tests"
                    }
                },
                "extra": {
                    "class": "yii\\composer\\Plugin",
                    "branch-alias": {
                        "dev-master": "2.0.x-dev"
                    }
                },
                "version": "dev-test",
                "dist": {
                    "url": "file:///tmp/yii2-composer.zip",
                    "type": "zip"
                }
            }
        }
    ]
}

Thats it. :)

For running repeatable tests I have also put together a script that does these things automatically.i
You can find the code at https://github.com/yiisoft/yii2-composer/tree/master/tests/scripts.

Conclusion

This example shows a situation where something seems very hard to test, which usually results in things being not tested detailed enough so that errors only show up after release.

With a bit of effort it is possible to dig into the problem and come to a solution that allows automated testing and make sure the plugin runs stable with different composer versions without the need for manually trying these out.