Indentation
January 21, 2025 ยท View on GitHub
Heredoc indentation
Heredocs and nowdocs are indented in one of four ways. Mixed indentation is enabled by default.
No heredoc indentation
Enabled when --heredoc-indent none is given.
<?php
$foo = [
'bar' => <<<EOF
Content
EOF,
];
Line heredoc indentation
Enabled when --heredoc-indent line is given.
<?php
$foo = [
'bar' => <<<EOF
Content
EOF,
];
Mixed heredoc indentation
Line indentation is applied to heredocs that start on their own line, otherwise hanging indentation is applied.
Enabled by default and when --heredoc-indent mixed is given.
<?php
$foo = <<<EOF
Content
EOF;
$bar =
<<<EOF
Content
EOF;
Hanging heredoc indentation
Enabled when --heredoc-indent hanging is given.
<?php
$foo = <<<EOF
Content
EOF;
$bar =
<<<EOF
Content
EOF;
Standard indentation
Code between brackets ((), [], {}) is always indented when the open
bracket is followed by a line break.
Comments
The PlaceComments rule aligns comments with subsequent code unless the next
code token is a close bracket or switch case:
<?php
switch ($foo) {
//
case 0:
// Indented
case 1:
bar();
// Indented
case 2:
break;
//
case 3:
// Indented
// Indented
//
case 4:
baz();
// Indented
// Indented
//
case 5:
break;
// Indented
//
//
case 6:
qux();
// Indented
//
case 7:
break;
//
//
default:
}
Hanging indentation
Hanging indentation is applied when the context of a line of code is unclear after standard indentation has been applied.
Consider this contrived example:
<?php
if (!$request->route()->isPrivate() ||
($request->hasUser() &&
($this->userHasRoute($request->user(),
$request->route()) ||
$request->user()->role() === User::ADMIN_ROLE ||
$request->user()->role() === User::EDITOR_ROLE)) ||
Session::hasCarteBlanche()) {
Console::log('Access granted');
}
Without pretty-php's "overhanging" indentation, more effort is required to
understand how it works because the same level of indentation has been applied
to adjacent lines that belong to different contexts:
<?php
if (!$request->route()->isPrivate() ||
($request->hasUser() &&
($this->userHasRoute($request->user(),
$request->route()) ||
$request->user()->role() === User::ADMIN_ROLE ||
$request->user()->role() === User::EDITOR_ROLE)) ||
Session::hasCarteBlanche()) {
Console::log('Access granted');
}
With no hanging indentation at all, even more effort is required:
<?php
if (!$request->route()->isPrivate() ||
($request->hasUser() &&
($this->userHasRoute($request->user(),
$request->route()) ||
$request->user()->role() === User::ADMIN_ROLE ||
$request->user()->role() === User::EDITOR_ROLE)) ||
Session::hasCarteBlanche()) {
Console::log('Access granted');
}
One solution (adopted in PSR-12 / PER) is to insert line breaks after open brackets. In many cases this is sensible, but it doesn't cover every PHP construct and isn't always desirable.
<?php
if (
!$request->route()->isPrivate() ||
(
$request->hasUser() &&
(
$this->userHasRoute(
$request->user(),
$request->route()
) ||
$request->user()->role() === User::ADMIN_ROLE ||
$request->user()->role() === User::EDITOR_ROLE
)
) ||
Session::hasCarteBlanche()
) {
Console::log('Access granted');
}
How HangingIndentation works
Using arrays as a metaphor for constructs with one or more entries that may be
arbitrarily nested, the HangingIndentation rule recognises the following
indentation scenarios.
.hh.represents a level of hanging indentation, and.HH.represents a level of "overhanging" indentation.
-
Standard indentation is sufficient
[ ___, ___, ___, ___ ]; -
One level of hanging indentation is required
[ ___, ___ .hh.___, ___, ___, ___ ]; -
One level of hanging indentation is sufficient
[___, ___, .hh.___, ___]; -
Two levels of hanging indentation are required
[___, ___ .hh..HH.___, ___, .hh.___, ___]; -
Two levels of hanging indentation are required per level of nesting
[___, [___, .hh..HH..hh.___], .hh.___,[___, ___ .hh..HH..hh..HH.___, .hh..HH..hh.___]];[[[___ .hh..HH..hh..HH..hh..HH.___, .hh..HH..hh..HH..hh.___], .hh..HH..hh.___], .hh.___]
Overhanging indentation is also applied to blocks that form part of a continuing
structure, e.g. the if block above.
Context
After finding a token to indent, HangingIndentation creates a context for it,
and if indentation for that context has already been applied, the token is not
indented further.
A token's context is an array{?Token, ?Token, ?Token, 3?:Token|int, 4?:int}
comprised of:
- its parent (or
nullif it's a top-level token) - the most recent assignment operator or
T_DOUBLE_ARROWin the same statement (ornullif it isn't part of an expression after an assignment or=>) - its ternary context (or
nullif it isn't part of a ternary expression) - an optional token and/or precedence value shared by any siblings that should receive the same level of indentation
The aim is to differentiate between lines where a new expression starts, and lines where an expression continues:
<?php
fn($a, $b) =>
$a === $b
? 0
: $a <=>
$b;
There are many more examples in the HangingIndentation unit
test.