Introducing PHP-CS-Fixer into legacy projects
You are working on a legacy PHP project and want to use friendsofphp/php-cs-fixer
to enforce a consistent coding standard. But you are unsure how to do that without causing problems.
What could be a strategy for introducing PHP-CS-Fixer into your legacy PHP that reduces risk and invites other developers to collaborate?
Requirements
If you want the introduction of PHP-CS-Fixer into your legacy PHP project to be a success, you are going to have the following requirements:
-
You want to run PHP-CS-Fixer in your continuous integration system. When you commit and push code that does not follow your coding standards, you want the build in your continuous integration system to fail.
-
You want to run PHP-CS-Fixer in your development environment. When you change your PHP code and that code that does not follow your coding standards, you want PHP-CS-Fixer to fix coding standard violations. You do not need to report coding standard violations in a development environment; you want to go straight for the fixes. You also want a simple command for running PHP-CS-Fixer in a development environment so you can run it frequently.
-
You want PHP-CS-Fixer to apply fixes only that are compatible with the version of PHP that you use in production. If your project runs on PHP 5.3, you do not want PHP-CS-Fixer to apply fixes that require running on PHP 8.1.
Installing PHP-CS-Fixer
You can install PHP-CS-Fixer with composer
, phive
, or by downloading a PHAR from the releases page. But you can study the installation options and instructions for PHP-CS-Fixer in detail in the README.md
of PHP-CS-Fixer; I will not repeat them here.
I recommend using composer
to install PHP-CS-Fixer so that you benefit from automated dependency updates by Dependabot, Renovatebot, or similar services. If you install PHP-CS-Fixer with phive
or download it from the releases page, you have to update PHP-CS-Fixer manually.
Your project does not yet use composer
? Why not start using composer
by making PHP-CS-Fixer your first development dependency?
You can not use composer
because your project does not yet run on PHP 5.3 in production? Why not use a different version of PHP to run development tools?
I also recommend using the latest version of PHP-CS-Fixer so that you benefit from features and fixes that the maintainers and contributors consistently add to the tool.
You can not use the latest version of PHP-CS-Fixer because your project does not yet run PHP 7.4 or above in production? Again, why not use a different version of PHP to run development tools?
For example, in the last couple of weeks, I have been busy updating a project from PHP 5.6 to PHP 8.1. After setting up a local development environment running on PHP 5.6 in Docker, I have set up a GitHub Actions workflow that uses PHP 8.1 to install and run development tools - including PHP-CS-Fixer.
If I can use PHP 8.1 to run development tools for a project that runs on PHP 5.6 in production, you can, too.
Your key to success is a careful configuration of your development tools.
Adding a basic configuration for PHP-CS-Fixer
PHP-CS-Fixer requires two things to report and fix coding standard violations: a list of files it should inspect and a configuration of rules and rulesets it uses to create and configure corresponding fixers.
The configuration file .php-cs-fixer.php
below configures a finder and empty array of rules.
<?php
$finder = PhpCsFixer\Finder::create()
->exclude([
'.build/',
'.docker/',
'.github/',
])
->ignoreDotFiles(false)
->in(__DIR__)
->name('.php-cs-fixer.php');
$config = new PhpCsFixer\Config();
$config
->setFinder($finder);
->setRules([]);
return $config;
The finder allows you to configure a list of directories, exclusions, file names, and more and returns a list of files that PHP-CS-Fixer should inspect.
Depending on your project layout, your configuration of the finder may look different.
The empty array of rules will override the default rules configuration. PHP-CS-Fixer lints a PHP file before and after applying fixers to ensure that it neither attempts to fix a file that contains invalid PHP code nor leaves a file with invalid PHP code behind. When PHP-CS-Fixer finds a file that does not contain valid PHP code, it will skip fixing it and emit a warning.
Even with an empty array of rules, PHP-CS-Fixer is a valuable tool for you as it can help you find files that contain invalid PHP code.
Now that you have an initial configuration for PHP-CS-Fixer, it is time to get started running PHP-CS-Fixer.
Running PHP-CS-Fixer on GitHub Actions
As mentioned, you want to run PHP-CS-Fixer in two environments: your continuous integration system and your local development environment.
The following command will run PHP-CS-Fixer with the --dry-run
option and report coding standard violations:
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --dry-run --show-progress=dots --verbose
Depending on how you installed PHP-CS-Fixer and named your configuration file, the command may look different.
But it is important to use the --dry-run
option when running PHP-CS-Fixer in a continuous integration system: PHP-CS-Fixer will end its execution with a non-zero exit code when it has found coding standard violations and break the build.
I feel at home at GitHub and like to use GitHub Actions as a continuous integration system. The GitHub Actions workflow below will check out your repository, set up PHP, install dependencies with composer
, and run PHP-CS-Fixer with the --dry-run
option.
name: "Integrate"
on:
pull_request: null
push:
branches:
- "main"
jobs:
coding-standards:
name: "Coding Standards"
runs-on: "ubuntu-latest"
strategy:
matrix:
php-version:
- "8.1"
steps:
- name: "Checkout"
uses: "actions/checkout@v3.5.0"
- name: "Set up PHP"
uses: "shivammathur/setup-php@v2.24.0"
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
- name: "Validate composer.json and composer.lock"
run: "composer validate --ansi --no-check-publish"
- name: "Install locked dependencies with composer"
run: "composer install --ansi --no-interaction --no-progress"
- name: "Run friendsofphp/php-cs-fixer"
run: "vendor/bin/php-cs-fixer fix --ansi --config=.php-cs-fixer.php --diff --dry-run --show-progress=dots --verbose"
Depending on your project setup and continuous integration system, your configuration may look different - but the steps will be roughly the same.
With this GitHub Actions workflow in place, you can not commit and push PHP code that does not follow your coding standards without failing the build.
💡 You can improve the coding-standards
job by caching dependencies installed with composer and caching the directory containing the cache file for PHP-CS-Fixer between runs.
Running PHP-CS-Fixer in a development environment
The following command will run PHP-CS-Fixer and fix coding standard violations:
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --show-progress=dots --verbose
Depending on how you installed PHP-CS-Fixer and named your configuration file, the command may look different. Again, you do not care about reporting coding standard violations in a development environment; you are going straight for the fixes.
The command is a bit long to type, and if you want to run PHP-CS-Fixer frequently, you probably want to use a task runner or some other tool that makes it easier to run tools in a development environment.
If you use Makefile
s, add a coding-standards
target to your Makefile.
.PHONY: coding-standards
coding-standards: vendor
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --show-progress=dots --verbose
vendor: composer.json composer.lock
composer validate --strict
composer install --no-interaction --no-progress
With a coding-standards
target in your Makefile
, you can run the following command to let PHP-CS-Fixer fix coding standard violations:
make coding-standards
💡 If you do not yet use Makefile
s or find that the command is still to long, read Makefile for lazy developers.
If you prefer composer
scripts, add a coding-standards
script to your composer.json
.
{
"scripts": {
"coding-standards": "@php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --show-progress=dots --verbose"
}
}
With a coding-standards
script in your composer.json
, you can run the following command to let PHP-CS-Fixer fix coding standard violations:
composer coding-standards
Evolving the configuring for PHP-CS-Fixer
Now that you are running PHP-CS-Fixer in your continuous integration system and your development environment, there is one thing left: you want PHP-CS-Fixer to apply fixes that are compatible with the version of PHP that you use in production, and so far, you have only configured an empty array of rules.
Here is what has worked well for me.
First, obtain a complete list of rules for all available fixers, for example, from the Custom
ruleset in ergebnis/php-cs-fixer-config-template
.
Second, adjust your rules configuration to configure, but disable all these fixers. At the time of writing, there should be 250 rules.
<?php
$finder = PhpCsFixer\Finder::create()
->exclude([
'.build/',
'.docker/',
'.github/',
])
->ignoreDotFiles(false)
->in(__DIR__)
->name('.php-cs-fixer.php');
$config = new PhpCsFixer\Config();
$config
->setFinder($finder);
->setRules([
'align_multiline_comment' => false,
'array_indentation' => false,
'array_push' => false,
'array_syntax' => false,
// ...
'visibility_required' => false,
'void_return' => false,
'whitespace_after_comma_in_array' => false,
'yoda_style' => false,
]);
return $config;
💡 Alternatively, if you already share configurations for PHP-CS-Fixer across projects as described in Sharing configurations for PHP-CS-Fixer across projects, override the existing rule configuration by disabling all rules.
Third, go through the list of rules, from top to bottom, from bottom to top, or whatever works best for you, pick a rule, carefully inspect its documentation, and enable and configure one rule at a time - but only when you can be sure that it only applies fixes that are compatible with the version of PHP running in production.
Example of enabling and configuring a rule at a time
Let's look at concrete examples, considering and configuring the array_syntax
rule, when you are working with pull requests and pre-merge code reviews.
- First, create a branch, for example,
feature/array-syntax.
- Second, copy the name of the rule.
- Third, navigate to https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.16.0 (replace
3.16.0
with the version of PHP-CS-Fixer that you actually use). Press T to open the file navigation. Paste the name of the rule into the input field (herearray_syntax
). Select the corresponding documentation file in the drop-down (herearray_syntax.rst
). - Fourth, inspect the documentation. Can you safely enable the
array_syntax
fixer? For example, PHP 5.4 introduced the short array syntax. Is your legacy PHP project running on PHP 5.3 in production? Then you should probably enable thearray_syntax
fixer and configure thesyntax
option to uselong
as value. Or is your legacy PHP project running on PHP 5.4 or above? Then you should probably enable the fixer and configure thesyntax
option to useshort
as value. - Fifth, commit and push the changes to your
.php-cs-fixer.php
configuration file. - Sixth, open a pull request and document in the body that this pull request will enable the
array_syntax
fixer. Opening the pull request will start a run of your GitHub Actions workflow. - Seventh, run PHP-CS-Fixer in your development environment. If PHP-CS-Fixer has applied fixes, the GitHub Actions workflow will fail. Commit and push the fixes in a separate commit to make the build pass.
- Eighth, review the changes in the pull request and verify that they make sense. If necessary, have someone else review your pull request as well.
- Ninth, merge the pull request and delete the branch.
- Tenth, check out your default branch in your development environment.
- Eleventh, pull the latest changes.
- Twelfth, delete the feature branch.
Repeat the steps above for every rule.
💡 You can find an example of a pull request that enables and configures the array_syntax
fixer for the official PHP website here.
Disadvantages of enabling and configuring one rule at a time
At the time of writing, PHP-CS-Fixer ships with 250 rules. Enabling and configuring one rule at a time can take a while, even more so when you are working with pull requests and require pre-merge code reviews. But you could significantly speed up the process by using Ship/Show/Ask.
If you are unwilling to invest the time, good luck applying all rules and reviewing all fixes at once!
Advantages of enabling and configuring one rule at a time
In my opinion, enabling and configuring a single rule at a time has the following advantages:
- You minimize risk: each pull request contains only related changes that are easier to review.
- You invite other developers to collaborate on your coding standard.
- You will probably touch code in all corners of your legacy PHP project, which allows you to discover and take note of weird spots that need closer inspection.
Do you find this article helpful?
Do you have feedback?
Just published: Introducing @PHPCSFixer into legacy projects.
— Andreas Möller (@localheinz) April 10, 2023
↓https://t.co/bhr6U7U1t6