It’s easy to get caught up in testing patterns, frameworks, and other
“correct” ways to to
handle testing, but we often overlook some of the simple things PHP can do to
make sure we aren’t making basic mistakes.
This article covers one of those overlooked tools: php -l
.
Running the PHP executable from the command line with the -l
tells PHP to check the syntax of the provided PHP file, or “lint”
the file.
Let’s take a look at what this does. Create a file called hello.php
with this as its content:
<?php
echo "Hello World!"
You might have noticed, there’s a syntax error. That’s
so we can demonstrate what php -l
does
when it encounters a syntax error.
Now, from the command line, run the following. Your output should be similar.
prompt> php -l hello.php
Parse error: syntax error, unexpected $end, expecting ',' or ';' in hello.php on line 4
Errors parsing hello.php
That tells us that PHP expected a comma, or a semicolon, but it got to the
end of the file instead. Add a semicolon after the echo "Hello World!"
line and run it again. This time it should look like this:
prompt> php -l hello.php
No syntax errors detected in hello.php
PHP’s lint mode is useful for making sure files you’re getting ready to commit are at least syntactically correct. It’s not a replacement for real unit tests, but it’s a nice gut check for code that doesn’t have tests written yet. Committing a simple syntax error is embarrassing, and this helps you double check yourself.
Running this command manually ends up taking way too much time. Fortunately, most VCSs
have the ability to add scripts that run before you make a commit. These
scripts are called pre-commit
hooks. A pre-commit
hook is just a script that can be run from the
shell. It can keep a commit from being allowed depending upon what
exit code it uses when it stops. A normal exit code, or zero, tells the VCS
to allow the commit; anything else tells it to not allow the commit.
Git is my VCS of choice, so that’s what I’ll use for this article. There’s a
quick crash course available if you’re
not familiar with Git and want to follow along. Adding a
pre-commit
hook script to a Git repository
is straight-forward. Create a .git/hooks/pre-commit
script with the following code in it.
#!/usr/bin/php
<?php
$output = array();
$return = 0;
exec('git rev-parse --verify HEAD 2> /dev/null', $output, $return);
$against = $return == 0 ? 'HEAD' : '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
exec("git diff-index --cached --name-only {$against}", $output);
$filename_pattern = '/\.php$/';
$exit_status = 0;
foreach ($output as $file) {
if (!preg_match($filename_pattern, $file)) {
// don't check files that aren't PHP
continue;
}
$lint_output = array();
exec("php -l " . escapeshellarg($file), $lint_output, $return);
if ($return == 0) {
continue;
}
echo implode("\n", $lint_output), "\n";
$exit_status = 1;
}
exit($exit_status);
There are some pieces of this code that might look like voodoo, but let’s walk through them. The first line is called a Shebang. It tells your system to execute the file using the executable specified. This is how your system knows that the file it’s executing is PHP code.
Some systems install PHP in different locations. Another common place is
/usr/local/bin/php
.
Adjust as necessary if your PHP executable lives somewhere else.
You can type whereis php
from most Unix-like operating systems to find it.
The <?php
is self-explanatory. The next two lines set
up temporary variables. exec()
takes
its second and third arguments by reference, so we define them here
to the defaults we want, then pass them in.
On the next line, we call exec()
. It executes the
git rev-parse
command which determines
if you’re getting ready to make the first commit to your repository or if there are other
commits. The 2> /dev/null
line at the end of the command keeps
any errors from being output. What we really care about here is the
$return
variable we created earlier.
It’s still 0
if our repository has commits in it.
After $return
is populated, we use it to determine what
we’re comparing our code against. If it is 0
, we can use
HEAD
to determine what files have
changed; otherwise, we use 4b825dc642cb6eb9a060e54bf8d69288fbee4904
to compare against an empty commit.
Next, we call git diff-index
with a few parameters. First,
we add --cached
to tell Git we only want files that are going
to be committed. Then, we add --name-only
to tell Git to
only output the name of the files that are being committed.
Notice this time we only pass in one extra argument to exec()
, the $output
variable. We don’t care what the return value is; we only want the output
from the command. $output
is an array populated with each line
as its own entry. git diff-index
outputs one file per line, so
it works perfectly to create an array of files that have changed.
Now we’re almost ready to start checking the files. There are a few variables
that need to be initialized. $filename_pattern
is a regular expression
to determine if the file we have needs to be linted. You can modify this if
you use something other than .php
for your file extensions.
Note: You can do this check much more elegantly using FilterIterators. I’m using regular expressions, because I imagine more people are familiar with them. Check out Elizabeth Smith’s article from last year’s PHP Advent explaining how to use FilterIterators instead if you’re interested.
Next, we set the $exit_status
to 0
.
Remember that our pre-commit
script needs to exit with a
zero to say it was successful. This sets the default state.
We’ve got a list of files that are about to be committed in $output
.
Now the script starts looping over that list.
Each file is checked against our regular expression to see if it is a PHP
file to test.
Files that make it past that first check are run through php -l
.
Notice we’re grabbing the output and the return value again. PHP will exit
with a zero if the file has no syntax errors. If that’s what we get, we just continue along.
If not, we display the output by imploding the $lint_output
array
and changing the $exit_status
to 1 which
signifies that there was an error.
The final line exits using the $exit_status
value. Now, let’s try
this. Put a copy of the above file in your .git/hooks/pre-commit
and make sure it is executable (chmod +x .git/hooks/pre-commit
).
Create the hello.php
from earlier with the syntax error in it
and try to commit that to your repository.
prompt> git add hello.php
prompt> git commit -m "testing pre-commit"
Parse error: syntax error, unexpected $end, expecting ',' or ';' in hello.php on line 4
Errors parsing hello.php
Now you’re set. Every time you run git commit
, Git runs your
pre-commit
script to verify that every PHP file you commit is at
least syntactically correct. You could extend this to run unit tests for your files,
or any number of tasks before you commit.
All this Git talk might have you worried if you’re running Subversion. You can do the same thing in Subversion. Matthew Turland put together a blog post explaining how he does it.
<shameless plug>
If this Git stuff is new to you and you’re interested
in learning more, in addition to the excellent
community documentation
available online, you can check out my book,
Pragmatic Version Control using Git,
for an introduction to Git. It’s scheduled to ship later this month.</shameless plug>