Writing Composer Scripts

The composer.json file schema contains a script section that allows projects to define actions that are executed under certain circumstances. The most recognizable use for this section is to define events that happen at defined times during the execution of Composer commands; for example, the post-update-cmd runs at the end of every composer update command.

However, Composer also allows custom commands to be defined in this same section; an example of this is shown in the composer.json schema documentation
{
    "scripts": {
        "test": "phpunit"
    }
}

Once this custom command definition has been added to your composer.json file, then composer test will do the same thing as ./vendor/bin/phpunit or, equivalently, composer exec phpunit. When Composer runs a script, it first places vendor/bin on the head of the $PATH environment variable, so that any tools provided by any dependency of the current project will be given precedence over tools in the global search path. Providing all the dependencies a project needs in its require-dev section enhances build reproducibility, and makes it possible to simply “check out and go” when picking up a new project. If a project advertises all of the common build steps, such as test and perhaps deploy as Composer scripts, it will make it easier for new contributors to learn how to build and test it.

Whatever mechanism your project uses for these sorts of tasks should be clearly described in your README or CONTRIBUTING document. Providing a wrapper script is a nice service, as it insulates the user from changes to the default parameters required to run the tests. In the Linux world, it the sequence of commands make, make test, make install has become a familiar pattern in many projects. Make has a rich history, and is often the best choice for managing project scripts. For Composer-based projects that are pure PHP, and include all of their test and build tools in the require-dev section of their composer.json file, it makes a lot of sense to use Composer scripts as a lightweight replacement for Make. Doing this allows contributors to your project to get started by simply running git clone and composer require. This works even on systems that do not have make installed.

An example of a scripts section from such a project is shown below:

{
    "scripts": {
        "phar": "box build .",
        "cs": "phpcs --standard=PSR2 -n src",
        "cbf": "phpcbf --standard=PSR2 -n src",
        "unit": "phpunit",
        "test": [
            "@unit",
            "@cs"
        ]
    }
}

The snippet above offers the following actions:

  • phar: build a phar using the box2 application.
  • cs: run the PHP code sniffer using PSR2 standards.
  • cbf: run the PHP code beautifier to correct code to PSR2 standards, where possible.
  • unit: run the PHP unit tests via phpunit.
  • test: run the unit tests and the code sniffer.

One thing to note about Composer scripts, however, is that you might notice some changes in behavior when you run certain commands this way. For example, PHPUnit output will always come out in plain unstyled text. Colored output makes it a lot easier to find the error output when tests fail, so let’s fix this problem. We can adjust our definition of the test command to instruct phpunit to always use colored output text, like so:
{
    "scripts": {
        "test": "phpunit --colors=always"
    }
}

There is another difference that may affect some commands, and that is that standard input will be attached to a TTY when the script is ran directly from the shell, but will be a redirected input stream when run through Composer. This can have various effects; for example, Symfony Console will not call the interact() method if there is no attached TTY. This could get in your way if, for example, you were trying to write functional tests that test interaction, as the consolidation/annotated-command project does. There are a couple of options for fixing this situation. The first is to explicitly specify that standard input should come from a TTY when running the command:

{
    "scripts": {
        "test": "phpunit --colors=always < /dev/tty"
    }
}

This effectively gets Symfony Console to call the interact() method again; however, the down-side to this option is that it is not as portable; composer test will no longer work in environments where /dev/tty is not available. We can instead consider a domain-specific solution tailored for Symfony Console.  The code that calls the interactive method looks like this:

if (!@posix_isatty($inputStream) && false === getenv('SHELL_INTERACTIVE')) {
    $input->setInteractive(false);
}

We can therefore see that, if we do not want to provide a TTY directly, but we still wish our Symfony Console command to support interactivity, we can simply define the SHELL_INTERACTIVE environment variable, like so:

{
    "scripts": {
        "test": "SHELL_INTERACTIVE=1 phpunit --colors=always"
    }
}

That technique will work for other Symfony Console applications as well. Note that the SHELL_INTERACTIVE environment variable has no influence on PHPUnit itself; the example above is used in an instance where PHPUnit is being used to run functional tests on a Symfony Console application. It would be equally valid to use putenv() in the test functions themselves.

That is all there is to Composer scripts. This simple concept is easy to implement, and beneficial to new and returning contributors alike. Go ahead and give it a try in your open-source projects—who knows, it might even increase contribution.


You may also like: 

Topics Development, Drupal Planet, Drupal