Migrating from PHP CS Fixer
How to migrate from PHP CS Fixer to DressCode: converting .php-cs-fixer.dist.php to a DressCode
configuration, the @PSR12, @PER-CS and @Symfony sets to standards and fixers to decisions,
a table of matching commands, and what a risky rule means here.
Before You Start
Go through the common steps first: you can leave the code and the comments as
they are, import converts the configuration, and migrate-suppressions rewrites the comments. This page
adds what is specific to PHP CS Fixer.
The most important thing first: the configuration of PHP CS Fixer is a PHP file that has to be executed, and it returns
an object of its library. So the dresscode import command reads it only when it runs as
vendor/bin/dresscode in a project where friendsofphp/php-cs-fixer is still installed. DressCode
installed outside the project (globally or with create-project) does not load the project's autoloader, cannot find
the classes of PHP CS Fixer, and stops with a configuration error and exit code 3:
Error: File `.php-cs-fixer.dist.php` needs the classes of PHP CS Fixer, which this dresscode cannot load; run the `vendor/bin/dresscode` of the project that has PHP CS Fixer installed: Class "PhpCsFixer\Finder" not found.
So for the conversion, add DressCode to your project as a development dependency, see Installation, and convert the configuration before you remove PHP CS Fixer.
Converting the Configuration
dresscode import .php-cs-fixer.dist.php dresscode.neon
Sets are translated to standards: @PSR1, @PSR2 and @PSR12 to psr12;
@PER, @PER-CS and their numbered variants to perCs, which matches PER Coding Style 3.1; and @Symfony to symfony,
which does not have most of the phpDoc rules yet. import reports sets with no counterpart and tells you where to
start; for @PhpCsFixer it is symfony. Sets with the :risky suffix have no counterpart,
because here risky fixes are not turned on by a set, but by the project for individual decisions (see below).
Fixers are translated to decisions, together with their options where an option has a counterpart. A configuration with the
@PER-CS standard, the no_unused_imports fixer and trailing_comma_in_multiline with
elements for arrays, arguments and parameters gives this dresscode.neon:
use: [perCs]
imports:
unused: forbidden
multiline:
trailingComma:
array: required
argument: required
parameter: required
matchArm: optional
closureUse: optional
import: optional
list: optional
classes:
instantiationWrapping: keep
no_unused_imports is imports.unused, which,
like the fixer, also counts a name in phpDoc as a use. trailing_comma_in_multiline was split into the multiline.trailingComma keys, one for each place where a
comma can go; what elements did not list got optional, which means the comma is left as the author wrote
it. import added classes.instantiationWrapping: keep by itself: from PHP 8.4, the perCs
standard writes new Foo()->bar() without parentheses around new, while the @PER-CS set
leaves them as they are. A fixer with no counterpart is listed on standard error, and that gives you a list of things you need to
decide about. Custom fixers from the kubawerlos/php-cs-fixer-custom-fixers package are translated too, as long as
DressCode knows the construct they deal with.
What import does not carry over, because in the Fixer configuration these are not rules:
- Finder. Rewrite the paths,
exclude()andnotPath()to thepathsandexcludePathskeys; the patterns are described in Configuration. - Indentation and line endings (
setIndent(),setLineEnding()). The standard sets them. If you use none, add the decisionsindentation.unitandfile.lineEnding; without a standard,importreminds you of them by itself. setRiskyAllowed(true). DressCode does not give permission for risky fixes to the whole configuration, but to individual decisions: their paths go in thefixRiskylist. Without that list, risky fixes are only reported.
One Thing, One Key
In PHP CS Fixer, several rules share one construct, and the result comes from how they work together. A project using PER
Coding Style that wants to write global classes with a backslash, \Exception, and functions without one, except those
PHP can optimize, needs two fixers and five options, and one of the options means the opposite of what it seems to say:
import_classes => false does not mean “do not import”, but “rewrite imported classes to the fully qualified
name”. On top of that, it needs no_unused_imports, so that the imports left over after the rewrite disappear:
$config->setRules([
'@PER-CS' => true,
'native_function_invocation' => [
'include' => ['@compiler_optimized'],
'scope' => 'namespaced',
'strict' => true,
],
'global_namespace_import' => [
'import_classes' => false,
'import_constants' => false,
'import_functions' => false,
],
'no_unused_imports' => true,
]);
In DressCode, these are keys of one section, qualification, and each one names what it wants;
imports.unused removes the unused imports:
use: perCs
qualification:
class:
global: backslashed
function:
global: bare
optimized: backslashed
imports:
unused: forbidden
From this code:
<?php
namespace App\Model;
use DateTimeImmutable;
use InvalidArgumentException;
class Cart
{
public function add(array $items, string $sku): DateTimeImmutable
{
if (strlen($sku) === 0 || in_array($sku, $items, true)) {
throw new InvalidArgumentException('Invalid SKU');
}
return new DateTimeImmutable(\date('Y-m-d'));
}
}
both tools make this:
<?php
namespace App\Model;
class Cart
{
public function add(array $items, string $sku): \DateTimeImmutable
{
if (\strlen($sku) === 0 || in_array($sku, $items, true)) {
throw new \InvalidArgumentException('Invalid SKU');
}
return new \DateTimeImmutable(date('Y-m-d'));
}
}
Except for one place: PHP CS Fixer qualifies in_array($sku, $items, true), because in_array is in the
@compiler_optimized list, while DressCode leaves it bare, because the compiler speeds up only a search in an array
written directly in the code, not in a variable. DressCode decides by the call, not by the name. It removes the backslash from
date() without risk when the configuration says nameResolution: certain; otherwise it is a risky fix,
just as native_function_invocation is a risky fixer. The rest of the section, constants and imports of global names,
is described in How Global Names Are
Written.
Risky Rules
PHP CS Fixer marks a rule as risky when, on an array of tokens, it cannot tell a safe case from a dangerous one, and it leaves
the decision to you. A typical example is ternary_to_elvis_operator: it cannot tell whether the condition of a
ternary operator is a variable or a function call with a side effect, so it watches only for ++ and --,
and the rest is up to you.
DressCode does not have this split, because the tree answers the question: the decision expressions.ternary.short shortens a ternary operator only
when the condition is safe to evaluate twice, and it leaves a function call alone. The same goes for other rules that were risky
for the same reason.
Fixes that can change the behavior of the program are a different thing. DressCode does not turn them on with a special set. It
reports them as risky, place by place, and for each one it says what would decide it: mistakes.strictArgument adds
the parameter $strict = true to in_array() right away where the types from PHPStan say an integer is searched for among integers, and elsewhere it only
reports the fix as risky. A risky fix is made only when you list its decision in the fixRisky list in the
configuration, or for one run with --fix-risky; until then it is a warning where types would decide it, and an error
where it changes behavior by its nature, like adding declare(strict_types=1). Writing a decision that changes the
meaning of code, like expressions.comparison.loose: forbidden, does not yet mean you agree with its fixes; you agree
only by listing it in fixRisky.
Some fixers form a special group: PHP CS Fixer marks them as risky because someone could declare a function of the same name in
a namespace: is_null, no_alias_functions, dir_constant and others. In DressCode, the
configuration settles this doubt. With the nameResolution: certain key and the lists in the namespaces
key, it knows what is in your namespaces, and the corresponding fixes are safe; see Functions and Constants in Namespaces.
Commands
| PHP CS Fixer | DressCode |
|---|---|
php-cs-fixer fix --dry-run |
dresscode check |
php-cs-fixer fix --dry-run --diff |
dresscode check --diff |
php-cs-fixer fix |
dresscode fix |
php-cs-fixer fix --config=file |
dresscode fix --config file |
php-cs-fixer fix --rules=name |
dresscode fix --set path=value, which adds the decision to the configuration, or
dresscode fix --only path, which runs only the ones you name |
php-cs-fixer fix src --rules=@PSR12 |
dresscode fix src --use psr12 in a project without a configuration |
php-cs-fixer fix --format=checkstyle |
dresscode check -f checkstyle |
php-cs-fixer fix --allow-risky=yes |
dresscode fix --fix-risky; permanently, a list of decisions in the fixRisky key |
php-cs-fixer describe name |
dresscode explain path |
.php-cs-fixer.cache |
the cache is on by itself; --no-cache bypasses it |
The exit code of PHP CS Fixer is a bit mask (8 means violations found, 16 a configuration error, and so on), while DressCode
returns 0 for clean, 1 for violations, 2 for a failure on some file and 3 for
an error on the command line or in the configuration. A CI script that evaluated the mask needs one change.
What PHP CS Fixer Did Not Have
- PER Coding Style 3.1. PHP CS Fixer implements version 3.0 (the
@PER-CS3x0set), while theperCsstandard implements version 3.1. After PHP CS Fixer, DressCode still finds 1,201 places in Laravel and 7,497 in Symfony 3.4; these are places where PER Coding Style wants one item per line, a heredoc indented with the surrounding code, or a comma after a multiline list, see the comparison. - A message at every place, including what the fix can change. For a file,
php-cs-fixer fix --dry-run -vprints the names of the fixers that would change it, and with--diffthe change itself; thecheckstyleformat has one entry per fixer and file, with no line number.php-cs-fixer describeexplains why a fixer is risky, once for all places; forstrict_param, it saysRisky when the fixed function is overridden or if the code relies on non-strict usage.With--allow-risky=yesit then addstrueto everyin_array(), without it to none. DressCode with types from PHPStan judges each call separately: where anintis searched for in alist<int>, it addstrueby itself, and where the value searched for ismixed, it reports the fix with the reasonRisky because a strict search no longer finds a value of another type.and waits for your consent. The same output also shows an overriding method whose parameter is named differently than in the parent, and a call to a method the library marked as deprecated; PHP CS Fixer reports neither of them, not even with the@PhpCsFixer:riskyand@Symfony:riskysets. The full output is shown in Getting Started. - Suppressing on a line. PHP CS Fixer cannot turn off a rule for one line or for a block, only for a whole file through
Finder. Here you have
// dresscode:ignore pathfor that, and the pairdresscode:disableanddresscode:enable, see Suppressions and the Baseline. - Different values for part of the project are written as an override in the
overrideskey, with no second configuration file; a decision there gets a different value, orkeep, see Configuration. - Trying a decision without losing your configuration. In PHP CS Fixer,
--rulesreplaces the rules from the configuration file, so a fixer you try runs without the rest of your set and with default options, unless you spell out others in JSON. In DressCode,dresscode fix --set path=valueadds the decision to the rest of the configuration, so its changes come out already formatted by your standard, ready for one commit, and--only pathalso narrows the output to them, see Command Line. - Baseline. A decision that your code does not meet yet, and that cannot be fixed for you, can be turned on right away:
the
dresscode baselinecommand records today's violations, and only new ones will be reported. PHP CS Fixer does not have one; how DressCode combines it with fixes is described in Baseline. - Your own rule is written against the tree, not against tokens. The tutorial takes one afternoon, and if you have a custom fixer, a separate page shows how to port it.