Skip to content

Instantly share code, notes, and snippets.

@pfrenssen
Last active May 10, 2022 14:00
Show Gist options
  • Save pfrenssen/498fc52fea3f965f6640 to your computer and use it in GitHub Desktop.
Save pfrenssen/498fc52fea3f965f6640 to your computer and use it in GitHub Desktop.
Git hook to check coding standards using PHP CodeSniffer before pushing

About

This is a git pre-push hook intended to help developers keep their PHP code base clean by performing a scan with PHP CodeSniffer whenever new code is pushed to the repository. When any coding standards violations are present the push is rejected, allowing the developer to fix the code before making it public.

To increase performance only the changed files are checked when new code is pushed to an existing branch. Whenever a new branch is pushed, a full coding standards check is performed.

If your project enforces the use of a coding standard then this will help with that, but it is easy to circumvent since the pre-push hook can simply be deleted. You might want to implement similar functionality in a pre-receive hook on the server side, or include a coding standards check in your continuous integration pipeline.

Usage

Add PHP CodeSniffer and this gist to your composer.json:

composer.json

{
  "require-dev": {
    "squizlabs/php_codesniffer": "~2.3",
    "pfrenssen/phpcs-pre-push": "1.0"
  },
  "repositories": [
    {
      "type": "package",
      "package": {
        "name": "pfrenssen/phpcs-pre-push",
        "version": "1.0",
        "source": {
          "url": "https://gist.github.com/498fc52fea3f965f6640.git",
          "type": "git",
          "reference": "master"
        }
      }
    }
  ],
  "scripts": {
    "post-install-cmd": "scripts/composer/post-install.sh"
  }
}

Create a post-install script that will symlink the pre-push inside your git repository whenever you do a composer install:

scripts/composer/post-install.sh

#!/bin/sh

# Symlink the git pre-push hook to its destination.
if [ ! -h ".git/hooks/pre-push" ] ; then
  ln -s "../../vendor/pfrenssen/phpcs-pre-push/pre-push" ".git/hooks/pre-push"
fi

Create a PHP CodeSniffer ruleset containing your custom coding standard:

phpcs.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- See http://pear.php.net/manual/en/package.php.php-codesniffer.annotated-ruleset.php -->
<ruleset name="MyRuleset">
  <description>Custom coding standard for my project</description>

  <rule ref="PSR2" />

  <!--
    Scan the entire root folder by default. If your application code is
    contained in a subfolder such as `lib/` or `src/` you can use that instead.
   -->
  <file>.</file>

  <!-- Minified files don't have to comply with coding standards. -->
  <exclude-pattern>*.min.css</exclude-pattern>
  <exclude-pattern>*.min.js</exclude-pattern>

  <!-- Exclude files that do not contain PHP, Javascript or CSS code. -->
  <exclude-pattern>*.json</exclude-pattern>
  <exclude-pattern>*.sh</exclude-pattern>
  <exclude-pattern>*.xml</exclude-pattern>
  <exclude-pattern>*.yml</exclude-pattern>
  <exclude-pattern>composer.lock</exclude-pattern>

  <!-- Exclude the `vendor` folder. -->
  <exclude-pattern>vendor/</exclude-pattern>

  <!-- PHP CodeSniffer command line options -->
  <arg name="extensions" value="php,inc,css,js"/>
  <arg name="report" value="full"/>
  <arg value="p"/>
</ruleset>

Now run make the post-install.sh script executable and run composer update and you're set! This will download the required packages, and will put the git pre-push hook in place.

$ chmod u+x scripts/composer/post-install.sh
$ composer update
#!/usr/bin/env php
<?php
/**
* @file
* Git pre-push hook to check coding standards before pushing.
*/
/**
* The SHA1 ID of an empty branch.
*/
define ('SHA1_EMPTY', '0000000000000000000000000000000000000000');
$file_list = [];
$full_check = FALSE;
// Loop over the commits.
while ($commit = trim(fgets(STDIN))) {
list ($local_ref, $local_sha, $remote_ref, $remote_sha) = explode(' ', $commit);
// Skip the coding standards check if we are deleting a branch or if there is
// no local branch.
if ($local_ref === '(delete)' || $local_sha === SHA1_EMPTY) {
exit(0);
}
// Do a full check if this is a new branch.
if ($remote_sha === SHA1_EMPTY) {
$full_check = TRUE;
break;
}
// Escape shell command arguments. These should normally be safe since they
// only contain SHA numbers, but you never know.
foreach (['local_sha', 'remote_sha'] as $argument) {
$$argument = escapeshellcmd($$argument);
}
$command = "git diff-tree --no-commit-id --name-only -r '$local_sha' '$remote_sha'";
$file_list = array_merge($file_list, explode("\n", `$command`));
}
// Remove duplicates, empty lines and files that no longer exist in the branch.
$file_list = array_unique(array_filter($file_list, function ($file) {
return !empty($file) && file_exists($file);
}));
// If a phpcs.xml file is present and contains a list of extensions, remove all
// files that do not match the extensions from the list.
if (file_exists('phpcs.xml')) {
$configuration = simplexml_load_file('phpcs.xml');
$extensions = [];
foreach ($configuration->xpath('/ruleset/arg[@name="extensions"]') as $argument) {
// The list of extensions is comma separated.
foreach (explode(',', (string) $argument['value']) as $extension) {
// The type of file can be specified using a slash (e.g. 'module/php') so
// only keep the part before the slash.
if (($position = strpos($extension, '/')) !== FALSE) {
$extension = substr($extension, 0, $position);
}
$extensions[$extension] = $extension;
}
}
if (!empty($extensions)) {
$file_list = array_filter($file_list, function ($file) use ($extensions) {
return array_key_exists(pathinfo($file, PATHINFO_EXTENSION), $extensions);
});
}
}
if (empty($file_list) && !$full_check) {
exit(0);
}
// Get the path to the PHP CodeSniffer binary from composer.json.
$command = getcwd() . '/vendor/bin/phpcs';
if ($composer_json = json_decode(file_get_contents(getcwd() . '/composer.json'))) {
if (!empty($composer_json->config->{'bin-dir'})) {
$bin_dir = escapeshellcmd(trim($composer_json->config->{'bin-dir'}, '/'));
$command = getcwd() . '/' . $bin_dir . '/phpcs';
}
}
// Check if the PHP CodeSniffer binary is present.
if (!is_executable($command)) {
echo "error: PHP CodeSniffer binary not found at $command\n";
exit(1);
}
// Run PHP CodeSniffer and exit.
$file_filter = $full_check ? '' : " '" . implode("' '", $file_list) . "'";
passthru($command . $file_filter, $return_value);
exit($return_value);
@danielpopdan
Copy link

Command arguments set from phpcs.xml are not passed to the final command. I fixed this issue, please se https://gist.github.com/danielpopdan/990f9521f84d693ccd1a .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment