DEV Community

Steve Crow
Steve Crow

Posted on • Originally published at smcrow.net on

Running phpcs on Vagrant as a Git Hook

One of the major advantages to using a virtual machine like Vagrant is that you are able to isolate your development environment from your development workstation.

However, sometimes not having a copy of PHP installed can cause some complications when you want to run scripts. For most things you can use vagrant ssh or some of PHPStorm’s built-in utilities, but what if you want to automate some of these things?

I’m going to assume that you already have vagrant setup and have installed phpcs. I’m also assuming you’re using either Mac or Linux. I have tried to get this working on my Windows box, but git bash won’t cooperate.

PHP_CodeSniffer

PHP_CodeSniffer is a set of PHP scripts that can be used to detect coding standards violations. It also comes with a companion script phpcbf which will automatically correct these violations.

Mine is installed via composer require "squizlabs/php_codesniffer=*" --dev

It can be really easy to forget to run phpcs so one might find it useful as a git pre-commit hook.

Git Hooks

Git has various different hooks located in .git/hooks/ that will automatically trigger on certain events. However, if I’m using git from my local machine, but running PHP on Vagrant, how am I going to take advantage of these hooks? That’s the question I’m aiming to answer.

The pre-commit Hook

The pre-commit hook is triggered when you type git commit before the commit is successful. It uses the return code from the script to determine whether or not to allow the commit to happen.

Some people would recommend using a different hook, but I like the pre-commit for the following reasons:

  • I commit often, and it would be nice for each one of these to be checked for code standards.
  • By blocking the commit I can then run phpcbf on the specific file and include those changes as part of the commit.
  • I generally don’t like to revert commits, or add unnecessary commits. If I were to use this as a post-commit or a pre-push hook, it would require adding to the history.

Creating the hook

You’ll notice in your .git/hooks/ directory there is a pre-commit.sample file. You can either rename this file, or create a new file:

cd .git/hooks
touch pre-commit
chmod +x pre-commit

Inside of this pre-commit file I have the following script:

#!/bin/bash
# Runs PHPCS on specified vagrant box

# Variables ###################################
###############################################
# Vagrant Directory
# Path to your Vagrant file
vagrantDirectory='/Users/crow/Vagrant/homestead'

# PHP Application Directory
# The full path to your PHP application
applicationDirectory='/home/vagrant/code'

# PHPCS Location
# The full path to PHPCS on your Vagrant box
# This is not necessary if phpcs is installed globally
# Can also reference it within your application directory (recommended)
phpcsPath="${applicationDirectory}/vendor/squizlabs/php_codesniffer/bin/phpcs"
###############################################

# First grab the staged files
stagedFiles=()
while read file
do
    stagedFiles+=("${applicationDirectory}/${file}")
done < <(git diff --cached --name-only --diff-filter=AM)

# Second construct the phpcs command.
command=$(echo "${phpcsPath} -n --report=full ${stagedFiles[@]}")

# Third get SSH config from Vagrant
# This is done because sometimes vagrant ssh -c can be wonky
currentDirectory=$(pwd)
cd $vagrantDirectory
sshConfigOptions=$(vagrant ssh-config | awk 'NR>1 {print " -o "$1"="$2'})
cd $currentDirectory

# Fourth run the SSH command and capture the results
results=$(ssh $sshConfigOptions localhost $command)

# Fifth setup blocking for git commit
# This is not the ideal way to do this, but we're going to use
# regular expressions to determine success.

# You may need to modify this line depending on your phpcs settings.
foundErrors=$(grep -o "FOUND [0-9]* ERRORS" <<< ${results})
returnCode=0 # RC 0 will let the commit pass

if [[$foundErrors != '']]
then
    echo "${results}"
    echo "Fix or commit --no-verify"
    returnCode+=1
fi
exit $returnCode

I’m going to start with a disclaimer that I am not a bash expert. This script is lacking quite a few things, most notably it won’t display any sort of nice messages should your vagrant box not be up. It also won’t verify the information that you’ve given it, but it works for me.

The Variable Section

The variable section is where you need to define a few things.

  • The vagrantDirectory is the directory where your Vagrantfile is located. The script will enter that directory and grab your vagrant ssh-config options for the SSH connection.
  • The applicationDirectory this is where your files are located on the vagrant box itself. When running commands in a non-interactive shell, it’s usually a good idea to fully qualify the location to various things.
  • The phpcsPath is used to tell the script where to find phpcs. Again, we’re running non-interactive here, so any alias you have setup will not work.

How it Works

The script itself is pretty simple. It starts by grabbing the list of staged files. This is done in the following way:

Step 1: Grab the Staged Files

stagedFiles=()
while read file
do
    stagedFiles+=("${applicationDirectory}/${file}")
done < <(git diff --cached --name-only --diff-filter=AM)

It’s using the git diff --cached --name-only --diff-filter=AM command to get a list of files that are currently staged. The diff-filter tells it to only look at files that have been A dded or M odified. We can’t validate a file that has been deleted, for obvious reasons.

The loop gets this information from the command and constructs and array of the files. There’s probably a way to condense the output from git down to a single line, but I like this approach because I can use that list of files to run other things if necessary.

Step 2: Construct the phpcs Command

The command we’re using expects you to give it a list of files at the end, separated by a space.

command=$(echo "${phpcsPath} -n --report=full ${stagedFiles[@]}")

Since we’re storing these files in an array, it’s easy to get a space-delimited list by using ${stagedFiles[@]}.

The -n flag tells the command to only give us errors, and the --report=full will give us a full report of all the errors that were found.

Step 3: Get the SSH config from Vagrant

Vagrant has a command vagrant ssh which accepts a -c flag for sending commands through SSH. This command can get a little weird when you want to send multiple commands, and I don’t find it as flexible. Instead, I chose to grab the ssh-config from Vagrant and use it to SSH and send the commands.

currentDirectory=$(pwd)
cd $vagrantDirectory
sshConfigOptions=$(vagrant ssh-config | awk 'NR>1 {print " -o "$1"="$2'})
cd $currentDirectory

We save the current working directory, change into the Vagrant directory, and then build a $sshConfigOptions string which we will pass to the SSH command. The awk part takes each line and converts it to -o key=value.

Step 4: Run the SSH Command

results=$(ssh $sshConfigOptions localhost $command)

Now we run the SSH command and store it in the $results variable. This allows us to block any sort of MOTD or banner that might appear.

Step 5: Block the Git Commit if Necessary

This is a bit hacky, but we use regular expressions to check and see if any errors are reported. Because phpcs has been instructed to ignore warnings, it won’t show any messages if there are no errors.

foundErrors=$(grep -o "FOUND [0-9]* ERRORS" <<< ${results})
returnCode=0 # RC 0 will let the commit pass

if [[$foundErrors != '']]
then
    echo "${results}"
    echo "Fix or commit --no-verify"
    returnCode+=1
fi
exit $returnCode

If there are errors, we increment the return code which will cause the commit to fail, and with give the user some instruction as well as print out the results.

The Results

Here are the results when I try to commit a file with some serious errors:

git commit

FILE: /home/vagrant/code/app/Sample.php
----------------------------------------------------------------------
FOUND 3 ERRORS AFFECTING 3 LINES
----------------------------------------------------------------------
 2 | ERROR | [] Missing file doc comment
 3 | ERROR | [] Missing class doc comment
 5 | ERROR | [x] Line indented incorrectly; expected 4 spaces, found
   | | 0
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Time: 97ms; Memory: 6Mb
Fix or commit --no-verify

Some Other Things to Consider

Here’s a few other things worth mentioning.

Stashing Files

You might have changes that you don’t want to get picked up in phpcs. It might be a good idea to stash the changes before running the commit, and then pop them afterwards.

That Pesky MOTD

I’m using Homestead as my test environment which doesn’t have an MOTD. However, another one of my boxes has the obnoxious Ubuntu MOTD.

It might be a good idea to figure out when the first error occurs, and to remove all of the text from $results before that error. That prevents a lot of the useless messages from appearing.

In another version of this hook I use the --report=summary and the following set of commands to trim out the motd. I also display a picture of Gandalf because that commit isn’t going to pass.

# You've angered the wizard - http://textart.io/art/tag/gandalf/1
# Art to show in case you messed up.
read -r -d '' gandalf << "EOM"
------------------------------------------
                           ,---.
                          / |
                         / |
You shall not pass! / |
                       / |
                  ___,' |
                < -' :
                 `-.__..--'``-,_\_
                    |o/ <o>` :,.)_`>
                    :/ ` ||/)
                    (_.).__,-` |\
                    /( `.`` `| :
                    \'`-.) ` ; ;
                    | ` /-<
                    | ` / `.
    ,-_-.. ____/| ` :__..-'\
   /,'-.__\\ ``-./ :` ; \
   `\ `\ `\\ \ : ( ` / , `. \
     \` \ \\ | | ` : : .\ \
      \ `\_ )) : ; | | ): :
     (`-.-'\ || |\ \ ` ; ; | |
      \-_ `;;._ ( ` / /_ | |
       `-.-.// ,'`-._\__/_,' ; |
          \:: : / ` , / |
           || | ( ,' / / |
           || ,' / |
------------------------------------------
EOM

foundMessage=$(grep -o "[0-9]* ERRORS AND [0-9]* WARNINGS WERE FOUND" <<< $results)
returnCode=0
if [[$foundMessage != '']]; then
    echo "${gandalf}"
    phpCsReport=$(sed '/PHP\ CODE\ SNIFFER\ REPORT\ SUMMARY/,$!d' <<< "${results}")
    echo "${phpCsReport}"
    returnCode+=1
fi

The sed '/PHP\ CODE\ SNIFFER\ REPORT\ SUMMARY/,$!d' <<< "${results} command basically deletes all of the text before the match.

Other Things to Check

My version only checks PHP. Well, technically, it sends everything to phpcs, but only the PHP will get checked. However, this could also be used to check JavaScript, Java, or any other languages you might be working in.

Multiple Commands

If you’re going to send multiple commands to the Vagrant box, I find it a lot easier to store them in a temporary file and then cat the file to the SSH command. Something like this:

"command one" >> commands.temp.sh
"command two" >> commands.temp.sh
"commands three" >> commands.temp.sh

results=$(cat commands.temp.sh | ssh $sshConfigOptions localhost}
rm commands.temp.sh

Conclusion

Working with Vagrant and using git hooks can be a super powerful workflow. It might have been easier to just bite the bullet and install PHP locally, but where’s the fun in that?

Let me know if you think of any other fun tricks!

Discussion (0)