Sharing PHP-CS-Fixer rules across projects and teams

Published on by

Sharing PHP-CS-Fixer rules across projects and teams image

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.

timacdonald photo

Developing engaging and performant web applications with a focus on TDD. Specialising in PHP / Laravel projects. ❤️ building for the web.

Filed in:
Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Laravel Idea for PhpStorm logo

Laravel Idea for PhpStorm

Ultimate PhpStorm plugin for Laravel developers, delivering lightning-fast code completion, intelligent navigation, and powerful generation tools to supercharge productivity.

Laravel Idea for PhpStorm
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
JetShip - Laravel Starter Kit logo

JetShip - Laravel Starter Kit

A Laravel SaaS Boilerplate and a starter kit built on the TALL stack. It includes authentication, payments, admin panels, and more. Launch scalable apps fast with clean code, seamless deployment, and custom branding.

JetShip - Laravel Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Streamlining Route Parameters in Laravel Using URL Defaults image

Streamlining Route Parameters in Laravel Using URL Defaults

Read article
Flexible Docker Images with PHP INI Environment Variables image

Flexible Docker Images with PHP INI Environment Variables

Read article
Dynamic Form Validation in Laravel with prohibited_if image

Dynamic Form Validation in Laravel with prohibited_if

Read article
Add Approvals to Your Laravel Application image

Add Approvals to Your Laravel Application

Read article
Enhancing Data Processing with Laravel's transform() Method image

Enhancing Data Processing with Laravel's transform() Method

Read article
Get Xdebug Working With Docker and PHP 8.4 in One Minute image

Get Xdebug Working With Docker and PHP 8.4 in One Minute

Read article