How To Automate Projects Using Composer Scripts

How To Automate Projects Using Composer Scripts at Master Zend Framework

How well do you truly know Composer? Do you just know install, update, and require, along with some of the composer.json configuration? Or do you know it deeply?

In the first part of this series, we started digging into Composer, looking at a range of command line options which Composer provides. We also looked at some of the switches which can be passed to those commands.

Here, in the second part of the series, we’ll look at the scripts section of composer.json. If you’ve never heard of this section, it provides a way to automate tasks in your project.

Perhaps you think that this is unnecessary, as there is already such a wealth of tools available; including Make, Ant, Phing, and so on. But I see a place for having automation in Composer — though at first I didn’t.

Why? Because you can bring everything that much closer together. Because you can keep everything in a very tidy, organized, and well-structured way.

You don’t have to look as far and wide as you may have before to know:

  • What’s going on
  • How it’s setup
  • Where it’s configured, and so on

What’s more, it’s a tool intimately intertwined with PHP. Why leave the language you’re using to create the rest of your application? Stay in the same mindset. This isn’t always a good thing; though it can be helpful.

And in a world we forever have less and less time but are expected to do ever more, this can only be a good thing.

Disagree with me? Shout at me in the comments.

How Does The Scripts Section Work

The scripts section of composer.json allows you to set up a range of commands which relate to your project, commands which call command-line executables and PHP callbacks.

The commands can be named as you see fit, such as test, clean, deploy and so on. Or they can use the names of events which Composer fires during its execution process, such as post-root-package-install, pre-install-cmd, and post-package-update.

In today’s tutorial, I’m going to take you through examples which highlight both approaches. The first one will highlight using arbitrary names, and the second will highlight using Composer events.

I've based both on real world examples, taken from the Zend Expressive Skeleton Installer and Laravel projects respectively. I've also included a custom one for use explicitly with Expressive projects.

The Scripts Section

But before we can do that, what does the scripts section look like. In essence, it’s a list of key -> value pairs. The key is the name of the command, and the value is what to run.

Take this example:

"scripts": {
  "cs-check": "phpcs"
}

Here, we’ve listed one script, or command, called cs-check. When called, it will invoke the phpcs command, assuming that you have it installed.

But you aren’t limited to just single commands. You can also combine commands into a chain to create more sophisticated structures.

Have a look at this next example:

"scripts": {
  "cs-check": "phpcs",
    "cs-fix": "phpcbf",
    "test": "vendor/bin/codecept run",
    "cs": [
      "@cs-check",
      "@cs-fix"
    ]
}

Here, we’ve created a command called cs, which runs a list of commands. Looking in the commands list, you can see that they’re prefixed with @. This means that the commands are referring to other commands, rather than to command-line executables.

So, this example would assume that you had two other commands listed, called cs-check and cs-test, which we do. When this command is run, it will call these, in the order listed.

Running Composer Scripts

Now that we’ve seen how to create some commands let’s see how to run them. To do so, we need to call Composer from the command line, and then pass the name of the command to it.

Assuming we wanted to call the cs-check command, then we’d execute it like so:

composer cs-check

Note: Make sure that you don’t name one of your commands the same as an existing Composer command.

A Short Example - From Zend Expressive

Now let’s look at a complete example, which you can see below, available with applications generated using the Zend Expressive Skeleton Project.

"scripts": {
    "check": [
      "@cs",
      "@test"
    ],
    "cs-check": "phpcs",
    "cs-fix": "phpcbf",
    "test": "vendor/bin/codecept run",
    "test-coverage": "phpunit --colors=always --coverage-clover clover.xml",
    "upload-coverage": "coveralls -v"
},

You can see that it has six sub-sections. The check command, as we’ve already seen, runs two existing commands: cs and test. cs-check calls phpcs and cs-fix calls phpcbf.

Both of these are part of the PHP CodeSniffer package. I’ve modified test from the one that comes with the Skeleton Installer to use Codeception, my preferred testing library.

Here, it calls vendor/bin/codecept passing the run switch to run all of the available unit, functional, and acceptance tests.

test-coverage calls phpunit specifying that colors are to be displayed and coverage data is to be generated. Finally upload-coverage runs the coveralls command.

A word of warning: Composer won’t check if you have the command-line executables installed. It will just attempt to do what you’ve asked. It’s your responsibility to ensure that the commands you wish to execute are present.

Responding to Events in The Composer Lifecycle

Now let’s see how to respond to events which Composer fires, as well as how to create a custom callback in PHP. Here is a stripped down version of the scripts section which comes with all Laravel projects.

"scripts": {
  "post-install-cmd": [
    "Illuminate\\Foundation\\ComposerScripts::postInstall",
    "php artisan optimize"
  ],
},

You can see that it’s setup a callback, Illuminate\\Foundation\\ComposerScripts::postInstall, and an executable, php artisan optimize, to respond to the post-install-cmd event. When that event is triggered, these will be called in turn.

So, what does Illuminate\\Foundation\\ComposerScripts::postInstall look like?

namespace Illuminate\Foundation;
use Composer\Script\Event;

class ComposerScripts
{
    public static function postInstall(Event $event)
    {
        require_once $event
          ->getComposer()
          ->getConfig()
          ->get('vendor-dir').'/autoload.php';

        static::clearCompiled();
    }

    protected static function clearCompiled()
    {
        $laravel = new Application(getcwd());
        if (file_exists($compiledPath = $laravel->getCachedCompilePath())) {
            @unlink($compiledPath);
        }
        if (file_exists($servicesPath = $laravel->getCachedServicesPath())) {
            @unlink($servicesPath);
        }
    }
}

Here, you can see that it has no inheritance hierarchy. So, there’s only minimal effort required to create one. Then, the method which is called is a static, which is passed a Composer\EventDispatcher\Event object.

With the Event object, you can get access to a wide range of information about the environment in which Composer is running, along with configuration details, and so on.

In this instance, it ran Composer’s autoloader and called the clearCompiled() method, which clears the cache directories.

A Zend Expressive Example

Now let’s bring things closer to home, by looking at an example for Zend Expressive which does largely what the Laravel code does.

Create a new directory, called src/Automation and in there create a new PHP class, called Events.php. After that, in your project’s composer.json file, ensure that a new namespace called Automation is registered, by having the autoload section include the following:

"autoload": {
  "psr-4": {
    "Automation\\": "src/Automation/"
  }
},

After that, add the following code, which we’ll step through, to Event.php. It starts off by defining the required namespace and referencing the needed classes.

<?php

namespace Automation;

use Composer\Script\Event;
use Zend\ServiceManager\ServiceManager;

After that, we define the core function postInstall, which will be called when Composer fires the post-install-cmd event, which is fired when the package is installed.

This function runs Composer’s autoloader, making all of the required dependencies available, and then calls the clearCacheDirectories method.

class Events
{
  public static function postInstall(Event $event)
  {
    $vendorDir = $event->getComposer()->getConfig()->get('vendor-dir');
    require $vendorDir . '/autoload.php';

    static::clearCacheDirectories();
  }

The clearCacheDirectories() function is the core of the command. It first gets access to Zend Expressive’s DI container, and through that, retrieves the cache configuration settings (which we’ll see in a moment).

With these, it can know the names, and locations, of the application’s cache directories. It then uses PHP’s glob function to get all of the files located in these directories, and iterates over and deletes them if they’re either a file or a directory.

  public static function clearCacheDirectories()
  {
    /** @var ContainerInterface $container */
    $container = require 'config/container.php';
    $cacheConfig = $container->get('config')['cache'];

    foreach ($cacheConfig['directories'] as $dir) {
      $files = glob(sprintf(
        __DIR__ . '/../../%s/%s/*',
        $cacheConfig['basePath'],
        $dir
      ));
      foreach ($files as $file) {
        if (is_file($file) || is_dir($file)) {
          unlink($file);
        }
      }
    }
  }
}

The Cache Configuration

Now that we have the code completed, create a new file called cache.global.php which contains the following:

<?php

return [
  'cache' => [
    'basePath' => 'cache',
    'directories' => [
      'logs',
      'data'
    ],
  ]
];

This says that the base cache path is the directory cache off of the root of the project directory and that it contains two subdirectories: logs and data.

Registering the Event

The last thing which we have to do is to have our function be called when the post-install-cmd event is fired. We do this, by adding the following snippet to the scripts section of our project’s composer.json:

"post-install-cmd": "Automation\\Events::postInstall"

With that done, when composer install is run inside of our project, our class will be called, and delete any file or directory inside of the cache directories, if they exist.

For a new project, it’s unlikely that you would have any cache files. And in a scalable application, you’d likely not use a filesystem cache. But, the example is a good one for showing how to create a callable for use with Composer.

Key Things To Remember About Event Callback Classes

It’s important to remember the following points about callbacks from the Events documentation:

  • PHP classes containing defined callbacks must be autoloadable via Composer's autoload functionality.
  • Callbacks can only autoload classes from PSR-0, PSR-4 and classmap definitions. If a defined callback relies on functions defined outside of a class, the callback itself is responsible for loading the file containing these functions

In Conclusion (tl;dr)

That’s the basics of the scripts section of composer.json. We’ve looked at how the section is laid out, that you can run existing executables, and that you can create custom callable functions in PHP to respond to events which Composer triggers during its lifecycle.

Now you’re in a great position to better automate your projects, all from the comfy confines of PHP, the language we use on a regular basis.

If you want to learn make sure to check out the other two parts of this series. In the first part of the series, we covered the Composer command-line essentials. And in the third and final part of the series, you’ll learn how to use forked repositories with Composer.

Further Reading



About Matthew

Matthew Setter Matthew Setter is a PHP & Zend Framework specialist. If you're in need of a custom software application, need to migrate an existing legacy application, or want to know your current application's GPA - get in touch.