PHP
Article

Writing PHP Git Hooks with Static Review

By Matthew Setter

If you’ve been using Git for more than a short length of time, you’ll hopefully have heard of Git hooks. If not, Git hooks, as Andrew Udvare introduced here on SitePoint back in August of last year, are scripts which run, based on certain events in the Git lifecycle, both on the client and on the server.

Hook illustration

There are hooks for pre- and post-commit, pre- and post-update, pre-push, pre-rebase, and so on. The sample hooks are written in Bash, one of the Linux shell languages. But they can be written in almost any language you’re comfortable or proficient with.

As Tim Boronczyk pointed out in September 2013, there are a wide range of meaningful purposes which hooks can be put to. These include linting, spell-checking commit messages, checking patches against accepted coding standards, running composer, and so on.

Now, PHP’s a great language, don’t get me wrong, but it’s not necessarily the best language for shell scripting. Admittedly, it’s gotten a lot better than it once was. But compared to languages such as Bash, Python, and Ruby, it hasn’t been quite as suitable for creating Git hooks; that is, until now.

Thanks to Static Review, by Samuel Parkinson, you can now write Git hooks with native PHP, optionally building on the existing core classes. In today’s post, I’m going to give you a tour of what’s on offer, finishing up by writing a custom class to check for any lingering calls to var_dump().

Installing the Library

Like most, if not all modern PHP libraries and packages, StaticReview is installable via Composer. To install it in your project, run composer require sjparkinson/static-review in the root of your project. With that, let’s get going.

A Working Example

Like any code, the best way to understand it is to step through a real example, such as the one below, taken from the StaticReview project repository.

Firstly, like all shell scripts, it defines what will run the script. In this case, it’s PHP. After that, the Composer autoload script is included, so that the script has access to all the required classes, as well as a warning, should the autoload script not be available.

#!/usr/bin/env php
<?php

$included = include file_exists(__DIR__ . '/../vendor/autoload.php')
    ? __DIR__ . '/../vendor/autoload.php'
    : __DIR__ . '/../../../autoload.php';

if (! $included) {
    echo 'You must set up the project dependencies, run the following commands:' . PHP_EOL
       . 'curl -sS https://getcomposer.org/installer | php' . PHP_EOL
       . 'php composer.phar install' . PHP_EOL;

    exit(1);
}

Next, as with most modern PHP scripts, all of the required classes are imported, and three variables are initialized. These are: a Reporter, a CLImate, and a GitVersionControl object. The Reporter object provides information about the hook, whether the hook succeeded or failed. The CLImate object makes outputting colored text and text formats simple. And the GitVersionControl object simplifies interactions with Git.

// Reference the required classes and the reviews you want to use.
use League\CLImate\CLImate;
use StaticReview\Reporter\Reporter;
use StaticReview\Review\Composer\ComposerLintReview;
use StaticReview\Review\General\LineEndingsReview;
use StaticReview\Review\General\NoCommitTagReview;
use StaticReview\Review\PHP\PhpLeadingLineReview;
use StaticReview\Review\PHP\PhpLintReview;
use StaticReview\StaticReview;
use StaticReview\VersionControl\GitVersionControl;

$reporter = new Reporter();
$climate  = new CLImate();
$Git      = new GitVersionControl();

Next, it creates a new StaticReview object, and passes in the types of reviews to run in the hook. In the case below, it adds five. These reviews:

  • Checks if the file contains any CRLF line endings
  • Checks if the set file starts with the correct character sequence
  • Checks if the file contains NOCOMMIT.
  • Checks PHP files using the built-in PHP linter, php -l.
  • Checks if the composer.json file is valid.

Then, it tells the review to check any staged files.

$review = new StaticReview($reporter);

// Add any reviews to the StaticReview instance, supports a fluent interface.
$review->addReview(new LineEndingsReview())
       ->addReview(new PhpLeadingLineReview())
       ->addReview(new NoCommitTagReview())
       ->addReview(new PhpLintReview())
       ->addReview(new ComposerLintReview());

// Review the staged files.
$review->review($Git->getStagedFiles());

Similar to other forms of validation, if issues are reported by any of the review classes, each issue is printed out to the terminal, in red, preceded by an , and the commit does not complete. However, if there are no problems, then a success message, ✔ Looking good. Have you tested everything?, is printed out, and the commit is allowed to complete.

// Check if any matching issues were found.
if ($reporter->hasIssues()) {
    $climate->out('')->out('');
    foreach ($reporter->getIssues() as $issue) {
        $climate->red($issue);
    }
    $climate->out('')->red('✘ Please fix the errors above.');
    exit(1);
} else {
    $climate->out('')->green('✔ Looking good.')->white('Have you tested everything?');
    exit(0);
}

So far, nothing you’d be unfamiliar with, if you’ve worked with Git hooks before. But how, specifically, does a review class operate? Every Review class extends AbstractReview, which implements ReviewInterface. This interface requires two methods to be implemented: canReview(), and review().

canReview, as the name implies, determines if a review can be run, and review does the actual review. Take ComposerLintReview.php as an example, which you can see below. canReview() checks if the file being reviewed is called composer.json. If so, review() can be called.

This then creates a command which invokes Composer’s validate functionality on composer.json, and passes that to the getProcess method, implemented in AbstractReview, running the process. If the process is not successful, an error message is created, and set on the Reporter object, by passing it to a call to Reporter’s error() method, as well as the file which was reviewed.

public function canReview(FileInterface $file)
{
    // only if the filename is "composer.json"
    return ($file->getFileName() === 'composer.json');
}

public function review(ReporterInterface $reporter, FileInterface $file)
{
    $cmd = sprintf('composer validate %s', $file->getFullPath());
    $process = $this->getProcess($cmd);
    $process->run();

    if (! $process->isSuccessful()) {
        $message = 'The composer configuration is not valid';
        $reporter->error($message, $this, $file);
    }
}

In a nutshell, that’s all that’s required to create a Git hook to validate files on one of the events in the Git lifecycle.

A Custom Review

If you browse under vendor/sjparkinson/static-review/src/Review/, you’ll see there are quite a number of pre-packaged Review classes available. They cover Composer, PHP, and general purpose reviews. But what if we want to create one ourselves, one to suit our specific use case?

What if we’re concerned that we might leave var_dump statements in our code? We wouldn’t, right? But hey, never hurts to be sure, as old habits can sometimes die hard. So what would a custom review look like? Let’s work through one and find out.

First, we’ll create a new directory structure to store our PSR-4 compliant code. Use the following command in the project’s root directory.

mkdir -p src/SitePoint/StaticReview/PHP

Then, in composer.json, add the following to the existing configuration, and run composer dumpautoload:

"autoload": {
  "psr-4": {
    "SitePoint\\": "src/SitePoint/"
  }
}

This will update Composer’s autoloader to also autoload our new namespace. With that done, create a new class, called VarDumpReview.php in src/SitePoint/StaticReview/PHP. In it, add the following code:

<?php

namespace SitePoint\StaticReview\PHP;

use StaticReview\File\FileInterface;
use StaticReview\Reporter\ReporterInterface;
use StaticReview\Review\AbstractReview;

class VarDumpReview extends AbstractReview
{
    public function canReview(FileInterface $file)
    {
        $extension = $file->getExtension();
        return ($extension === 'php' || $extension === 'phtml');
    }

    public function review(ReporterInterface $reporter, FileInterface $file)
    {
        $cmd = sprintf('grep --fixed-strings --ignore-case --quiet "var_dump" %s', $file->getFullPath());
        $process = $this->getProcess($cmd);
        $process->run();

        if ($process->isSuccessful()) {
            $message = 'A call to `var_dump()` was found';
            $reporter->error($message, $this, $file);
        }
    }
}

Based off of PhpLintReview.php and ComposerLintReview.php, canReview checks if, based on the extension, the file being checked is a PHP file. If so, review then uses grep to scan the file for any references to var_dump.

If any are found, an error is registered, which tells the user that a call to var_dump was found, and the commit fails. If you were to run it, you could expect output as in the screenshot below.

StaticReview failure

Creating the Hook

There’s just one last step to go, which is to create a Git hook from our hook class. In a new directory Hooks in my project root, I’ve copied the hook code we worked through at the beginning of the article, making one small change.

I added our new Review file to the list of Reviews, as follows:

$review->addReview(new LineEndingsReview())
       ->addReview(new PhpLeadingLineReview())
       ->addReview(new NoCommitTagReview())
       ->addReview(new PhpLintReview())
       ->addReview(new ComposerLintReview())
       ->addReview(new VarDumpReview());

With that done, create a pre-commit hook, by running the following command.

./vendor/bin/static-review.php hook:install hooks/example-pre-commit.php .Git/hooks/pre-commit

And with that, if you look in .git/hooks, you’ll now see a symlink created from pre-commit, to our new hooks file. To test the hook, make a call to var_dump() in any PHP file, stage the file, and attempt to commit it.

You shouldn’t even be allowed to create a commit message before the error message shows. If you then update the file to remove the call to var_dump(), you should see a success message, before being able to add a commit message as in the image below.

StaticReview success

Wrapping Up

And that’s all it takes to create simple or powerful Git hooks, using one of the most versatile languages around, PHP. I only came across Static Review thanks to the ever vigilant Bruno Skvorc. But I’m really thankful he suggested checking it out.

With it, I can now do one more, ever important development task, using my favorite software development language – PHP. Are you already using Static Review? If so, share your experience in the comments. I’m keen to know how people more experienced than myself are using it.

  • http://www.skooppa.com s.molinari

    Nice article Matthew. Something I’ll be coming back to for reference in the future for sure!

    Scott

    • http://www.matthewsetter.com/ Matthew Setter

      Great to hear Scott. Though I wrote it, I have the same sentiment.

  • http://www.matthewsetter.com/ Matthew Setter

    From a superficial read through, it looks good. I love the last two screenshots. I’m sure grmpyprogrammer would love them. Thanks for sharing.

    • http://careersreport.com Rita Moran

      Follow success of many who& are making profit monthly by doing an online job… Learn more on my~profile

    • http://careersreport.com Rita Moran

      Follow success of many who& are making profit monthly by doing an online job… Learn more on my~profile

  • Karl Merkli

    Whats that ZSH theme?

    • http://www.matthewsetter.com/ Matthew Setter

      Just the standard one. Nothing special.

  • abc

    very nice

  • abc

    Very nice is there any work availble for PHP ??

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.