pmjones / php-styler
Companion to PHP-Parser that rebuilds PHP code from AST.
Requires
- php: ^8.1 | ^8.2 | ^8.3
- nikic/php-parser: ^4.19
- pmjones/auto-shell: ^1.0
Requires (Dev)
- pds/composer-script-names: ^1.0
- pds/skeleton: ^1.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
README
WARNING!!!
PHP-Styler will completely reformat your PHP code, discarding any previous formatting entirely.
McCoy: What if this thing were used where [formatting] already exists?
Spock: It would destroy such [formatting] in favor of its new matrix.
McCoy: Its new matrix? Do you have any idea what you're saying?
Spock: I was not attempting to evaulate its [aesthetic] implications.
-- Star Trek II: The Wrath of Khan (paraphrased)
You can try an online demonstration of PHP-Styler at http://php-styler.com/.
Introduction
PHP-Styler is a companion to PHP-Parser for reconstructing PHP code after it has been deconstructed into an abstract syntax tree.
Whereas the PHP-Parser pretty printer does not have output customization as a main design goal, PHP-Styler does. (Please review README-CUSTOM.md for more information.)
PHP-Styler is targeted toward declaration/definition files (class, interface, enum, trait) and script files.
PHP-Styler is not appropriate for PHP-based templates, as it does not use the alternative control structures. Perhaps a future release will include a custom AlternativeStyler for PHP-based templates using alternative control structures.
How It Works
PHP-Styler uses a multiple-pass system to reformat and style PHP code:
- The Parser converts the code to an abstract syntax tree of Node elements, applying transformations from a Visitor along the way.
- The Printer flattens the Node tree into a list of Printable elements.
- The Styler converts each Printable back into text using a series of Line objects; it applies horizontal spacing, vertical spacing, and line-splitting rules as it goes.
Note:
The Parser additionally converts all uses of
else if
(with a space between the keywords) toelseif
(without the space) as a pre-processing step; this is both a practical and a stylistic matter. Cf. #4 and nikic/PHP-Parser#948.
Design Goals
-
Logic Preservation. Restructured PHP code will continue to operate as before.
-
Horizontal and Vertical Spacing. Automatic indenting and blank-line placement.
-
Line Length Control. Automatic splitting across multiple lines when a single line is too long.
-
Diff-Friendly. Default output should aid noise-reduction in diffs.
-
Customization. Change the output style of printable elements by extending the Styler and overriding the method for each Printable you want to change.
-
Comment Preservation. As much as the PHP-Parser will allow.
Styling Examples
See the Examples directory for a nearly-exhaustive series of styling examples, or try the safe preview
command on one of your own source files.
Comparable Offerings
PHP CS Fixer is the category leader for PHP here. It offers a huge range of customization options to fix (or not fix) specific elements of PHP code. However, it is extremely complex, and can be difficult to modify.
The oldest PHP code fixer I know of is PHP_Beautifier. Other newer fixers include PHP_CodeSniffer/PHPCBF and ECS.
The Black formatter for Python appears to have similar design goals and operation as PHP-Styler.
Likewise, dart_style is a formatter for Dart. (Read more about how it works here.)
Finally, there is a PHP plugin for Prettier that uses JavaScript to replace all PHP code formatting using its own rules.
Usage
Installation
Use composer
to add PHP-Styler as a dev requirement:
composer require --dev pmjones/php-styler 0.x@dev
Copy the default php-styler.php
config file to your package root:
cp ./vendor/pmjones/php-styler/resources/php-styler.php .
Preview Formatting
Safely preview how PHP-Styler will restructure a source PHP file:
./vendor/bin/php-styler preview ./src/My/Source/File.php
Pass -c
or --config
to specify an alternative config file:
./vendor/bin/php-styler preview \
-c /path/to/other/php-styler.php \
./src/My/Source/File.php
Pass --debug-parser
to dump the PHP-Parser AST Node objects into the preview, and/or --debug-printer
to dump the PHP-Styler array of Printable objects into the preview.
Apply Formatting
Apply PHP-Styler to all files identified in the config file, overwriting them with new formatting:
./vendor/bin/php-styler apply
Pass -c
or --config
to specify an alternative config file:
./vendor/bin/php-styler apply -c /path/to/other/php-styler.php
PHP-Styler will only apply formatting to files with a modification time later than the cache file. To force formatting on all files regardless of modification time, pass the --force
option:
./vendor/bin/php-styler apply --force
Changing the config file after apply
will invalidate the cache, implying --force
and thereby causing PHP-Styler to apply formatting to all files.
To explictly apply styling to paths other than those specified in the config file, pass a space-separated list of files and directories as arguments:
./vendor/bin/php-styler apply ./src/File.php ./resources/
When explicitly specifying paths, the cache time is not honored, just as if the --force
option had been passed.
Check Formatting
Check all files identified in the config file to see if they need formatting, without changing any of the files:
./vendor/bin/php-styler check
Pass -c
or --config
to specify an alternative config file:
./vendor/bin/php-styler apply -c /path/to/other/php-styler.php
If all files look OK, the return code is 0
. If one or more files look like they need to be styled, the return code is 1
.
Configuration
The default php-styler.php
config file looks like this:
<?php use PhpStyler\Config; use PhpStyler\Files; use PhpStyler\Styler; return new Config( files: new Files(__DIR__ . '/src'), styler: new Styler(), cache: __DIR__ . '/.php-styler.cache', );
-
iterable $files
is anyiterable
of file names to which PHP-Styler should be applied. (If the PHP-Styler Files object is not sufficient for your purposes, try Symfony Finder instead.) -
Styler $styler
is any instance of Styler, whether the default one or any custom extended class. -
?string $cache
is the name of the cache file; the last-modified time of this file indicates the last time PHP-Styler was applied. If$cache
is null then no caching will be used.
The Styler instance can be configured with these constructor parameters:
-
string $eol = "\n"
: The end-of-line string to use. -
int $lineLen = 88
: The maximum line length before PHP-Styler tries to split lines automatically. -
int $indentLen = 4
: The indent length in spaces. -
bool $indentTab = false
: Whentrue
, use a tab ("\t"
) for indenting instead of spaces;$indentLen
is used as the tab width when calculating line length.
Here is a Styler configured for Windows line endings on 120-character lines with tab indentation at 8 spaces wide:
<?php use PhpStyler\Config; use PhpStyler\Files; use PhpStyler\Styler; return new Config( files: new Files(__DIR__ . '\\src'), styler: new Styler( eol: "\r\n", lineLen: 120, indentLen: 8, indentTab: true, ), );
Avoiding Blame
Applying PHP-Styler to your source files for the first time may introduce a volume of changes that will make it difficult to track authorship via git blame
.
You can tell Git to overlook this initial reformatting pass by adding a .git-blame-ignore-revs
file to your repository, and adding the full hash of the initial reformatting commit to it.
- Issue
php-styler apply
to your codebase and commit the changes. - Issue
git log
and copy the full 40-character hash string from that commit. - Create and commit a file named
.git-blame-ignore-revs
with that hash pasted into it, perhaps with a comment. - Configure Git to look at that file:
git config blame.ignoreRevsFile .git-blame-ignore-revs
Voila: git blame
will now ignore that file when looking at authorship history, as will the GitHub blame
user interface.
(See also https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt.)
Line Splitting
Automatic
At first, PHP-Styler builds each statement/instruction as a single line. If that line is "too long" (88 characters by default) the Styler reconstructs the code by trying to split it across multiple lines. It does so by applying one or more rules in order:
implements
are split at commas.- Arrow functions are split at
=>
. - String concatenations are split at dots.
- Conditions are split at parentheses.
- Precedence-indicating parentheses are split.
- Ternaries are split at
?
,:
, and?:
. - Boolean
||
and logicalor
operators are split. - Boolean
&&
and logicaland
operators are split. - Array elements are split at commas.
- Argument lists are split at commas.
- Coalesce
??
operators are split. - Member operators are split at
::
,::$
,->
and?->
. - Parameter lists are split at commas.
If the first rule does not make the line short enough, the second rule is applied in addition, then the third, and so on.
Finally, PHP-Styler will add one blank line of margin around each line that has been automatically split.
The line splitting logic attempts to be idiomatic; that is, PHP-Styler tries to take common line-splitting idioms into account, rather than making weighted calculations of elements. Reference projects were:
- cakephp/database
- laminas/laminas-mvc
- nette/application
- qiq/qiq
- sapien/sapien
- slim/slim
- symfony/http-foundation
Annotated
Sometimes you may want to force lines to split expansively across lines. For example, a deeply-nested array with many elements per nesting level may look better when every element is on its own line, regardless of how short that element may be.
To force expansiveness of line splitting, add the annotation @php-styler-expansive
above the line in question. For example, this array ...
$foo = ['bar', 'baz', 'dib'];
... would normally be presented on a single line. However, when adding the @php-styler-expansive
annotation ...
/** @php-styler-expansive */ $foo = [ 'bar', 'baz', 'dib', ];
... the elements are made to split expansively across lines.
PHP-Styler recognizes the one-liner annotations /** @php-styler-expansive */
and // @php-styler-expansive
, as well as typical docblock annotations:
/** * @php-styler-expansive */
Fixing Mangled Output
If PHP-Styler generates "ugly" or "weird" or "mangled" results, it might be a problem with how PHP-Styler works; please submit an issue.
Alternatively, it may be an indication that the source line(s) should be refactored. Here are some suggestions:
-
Increase the maximum line length. The default length is 88 characters (10% more than the commonly-suggested 80-character length to allow some wiggle room). However, some codebases tend to prefer much longer lines, so increasing the line length may result in more-agreeable line splits.
-
Remove comments from within parameter and argument lists.
-
Move inline comments from the beginning or end of the line to above the line.
-
Break up a single long line into multiple shorter lines.
-
Assign closures embedded in arguments to separate variables.
-
Assign function calls embedded in concatenations to separate variables.
-
Assign multiple ternaries embedded in a single statement to separate variables.
Unfortunately, because of how PHP-Parser handles double-quoted strings with interpolated variables ("encapsed" strings), newlines and some other whitespace characters (\f
, \r
, \t
, \v
) render as a literal \n
(etc.) within the string. For example, this code ...
$sql = " SELECT * FROM {$table} ";
... will be rendered as ...
$sql = "\n SELECT TABLE_NAME\n FROM {$table}\n";
... which is not what I would expect to see.
Until there is a change to how PHP-Parser works, the only solution I can think of is to use heredoc syntax instead. Then this code ...
$sql = <<<SQL SELECT * FROM {$table} SQL;
... should be rendered as provided.
Caveats
Line Length
Even after all line splitting rules are applied, a line may still end up "too long." For example, if a line has a very long quoted string, PHP-Styler cannot split it for you.
Reordering Code
PHP-Styler does not:
- Regroup
use
imports - Split comment lines
- Split quoted strings, heredocs, or nowdocs
Horizontal Alignment
PHP-Styler will de-align lines like this ...
$foo = 'longish' . $bar
$foo = 'short' . $bar;
$foo = 'muchlonger' . $bar;
... into this:
$foo = 'longish' . $bar
$foo = 'short' . $bar;
$foo = 'muchlonger' . $bar;
Vertical Spacing
PHP-Styler will compress lines like this ...
$foo = 'longish' . $bar
$foo = 'short' . $bar;
$foo = 'muchlonger' . $bar;
... into this:
$foo = 'longish' . $bar
$foo = 'short' . $bar;
$foo = 'muchlonger' . $bar;
If you want extra vertical spacing, add a comment; comment lines get one blank line above them.
// baseline foo
$foo = 'longish' . $bar
// reassign foo
$foo = 'short' . $bar;
// reassign foo again
$foo = 'muchlonger' . $bar;
Comment Lines
Comment lines are always "attached" to their following line, not their previous line. For example, the // no break
comment that looks attached to its previous line ...
case 'foo': $foo = 'bar'; // no break case 'baz': $baz = 'dib' break;
... will be presented as if attached to its following line, like so:
case 'foo': $foo = 'bar'; // no break case 'baz': $baz = 'dib' break;
This is a limitation of PHP-Parser; becasue it does not currently report the column number on which nodes start and end, PHP-Styler cannot divine how the comment should be attached.
Further, some comments may disappear entirely when they are the only element within certain structures:
switch ($foo) { /* this comment disappears */ } function bar(/* this comment disappears */) { } $fb = 'veryVeryLongStringToConcatenate' // this comment disappears . 'veryVeryLongStringToConcatenate' // this comment disappears . 'veryVeryLongStringToConcatenate' // this comment disappears . 'veryVeryLongStringToConcatenate' // this comment disappears . 'veryVeryLongStringToConcatenate';
This appears to be an issue with PHP-Parser itself; cf. nikic/PHP-Parser#950.
Likewise, a final inline comment on a final array element may disappear:
$map = [ 34 => 'quot', // quotation mark 38 => 'amp', // ampersand 60 => 'lt', // less-than sign 62 => 'gt', // greater-than sign -- this comment disappears ];
This too appears to be an issue with PHP-Parser iself.