Automating Code Style Fixes with Git and PHP Coding Standards Fixer

We all know coding standards in the PHP community and try to follow them. Despite this, we are all human beings and make mistakes from time to time. That being said means that we have a few options here:

1) Fix all code style issues manually

2) Use plugins for IDE or run CLI tools to fix everything

These two options are not the ones I want to use. There is one more option - automate everything. Even though I'm going to use Git and PHP CS Fixer, the principles are simple and you can apply them to any language and any control version system. Keep reading to remove the routine and become a more productive developer.

Why Not First Two Options?

Fix Issues Manually

Yes, it's better to write everything right from the beginning, but as I said, we all make mistakes. Here are a few cons of the first point:

- The routine. Imagine that you fix code style issues day after day. Wouldn't you like to do something more interesting? I bet you would!

- Waste of time. Check the line above.

- Lack of permanency. You want to restrict any code style issues in your projects, right? If you do checks manually from time to time, then you will end up with additional code-style only commits in your Git history. It will look bad, like this:

- add feature 3
- code style fixes
- add feature 2
- add feature 1

Ugly, right? Sure, you can fixup commits, but then they won't be monatomic and will lose the main idea behind them.

One more: it makes code review harder and less productive. Instead of focusing on a serious parts of the code from the pull request, the reviewer will be distracted by the noise - code style fixes.

Plugins for IDE

This approach is not as bad as the first one, but:

- There may be no plugin for your IDE at all.

- You will need to configure your IDE to use code style fixer on each file save operation to be consistent. If your IDE can't do it, then you're out of luck.

- I don't want to force anybody on our team to use one particular IDE. Everybody should be able to use his preffered tool without any restrictions.

That's why we need to solve the code style consistency problem at the lowest possible level that isn't IDE aware and will work for everybody.

CLI Tools

If you're out of luck with your IDE, then this option is the one you'd probably go with, but it has one con:

- Lack of permanency. Again, you mustn't forget to run it.

- Waste of time. Just imagine: you added a few files to the staging area in Git, only then remembered that you need to run PHP CS Fixer. You ran it, and now you need to add modified versions of these files back to the staging area from the working directory. It may be even worse: you created a commit and only then remembered about code style... You also need to feed the fixer with only needed files to speed up the process. It doesn't make too much sense to fix all files in the project if you modified just a few of them.

Automation

I want to describe a few simple steps that will help you improve the whole picture. If you don't use Git and still strugle with SVN, read about the Git-SVN bridge.

To follow coding standards, we need to complete a few steps:

1) Check and fix code style issues on the client before files are committed and definitely before code gets to the main repository.

2) As we can't believe the client side validation, we will need some checks on the server side, too.

This time I'm going to describe only the first step.

Client Side Automation

I use PHP in my projects, so I'm going to use PHP-related code style fixers, but you can easily adopt the idea for your project.

We have a beautiful tool named PHP Coding Standards Fixer. It allows you to validate your code or even to fix it. As our purpose is to automate the fixing code style issues process, we want the fixer to fix our mistakes, not just show them.

Git pre-commit Hook

In Git we have the pre-commit hook. As you can guess from its name, the main idea behind this hook is deadly simple: emit this event just before you "commit". If this hook returns 0, then commit will be allowed, if the hook returns 1, the commit will be rejected.

All hooks are located in the .git/hooks directory in the root directory of your project. You can find *.sample files there. All we need now is to create the pre-commit file or rename pre-commit.sample to pre-commit. After that, Git will run it each time you perform commit.

Great! Now we need to better form and describe our requirements to this hook one more time.

Requirements to the Automation

If you aren't comfortable with the file states in Git, please read about The Three States in Git.

1) I don't want to run the fixer over all of the files in my project, but only through the ones that are staged for commit. This will speed up the process.

2) I want the fixer to fix the files from the staging area, not just show issues.

3) Fixed files must be added back to the staging area. If the files in the staging area are modified by the fixer, their modified versions will be in the working directory area, not in the staging one. That's why I want the automation to be smart enough to add the modified by the fixer files back to the staging area. This way I will be sure that I will commit the fixed versions of my files.

4) Force the fixer to check only *.php files because it doesn't make sense to check the files with other extensions.

5) We need a way to use a config file for the PHP fixer without altering the Git pre-commit hook.

Ok, the requirements are formed, let's implement them.

The .git/hooks/pre-commit file

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

// get a list of files in the staging area
exec('git diff --cached --name-only', $stagedFiles);

$fixedFiles = [];
foreach ($stagedFiles as $fileName) {
    // check only .php files
    // is_file - to avoid problems with "renamed" and "deleted" files.
    if (preg_match('/\.php$/', $fileName) && is_file($fileName)) {
        exec(sprintf('php-cs-fixer fix %s -q', $fileName), $output, $exitCode);

        // 1 code means that there were fixes
        if ($exitCode === 1) {
            // add the fixed file back to the staging area
            exec('git add ' . $fileName);
            $fixedFiles [] = $fileName;
        }
    }
}

if (count($fixedFiles)) {
    echo sprintf("Code style fixes were applied to the following files:\n\n%s\n\nFiles were added to the commit after fixes.\n\n", implode("\n", $fixedFiles));
}

// allow commit
exit(0);

That's it. Now, each time before I commit, Git will execute my pre-commit file and all PHP related code style issues on the client will be fixed automatedly. Don't forget to set up right permissions:

chmod +x .git/hooks/pre-commit

Testing the Fixer

For our test we will create a few files:

1) with-issues.php (this one we will add to the staging area).

<?

function sum    ($a, $b) {


    return $a+$b;
}

2) unstaged.php (this one will be modified, but not added to the staging area).

<?

function sadNews      () {return 'I will not be fixed :(';}

3) test.txt (this one is empty, it's just to test that the fixer doesn't care about *.txt files).

<?php

function test ()

{return uniqid();}

Ok, we've just created our demo files. Let's add two of them to the staging area:

git add with-issues.php
git add test.txt

If we perform git status, we will see:

On branch master

Changes to be committed:

(use "git reset HEAD <file>..." to unstage)

new file: with-issues.php

new file: test.txt

Untracked files:

(use "git add <file>..." to include in what will be committed)

unstaged.php

As you can see, the only two files will be commited. Let's commit them:

git commit -m 'the php cs fixer automation test'

Output:

Code style fixes were applied to the following files:

with-issues.php

Files were added to the commit after fixes.

[master 753b5bd] automated code style check
 2 files changed, 11 insertions(+)
 create mode 100644 with-issues.php
 create mode 100644 test.txt

From this output, we can see that our two files were added to the commit, but the PHP fixer fixed only one of them with the .php extension. I also want to point out, that the fixer didn't check the unstaged.php file, because it wasn't in the staging area and this is exactly what we need.

If you run git status again, you will see that unstaged.php is still there. Let's cat our with-issues.php file to check if it was really fixed:

cat with-issues.php
<?php

function sum($a, $b)
{
    return $a + $b;
}

As you can see, everything is fixed. The only one thing that is left to check is our point number 5 - usage of a custom config file. 

A Custom Config File

Luckily, the PHP Coding Standards Fixer looks for a .php_cs file in the root directory of your project. If you want to use a custom config, then just add this file and write there your own, very custom configuration. This file must return the Symfony\CS\ConfigInterface interface.

Note

You can't store the pre-commit hook in the code repository, so you will need to keep an eye on it and ensure that all your team members have it.

Conclusion

As you can see, the implementation was quite simple. It will save you time, make your life a little bit easier and the Git history more pretty. Hope this small instruction will help you automate the routine tasks and avoid code style issues in your project. This way your eyes won't be distracted by code style fixes noise and you will be focused on valuable things in PRs instead.

See also:

Working with svn through git

I thought that nobody used SVN anymore and our world was a better place to live and work in. I was wrong. I recently agreed to fix some issues on a project and only when I got it did I understand that some people still use svn in their projects. For me it was total frustration, but it was too late to reject the project (yes, now you can laugh :) ). I didn't want to work with svn, so, I needed a way to work with it through git, and I found it. In this article I want to describe how to work with svn through your best dcvs - git. If you're out of luck for some reason and the project you've got uses svn, then you definitely need to read this article. Working with svn through git is as easy and almost as pleasant as working with git itself, I promise