PHP 8.6 arrives on November 19. DressCode already rewrites code for it. 16 days Upgrading to a newer PHP →

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`

Show on GitHub · Report a problem with this page on GitHub · Nov 2, 2026