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.

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 Makefiles, 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 Makefiles 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 (here array_syntax). Select the corresponding documentation file in the drop-down (here array_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 the array_syntax fixer and configure the syntax option to use long as value. Or is your legacy PHP project running on PHP 5.4 or above? Then you should probably enable the fixer and configure the syntax option to use short 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?

Do you need help with your PHP project?