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 apre-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 yourVagrantfile
is located. The script will enter that directory and grab yourvagrant 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!
Top comments (0)