Β
β
Fixes style, upgrades PHP and libraries, in one run
β
Changes only what it fixes, not a byte more
β
PER, PSR-12, Nette or Symfony out of the box
β
Made to work hand in hand with AI coding agents
Β
A dress code for your PHP code. DressCode checks the style of your code and fixes it in the standard you
pick; it has no style of its own. And while it is at it, it rewrites the code to what newer PHP offers. With
PER Coding Style and the group modernization, one dresscode fix turns this:
function getName($user){return $user===null?null:$user->getName();}into this:
function getName($user)
{
return $user?->getName();
}The layout comes from PER Coding Style, the ?-> from PHP 8.0, which the composer.json of the project
allows.
Β
1οΈβ£ Install it: composer global require dresscode/dresscode
2οΈβ£ Have a configuration made to measure: dresscode init
3οΈβ£ Check your code and fix it: dresscode check, dresscode fix
DressCode is a tool, not a library, so it does not have to be installed in the project it checks, and a
global installation is the simplest way; just make sure the directory of global Composer binaries is in your
PATH. For CI, where you want the version of the tool pinned, install it into a directory of its own with
composer create-project dresscode/dresscode temp/dresscode. It can also be a development dependency of your
project (composer require --dev dresscode/dresscode), which is the way to go once you want DressCode to
ask your PHPStan about the types of your code; the project then has to run on
PHP 8.4 to 8.6 too.
DressCode itself runs on PHP 8.4 to 8.6. That is the PHP of the tool, not the PHP your code is written for.
The second number DressCode reads from require.php in your composer.json, and it never writes syntax that
version does not have. So DressCode running on PHP 8.5 can check a project written for PHP 8.1. Code for
PHP 8.0 and newer is supported.
A check looks like this:
DRESS|CODE 1.0.0
Config /var/www/shop/dresscode.neon
Target PHP 8.2 from composer.json
Checking 214 files in /var/www/shop
src/Cart.php
error 9:52 A line break before the opening brace braces-position
error 10:18 An array must be written with the short syntax short-array-syntax
error 11:22 At least one space before the == operator binary-operator-spacing
error 11:22 At least one space after the == operator binary-operator-spacing
FOUND 4 violations, a fix leaves none in 1 file
Every finding ends with the name of the rule that made it. That name is all you need: to switch the rule off,
to set its options, to suppress it on one line, or to read what it is for with dresscode explain braces-position.
A clean run ends with OK 214 files, all up to the dress code, and the exit code is the verdict: 0 clean,
1 violations or syntax errors found, 2 a failure of the run.
DressCode has no style of its own, so without a configuration it does not start: a silent default would
rewrite nearly every line of a project written another way. dresscode init measures your code (the
indentation, the quotes, the shape of conditions) and writes dresscode.neon into the root of the project;
to just try the tool, name a standard instead with dresscode check src --preset per. The configuration
can also be written by hand, in NEON or as dresscode.php with the same keys:
paths:
- src
- tests
presets:
- per
groups:
- cleanup
- modernizationΒ
PHP style tools have been built on token_get_all() for twenty years. It returns a flat list: this element is
an if, the next a parenthesis, the next a space. The list does not say where the if ends, or whether [
starts an array or reads an item of one, so every rule has to work the structure out again by itself.
DressCode reads each file into a syntax tree in which every part of the code knows what it is: an if
has its condition and its body, a ternary operator its three parts. And nothing of the source is left out of
the tree, spaces, blank lines and comments included, so printing the tree gives the original file back byte
for byte. That is what "lossless" means, and the tree is a library of its own,
PhpSyntax.
A rule therefore changes the piece of code it fixed and nothing else. Here is a file nobody has formatted in
years, fixed with the modernization rules alone and no standard at all (dresscode fix --group modernization):
{
$sum = 0;
foreach ( $items as $item )
- $sum = $sum + $item->price; // yes, floats
+ $sum += $item->price; // yes, floats
return $sum;
}
- function hasHttps($url) { return strpos( $url, 'https://' ) === 0; }
+ function hasHttps($url) { return str_starts_with($url, 'https://'); }
}The two-space indentation, the foreach without braces, the spaces inside its parentheses and the comment
are all still there, because no rule was asked about them. In a project that already keeps its style, the diff
of an upgrade contains the upgrade and nothing else.
Β
DressCode has 211 rules, and you do not have to go through them one by one. The configuration comes down to two questions you can answer right away.
What should the code look like? That is a standard, just one, chosen by name:
| preset | what it is |
|---|---|
per |
PER Coding Style 3.1 in full, the successor of PSR-12 |
psr12 |
PSR-12, section by section |
nette |
Nette Coding Standard, PER with tabs and a few departures |
symfony |
Symfony Coding Standards, as the @Symfony set of PHP CS Fixer has them |
What else do you want from the code? That is a group of rules, and there are exactly six:
| group | what it turns on |
|---|---|
cleanup |
code that is there for nothing |
modernization |
the construct newer PHP has, where an older one says the same |
types |
types written where PHP reads them |
deprecations |
what newer PHP or a library deprecated or removed |
correctness |
what is most likely a mistake |
optimized-calls |
calls in the form PHP optimizes when it compiles the code |
A group names what you want, not a list of rules: instead of twenty rule names you write cleanup, and
every rule that removes code which is there for nothing runs. A rule that cannot run in your project,
because it writes a newer PHP than yours or needs types you have not set up, quietly stays out, so turning
on a group never breaks anything.
Your configuration still has the last word. Any rule is switched off or set up by the name it reports under.
Where the standards differ, the rule has an option for the difference rather than an exception hidden in a
preset, so your own preset can choose the same way. And a part of the project, such as tests or legacy
code, can get settings of its own under the key overrides.
Β
A tool that rewrites your code is only useful while you can trust what it writes. DressCode does not leave that to the care of each rule's author; the core of the tool enforces it.
- First report, then fix. A rule may change the code only after it has reported the violation and the report was accepted. The core checks that every change comes with a report, so a rule that changes code without a report fails its own tests.
- A suppression stops the fix, not only the message.
// dresscode:ignoreon a line,dresscode:disableanddresscode:enablearound a block,dresscode:ignore-file, or a rule switched off for a path: in all of these cases the code stays untouched. - A fix that may change what the code does is made only with your consent. Take
strpos()insidenamespace App\Model: PHP callsApp\Model\strpos()if such a function exists, and the global one only if it does not. From one file you cannot tell, so rewriting it tostr_contains()is a risky fix. DressCode reports it and waits until you allow it, for one rule in the keyfixRiskyor for one run with--fix-risky. Or you tell it the truth once,nameResolution: certain(or list what your namespaces do declare undernamespaces), and such fixes stop being risky at all. - No priorities. When two rules touch the same code, their order matters. DressCode does not number the rules; it runs them again and again until the code stops changing. Two rules pulling the same code back and forth are detected, and the run names them.
- Every space has one owner. No rule writes whitespace directly. A rule says what it wants between two tokens, one space or a line break, and the core decides and fixes it, so two rules never fight over a space. Indentation is computed from the structure of the code, so one badly indented line does not drag the lines below it along.
- A baseline for what cannot be fixed yet.
dresscode check --generate-baselinerecords what a fix leaves behind, identified by the content of the line rather than its number, so the record survives edits elsewhere in the file. The summary of every run says how many violations the baseline hides.
Β
Raise the PHP version in your composer.json, run dresscode fix, and lines like these change:
- strpos($url, '://') !== false
+ str_contains($url, '://') // PHP 8.0
- $user === null ? null : $user->getName()
+ $user?->getName() // PHP 8.0
- trim(strtolower(strip_tags($title)))
+ $title |> strip_tags(...) |> strtolower(...) |> trim(...) // PHP 8.5
- max(0, min(5, $votes))
+ clamp($votes, 0, 5) // PHP 8.6The rewritten code comes out already formatted by your standard. Inside a namespace, str_contains() and
clamp() wait for your consent, for a reason explained in
Fixes you can trust.
Every rule that writes newer PHP knows which version brought what it writes, and it switches itself on the
day your composer.json allows that version. While the constraint says ^8.3, no array_any() gets into
your code; raise it to ^8.4 and the next fix writes it. You never have to keep track of it, and
dresscode config tells you which rules are waiting and why:
Not running
dresscode/array-function-for-foreach it needs PHP 8.4 and the target is 8.3
dresscode/clamp-for-min-max it needs PHP 8.6 and the target is 8.3
What gets rewritten, by the version of PHP that brought it:
| PHP | what gets rewritten |
|---|---|
| before 8.0 | a closure returning one expression to fn, a ternary testing null to ??, a ternary repeating its condition to ?:, array() and list() to [], $a = $a + $b to += and its kin |
| 8.0 | strpos() !== false to str_contains(), substr() comparisons to str_starts_with() and str_ends_with(), a simple switch to match, a property assigned in the constructor to one declared there, a ternary testing null to ?->, Stringable, get_debug_type() |
| 8.1 | $this->save(...) instead of [$this, 'save'], array_is_list(), octal numbers as 0o755 |
| 8.3 | json_validate() instead of json_decode() called only as a test, Foo::{$name} instead of constant(), #[\Override] on a method overriding its parent (with types) |
| 8.4 | a loop that only searches or tests items to array_any(), array_all(), array_find() or array_find_key(), the RoundingMode enum in round(), #[\Deprecated] instead of @deprecated |
| 8.5 | nested calls to the pipe operator |>, array_first() and array_last() |
| 8.6 | max() around min() to clamp() |
All of this is turned on by the group modernization. Two more groups go with it. deprecations fixes what
newer PHP deprecated: a parameter with the default null gets its ?, the CSV functions get their escape
argument, ${name} in a string becomes {$name}, backticks become shell_exec() and (integer) becomes
(int). cleanup removes what newer PHP no longer needs, such as curl_close(), setAccessible() or an
unused variable in catch.
Upgrade tools such as Rector work on an abstract syntax tree, which keeps what the code means but not how it is laid out, so they need a coding standard tool after them; Rector's own readme says so. DressCode is both at once: the rewrite and the formatting happen in the same run, with one configuration.
DressCode understands the syntax of new PHP even when it runs on an older one: it fills in the pieces of syntax the running PHP does not know yet, so DressCode on PHP 8.4 reads a file with the pipe operator of PHP 8.5, a file PHP 8.4 itself would reject.
Β
When a library renames a class, a constant, a method or a parameter, DressCode rewrites your code for you,
and what it cannot rewrite it reports together with what to write instead. What changed in which version
comes as data in a package of rules for the ecosystem: dresscode/rules-nette knows all twenty Nette
libraries, most of them from version 3.0 to the current one. Install the package and DressCode finds it by
itself:
- /** @persistent */
+ #[Persistent]
- $this->invalidateControl('list');
+ $this->redrawControl('list');
- $form->addText('title')->addRule(Form::MAX_LENGTH, null, 100);
+ $form->addText('title')->addRule(Form::MaxLength, null, 100);
- $page = $this->getParameter('page', 1);
+ $page = $this->getParameter('page') ?? 1;
- Json::encode($values, Json::PRETTY)
+ Json::encode($values, pretty: true)None of this is a search and replace: an annotation became an attribute along with its import, the default
of a parameter moved behind ?? and a flag became a named argument. And DressCode knows that $form is a
form and $this a presenter, because it asks PHPStan about the types. The data are turned on by the group
deprecations; most of the rewrites also need the types, and without them only
classes, functions and annotations are fixed:
types: phpstan
groups:
- deprecationsA package of rules can be written for any library, and it can ship rules of its own besides the data.
Β
The syntax tree knows every byte of a file, but not that $form is a form, what a class inherits, or what
the declaration of a method says. If your project has PHPStan, DressCode asks it, with your phpstan.neon
and its extensions, so the answers are as good as the analysis you already trust. For that, DressCode has
to be installed in the project next to PHPStan (composer require --dev phpstan/phpstan dresscode/dresscode),
and one line of the configuration does the rest:
types: phpstanThat unlocks the rules a style tool cannot otherwise have: the deprecated API of a library rewritten
according to what the variable really is, #[\Override] on a method that overrides its parent, and
UserRepository::class in place of a string naming an existing class. PHPStan only answers; it rewrites
and reports nothing. Without the key, the rules that need types quietly stay out of every group.
Β
DressCode knows its rules not only by their own names, but also by the names they have in PHP CS Fixer, PHP_CodeSniffer and Slevomat. So the move takes three steps, and you can stop after any of them.
First, leave the code as it is. Comments // phpcs:ignore, phpcs:disable, phpcs:enable,
phpcs:ignoreFile and the annotation @phpcsSuppress keep working: DressCode translates the foreign rule
name to its own, and the suppression holds.
Second, translate the configuration. import reads it and writes its equivalent, and it tells you what it
could not carry over:
dresscode import phpcs.xml > dresscode.phpRead 4 rules; enabled 2 rules and 1 preset.
No DressCode rule covers Squiz.Commenting.FunctionComment.
.php-cs-fixer.dist.php is translated the same way, as long as PHP CS Fixer is still installed in the
project, because that file is PHP which has to run.
Third, rewrite the comments when you like: dresscode migrate-suppressions src tests turns
phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses into dresscode:ignore unused-imports.
Β
The output comes in five formats: console, github (annotations right in the pull request, chosen
automatically in GitHub Actions), checkstyle, json and bare. Files are processed in parallel, and a
file already known to be clean is skipped on the next run.
An AI coding agent should have every file it writes put into shape right away, and it has to learn when a
file changed under it. Run DressCode after each write with the format bare, which is made for that:
dresscode fix src/Cart.php -f bare --skip-excludedsrc/Cart.php rewritten
A clean file prints nothing, so the check costs the agent none of its context. Violations the fix could
not resolve are listed, for the agent to fix itself. And a file the fix rewrote says so in one line, which
tells the agent to read the file again before its next edit instead of working with a version that no
longer exists.
--skip-excluded leaves alone a file the configuration excludes, such as a test fixture the agent wrote.
A pre-commit hook should check what goes into the commit, not the working tree, and --stdin does exactly
that:
for file in $(git diff --cached --name-only --diff-filter=ACM -- '*.php'); do
git show ":$file" | dresscode check --stdin "$file" -f bare || exit 1
doneΒ
A rule is a small class that says which parts of the code it is interested in and asks each of them a few
questions. This is the complete rule that shortens $a ? $a : $b to $a ?: $b:
#[RuleInfo(
'dresscode/short-ternary-operator',
Stage::Structure,
description: 'Uses ?: where the ternary repeats its condition',
group: Group::Modernization,
)]
final class ShortTernaryOperatorRule extends NodeRule
{
public function getVisitedTypes(): array
{
return [TernaryNode::class];
}
public function enter(Node|Token $node, RuleContext $context): void
{
if (
!$node instanceof TernaryNode
|| $node->then === null
|| !$node->condition->isRepeatableRead()
|| !$node->condition->matches($node->then)
|| $node->question->getLine() !== $node->colon->getLine()
|| $node->question->hasCommentUpTo($node->colon)
|| !$context->report($node, "A ternary repeating its condition must be written '?:'")
) {
return;
}
$node->then = null;
$node->question->setTrailingTrivia([]);
}
}The conditions read like a sentence: it is a ternary, it has a middle part, its condition can be evaluated twice without side effects, the middle part is the same code as the condition, both are on one line, there is no comment between them, and nobody suppressed the rule here. A question like "is it safe to read this expression twice" is answered by the tree, so the rule does not have to work it out.
Your project turns its own rule on by the class name (App\CodeStyle\MyRule: true under rules), and
DressCode\Testing\RuleTester tests it on pairs of files, the code before and after the fix. Rules and
presets that several projects share are packaged as an extension.
Β
- It runs on PHP 8.4 to 8.6. The code it checks can be written for PHP 8.0 and newer; code for an older version is checked as PHP 8.0, with a warning.
- A file that does not parse is left alone. It is reported as a syntax error and not touched.
- Types come from PHPStan. Without PHPStan in the project, DressCode does not know what a variable is or what a class inherits, and the rules that would need to know stay out.
Β
- dresscode.run - the user manual
- docs/reference/rules.md - every rule with its options, generated from the code
- docs/reference/presets.md - what each preset turns on
dresscode explain <rule>- what a rule is for, its options in your project and its examples
Β
For years I used PHP CS Fixer and PHP_CodeSniffer and took them as a given. Then I needed one new rule and found out how much work it takes when a tool sees only a flat list of tokens. DressCode exists so that a rule can be written in an afternoon, and so that you can trust what it fixes.
DressCode comes from David Grudl, the author of Nette, Latte and Tracy. The syntax tree is PhpSyntax, a library of its own without a single dependency; besides it, DressCode needs four small Nette packages, the phpDoc parser of PHPStan and the version comparator of Composer. No framework.