---
title: Adding Git Hooks Through Composer Dev-Dependencies
canonical: "https://www.alainschlesser.com/thinking/php-composter/"
pubDate: "2016-03-28T00:00:00.000Z"
updatedDate: "2016-07-10T00:00:00.000Z"
author: Alain Schlesser
description: "A Composer custom installer that wires PHP methods into Git hooks as dev-dependencies, so a single composer install replaces the README ritual of symlinking pre-commit scripts. The symlink-and-bootstrap mechanics, and why deduplicated, autoloaded actions beat copied Bash across projects."
tags: [PHP, "Tooling & Automation"]
---

I have been working on refining my development workflow for some time now, in order to optimize the quality of my code. And as I am a big fan of automation, I tend to look for tools that just do their work in the background and only need my input when my mental processing power is truly needed. This leads to a greater focus on the coding and problem solving area of development.

One of the areas that was missing a satisfactory solution so far was the <code>pre-commit</code> checks that <code>git</code> was doing when new changes were committed to a repository. I had a bash script that was provided with my code, together with a small instruction in the <code>README.md</code> file that was explaining how to symlink that file into the <code>.git/hooks</code> folder. It did work, but it needed manual interaction first in order to be able to do its work.

<h3>Meet PHP Composter (<em>or</em> It Which Has Been Misnamed)</h3>

To improve upon this, I have written a <em>Composer</em> package that acts as a custom installer for <em>Git</em> hooks. It has the very silly name of <a href="https://github.com/php-composter/php-composter" target="_blank"><code>PHP Composter</code></a>, which is a word play on both "<em>Composer</em>" and "<em>Post-Commit</em>" (unfortunately, the package name <code>PrePosterous</code> was already taken).

You wouldn't use this package directly, though, as it will not do much. Instead, you will use packages of the type <code>php-composter-action</code>, that depend upon the above package to be installed and configured as <em>Git</em> hooks. Through a combination of custom <em>Composer</em> installer code, the <code>"extra"</code> key, and lots of intermediary files and symbolic links, this enables automatically attaching <em>PHP</em> methods to <em>Git</em> hooks.

<h3>The benefits of this approach</h3>

<ul>
    <li>Code to be executed through <em>Git</em> hooks is correctly defined and used as a development dependency. For production deployments (using <code>composer install --no-dev</code>), the entire <em>PHP Composter</em> stuff is simply ignored, without leaving any traces.</li>
    <li>The dependency chain is used in the correct order. Your project does not depend on an executable like <em>PHP CodeSniffer</em>, but rather on the one action that needs to be executed upon commit. This action then takes care of whatever stuff it needs.</li>
    <li>The onboarding process for new developers is simplified, as they don't need to "<em>create symlinks</em>", "<em>copy binaries</em>", or whatever else is usually done. A single <code>composer install</code> takes care of all that is needed to start developing.</li>
    <li>The <em>Git</em> hooks can be written in <em>PHP</em> instead of <em>Bash</em>, which is probably preferable for developers using <em>Composer</em>.</li>
    <li>The wiring up of the <em>Git</em> hooks and the tools used by them is done through <em>Composer</em>'s A<em>utoloader</em>, which avoids messing around with the local system's <code>PATH</code> settings and environment variables.</li>
    <li>As <em>Composer</em>'s <em>Autoloader</em> is used, this also means that a large project with several dozen libraries will still only pull in each dependency like <em>PHP CodeSniffer</em> once, instead of having small <em>Bash</em> scripts duplicating across all projects.</li>
    <li>The actual actions can be centrally maintained and updated, and a simple <code>composer update</code> will incorporate any changes into existing packages.</li>
</ul>

If you're not interested in knowing more about the inner workings of this system, you don't need to read the rest of this blog post and should head straight to the <a href="https://github.com/php-composter/php-composter#existing-php-composter-actions" target="_blank">Existing PHP Composter Actions</a> section of <em>PHP Composter</em>'s README file. There's only one existing action as of the time of this writing (<a href="https://github.com/php-composter/php-composter-phpcs-psr2" target="_blank">checking <em>PSR-2 Coding Style Guide</em> compliance through <em>PHP CodeSniffer</em></a>), and a second one is waiting for a <a href="https://github.com/squizlabs/PHP_CodeSniffer/pull/936" target="_blank">pull request</a> to be merged into <em>PHP CodeSniffer</em> to work correctly (checking <em>WordPress Coding Standards</em> compliance through <em>PHP CodeSniffer</em>).

<h3>Technical Details</h3>

To make this work, <em>PHP Composter</em> will first tell <em>Composer</em> to use an alternative installation path for packages of type <code>php-composter-action</code>; instead of being installed into <code>vendor//</code>, they will be installed into <code>.git/php-composter/actions/</code>.

Then, <em>PHP Composter</em> will look through the package's <code>"extra"</code> key in <code>composer.json</code> to find a key named <code>"php-composter-hooks"</code> which contains mappings between <em>Git</em> hook names and <em>PHP</em> methods. It will then create a file <code>.git/php-composter/config.php</code> that collects the mappings of all such packages.

```php
<?php
// PHP Composter configuration file.
// Do not edit, this file is generated automatically.
// Timestamp: 2016/03/28 05:03:19

return array(
    'applypatch-msg' => array(
    ),
    'pre-applypatch' => array(
    ),
    'post-applypatch' => array(
    ),
    'pre-commit' => array(
        10 => array(
            'PHPComposter\PHPComposter_PHPCS_PSR2\Sniffer::preCommit',
        ),
    ),
    'prepare-commit-msg' => array(
    ),
    'commit-msg' => array(
    ),
    'post-commit' => array(
    ),
    'pre-rebase' => array(
    ),
    'post-checkout' => array(
    ),
    'post-merge' => array(
    ),
    'post-update' => array(
    ),
    'pre-auto-gc' => array(
    ),
    'post-rewrite' => array(
    ),
    'pre-push' => array(
    ),
);
```

Then, it will create symbolic links from each <em>Git</em> hook ( <code>.git/hooks/</code>) to the <em>Bash</em> script <code>vendor/php-composter/php-composter/bin/php-composter</code>.

```bash
#!/bin/bash

# ------------------------------------------------------------
# Git Hooks Management through Composer.
#
# @package   PHPComposter\PHPComposter
# @author    Alain Schlesser <alain.schlesser@gmail.com>
# @license   MIT
# @link      http://www.brightnucleus.com/
# @copyright 2016 Alain Schlesser, Bright Nucleus
# ------------------------------------------------------------

# Collect environment data.
hook=`basename "$0"`
package_root=`pwd`

# Make sure we have a PHP binary.
php=`which php`
if [[ $? -ne 0 ]]; then
    printf "PHP binary could not be found. PHP Composter aborted.\n"
    exit
fi

# Verify that the bootstrap.php file exists.
if [[ ! -f $package_root/.git/php-composter/includes/bootstrap.php ]]; then
    printf "PHP Composter bootstrap file was not found. Please reinstall.\n"
    exit
fi

# Launch the engine.
$php $package_root/.git/php-composter/includes/bootstrap.php $hook $package_root
```

This shell script is only a small wrapper to launch the file <code>.git/php-composter/includes/bootstrap.php</code> (a symbolic link to <code>vendor/php-composter/php-composter/includes/bootstrap.php</code>), which is responsible for parsing the previously generated configuration file and calling the corresponding action.

```php
<?php
/**
 * Git Hooks Management through Composer.
 *
 * @package   PHPComposter\PHPComposter
 * @author    Alain Schlesser <alain.schlesser@gmail.com>
 * @license   MIT
 * @link      http://www.brightnucleus.com/
 * @copyright 2016 Alain Schlesser, Bright Nucleus
 */

use PHPComposter\PHPComposter\Paths;

// Get the command-line arguments passed from the shell script.
global $argv;
$hook = $argv[1];
$root = $argv[2];

// Initialize Composer Autoloader.
if (file_exists($root . '/vendor/autoload.php')) {
    require_once $root . '/vendor/autoload.php';
}

// Read the configuration file.
$config = include Paths::getPath('git_config');

// Iterate over hook methods.
if (array_key_exists($hook, $config)) {

    $actions = $config[$hook];

    // Sort by priority.
    ksort($actions);

    // Launch each method.
    foreach ($actions as $calls) {
        foreach ($calls as $call) {

            // Make sure we could parse the call correctly.
            $array = explode('::', $call);
            if (count($array) !== 2) {
                throw new RuntimeException(
                    sprintf(
                        _('Configuration error in PHP Composter data, could not parse method "%1$s"'),
                        $call
                    )
                );
            }
            list($class, $method) = $array;

            // Instantiate a new action object and call its method.
            $object = new $class($hook, $root);
            $object->init();
            $object->$method();
            $object->shutdown();
            unset($object);
        }
    }
}
```

As you can see from the code above, this will get the hook name from the small <em>Bash</em> script that was executed, iterate over the configuration file to find the corresponding action, instantiate the action's class, and finally execute that action.

This whole "symlinking business" is done on each <code>composer install</code> and <code>composer update</code> to always keep your <em>Git</em> hooks current, and to avoid having stale links (for example, by removing the <code>vendor</code> folder), <em>Composer</em> is always told that the actions are not yet installed, to force a complete redeploy on each change.

This is still a very early release for <em>PHP Composter</em>, so I appreciate any feedback. I do think that this is very useful and I will incorporate it into all of my projects. I would be happy to hear about any other uses of <em>PHP Composter</em> out in the wild, so do let me know of your specific use cases!
