{
    "href": "/post/2026/04/24/php-styler-a-back-to-formula-rewrite/",
    "relId": "2026/04/24/php-styler-a-back-to-formula-rewrite",
    "title": "PHP-Styler: A Back-To-Formula Rewrite",
    "author": "pmjones",
    "tags": [
        {
            "href": "/tag/programming/",
            "relId": "programming",
            "title": "Programming",
            "author": null,
            "created": null,
            "updated": [],
            "markup": "markdown"
        },
        {
            "href": "/tag/php/",
            "relId": "php",
            "title": "PHP",
            "author": null,
            "created": null,
            "updated": [],
            "markup": "markdown"
        },
        {
            "href": "/tag/styler/",
            "relId": "styler",
            "title": "Styler",
            "author": null,
            "created": "2023-08-14 14:20:33 UTC",
            "updated": [
                "2023-08-14 14:20:33 UTC"
            ],
            "markup": "markdown"
        }
    ],
    "created": "2026-04-24 17:44:43 UTC",
    "updated": [
        "2026-04-24 17:44:43 UTC",
        "2026-04-24 17:55:19 UTC",
        "2026-04-24 17:56:02 UTC",
        "2026-04-24 17:57:27 UTC"
    ],
    "markup": "markdown",
    "html": "<p>bluf: <a href=\"https://github.com/pmjones/php-styler\">PHP-Styler</a> will turn this ...</p>\n<pre><code class=\"language-php\">namespace App\\Report;use App\\Db\\{Connection,Result}; function\nbuildUserReport(Connection $db,string $region,int $limit):Result{\nreturn $db-&gt;table('users')-&gt;select('id','display_name','email_address',\n'last_login_at','current_region')-&gt;where('status','=','active')-&gt;where(\n'region','=',$region)-&gt;whereIn('role',['administrator','editor',\n'contributing_author','subscriber'])-&gt;orderBy('last_login_at','desc')-&gt;\nlimit($limit)-&gt;get();}\n</code></pre>\n<p>... into this:</p>\n<pre><code class=\"language-php\">namespace App\\Report;\n\nuse App\\Db\\Connection;\nuse App\\Db\\Result;\n\nfunction buildUserReport(Connection $db, string $region, int $limit) : Result\n{\n    return $db-&gt;table('users')\n        -&gt;select(\n            'id',\n            'display_name',\n            'email_address',\n            'last_login_at',\n            'current_region',\n        )\n        -&gt;where('status', '=', 'active')\n        -&gt;where('region', '=', $region)\n        -&gt;whereIn(\n            'role', ['administrator', 'editor', 'contributing_author', 'subscriber'],\n        )\n        -&gt;orderBy('last_login_at', 'desc')\n        -&gt;limit($limit)\n        -&gt;get();\n}\n</code></pre>\n<p>Try reformatting your own code at <a href=\"https://php-styler.com\">the php-styler.com demo site</a>.</p>\n<hr>\n<h3>I.</h3>\n<p><a href=\"https://cs.symfony.com/\">PHP CS Fixer</a>, <a href=\"https://github.com/PHPCSStandards/PHP_CodeSniffer\">PHP_CodeSniffer</a>/PHPCBF,\n<a href=\"https://github.com/easy-coding-standard/easy-coding-standard\">ECS</a>, and <a href=\"https://pear.php.net/package/PHP_Beautifier\">PHP_Beautifier</a> are <strong>code fixers</strong>. They detect rule violations in your existing code and patch them in place. You turn on the rules you care about, and they reshape the parts of your source that break those rules.</p>\n<p><a href=\"https://github.com/pmjones/php-styler\">PHP-Styler</a>, on the other hand, is a <strong>complete code re-formatter</strong>. It parses PHP source files into tokens, applies configurable formatting rules and styles, and reconstructs the code with consistent horizontal spacing, vertical spacing, and automatic line splitting. It discards your existing layout entirely and arranges each element of the source code one by one. That puts it in the same family as <a href=\"https://prettier.io/\">Prettier</a> for JavaScript, <a href=\"https://black.readthedocs.io/en/stable/\">Black</a> for Python, <a href=\"https://pub.dev/packages/dart_style\">dart_style</a> for Dart, and <code>gofmt</code> for Go.</p>\n<p>This nets some benefits and some drawbacks. Among others:</p>\n<ul>\n<li>\n<p><strong>Line-length-aware reflow.</strong> Long function calls, arrays, and fluent chains are split across lines automatically. (Fixers generally do not split lines for you.)</p>\n</li>\n<li>\n<p><strong>Deterministic pipeline.</strong> The same input always produces the same output regardless of its prior layout, and running the tool twice is idempotent.</p>\n</li>\n<li>\n<p>However, <strong>it does not preserve hand-aligned columns,</strong> and it compresses runs of blank lines to one.</p>\n</li>\n</ul>\n<p>As a side note, <strong>parallel execution is built in.</strong> The <code>--workers=auto</code> flag uses <code>proc_open</code> to spread files across child processes, speeding up the processing of large codebases.</p>\n<h3>II.</h3>\n<p>In the 0.16 release from 2+ years ago, PHP-Styler would style code based on <a href=\"https://github.com/nikic/PHP-Parse\">PHP-Parser</a> AST tokens. That approach went a long way, but it brought two persistent problems along with it. The AST-based approach could lose or misplace comments in several contexts -- inside argument lists, at the end of <code>switch</code> cases, on concatenation lines, and as the sole content of blocks. Interpolated strings and heredocs were also reconstructed from AST nodes, which meant literal newlines inside double-quoted strings turned into <code>\\n</code> escape sequences on the way out. (In fairness, the PHP-Parser docs said that the AST itself was not suitable for code formatting and styling, but early success encouraged me to keep going; it was only later that these behaviors became apparent.)</p>\n<p>After taking <a href=\"https://sandimetz.com/99bottles\">the excellent Sandi Metz \"99 Bottles of OOP\"</a> course, I began to wonder if PHP-Styler could build up a custom set of tokens based not on an AST, but instead on the code elements to be formatted. Those tokens would be more polymorphic than not, and carry most (if not all) of the parsing and dispatching logic themselves.</p>\n<p>That approach has been hugely successful. For one, comments and interpolated strings were finally honored properly. But it also ended up creating a catalog of about <strong>500+ highly specific token classes</strong>. The level of detail is at \"this is the opening brace of an <code>if</code> statement\" -- with a separate token for each of the different kinds of opening and closing braces, brackets, parentheses, etc.</p>\n<p>The resolution is very finely grained, and the parsing process is specific to each token, including whether or not it should dispatch to another token for parsing. For example, when the parser encounters <code>T_AS</code>, the <em>TAs</em> token can look at the surrounding context to determine if it is a <code>foreach ... as</code> or a <code>use ClassName as</code> or a <code>use TraitName as</code>, and so on. Thus, the 500-odd token classes.</p>\n<p>And when you look at the tests, you can see exactly what the source code parsed as. For example, this source code ...</p>\n<pre><code class=\"language-php\">&lt;?php\nabstract class Foo\n{\n    abstract function bar();\n}\n</code></pre>\n<p>... gets parsed into these styler token classes:</p>\n<pre><code class=\"language-php\">[\n    TPhpOpeningTag::class,\n    TAbstract::class,\n    TClass::class,\n    TClassName::class,\n    TClassOpeningBrace::class,\n    TAbstract::class,\n    TFunction::class,\n    TFunctionName::class,\n    TParamsOpeningParen::class,\n    TParamsClosingParen::class,\n    TAbstractMethodEndSemicolon::class,\n    TClassClosingBrace::class,\n]\n</code></pre>\n<p>That level of detail means all the styling for each token can live with that token: space before or after, line break before or after, blank line before or after, etc. That in turn means the styling can be as specific or as generic as you like. A colon is not just a colon, it is a \"ternary colon\" or a \"return type colon\" or \"an alternative-if colon\" and so on. You can set very specific styling for each one, which gives tremendous flexibility -- not all of which is needed, of course.</p>\n<p>In the end, the tokens did not carry <em>all</em> of the parsing logic. The <em>Parser</em> class itself still needs to coordinate a lot more activity than I'd like, but it's a lot less coordination than in Printer+Styler classes of the 0.16 version. And the old Visitor pattern has been removed as entirely unnecessary.</p>\n<h3>III.</h3>\n<p>There's a ton more, much more than I can put in a blog post:</p>\n<ul>\n<li>support all the way back to PHP 8.1;</li>\n<li>\n<a href=\"https://github.com/pmjones/php-styler/tree/0.x/src/Format/Vendor\">approximations of older vendor formats for Doctrine, PER-CS 3, and Symfony</a>;</li>\n<li>\n<a href=\"https://github.com/pmjones/php-styler/blob/0.x/README-CUSTOM.md#rules\">a rule system for structural transformations</a>;</li>\n<li>and <a href=\"https://github.com/pmjones/php-styler/blob/0.x/README-CUSTOM.md\">highly detailed customization options</a>.</li>\n</ul>\n<p>If you want a complete code reformatter, not just a fixer, try out <a href=\"https://github.com/pmjones/php-styler\">PHP-Styler</a>, either at <a href=\"https://php-styler.com\">the php-styler.com demo site</a> or by installing it with\n<code>composer require --dev pmjones/php-styler</code>.</p>\n<hr>\n"
}
