Standards and Sets
What exactly do PSR-12, PER Coding Style 3.1, Nette, Symfony and Laravel decide? The preset file says it best, because it has the same shape as your configuration, so this page shows each one in full, with the comments that explain why a decision has its value. A standard decides how the code looks, and a project takes one as its base; a set adds a single intent and can be added on top of any standard.
A preset builds on the presets named in its use key and changes only what it sets itself: nette
builds on perCs, and perCs on psr12. dresscode config --preset nette prints
what finally applies and where each value came from. The Configuration page describes how presets combine with
your configuration, and the Rules page what each key means.
Standards
Five standards decide everything you see: indentation, braces, spaces, blank lines and line breaks. psr12 and
perCs are written from the text of the specifications, section by section, and add nothing of their own.
nette, symfony and laravel build on PER Coding Style and add what their projects want.
A standard splits a line longer than file.lineLength wherever the constructs on it allow. When a line cannot be
split, for example because of a long string or a comment, psr12 and perCs only warn, because
PSR-12 treats the 120-character limit as soft; nette reports an error, and symfony and
laravel do not check the line length at all.
psr12
PSR-12, section by section.
# PSR-12 Extended Coding Style
#
# PSR-12, section by section; the specification defines the style and this preset adds nothing of its own.
severity:
file.longLines: warning # what nothing could spread only warns, the limit being soft
file:
bom: forbidden
openingTag: required # `<?php`, not `<?` or `<?PHP`
strictTypes:
position: nextLine # §3, `<?php` on a line of its own
closingTagAtEnd: forbidden # a `?>` before HTML stays
lineEnding: majority # as most lines of the file, LF on a tie
finalLineEndings: 1
trailingWhitespace: forbidden # the text of a string stays
lineLength: 120 # what is wider is spread
longLines: forbidden
statementsPerLine: 1 # `foo(); bar();` takes two lines
indentation:
unit: '4 spaces'
binaryOperator: 0 # `&& $b` right below `$a`, a level deeper after `$x =` or `return`
ternary: 1 # `?` and `:` opening a line
switchCase: 1 # `case` inside `switch`
chain: flat # every link one level below the start of the chain
commentLines: aligned
braces:
position:
class: nextLine
function: nextLine # one with parameters on one line
multilineSignature: sameLine # `) {` on a line of their own
closure: sameLine
anonymousClass: sameLine
controlStructure: sameLine
continuingKeyword: sameLine # `} else {`
emptyBody: # not `{}`
class: expanded
function: expanded
closure: expanded
propertyHook: expanded
bracelessBody: forbidden # `if ($a) foo();` gets braces
singlelineClosure: forbidden # `function () { return 1; }` is spread
blankLines:
afterOpeningTag: 1
afterDeclare: 1
beforeNamespace: 1
afterNamespace: 1
afterImports: 1
beforeDeclaration: 1
betweenImports: 0
betweenImportKinds: 1
beforeOpenBrace:
class: 0 # §4.1: no blank line between the head and `{` below it
bodyStart:
class: 0
anonymousClass: 0
controlStructure: 0 # §5: the body on the next line after `{`
bodyEnd:
class: 0
anonymousClass: 0
function: 0 # §4.4, §7: `}` on the next line following the body
closure: 0
controlStructure: 0 # §5
branch: 0
betweenTraitUses: 0
afterTraitUses: 1
multiline:
items:
argument: perLine # an argument per line once spread
parameter: perLine # a parameter per line once spread or too wide
for: perLine # a section of `for` per line
implements: perLine # an interface per line below `implements`
matchCondition: perLine # a condition of a `match` arm per line, `=>` after the last
# the parts of a condition and the operands as their author spread them, a line each where too wide
condition: [perLine, fill]
logical: [perLine, fill]
concatenation: [perLine, fill]
arithmetic: [perLine, fill]
bitwise: [perLine, fill]
edges:
argument: apart # `foo(` above the arguments, `)` below them
parameter: apart
implements: apart
condition: apart # `if (` above `$a`, `) {` below the last part
operatorPosition: # `+ $b` opens the line, not ends the one before
operators: lineStart
concatenation: keep
ternary: keep
spacing:
declare: 'declare(strict_types=1)'
call: 'foo($a, $b)' # `function foo($a)` too
padding: # `foo($a)`, `if ($a)`, `($a + $b)`
argument: forbidden
parameter: forbidden
closureUse: forbidden
controlStructure: forbidden
grouping: forbidden
keyword: required # `return $x`, `if (`, `} else {`, `new`, `class Foo extends Bar`
openBrace: required # `if ($a) {`, `function () {`
alternativeSyntax: 'if ($a):'
body: required # `{ return 1; }`, `{ get; set; }`
import: 'use Acme\{Foo, Bar};'
comma: '$a, $b'
alignment:
comma: forbidden # no extra spaces after a comma
cast: '(int) $x'
reference: '&$x'
spread: '...$x'
unaryOperator: '-$x' # `++$i` too
logicalNot: ['!$x', '! $x'] # `! $x` may stay
binaryOperator: '$a + $b' # `=`, `=>`, `instanceof` too
concatenation: '$a . $b'
ternary: '$a ? $b : $c' # `$a ?: $b` too
anonymousClass: 'new class ($a)'
anonymousFunction: 'function ($x)'
typeDeclaration: '?int $x' # `): int` too
catchType: 'catch (A | B $e)'
switchCase: 'case 1:'
# names are reported, never renamed
naming:
class: PascalCase
method: camelCase # `__construct()` passes
constant: UPPER_CASE
casing:
keyword: lowercase # `self`, `parent` too
trueFalseNull: lowercase
type: lowercase # `int`, `string`, `void`
imports:
leadingBackslash: forbidden # `use Foo\Bar;`, not `use \Foo\Bar;`
order:
kinds: [class, function, constant] # the names of a kind in the order written
classes:
visibility: required # `var` is `public`; interface methods and set visibility too
modifiers:
order: canonical # `final public static`
members:
order: [traitUse]
grouping: # `public $a, $b;` is split
property: separated
traitUse: separated
emptyParentheses:
instantiation: required # `new Foo()`
controlFlow:
elseif: 'elseif' # not `else if`
switch:
fallThroughComment: no break # the comment of a fall-through
emptyStatement: forbidden # `foo();;`
expressions:
cast: canonical # `(int)`, not `(integer)` or `( int )`
upgrading:
php:
caseSemicolon: forbidden # `case 1:`, not `case 1;`
perCs
PER Coding Style 3.1, the successor of PSR-12: it adds trailing commas
in multiline lists, arrow functions, empty bodies as {}, attributes, enums, heredocs and property hooks.
# PER Coding Style 3.1
#
# PSR-12 plus what PER Coding Style added (trailing commas, short closures, empty bodies as `{}`, attributes,
# enumerations, heredoc, property hooks); the specification defines the style and this preset adds nothing of its own.
use: psr12
indentation:
heredoc: 1 # body and closing marker one level deeper than the start
braces:
emptyBody: # `function foo(): void {}`, `new class {}`
class: collapsed
function: collapsed
closure: collapsed
propertyHook: collapsed
anonymousClass: collapsed
blankLines:
afterPhpdoc: 0 # §12.2: no blank line between the docblock,
afterAttributes: 0 # the attributes and the structure
multiline:
items:
array: perLine # an item per line once spread
list: perLine
chain: perLine # a link per line once spread
ternary: perLine # `?` and `:` each on a line once spread
edges:
array: apart # `[` above the items, `]` below them
list: apart
chain: apart # `$form` above `->addText()`
operatorPosition:
concatenation: lineStart # `. $b` opens the line
ternary: lineStart # `? $b` and `: $c` open the lines
# a comma after the last item where the closing bracket has a line of its own, none on one line
trailingComma:
array: required
argument: required
parameter: required
matchArm: required
closureUse: required
import: optional
list: optional
attributePosition: above # a group per line above the declaration, and above a parameter in a list spread over lines
spacing:
attribute: '#[Foo($a)]'
arrowFunction: 'fn($x) => $x'
semicolon:
before: '$a;'
catchType: 'catch (A|B $e)' # as a union type
namedArgument: 'foo(name: $x)'
naming:
enumCase: PascalCase # reported, never renamed
classes:
visibility:
publicWithSet: keep # `public` may be omitted with a set visibility
members:
grouping:
constant: separated # `const A = 1, B = 2;` is split
emptyParentheses:
anonymousClass: forbidden # `new class {}`, not `new class() {}`
attribute: forbidden # `#[Foo]`, not `#[Foo()]`
instantiationWrapping: forbidden # `new Foo()->bar()`, not `(new Foo())->bar()`, from PHP 8.4
protectedInEnum: forbidden # `private const X`
functions:
arrowFunction:
emptyBody: required # `fn() => null`, not `function () {}`
types:
nullable: '?int' # not `int|null`
nullPosition: last # `int|string|null`
literals:
heredocWithoutInterpolation: forbidden # a nowdoc `<<<'EOT'` instead
arraySyntax:
array: short # `[1, 2]`, not `array(1, 2)`
phpdoc:
aboveAttributes: required
nette
Nette Coding Standard is PER Coding Style with tabs and the
layout of the Nette libraries, declare(strict_types=1) on the same line as <?php, and the code
hygiene the Nette libraries follow. On top of perCs, it uses the sets cleanup, types and
correctness, and sets a few of their decisions back to keep: the else branch after
return, debugging calls, a class with __toString() without Stringable, both notations of
the array type, and native types on parameters, properties and return values. A project that adds the same sets alongside
nette does not get those decisions back, because each set is applied only once, beneath nette. If you
want them, set those decisions yourself.
# Nette Coding Standard
#
# PER Coding Style with tabs and the layout of the Nette libraries, the hygiene of the code, and the choices the Nette
# libraries make: strict types declared on the line of the opening tag, how names are cased, in what order the members
# of a class stand, which functions and constructs are out, and what a doc comment of theirs holds. The layout departs
# from PER Coding Style in two blank lines between methods, none inside the braces of a body, one closing a branch or
# a case another one follows when its last statement stands apart, a tab that may align commas, the brace of
# a multi-line signature below a return type, promoted properties on lines of their own, an expression on the line of
# its return, and a chain, an array or a condition that may keep the shape it has.
use:
- perCs
- cleanup
- types
- correctness
file:
strictTypes:
declaration: required # risky, `'1'` passed to `int` then throws
position: sameLine
lineLength: 140 # a tab counts as 4 columns
lineLengthExcept: [imports, strings] # a line too wide only for its strings is not reported
indentation:
unit: tab
chain: nested # a link may stand one level deeper than the one before
braces:
position:
typedMultilineSignature: nextLine # `{` below `): Foo`, otherwise `) {`
emptyBody: # not `{}` as in PER Coding Style
class: expanded
function: expanded
closure: expanded
propertyHook: expanded
anonymousClass: keep
singlelineClosure: keep
alternativeSyntax: forbidden # no `if: ... endif;`
# a range `1-2` leaves the choice to the author
blankLines:
betweenImportKinds: 0
beforeDeclaration: 2 # above the class of a file
aroundDeclarations: 1-2 # around one among statements
betweenStatementsInFunction: 0-2
aroundMethods: 2
aroundInterfaceMethods: 1
betweenMemberKinds: 0-1 # members without a doc comment
betweenMembers: 0-1
beforeDocumentedMember: 1
bodyStart:
blocks: 0
bodyEnd:
blocks: 0
branch: lastStatementSetApart # before `} else {` where the last statement stands apart
switchCase: lastStatementSetApart # likewise before `case`
multiline:
items:
array: fill # items may share a line
list: fill
import: fill # the names filling the lines
argument: fillPlain # `foo($a, $b, self::X)` share lines, `foo(fn() => 1, bar())` one on a line
matchArm: perLine
matchCondition: fill # conditions may share a line
edges:
array: [apart, attached] # `]` begins a line where the first item does
list: [apart, attached]
import: apart
chain: attached # `$form->addText()` above `->setRequired()`
condition: [apart, closingApart] # `if (` above `$a`, or `if ($a` above `&& $b`
maxWidth:
array: 130
fillMinItems:
array: 6 # fewer items stand one on a line
list: 6
matchCondition: 6
spreadItem: sole # `foo(bar(` above the arguments of `bar()`, `))` below them
spreadAlways: [promotedProperty] # however short
expressionStart: # `return [`, not `return` above `[`
return: sameLine
throw: sameLine
yield: sameLine
echo: sameLine
semicolon: sameLine
trailingComma: required # a group use and `list()` too
spacing:
alignment:
comma: tabs # tabs aligning columns may stay
ternary: forbidden # no extra spaces around `?` and `:`
semicolon:
after: 'for ($i = 0; $i < 5; $i++)'
logicalNot: '!$x' # never `! $x`
doubleColon: 'Foo::bar()'
objectOperator: '$a->b()'
padding:
array: forbidden # `[1, 2]`, `[$a, $b] = $x`
list: forbidden
offset: forbidden # `$a[0]`
comment:
marker: '// text' # also `#` and `/* */`
afterCode: '$a; // text' # at least one space
# names are reported, never renamed
naming:
function: camelCase
constant: PascalCase
property: camelCase
variable: camelCase
abstractClassAffix: forbidden # `Foo`, not `AbstractFoo`
interfaceAffix: forbidden # `Foo`, not `FooInterface`
traitAffix: forbidden
errorAffix: forbidden
casing:
magicConstant: uppercase
class: declared # `DateTime`, not `datetime`
function: declared # `strlen()`, not `StrLen()`
method: declared # `$o->getTotal()`, not `$o->gettotal()`, with `typeAnalysis`
qualification:
class:
otherNamespace: imported
currentNamespace: bare
phpdoc: imported # `@param Order $order`, not `@param \Acme\Shop\Order $order`
currentClass: self # `self::bar()` inside `Foo`, not `Foo::bar()`
imports:
order:
names: alphabetical # classes, functions, constants, each alphabetically
grouping:
class: [separated, grouped] # `use A; use B;`, a group use stays
function: [combined, grouped] # `use function a, b;`
constant: [combined, grouped] # `use const A, B;`
classes:
visibility:
interfaceMethod: forbidden # public anyway
publicWithSet: required # `public private(set)`
members:
# the methods then in the order written
order: [traitUse, enumCase, publicConstant, protectedConstant, privateConstant, publicProperty, protectedProperty, privateProperty]
grouping:
constant: keep # `const A = 1, B = 2;` may stay
emptyParentheses:
instantiation: forbidden # `new Foo`, but `new Foo()->bar()`
functions:
innerFunction: forbidden
arrowFunction:
oneExpression: required # `fn() =>` for a closure only returning
emptyBody: keep # `function () {}` stays a callback doing nothing
arrowFunctionNested: yes # also one holding another closure
types:
declaration: # a type only where it says something
parameter: keep
property: keep
return: keep
anonymousFunction: keep
controlFlow:
switch:
fallThroughComment: break omitted # the comment of a fall-through
elseAfterReturn: keep # may stay
expressions:
comparison:
loose: forbidden # risky
notEquals: '!='
yoda: forbidden # `$a === 1`
assignment:
addingOne: forbidden # `$i++`, not `$i += 1`
increment: postfix # `$i++;`
ternary:
short: required # `$a ?: $b`
coalesce: required # `$a ?? $b`
wordLogicalOperator: forbidden
explicitPrecedence: required # `($a && $b) || $c`
doubleNegation: forbidden # `(bool) $a`, not `!!$a`
literals:
quotes: single
arraySyntax: short # `[1, 2]`, `[$a] =`
implicitBackslash: forbidden # `"\\d"`, not `"\d"`
digitSeparator:
integer: required # `1_000_000`
fraction: required
digitSeparatorMinDigits:
integer: 7
fraction: 20
comments:
singleline: '//' # not `#`
phpdoc:
annotation:
casing: canonical # `@inheritDoc`, not `@inheritdoc`
inlineVar: forbidden # becomes `assert()`, risky
stars: aligned # below the first `*` of `/**`
blankLines: trimmed
property:
plainComment: forbidden # not `//` or `/* */`
lineSpan: collapsed # `/** @var int */`
promoted: '@var' # not `@param` of the constructor
forbiddenAnnotations: [@access, @author, @copyright, @created, @license, @package, @since, @subpackage, @todo, @version]
# `Constructor.`, `Name getter.`
forbiddenLines: ['~^(?:(?!private|protected|static)\S+ )?(?:con|de)structor\.\z~i', '~^\S+ [gs]etter\.\z~i']
types:
array: keep # both notations stay
nullPosition: last
hygiene:
is_null: forbidden # `$x === null`
settype: forbidden # `$x = (int) $x`
conversionFunction: forbidden # `(int) $x`, not `intval($x)`
dirnameOfFile: forbidden # `__DIR__`
nestedDirname: forbidden # `dirname($p, 2)`
classNameNotation:
currentClass: required # `self::class`, `static::class`
object: required # `$object::class`
mistakes:
globalStatement: forbidden
debugOutput: keep # may stay
upgrading:
php:
deprecatedApi: forbidden # `mb_convert_encoding()`, not `utf8_encode()`, `E_STRICT` reported
deprecatedArgument: forbidden # `trigger_error($m, E_USER_ERROR)` reported
dollarBraceInterpolation: forbidden # `{$name}`, not `${name}`
implicitNullable: forbidden # `?int $x = null`, not `int $x = null`
backticks: forbidden # `shell_exec()`
syntax:
octalPrefix: required # `0o17`, not `017`
classes:
Stringable: keep # implied by `__toString()`
# comments that silence a decision on their line
suppressionComments:
'~\bintentionally\s+[!=]=~': {decisions: [expressions.comparison.loose, mistakes.strictArgument], example: '// intentionally =='}
'~^//\s*@\s+\S~': {decisions: mistakes.errorSuppression, example: '// @ <why>'}
symfony
The Symfony Coding Standards as defined by the @Symfony rule set of PHP CS Fixer, for now without most of its
phpDoc rules.
# Symfony Coding Standards
#
# The Symfony Coding Standards as the @Symfony rule set of PHP CS Fixer defines them, without most of its rules about
# phpDoc, of which it takes only an empty doc comment, the trimming and the canonical types: PER Coding Style, which the
# set builds on, with the casing, the imports, the blank lines and the whitespace Symfony adds, and without the rules
# that would break a construct over lines, which Symfony leaves to the author.
use: perCs
file:
lineLength: none
longLines: keep
indentation:
binaryOperator: keep
chain: keep
heredoc: keep # heredoc_indentation belongs to another set
braces:
emptyBody: # not `{}` as in PER Coding Style
class: expanded
function: expanded
closure: expanded
propertyHook: expanded
anonymousClass: keep
singlelineClosure: keep
bareStatementGroup: forbidden # no `{ ... }` that nothing opens
alternativeSyntax: forbidden # no `if: ... endif;`
# a range `0-1` leaves the choice to the author, `1+` is one or more
blankLines:
afterDeclare: keep # @Symfony leaves the line below a declare
aroundDeclarations: 0-1 # around one among statements
aroundMethods: 1
aroundInterfaceMethods: 1
betweenTraitUses: keep
afterTraitUses: keep
beforeStatement: {return: 1+} # a `return` set apart from the code above it
belowComment:
inlineVar: 0 # `/** @var Order $order */` right above its statement
multiline:
# no fixer of the set breaks a call, a signature, a chain, a condition, a ternary, an operator or an array
items: keep
edges: keep
operatorPosition: # operator_linebreak moves only a boolean operator
operators: keep
logical: lineStart
condition: lineStart
trailingComma:
argument: optional # may stay or not where spread, never on one line
closureUse: keep
attributePosition:
parameter: keep
spacing:
arrowFunction: 'fn ($x) => $x'
alignment:
comma: optional # aligning spaces and tabs may stay
binaryOperator: forbidden # no extra spaces around an operator
semicolon:
after: 'for ($i = 0; $i < 5; $i++)'
logicalNot: '!$x' # never `! $x`
concatenation: '$a.$b'
objectOperator: '$a->b()'
padding:
array: forbidden # `[1, 2]`, `[$a, $b] = $x`
list: forbidden
offset: forbidden # `$a[0]`
anonymousClass: 'new class($a)'
comment:
marker: '// text' # also `#` and `/* */`
afterCode: '$a; // text' # at least one space
casing:
magicConstant: uppercase
class: declared # `DateTime`, not `datetime`
function: declared # `strlen()`, not `StrLen()`
method: declared # `__toString()`, other methods with `typeAnalysis`
hexDigit: keep # integer_literal_case left to the project
# global_namespace_import imports nothing: a class is written with the backslash, a function and a constant too where
# they are qualified, and a bare one stays bare
qualification:
class:
global: backslashed # `\DateTime`
function:
global: [backslashed, bare] # `\ucfirst()` or `ucfirst()`, never imported
optimized: [backslashed, bare]
constant:
global: [backslashed, bare]
optimized: [backslashed, bare]
imports:
order:
names: alphabetical # classes, functions, constants, each alphabetically
grouping: separated # `use A; use B;`, not `use A, B;` or `use Acme\{A, B};`
unused: forbidden
aliasEqualToName: forbidden # `use Foo;`, not `use Foo as Foo;`
classes:
visibility:
publicWithSet: required # `public private(set)`, as modifier_keywords writes it
emptyParentheses:
attribute: keep # `#[Foo()]` stays, as in @Symfony
untypedPropertyNullDefault: forbidden # `public $a;`, not `public $a = null;`
instantiationWrapping: keep # @Symfony leaves the parentheses
protectedInFinalClass: forbidden # `private` in a final class with no parent and no trait
functions:
trailingBareReturn: forbidden # no `return;` closing a function
controlFlow:
elseAfterReturn: forbidden # the `else` after a `return` goes, its body following the `if`
expressions:
comparison:
notEquals: '!=' # not `<>`
yoda: required # `null === $a`
assignment:
addingOne: forbidden # `++$i`, not `$i += 1`
increment: prefix # `++$i;`
doubleNegation: forbidden # `(bool) $a`, not `!!$a`
parenthesesAfterConstruct: forbidden # `return $x;`, not `return ($x);`
literals:
quotes: single
heredocWithoutInterpolation: keep # heredoc_to_nowdoc belongs to another set
concatenatedLiterals: keep # no_useless_concat_operator left to the project
comments:
singleline: '//' # not `#`
empty: forbidden
phpdoc:
aboveAttributes: keep # @Symfony leaves the order alone, unlike PER Coding Style
blankLines: trimmed
empty: forbidden
types:
builtin: canonical # `int`, not `integer`
duplicate: forbidden # each type of a union once
nullPosition: last
mistakes:
continueInSwitch: forbidden # a `continue` only leaving a `switch`, reported
upgrading:
php:
dollarBraceInterpolation: forbidden # `{$name}`, not `${name}`
implicitNullable: forbidden # `?int $x = null`, not `int $x = null`
backticks: forbidden # `shell_exec()`
laravel
The Laravel style as defined by the laravel preset of Laravel Pint: PER Coding Style with concatenation without
spaces, ! $x, new Foo, and imported names in phpDoc too. The Migrating from Laravel Pint page describes how it differs from Pint.
# Laravel
#
# The style of Laravel as the preset laravel of Laravel Pint defines it: PER Coding Style with the concatenation, the
# negation and the instantiation Laravel writes, the arrow function spaced, the imports qualified and sorted, a blank
# line before `return` and `continue`, the phpDoc trimmed, and without the rules that break a construct over lines,
# which Pint leaves to the author.
use: perCs
file:
lineLength: none
longLines: keep
strictTypes:
position: keep
indentation:
heredoc: keep
binaryOperator: keep
ternary: keep
braces:
alternativeSyntax: forbidden
bareStatementGroup: forbidden
position:
anonymousClass: nextLine
emptyBody:
propertyHook: keep
blankLines:
bodyStart:
controlStructure: keep
bodyEnd:
function: keep
closure: keep
controlStructure: keep
branch: keep
class: keep
anonymousClass: keep
aroundMethods: 1
aroundInterfaceMethods: 1
beforeOpenBrace:
class: keep
afterTraitUses: keep
betweenMemberKinds: 1
betweenMembers: # enum cases together
constant: 1
property: 1
beforeDocumentedMember: 1
afterDeclare: keep
afterAttributes: keep
beforeStatement:
continue: '1+'
return: '1+'
# method_argument_space ignores what spreads over lines, and Pint has nothing that would spread a construct
multiline:
items:
array: keep
list: keep
implements: keep
condition: keep
for: keep
logical: keep
concatenation: keep
arithmetic: keep
bitwise: keep
chain: keep
matchCondition: keep
ternary: keep
argument: keep
parameter: keep
edges:
array: keep
list: keep
implements: keep
condition: keep
chain: keep
argument: keep
parameter: keep
trailingComma:
argument: optional # arrays only
parameter: optional
matchArm: keep
closureUse: keep
operatorPosition:
condition: keep
comparison: keep
logical: keep
concatenation: keep
arithmetic: keep
bitwise: keep
coalesce: keep
pipe: keep
ternary: keep
attributePosition:
declaration: keep
parameter: keep
semicolon: sameLine
spacing:
anonymousClass: 'new class($a)'
comment:
marker: // text
concatenation: $a.$b
alignment:
binaryOperator: forbidden
comma: optional
objectOperator: '$a->b()'
doubleColon: 'Foo::bar()'
logicalNot: ! $x
reference: keep
spread: keep
namedArgument: keep
attribute: keep
alternativeSyntax: keep
body: keep
import: keep
arrowFunction: 'fn ($x) => $x'
padding:
array: forbidden
list: forbidden
offset: forbidden
semicolon:
after: 'for ($i = 0; $i < 5; $i++)'
naming:
class: keep
method: keep
constant: keep
enumCase: keep
casing:
magicConstant: uppercase
hexDigit: uppercase
function: declared # `strlen()`, not `StrLen()`
qualification:
staticInFinalClass: required # `self::` in a final class
class:
otherNamespace: imported
currentNamespace: bare
phpdoc: imported # `@param Request $request`, not `@param \Illuminate\Http\Request $request`
imports:
grouping:
class: separated
function: separated
constant: separated
order:
kinds: [constant, class, function]
names: alphabetical
unused: forbidden
aliasEqualToName: forbidden
classes:
members:
order: keep
grouping:
traitUse: keep
protectedInEnum: keep
visibility:
constant: keep
emptyParentheses:
instantiation: forbidden # `new Foo`
attribute: keep
instantiationWrapping: keep
functions:
trailingBareReturn: forbidden
arrowFunction:
emptyBody: keep
unreachableParameterDefault: forbidden
types:
nullPosition: keep
controlFlow:
switch:
fallThroughComment: keep
expressions:
parenthesesAfterConstruct: forbidden # `return $x;`, not `return ($x);`
assignment:
increment: postfix
doubleNegation: forbidden
comparison:
notEquals: '!='
yoda: forbidden
literals:
arraySyntax:
list: short
quotes: single
comments:
singleline: //
phpdoc:
empty: forbidden
forbiddenAnnotations: [@access, @package, @subpackage]
aboveAttributes: keep
stars: aligned
blankLines: trimmed
types:
builtin: canonical
annotation:
nativeTypeOnly: forbidden
hygiene:
aliasFunctions: [core, imap, pg]
upgrading:
php:
implicitNullable: forbidden
Sets
A set decides nothing about how the code looks, so it never conflicts with a standard. It is added on top of the standard
(use: [nette, modernizations]), and above all, it grows: a decision that DressCode gains in a new version is added to
its set, and a project that uses the set gets it without changing its configuration.
modernizations
Newer PHP constructs in place of older notation with the same meaning, each only where the project's target PHP version allows it, plus the newer notation offered by the data files of installed packages.
# Modernizations
#
# The construct a newer PHP has, written where an older one says the same, each where the target version of the
# project has it, and the newer form the upgrading files of the installed packages offer. It decides nothing about the
# layout, so it goes with any standard.
classes:
instantiationWrapping: forbidden # `new Foo()->bar()`, not `(new Foo())->bar()`
functions:
arrowFunction:
oneExpression: required # `fn() =>` for a closure only returning
arrowFunctionNested: yes # also one holding another closure
expressions:
ternary:
short: required # `$a ?: $b`
coalesce: required # `$a ?? $b`
literals:
arraySyntax: short # `[1, 2]`, `[$a] =`
hygiene:
classNameNotation:
currentClass: required # `self::class`, `static::class`
object: required # `$object::class`, PHP 8.0
upgrading:
packages: yes # as the upgrading files of the packages say
php:
sleepAndWakeup: forbidden # `__serialize()` and `__unserialize()` instead
syntax:
spreadForArrayMerge: required # `[...$a, ...$b]`, not `array_merge($a, $b)`
dynamicClassConstant: required # `X::{$name}`, not `constant(X::class . '::' . $name)`
match: required # for a `switch` returning or assigning
throwExpression: required # `$a ?? throw new E`
cloneWith: required # `clone($o, [...])`
nullsafe: required # `$a?->b`, not `$a !== null ? $a->b : null`
pipe: required # `$x |> a(...) |> b(...)`, not `b(a($x))`
firstClassCallables: required # `foo(...)`, not `Closure::fromCallable('foo')`
octalPrefix: required # `0o17`, not `017`
newInInitializer: required # `Clock $c = new SystemClock`, not a `null` swapped in the body
promotedProperties: required
readonlyClasses: required # `readonly class`, not `readonly` on every property
functions:
array_is_list: required
arraySearchFunctions: required # `array_find()`, `array_any()` for a `foreach`
arrayFirstLast: required # `array_first()`, not `reset()`
clamp: required # `clamp($x, $lo, $hi)`, not `max($lo, min($x, $hi))`
get_debug_type: required
json_validate: required # not `json_decode()` only testing
substringFunctions: required # `str_contains()`, not `strpos() !== false`
roundingMode: required # `RoundingMode::HalfAwayFromZero`, not `PHP_ROUND_HALF_UP`
phpdoc:
deprecated: required # `#[\Deprecated]`, not `@deprecated`
deprecations
What newer PHP versions have deprecated or removed, fixed where a replacement exists and reported where it does not, plus what the data files of installed packages list as deprecated.
# Deprecations
#
# What a newer PHP deprecated or removed, fixed where a replacement exists and reported where none does, and what the
# upgrading files of the installed packages list as retired. A deprecation is reported whatever the target, the code
# meeting it wherever it runs on the version that made it.
classes:
overridingMethod:
signature: required # a child matching a parent changed by an upgrade
parameterName: required
expressions:
cast: canonical # `(int)`, not `(integer)`, PHP 8.5
# what a library or PHP breaks in a major version, reported
mistakes:
finalParent: forbidden # a class extending a final one
unimplementedAbstractMethod: forbidden
__serialize: required # beside `Serializable`, PHP 8.1
upgrading:
packages: yes # as the upgrading files of the packages say
declarations:
deprecatedClass: forbidden # by what `@deprecated` names
deprecatedMember: forbidden
deprecatedFunction: forbidden
php:
nullArrayKey: forbidden # `['' => 1]`, not `[null => 1]`, PHP 8.5
nullArgument: forbidden # `trim($text ?? '')`, not `trim($text)` of a null, PHP 8.1
floatArrayKey: forbidden # `$a[(int) $ratio]`, not `$a[$ratio]` of a float, PHP 8.1
dynamicProperty: forbidden # a property the class declares, reported, PHP 8.2
caseSemicolon: forbidden # `case 1:`, not `case 1;`, PHP 8.5
implicitNullable: forbidden # `?int $x = null`, not `int $x = null`, PHP 8.4
deprecatedApi: forbidden # `mb_convert_encoding()`, not `utf8_encode()`, `E_STRICT` reported
deprecatedArgument: forbidden # `trigger_error($m, E_USER_ERROR)` reported
constructorReturnValue: forbidden # PHP 8.6
sleepAndWakeup: forbidden # `__serialize()`, PHP 8.5
returnInFinally: forbidden # PHP 8.6
csvEscapeArgument: required # the `escape` of `fgetcsv()` written out, PHP 8.4
dollarBraceInterpolation: forbidden # `{$name}`, not `${name}`, PHP 8.2
reservedNames: forbidden # `let`, `is`, `_`, PHP 8.6
backticks: forbidden # `shell_exec()`, PHP 8.5
syntax:
firstClassCallables: required # `foo(...)`, not `Closure::fromCallable('foo')`
cleanup
Code that serves no purpose: repetition of what the language already says, code that never runs, function aliases, comments that say nothing.
# Cleanup
#
# Code that is there for nothing: what repeats what the language already says, what can never run, an alias of a
# function, a comment or a doc comment that says nothing. It decides nothing about the layout, so it goes with any
# standard.
braces:
bareStatementGroup: forbidden # no `{ ... }` that nothing opens
qualification:
leadingBackslash: forbidden # `strlen()` in a file without a namespace, not `\strlen()`
imports:
leadingBackslash: forbidden # `use Foo\Bar;`, not `use \Foo\Bar;`
unused: forbidden
aliasEqualToName: forbidden # `use Foo;`, not `use Foo as Foo;`
fromCurrentNamespace: forbidden # `use Acme\Shop\Order;` in `namespace Acme\Shop`
classes:
modifiers:
implied: forbidden # `final` in a final class
overridingMethod:
parentCallOnly: forbidden # one only forwarding to `parent::`, risky
ReturnTypeWillChange: forbidden # on a method declaring its return type
untypedPropertyNullDefault: forbidden # `public $a;`, not `public $a = null;`
functions:
trailingBareReturn: forbidden # no `return;` closing a function
unreachableParameterDefault: forbidden # a default before a required parameter
controlFlow:
emptyStatement: forbidden # `foo();;`
ifReturningTrueFalse: forbidden # `return $a > 1;` for an `if` returning `true` or `false`
elseAfterReturn: forbidden # the `else` after a `return` goes, its body following the `if`
expressions:
parenthesesAfterConstruct: forbidden # `return $x;`, not `return ($x);`
assignment:
targetInValue: forbidden # `$a += $b`, not `$a = $a + $b`
ternary:
ofTrueFalse: forbidden # `$a > 1`, not `$a > 1 ? true : false`
literals:
classNameInString: forbidden # `Order::class`, not `'Acme\Shop\Order'`
concatenatedLiterals: forbidden # `'ab'`, not `'a' . 'b'`
emptyStringConcatenation: forbidden # no `. ''`
comments:
empty: forbidden
phpdoc:
empty: forbidden
inheritdocOnly: forbidden # one holding only `{@inheritDoc}`
annotation:
nativeTypeOnly: forbidden # `@param int $a` on `int $a`
constantVar: forbidden # a bare `@var int` on a class constant
hygiene:
aliasFunctions: [core, imap, pg] # `count()`, not `sizeof()`
strlenEmptyTest: forbidden # `$s === ''`, not `strlen($s) === 0`
countEmptyTest: forbidden # `$a === []`, not `count($a) === 0`
call_user_func: forbidden # `$f($x)`, not `call_user_func($f, $x)`, risky
__invokeCall: forbidden # `$f($x)`, not `$f->__invoke($x)`
defaultArgument: forbidden # `json_encode($a)`, not `json_encode($a, 0)`
unreadCatchVariable: forbidden # `catch (E)` where `$e` is not read
repeatedCatchBody: forbidden # `catch (A|B $e)` for two catches of one body
separateIssets: forbidden # `isset($a, $b)`
separateUnsets: forbidden # `unset($a, $b);`
uselessVersionCheck: forbidden # no `if (PHP_VERSION_ID >= 80000)` the target always passes
correctness
What is most likely a bug, whatever the style.
# Correctness
#
# What is most likely a mistake whatever the style: a call comparing loosely where it should not, `$this` where no
# object is, a `catch` nothing reaches, a doc comment contradicting itself or written so that no tool reads it,
# invisible characters in the code.
phpdoc:
annotation:
extraParam: forbidden # a `@param` of no parameter
duplicateParam: forbidden # a second `@param` of one parameter
duplicateReturn: forbidden
empty: forbidden # `@param`, `@return`, `@var`, `@see` without content
duplicateVar: forbidden
plainComment: forbidden # `/* @var */` and `/**@var`, which no tool reads
mistakes:
__debugInfoReturningNull: forbidden # `[]`, not `null`
thisOutsideObject: forbidden # in a static method or a function, reported
staticSetState: required
continueInSwitch: forbidden # a `continue` only leaving a `switch`, reported
unreachableCatch: forbidden # after `catch (Throwable)`
exceptionWhereThrowableBelongs: forbidden # `catch (Exception)` where `Throwable` is meant, reported
errorSuppression: forbidden # the `@` operator, reported
invisibleCharacter: forbidden # zero-width, no-break and bidi characters, escaped
strictArgument: required # `in_array($a, $b, true)`, risky
trailingWhitespaceInString: forbidden # in a multi-line string or inline HTML
consecutivePhpdocs: forbidden # the first of two is lost, reported
repeatedAssignment: forbidden # `$a = $a = 1`
looseNumberComparison: forbidden # `0 == 'a'`, true before PHP 8.0, reported
countOfUncountable: forbidden # `count()` of no array and no `Countable`, reported
debugOutput: forbidden # `// var_dump($a);`, risky
types
Types written where PHP reads them.
# Types
#
# Types written where PHP reads them: a native type on every parameter, property and return that has one, one notation
# of each type (`?int`, `int` rather than `integer`, `array<T>` rather than `T[]`), `never` on a function that only
# throws, `Stringable` on a class with `__toString()`.
types:
declaration:
parameter: required # from `@param`, risky
property: required # from `@var`, risky
return: required # from `@return`, `void` where nothing returns, risky
anonymousFunction: required # `void` where a closure returns nothing, risky
nullable: '?int' # `?int`, not `int|null`
nullPosition: last # `int|string|null`
phpdoc:
annotation:
nativeTypeOnly: forbidden # the annotation the native type now says
types:
builtin: canonical # `int`, not `integer`
duplicate: forbidden # each type of a union once
array: 'array<T>' # `array<int>`, not `int[]`
mistakes:
neverReturnType:
function: required # on a function that always throws
method: required
upgrading:
classes:
Stringable: required # on a class with `__toString()`
compilerOptimizations
Global functions and constants in the form PHP optimizes at compile time, see Faster Calls When the Name Is Certain.
# Compiler optimizations
#
# Global names written in the form PHP optimizes when it compiles the code: a function the compiler turns into an
# opcode and a constant it computes with are imported, so that the compiler knows them, and every other global name is
# written bare.
qualification:
function:
global: bare # `array_map()`, reached by the fallback at run time
optimized: imported # `use function strlen;`, its arguments positional
constant:
global: bare # `E_ALL`
optimized: [imported, backslashed] # `PHP_VERSION_ID` imported or `\PHP_VERSION_ID`