pmjones/php-styler
PHP-Styler is a PHP 8.1+ code formatter that completely rewrites formatting for consistent spacing, indentation, and line wrapping. It preserves program logic and comments, aims for diff-friendly output, and supports customizable styles, rules, and parses via a token-based pipeline.
Added NormalizeMemberOrder line rule. Reorders class members into a
configurable canonical order: trait uses, enum cases, constants, properties,
magic methods, then regular methods. Accepts an order parameter to customize
the sequence. Blank lines between reordered members are determined by the
blankLineAfter token style. Skips reassembly when order is unchanged.
Included in DeclarationFormat by default.
Converted NormalizeMemberSpacing from TokenRule to LineRule. Now runs
after assembly and splitting, giving it access to line structure. Sole
authority for overriding default inter-member spacing (e.g., collapsing blank
lines between consecutive constants). Added betweenMagicMethods parameter.
Fixed anonymous class constants and properties not getting specialized
ending tokens. TSemicolon now recognizes TAnonymousOpeningBrace as a
class body context, so constants get TConstEndSemicolon and properties get
TPropertyEndSemicolon inside anonymous classes.
Added magic method tokens. The parser now identifies PHP magic methods
(__construct, __destruct, __toString, etc.) at parse time via
TMagicMethod, TMagicMethodName, TMagicMethodClosingBrace, and
TAbstractMagicMethodEndSemicolon tokens. Only recognized PHP magic method
names are classified; user-defined __ methods are not affected.
Fixed InsertPublicVisibility not inserting public after method bodies.
The scope stack was never popped for closing structures because
AClosingStructure.openingToken points to the semantic token (e.g.
TFunction), not the brace token (TFunctionOpeningBrace), so the
AnOpeningStructure instanceof check always failed.
Fixed context-dependent keyword parsing. PHP keywords used as names after
::, ->, and ?-> (e.g., Foo::match(), $obj->static) are now
reclassified as T_STRING before dispatch, routing them through
TString::parse(). Similarly, keywords used as named argument names (e.g.,
case: in function calls) are reclassified by TArgsOpeningParen and
TArgsComma.
Added AMemberClosing interface on member-ending tokens with
memberType() method and closesStaticMember property. Eliminates
instanceof chains in member ordering/spacing rules. Member type constants
(CONSTANT, PROPERTY, METHOD, MAGIC_METHOD, ENUM_CASE, USE_TRAIT)
defined on the interface.
Added AMemberNormalizer abstract base class with shared
findClassBodyRegions() for NormalizeMemberOrder and
NormalizeMemberSpacing.
Removed cache parameter from Config. The --force/-f option on the
apply command has also been removed. All files are now formatted on every
run.
Extracted NestingStack from Parser. The nesting stack (push, pop, and
query operations for tracking nested structures during parsing) has been moved
from inline Parser logic to a dedicated NestingStack class, including
inEncapsedString() and popTernary() methods.
Parser refactoring:
AToken::new() static factory replaces direct construction; $style and
$parenDepth are public private(set).AToken::pair() encapsulates bidirectional opener/closer token linking.add() broken into applyStyleBefore(), createToken(),
applyStyleAfter().scanParsedTrail() helper deduplicates 6 backwards-scanning methods.replaceLastSplit() O(n) → O(1) via cached index.inEncapsedString() reduced to single AnEncapsedStringOpening instanceof.popTernaryNesting() uses ATernaryNesting/AFnNesting marker interfaces.endBracelessBody() match expression → constant maps.Nesting caches class name, opening brace, closing brace, and end semicolon
from token constants.TOpeningBrace, TClosingBrace, TSemicolon)
replaced large match expressions with constant lookups on nesting tokens
(OPENING_BRACE, CLOSING_BRACE, END_SEMICOLON).protected to private; $lastSplit
is public private(set).$lastAddedIndex removed.NormalizeImports truncating output at closure use clauses. The
rule mistakenly treated the use keyword in function () use ($var) as a
file-level import, consuming all remaining tokens and silently dropping them
from output. Introduced a new TUseVariables token class to distinguish
closure variable bindings from import statements at the parser level.Most transforms extracted to rules. Most source-manipulating behavior has
been removed from token parse() methods. The parser and tokens now specify
only formatting changes (spacing, line breaks, blank lines). Other
transformations are implemented as rules applied after parsing:
ConvertToShortArraySyntax — converts array() to []ConvertToShortListSyntax — converts list() to []ConvertVarToPublic — converts var to publicExpandConstDeclarations — expands const A = 1, B = 2 into separate declarationsExpandGroupedImports — expands use Foo\{Bar, Baz} into individual statementsExpandPropertyDeclarations — expands public int $a, $b into separate declarationsInsertNewParens — adds () to bare new FooInsertPublicVisibility — adds public to class functions/consts without visibilityRemoveEmptyAnonymousClassParens — removes empty () from new class()RemoveEmptyAttributeParens — removes empty () from #[Attr()]RemoveLanguageConstructParens — removes optional parens from echo(), print(), etc.RemovePhpClosingTag — removes trailing ?>RemoveRepeatedSemicolons — removes duplicate ;;NormalizeModifierOrder — sorts modifiers by priority (abstract/final, visibility, static, readonly)Rules have organized into separate namespaces: PhpStyler\Rule\TokenRule\*
and PhpStyler\Rule\LineRule\*.
New marker interfaces on token classes.
AComparisonOperator — TIsIdentical, TIsNotIdentical, TIsEqual, TIsNotEqualALiteral — TNull, TTrue, TFalse, TIntegerLiteral, TFloatLiteral, TStringLiteralAUnaryPrefixOperator — TNot, TUnaryMinus, TUnaryPlus, TTildeAModifier — all 10 modifier token typesAType — all 21 type-declaration token typesAUseGroupOpener / AUseGroupCloser — use-group brace tokensACommaListOpener — opener tokens with commaClass() methodAbstract property hooks on one line. Interface property hooks and abstract
class property hooks ({ get; }, { set; }, { get; set; }) now render on a
single line instead of expanding to multiple lines. When both get and set
are present, get is always placed before set regardless of source order.
parseAs reduced. DeclarationFormat::$parseAs reduced from 6 entries to 2.
TElse→TElseAsElseIf and TVariable→TVariableWithExplicitInterpolation
remain as parseAs (require downstream token reclassification that is impractical
as a post-parse rule).
Binary and comparison operator splitting. Long lines now split at binary
arithmetic operators (+, -, *, /, %) and comparison operators (<,
>, <=, >=, ==, !=, ===, !==, <=>) in addition to the
previously supported boolean, coalesce, concatenation, and ternary operators.
Unified split strategy system. All split strategies — operator splits,
paren/bracket expansion, and condition paren expansion — are now unified into
a single priority-ordered loop using ksort(). The former three-phase
approach (condition parens, operator splits, paren fallback) is replaced by
a flat strategy map where each strategy competes by priority.
Refined split priorities. The two coarse operator split priorities
(LOOSE_OPERATOR and TIGHT_OPERATOR) have been replaced with fine-grained
priorities that follow PHP operator precedence. Paren/bracket expansion is
now integrated into the same priority system. Lower numbers split first:
| Priority | Constant | Splits at |
|---|---|---|
| 10 | CONDITION_PAREN |
if/while/for/foreach/switch/match parens |
| 20 | ATTRIBUTE |
Attributes |
| 30 | FOR_SEMICOLON |
for semicolons |
| 40 | COMMA |
Commas |
| 50 | FOR_COMMA |
for commas |
| 60 | FN_ARROW |
fn() => |
| 70 | TERNARY |
?, :, ?: |
| 80 | COALESCE |
?? |
| 90 | BRACKET |
Array literal [...] expansion |
| 100 | MATCH_ARROW |
match arm => |
| 110 | BOOLEAN_OR |
|| |
| 120 | BOOLEAN_AND |
&& |
| 130 | COMPARISON |
<, >, <=, >=, ==, !=, ===, !==, <=> |
| 140 | ADDITION |
+, -, . |
| 150 | MULTIPLICATION |
*, /, % |
| 160 | FLUENT |
->, ?->, :: |
| 170 | OTHER_PAREN |
Args, params, expression parens |
| 180 | ELEMENT_BRACKET |
Array element access $arr[...] |
Polymorphic expansion priorities. Opener tokens (TIfOpeningParen,
TArrayOpeningBracket, TArgsOpeningParen, etc.) now declare their own
expansion priority via expandPriority(). The Splitter discovers expansions
by calling Line::collectExpansionPairs() instead of filtering with
instanceof checks.
Match => splitting. TMatchDoubleArrow now implements
TSplittableOperator, splitting at MATCH_ARROW priority (100). This
ensures match arms split at => before boolean or comparison operators
within the arm condition.
Blank lines around split lines. When a line is split into multiple lines,
blank lines are automatically inserted above and below the split group to
visually separate it from surrounding code. Blank lines are suppressed at block
boundaries (after opening braces, before closing braces), after comments and
docblocks (which stay attached to the next line), after attribute brackets, and
before opening braces in brace-position configurations. Suppression is
controlled by blankLineBefore/blankLineAfter style values (set to false
to deny), which can be overridden per-token via the styles constructor
parameter.
Style attached to tokens. Each instance of AToken now carries its own
Style instance, set during parsing. The Style class is now readonly.
Fluent chain improvement. The base object now stays with its first method
call when a fluent chain is split. All fluent split types
(TSplitMethodCall, TSplitPropertyAccess, TSplitStaticMember,
TSplitStaticMethodCall) now consistently skip the first split position via
shouldSkipFirst(). The unused $totalPositions parameter has been removed
from the shouldSkipFirst() signature.
AToken enhancements. New polymorphic methods on the base token class:
isContent() — returns true by default, false for TSplit and
TSpace. Replaces ~9 scattered instanceof checks.expandPriority() — returns null by default; opener tokens override to
declare their expansion priority.splObjectId() — wraps spl_object_id($this) for shorter call sites.Splitter simplifications. Extracted shouldInsertBlankLine() predicate,
createLine() helper with wasSplit propagation, and
advancePastComma() method. Combined the double-loop in expandCommas()
into a single pass. Replaced string-based dispatch in opener/closer
expansion with direct method calls.
Extra paren/bracket indent. Content inside parentheses or brackets gets one extra indent level when followed by a continuation line, improving readability of nested split expressions.
Performance improvements. Comma splitting and the overall Splitter have
been optimized, with improvements to Line and Parser as well.
PHP-Styler has been rewritten from the ground up; a custom token-based parser replaces the nikic/php-parser entirely. This eliminates the only non-trivial external dependency, fixes longstanding comment-handling bugs, introduces a declarative customization model, and adds parallel worker capability for increased performance. The rewrite also allows a change from the BSD-3-Clause license to the MIT license.
PHP 8.4 required. The minimum PHP version is now 8.4 (was 8.1).
nikic/php-parser removed. PHP-Styler now uses PHP's built-in PhpToken
lexer. The only runtime dependency is pmjones/auto-shell for the CLI.
Configuration API changed. Config no longer accepts a Styler instance.
It accepts a Format instance instead:
// old
new Config(styler: new Styler(lineLen: 84), files: ..., cache: ...);
// new
new Config(files: ..., cache: ..., format: new PlainFormat(lineLen: 84));
Customization model replaced. Extending Styler and overriding s*()
methods is gone. All customization is now declarative through Format objects
using styles, rules, and parseAs arrays. See UPGRADE.md for migration
details.
Default line length changed. DeclarationFormat defaults to 84 characters
(was 88). PlainFormat retains the 88-character default.
A new Format interface replaces the monolithic Styler. Two built-in
implementations:
PlainFormat — minimal formatting, no structural rules. Constructor
parameters for brace placement, keyword case, concatenation spacing, return
type colon spacing, and blank-line-after-block behavior.
DeclarationFormat — opinionated defaults for class files: next-line
braces, import sorting, type ordering, trailing-comma normalization, and more.
Pre-built formats approximating well-known coding standards:
Percs30Format — PER Coding Style 3.0 (120-char lines, collapsed empty
bodies, nowdoc conversion)SymfonyFormat — Symfony (120-char lines, yoda conditions, no
concatenation spacing, single-quoted strings)DoctrineFormat — Doctrine (120-char lines, non-yoda conditions, null-last
type ordering, blank lines before return/throw/yield)Eleven composable rules for structural transformations:
RemoveBom — remove UTF-8 byte-order markNormalizeImports — remove unused and sort use statementsNormalizeTypeOrder — sort union/intersection types (configurable priority)MergeParenBracket — merge ]) onto the same lineMergeParenBrace — merge closing paren and opening brace onto one lineNormalizeTrailingCommas — add trailing commas on split lists, remove on single-lineRemoveTrailingBlankLines — remove trailing blank lines from blocksCollapseEmptyBody — collapse empty class/function bodies to {}ConvertToYodaConditions — $x === null to null === $xConvertFromYodaConditions — null === $x to $x === nullNormalizeMemberSpacing — normalize blank lines between class membersToken-class replacements applied during parsing — convert array() to [],
else if to elseif, double-quoted strings to single-quoted, heredocs to
nowdocs, and/or to &&/||, remove closing PHP tags, and more.
diff CommandNew command to show a unified diff of source files vs. their styled versions:
./vendor/bin/php-styler diff
The apply, check, and diff commands now accept --workers=N (or auto)
to process files in parallel using multiple child processes via proc_open.
This requires no additional PHP extensions and works on both Linux and Windows.
With --workers=auto on a 12-core machine, check runs ~4x faster on ~1000
files.
Basic docblock structure is now parsed, enabling future rules that operate on docblock content.
The old AST-based approach could lose or misplace comments in several contexts: inside argument lists, at the end of switch cases, on concatenation lines, and as the sole content of blocks. The new token-based parser preserves all comments faithfully.
The old parser reconstructed double-quoted strings from AST nodes, converting
literal newlines to \n escape sequences. The new parser preserves the original
string content, so heredocs and interpolated strings render as written.
End-of-line comments now stay on their original lines rather than being shifted to the next line.
PHP-Styler now preserves up to one blank line between statements from the original source. The old version always compressed all vertical spacing.
The split priority system has been refined. Priorities are now:
fn() =>)||, or, ??, ternary)&&, and, .)->, ?->, ::)for semicolonsSeveral normalizations are now always applied regardless of format:
exit/die and non-anonymous new always receive parentheses!= always used instead of <>boolean → bool, integer → int, etc.)use imports, class constants, class properties, trait uses, and attributes
always expanded to one-per-linePreviously, PHP-Styler converted else if to elseif as part of
the Printer operations on parsed Node objects. Now, it converts them as
part of a custom Parser pre-processing step by modifying the original code
PHP tokens themselves. Doing so required a minor change to the testing of
typehints and declare values.
Also renamed the default config file from resources/php-styler.dist.php to
resources/php-styler.php.
Make line control more explicit.
Remove Printable::$hasAttribute, $hasComment, $isFirst, hasAttribute(), hasComment(), and isFirst().
Remove Styler::$atFirstInBody, $hadComment, $hadAttribute, forceSingleNewline(), maybeDoubleNewline(), and rtrim().
The Line object now tracks if a margin above or below is advised for addition, and if a margin above or below is allowed.
The Styler methods now specify margin additions and allowances on the current Line object.
Protect various Styler methods that were mistakenly public.
Add method Styler::blankLine() to create new Line instances.
Rename Line::newline() to Line::blankLine().
Styler::newline() now applies one line of margin above and below auto-split lines, subject to margin allowances; updated tests to reflect this expectation.
Fix a logic-breaking bug where final else gets lost after else if
(refs #4).
apply styling to arbitrary paths by passing file and directory
names at the command line.Force visibility on class members
Rename Property to ClassProperty
Style ClassMethod separately from Function
Make heredoc and nowdoc expansive in args and arrays
Comments now track what kind of Node they are on
Improve presentation of inline comments.
Rename Styler::functionBodyClipWhen() to functionBodyCondenseWhen().
Rename Styler::maybeNewline() to maybeDoubleNewline().
In Styler, replace clip+newline idiom with forceSingleNewline() method.
Fixes a logic-breaking bug with inline docblock comments.
Previously, this source code ...
// set callbacks
$foo =
/** [@param](https://github.com/param) array<array-key, string> $bar */
function (array $bar) : string {
return baz($bar);
};
... would be presented as ...
// set callbacks$foo =
/** [@param](https://github.com/param) array<array-key, string> $bar */
function (array $bar) : string {
return baz($bar);
};
... thereby breaking the code. With this fix, it is presented as ...
// set callbacks
$foo =
/** [@param](https://github.com/param) array<array-key, string> $bar */
function (array $bar) : string {
return baz($bar);
};
... which corrects the logic-breaking bug, though the presentation leaves something to be desired.
Inline comments, including end-of-line comments, are now presented with greater fidelity to the original code.
Other internal QA tooling changes and additions.
Fix testing on Windows, with related Github workflow changes.
Add check command, to see if any files need styling.
Fix #4 (else if now presented as elseif)
Rename Styler::lastSeparatorChar() to lastSeparator()
Add methods Styler::lastArgSeparator(), lastArraySeparator(), lastParamSeparator(), lastMatchSeparator() to allow for different last-item-separators on different constructs.
Updated docs & tests
Add Styler::lastSeparatorChar() to specify comma (or no comma) on last item of split list.
Fix apply command to honor the --force option again.
Floats, integers, single-quoted strings, and non-interpolated double-quoted strings now display their original raw value, not a reconstructed value.
A file that starts with a return now has the return on the same line as the opening <?php tag.
Imports now get a blank line between use statements of different types; this means groups of use, use const, and use function will be visually separate from each other, though the Styler will not regroup or rearrange them.
Standalone const statements are now grouped together instead of getting a blank line between them; class constants still have the blank line.
Fix name in help output.
Handle class constants separately from non-class constants.
Add Styler::functionBodyClipWhen().
Change how operators get modified; was setOperators(), now modOperators().
Modify cache sytem:
Use the filemetime on the cache file instead of on the config file.
Add Cache::$config parameter to specify cache file location.
Consolidate open-brace and end-brace styling logic to their own methods.
Refactor Visitor.php to put enter/leave logic in separate methods.
Remove escaping on single-quoted strings.
Remove escaping on non-interpolation double-quoted strings.
Remove escaping on non-interpolation heredoc strings.
Second and subsequent parameters with attributes are now newline-separated.
Add basic customization document.
Re-enable caching.
Config param cache is removed; remove it from your config file.
Instead, caching uses the filemtime of the config file as the cache time.
Introduce package-specific exception class.
Clip condition and append-string are now customizable.
Moved spacing classes to main namespace.
Use class names, not other strings, for nesting identifiers.
A single array argument now nestles up with parentheses.
Improved fluency on statics, and initial fluent property is not split.
Consolidate types of splits.
Clip newline above attributes.
Mark expansives within arrays.
Comments in arguments make arguments expansive.
Consolidate attribute arguments into general arguments.
Split arrow functions at =>.
new is no longer expansive.
Address some addcslashes() handling of newlines when escaping strings.
Complete rewrite of code reassembly process using a Line object that splits into sub-Line objects, and applies splits to each Line independently.
Improved expansiveness handling.
Split rules now operate on a shared generic level rather than separate independent levels.
Styler sets default operators in constructor now; no need to call parent::setOperator() in extended Styler classes.
Something of a performance reduction (runs about 25% slower and uses about 25% more memory). When running PHP-Styler against itself:
0.2.0 styled 125 files in 1.234 seconds (0.0099 seconds/file, 19.31 MB peak memory usage),
0.3.0 this release styles 107 files in 1.308 seconds (0.0122 seconds/file, 23.61 MB peak memory usage).
Roughly 8x speed improvement from removing php -l linting in favor of PHP-Parser linting.
Cache is now ignored, though cache config remains.
Substantial improvements to line splitting and configurability of operators.
Still some problems with splitting when there are intervening unsplittable lines.
Initial release.
Added NormalizeMemberOrder line rule. Reorders class members into a
configurable canonical order: trait uses, enum cases, constants, properties,
magic methods, then regular methods. Accepts an order parameter to customize
the sequence. Blank lines between reordered members are determined by the
blankLineAfter token style. Skips reassembly when order is unchanged.
Included in DeclarationFormat by default.
Converted NormalizeMemberSpacing from TokenRule to LineRule. Now runs
after assembly and splitting, giving it access to line structure. Sole
authority for overriding default inter-member spacing (e.g., collapsing blank
lines between consecutive constants). Added betweenMagicMethods parameter.
Fixed anonymous class constants and properties not getting specialized
ending tokens. TSemicolon now recognizes TAnonymousOpeningBrace as a
class body context, so constants get TConstEndSemicolon and properties get
TPropertyEndSemicolon inside anonymous classes.
Added magic method tokens. The parser now identifies PHP magic methods
(__construct, __destruct, __toString, etc.) at parse time via
TMagicMethod, TMagicMethodName, TMagicMethodClosingBrace, and
TAbstractMagicMethodEndSemicolon tokens. Only recognized PHP magic method
names are classified; user-defined __ methods are not affected.
Fixed InsertPublicVisibility not inserting public after method bodies.
The scope stack was never popped for closing structures because
AClosingStructure.openingToken points to the semantic token (e.g.
TFunction), not the brace token (TFunctionOpeningBrace), so the
AnOpeningStructure instanceof check always failed.
Fixed context-dependent keyword parsing. PHP keywords used as names after
::, ->, and ?-> (e.g., Foo::match(), $obj->static) are now
reclassified as T_STRING before dispatch, routing them through
TString::parse(). Similarly, keywords used as named argument names (e.g.,
case: in function calls) are reclassified by TArgsOpeningParen and
TArgsComma.
Added AMemberClosing interface on member-ending tokens with
memberType() method and closesStaticMember property. Eliminates
instanceof chains in member ordering/spacing rules. Member type constants
(CONSTANT, PROPERTY, METHOD, MAGIC_METHOD, ENUM_CASE, USE_TRAIT)
defined on the interface.
Added AMemberNormalizer abstract base class with shared
findClassBodyRegions() for NormalizeMemberOrder and
NormalizeMemberSpacing.
Parser refactoring:
AToken::new() static factory replaces direct construction; $style and
$parenDepth are public private(set).AToken::pair() encapsulates bidirectional opener/closer token linking.add() broken into applyStyleBefore(), createToken(),
applyStyleAfter().scanParsedTrail() helper deduplicates 6 backwards-scanning methods.replaceLastSplit() O(n) → O(1) via cached index.inEncapsedString() reduced to single AnEncapsedStringOpening instanceof.popTernaryNesting() uses ATernaryNesting/AFnNesting marker interfaces.endBracelessBody() match expression → constant maps.Nesting caches class name, opening brace, closing brace, and end semicolon
from token constants.TOpeningBrace, TClosingBrace, TSemicolon)
replaced large match expressions with constant lookups on nesting tokens
(OPENING_BRACE, CLOSING_BRACE, END_SEMICOLON).protected to private; $lastSplit
is public private(set).$lastAddedIndex removed.How can I help you explore Laravel packages today?