PSR-12 decides where every brace and space goes. This page applies those rules and flags the ones needing a person.
PHP went fifteen years without a shared style guide. In 2012 the PHP Framework Interop Group published PSR-1 and PSR-2, and in 2019 PSR-12 replaced PSR-2 with rules for syntax PSR-2 predated: return types, nullable types, declare(strict_types=1) and grouped imports. The group now maintains the PER Coding Style on top of PSR-12 for PHP 8 syntax such as enums, attributes and match. Laravel Pint, PHP-CS-Fixer and PHP_CodeSniffer all ship a PSR-12 preset, so PSR-12 output passes review on most PHP teams without a style argument.
The formatter here handles the parts of PSR-12 a tool decides on its own: brace placement, indentation depth, operator spacing and keyword case. A rule with more than one valid fix, such as a method missing its visibility keyword, gets listed under the output with a line number. Picking private over public for you would hide a design decision inside a whitespace diff.
Before and after, from the class sample
class InvoiceController extends Controller {var $perPage=25;function show($id){if($id===NULL){abort(404);}
else if(!is_numeric($id)){$id=(INTEGER)$id;}
return new InvoiceView;}}class InvoiceController extends Controller
{var $perPage = 25;function show($id){if ($id === null) {abort(404);} elseif (!is_numeric($id)) {$id = (int) $id;}
return new InvoiceView();}}- The class and method braces drop to their own line. The
ifbrace stays on the keyword line. PSR-12 4.1, 4.4, 5 NULLbecomesnull, and(INTEGER)becomes(int)followed by one space. PSR-12 2.5, 6.1else ifturns intoelseif, and the closing brace joins it on one line. PSR-12 5.1new InvoiceViewgains empty parentheses. PSR-12 4varand the missingpubliconshow()stay put. Both appear under Left for you to decide. PSR-12 4.3, 4.4
Two brace rules, and the exceptions people miss
PSR-12 uses two placements. Classes, interfaces, traits, enums, functions and methods put the opening brace on the line after the declaration. if, foreach, while, switch, try and closures keep it on the keyword line. Developers arriving from JavaScript tend to write every brace on the same line. Developers arriving from C# put every brace on the next one. Both fail a PSR-12 check.
The formatter also knows the two exceptions careful writers still get wrong. When a method signature is split across several lines, the closing parenthesis and the opening brace share one line:
public function store(Request $request,InvoiceService $service) {return $service->create($request->validated());}An anonymous class keeps its brace on the opening line, the same as a closure, because new class () extends Base { sits inside an expression. Section 8 of PSR-12 covers this. Prefer K&R braces everywhere? Set Class and method braces to Same line and every other rule keeps running.
Rules applied, with the PSR-12 section behind each
- Closing tag removed 2.2
- A file holding only PHP loses its final
?>. A stray newline after the tag reaches the browser and triggers the "headers already sent" warning on the nextheader()call. - One statement per line 2.3
$a = 1; $b = 2;splits into two lines. Trailing whitespace goes, except inside heredocs, where the spaces belong to the string.- Four space indent 2.4
- Depth comes from open braces and brackets, not from the old whitespace. A tab indented file and a two space file land on the same grid.
- Lowercase keywords, short casts 2.5
TRUE,NULL,ArrayandFUNCTIONgo lowercase.(integer)becomes(int),(boolean)becomes(bool),(double)becomes(float).- Header blocks 3
- One blank line after the opening tag, after
declare(strict_types=1);, after the namespace line and after the last top leveluse.strict_types=1keeps no spaces around its equals sign. - new with parentheses 4
new Invoicebecomesnew Invoice(), includingnew staticandnew self.- Control structures 5
- One space after the keyword, none inside the parentheses, one before the brace.
} else {,} catch (...) {and} while (...);after adoblock share a line. - switch and case 5.2
casesits one level insideswitch, the body one level deeper. A body written on the same line as itscasemoves down.- Operators 6
- One space around binary, comparison, assignment and ternary operators. None after
!, none between$iand++, one after a cast:(int) $input. - Closures 7
function ($line) use ($rate) {with a space afterfunctionand on both sides ofuse. Arrow functions getfn ($x) =>the same way.
Reported, never rewritten
Some PSR-12 violations have more than one correct fix. The panel under the output lists each one with its line number in the formatted code:
- A method without a visibility keyword. PHP treats it as public, though the author often meant private. Section 4.4 requires the keyword, and the choice belongs to whoever owns the class.
- A property declared with
var. Section 4.3 bans the keyword. Swapping inpublickeeps the old behavior, and swapping inprivatefixes what most legacy code meant. Only you know which. - Lines past 120 characters. Section 2.3 sets 120 as a soft limit and asks for 80 where practical. Breaking a long call means choosing where arguments go, so the lines get counted, not wrapped.
- Unbalanced braces. A missing
}throws off every indent below it. The report says so, and the output is worth a second look before you trust it.
Templates with HTML in them
When the input mixes HTML and PHP, as WordPress themes and older views do, the HTML passes through byte for byte and each PHP block is formatted on its own. A one line block stays on one line, so <?=$total?> comes back as <?= $total ?>. Alternative syntax such as foreach (...): with endforeach; gets indented inside a single block. Across separate blocks the indentation stays as you wrote it, since the HTML between them decides the layout. Load template sample shows the result on a short invoice list.
Laravel Blade files are a different language. @if, @foreach and {{ $var }} are not PHP tokens, so input looking like Blade gets refused with a message instead of being mangled.
This page, Pint, PHP-CS-Fixer or PHP_CodeSniffer?
| Tool | Runs where | Command | Pick it when |
|---|---|---|---|
| This page | A browser tab, nothing installed | None | You need one snippet cleaned before pasting it into a pull request comment or a forum answer. |
| Laravel Pint | Composer dev dependency | ./vendor/bin/pint --preset psr12 | The project runs Laravel or you want zero configuration. |
| PHP-CS-Fixer | Composer dev dependency or PHAR | php-cs-fixer fix src --rules=@PSR12 | You want PSR-12 plus extra rules such as ordered imports or strict comparisons. |
| PHP_CodeSniffer | Composer dev dependency | phpcbf --standard=PSR12 src | CI already runs phpcs and the fixer has to match the checker rule for rule. |
All three command line tools parse your code into tokens with PHP's own tokenizer, so they see every construct the way the interpreter does. For a repository with a style check in CI, install one of them. Use this page for the code sitting in a chat message or a ticket.
Your code stays in this tab
Tokenizing, formatting and highlighting all run in JavaScript on this page. Nothing gets sent to a server once the page has loaded, and the formatter keeps working with your connection switched off. PHP pasted into online tools often carries a database password from a config array or an API key sitting in an env() fallback, and a formatter making network requests adds risk for no gain.
Where this formatter falls short
- No syntax check. The page reads tokens, not a parse tree. A missing semicolon passes straight through, so run
php -l file.phpbefore committing the result. - No line wrapping. Long method chains and argument lists stay as wide as you wrote them. The report lists them by line.
- Your line breaks inside brackets stay. A one line array stays on one line and a multi line array gets reindented. A chain line starting with
->gets one continuation indent. - No import sorting. Section 3 orders
usestatements as classes, then functions, then constants. PHP-CS-Fixer'sordered_importsrule does this job. - Aligned arrows collapse. Arrays padded so every
=>lines up in a column come back with one space on each side. - Union types keep your spacing.
int|stringandint | stringboth survive as written, because PSR-12 predates union types and teams split on the style. - Heredoc and nowdoc bodies are copied untouched. Since PHP 7.3 the closing marker's indent gets stripped from every body line, so reindenting the body would change the string.
- Docblocks get reindented, not rewritten. Tag order,
@paramalignment and missing types stay as written. - Inputs above 1 MB are refused. The browser slows down well before then, and a file that size belongs in a local formatter.