Sharing PHP-CS-Fixer rules across projects and teams
Published on by timacdonald
PHP-CS-Fixer is an open-source tool that can enforce, and detect violations of, PHP coding styles. With predefined rules, it allows you to have a strict coding style that is enforced by the tool so that you can spend time on more important things.
Rule examples
Here are a few examples of the kinds of things PHP-CS-Fixer can do to your codebase:
Rule: is_null
Replaces is_null($var)
expression with $var === null
.
// before...if (is_null($account->closed_at)) { //} // after...if ($account->closed_at === null) { //}
Rule: mb_str_functions
Replace non-multibyte-safe functions with corresponding mb function.
// before...$length = strlen($request->post('slug')); // after...$length = mb_strlen($request->post('slug'));
Rule: not_operator_with_successor_space
Logical NOT operators (!)
should have one trailing whitespace.
// before...- if (!$user->is_active) { //} // after...if (! $user->is_active) { //}
The available rules are very comprehensive and always growing in number. You can see a full list of the available rules in the project’s readme. You might also like to checkout PHP-CS-Fixer configuration, which is a site that gives an example of what each rule does, in case their descriptions are not clear.
Hot tip: Beyond fixing styles, it can often be used as an upgrade tool. PHPUnit 8 added a void
return type to several methods. Running PHP-CS-Fixer’s void_return
rule over the /tests
directory can instantly upgrade your test suite, making it compatible with these changes.
Sharing your rules
I use PHP-CS-Fixer across my projects and have a rule set that defines the style. Up until now, I have been copying and pasting my rules when I start a new project, and as new rules come out, I then have to update my config across my existing projects. This is not an ideal workflow as you can easily forget to update a specific project, and it is a bunch of manual work.
It turns out that it is possible to share your rules across multiple projects and teams so that a composer update
will have all your projects using the latest version of your ruleset at any given time.
Scaffolding the repo
We are going to build out a git repo to house our share rule set. To get started, we will initialize a local git repo and create the required files.
$ mkdir php-styles $ cd php-styles $ git init $ echo "/composer.lock/vendor/.php_cs.cache" >> .gitignore $ mkdir src $ touch src/rules.php src/helpers.php composer.json
Defining your rules
As I’ve mentioned before, there is an extensive set of rules, and new releases sometimes contain new rules, so I’ve found it very handy to keep track of which release of PHP-CS-Fixer you last reviewed the available rules. This means when you upgrade to a newer version, you’ll know the releases to look over for new rules that you may want to incorporate into your shared ruleset. I like to add the last reviewed release right at the top of the rules.php
file.
<?php // last reviewed: v2.16.3 Yellow Bird
Next, we want to make our rules.php
file return an array that contains our ruleset. Housing the rules in a separate file is handy as the list can get very long, depending on your specifications. It also allows other developers to pull in your ruleset and merge them with their own, for example, if you wanted to pull in the Laravel coding standard and combine it with some of your team’s additional standards.
<?php // last reviewed: v2.16.3 Yellow Bird return [ '@PSR2' => true, 'array_syntax' => ['syntax' => 'short'], 'final_class' => false, 'new_with_braces' => true, // ...];
PHP-CS-Fixer comes with a few predefined rule sets. All the rules related to the PSR-2 standard are all bundled into the ruleset @PSR2
. This allows us to opt into the standard without having to specify each rule individually.
Some rules have options associated with them. The array_syntax
rule allows you to specify if you’d like short or long array syntax.
Other rules are specified in the list by using their name and a boolean value, as seen with the new_with_braces
rule. Although you can omit the boolean, I like to include it for consistency, so each item in the array is a key ⇒ value
pair, and so I know I’ve opted-out of specific rules explicitly.
Helper method
To make consuming your shared rules painless across your projects, we are going to create a helper function. It might not make sense why this exists just yet, but follow along, and it will all come together.
To make sure the function does not conflict with any other global functions in your projects or their dependencies, it is a good idea to put the function in a namespace. Open the helpers.php
file and define a namespace that makes sense for your context.
<?php namespace TiMacDonald;
Now we need to define the method signature. The method will accept an instance of PhpCsFixer\Finder
and also an array of rules which will allow projects to identify any additional enforced rules on a project-by-project basis. Usually, I’d say we want consistency and shouldn’t allow each project to change the shared ruleset – but I’m not here to tell you what to do, so we’ll let you do it, but you can always remove that functionality if that is your preference.
<?php namespace TiMacDonald; use PhpCsFixer\Config;use PhpCsFixer\Finder; function styles(Finder $finder, array $rules = []): Config { //}
Amazing. It is coming together nicely. The last thing now is to fill out the body of the helper function.
<?php namespace TiMacDonald; use PhpCsFixer\Config;use PhpCsFixer\Finder; function styles(Finder $finder, array $rules = []): Config { $rules = array_merge(require __DIR__.'/rules.php', $rules); return Config::create() ->setFinder($finder) ->setRiskyAllowed(true) ->setRules($rules);}
The $finder
is how PHP-CS-Fixer will know where to look for the PHP files you want to be fixed. We defer this decision to the project, as each one may have a different directory structure, e.g., a Laravel project compared to a Laravel package.
Risky rules
Within the body of the styles
function you can see we are telling the config to allow “risky” rules with the call to setRiskyAllowed(true)
. You must read the documentation and understand how risky rules could impact your project. As an example, we’ll take a look at the implode_call
rule. If you read over the PHP documentation on implode
you’ll note that:
implode() can, for historical reasons, accept its parameters in either order. For consistency with explode(), however, it is deprecated not to use the documented order of arguments.
This means that both of these implode calls will have the same outcome:
<?php implode($array, ','); implode(',', $array);
The implode_call
rule will rearrange the arguments to be in the documented order, but if your project has redefined the behavior of implode()
to expect the parameters in the incorrect order, by some means such as runkit, changing their order may break your code.
So please read over what each risky rule does and understand how it works before you add it to your ruleset.
composer.json
To pull the package into our projects, we need to populate our composer.json
file. I recommend running style checks in Continuous Integration (C.I.), so I’m going to include PHP-CS-Fixer locally (you can alternatively download PHP-CS-Fixer as a Phar file if you have package conflicts).
Open the composer.json
file and make sure to set a unique "name"
that makes sense for you.
{ "name": "timacdonald/php-style-example", "description": "Tim's shared PHP style rules for PHP-CS-Fixer", "license": "MIT", "require": { "friendsofphp/php-cs-fixer": "^2.0" }, "autoload": { "files": [ "./src/helpers.php" ] }}
Pushing to a provider
The final step in setting up our repository is pushing it to a hosted git solution. Create a repository on your provider of choice and add it as an origin to your local repo. I’m going to use GitHub.
$ git add . $ git commit -m wip $ git remote add origin git@github.com:timacdonald/php-style-example.git $ git push -u origin master
We now have our shared rules package available on GitHub. Congrats!
Consuming your shared rules
Now we are switching gears a little to focus on how you can use your new shared rules in other projects. Close your shared rules repo and open a project you’d like to use them on. There is some initial setup, but after you’ve done it, a composer update
will be all you’ll need.
Require your repository
Composer allows us to require repositories from hosted Git platforms, without needing to submit them to Packagist. Considering this kind of repo will likely be internal, there isn’t much benefit to gain by adding it to Packagist.
To get this going, manually add your package name to the projects "require-dev"
block. I do not need to lock down my styles to a specific version, so I just use "dev-master"
which means the latest styles will always be pulled through. You could, however, version your styles – but that is up to you.
Because we aren’t going to submit to Packagist, we have to tell Composer where to find the package. We do that using the repositories block.
"repositories": [ { "type": "vcs", "url": "https://github.com/timacdonald/php-style-example" }]
Now we tell Composer to require our package as a --dev
dependency.
$ composer require timacdonald/php-style-example --dev
Setup PHP-CS-Fixer Finder config
For PHP-CS-Fixer to know what files you want to target, you need to specify each directory or file with an instance of PhpCsFixer\Finder
. This is an inherited version of the Symfony\Component\Finder\Finder
, so for full documentation on all the constraints you can use, check out the docs.
For our example, I’m going to pretend we are in a Laravel application and set up my finder to search through directories I know I want to adhere to my style conventions.
PHP-CS-Fixer is going to expect your configuration to be in a /.php_cs.dist
file, so we’ll create that.
$ touch .php_cs.dist
Open this file and add the following finder setup for your Laravel app. You can include any other folders you would like fixed as well, but these serve as reasonable defaults.
<?php $finder = PhpCsFixer\Finder::create() ->in([ __DIR__.'/app', __DIR__.'/config', __DIR__.'/database', __DIR__.'/routes', __DIR__.'/tests', ]);
We are now ready to pass our finder to the helper function we created a few minutes ago. PHP-CS-Fixer is expecting this file to return an instance of PhpCsFixer\Config
, which is precisely what our helper function returns.
<?php $finder = PhpCsFixer\Finder::create() ->in([ __DIR__.'/app', __DIR__.'/config', __DIR__.'/database', __DIR__.'/routes', __DIR__.'/tests', ]); return TiMacDonald\styles($finder);
Now get yourself into the brace position while we automatically fix our coding style across all these directories! Jump into the terminal and run the following command to watch the magic happen…
./vendor/bin/php-cs-fixer fix
Running in CI
It is a good idea to have your style rules enforce during C.I. You can do this in a couple of ways: you could do a “dry run”, which will fail if it detects code style violations.
./vendor/bin/php-cs-fixer fix --dry-run
Alternatively, you could have C.I. run the fixers and auto-commit the changes to your repo. If you are using GitHub actions, check out this great article written by Stefan Zweifel on how you implement that.
Wrap up
Thanks for coming on this journey. PHP-CS-Fixer is a great tool, and hopefully, if you run multiple projects that share a standard coding style, this approach might come in handy.
Developing engaging and performant web applications with a focus on TDD. Specialising in PHP / Laravel projects. ❤️ building for the web.