galaxon / coding-standard
PHP_CodeSniffer coding standard extending PSR-12 with lowerCamelCase naming and modern PHP 8.4+ rules.
Package info
github.com/mossy2100/Galaxon-PHP-Coding-Standard
Type:phpcodesniffer-standard
pkg:composer/galaxon/coding-standard
Requires
- php: ^8.4
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- slevomat/coding-standard: ^8.25
- squizlabs/php_codesniffer: ^4.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
README
A PHP_CodeSniffer coding standard for Galaxon PHP libraries extending PSR-12 with custom rules.
Description
This package provides a custom PHP_CodeSniffer coding standard for the Galaxon PHP libraries by Shaun Moss. It extends PSR-12 with additional rules for consistent naming conventions and modern PHP 8.4+ syntax.
Key Features:
- Extends PSR-12 coding standard
- Enforces
$lowerCamelCasenaming for variables, parameters, and properties - Enforces consistent array formatting: single-line, grid, one-per-line, and associative with aligned arrows
- Removes unnecessary parentheses around class instantiation (PHP 8.4+)
- Enforces correct indentation for property hooks
- Automatic registration with PHP_CodeSniffer
The package provides several custom sniffs to cover gaps in the available standards. These include:
- Galaxon.Arrays.ArrayDeclaration: Enforces consistent array formatting with lists and associative arrays
- Galaxon.Classes.ClassInstantiationNoBrackets: Removes unnecessary parentheses around class instantiation when accessing members (PHP 8.4+)
- Galaxon.Classes.PropertyDeclaration: Verifies property declarations, with PHP 8.4 property hook support
- Galaxon.WhiteSpace.ScopeIndent: Checks that control structures and code are indented correctly, with PHP 8.4 property hook support
See Custom Sniffs for more details.
Development and Quality Assurance / AI Disclosure
Claude Chat and Claude Code were used in the development of this package. The core classes were designed, coded, and commented primarily by the author, with Claude providing substantial assistance with code review, suggesting improvements, debugging, and generating tests and documentation. All code was thoroughly reviewed by the author, and validated using industry-standard tools including PHP_Codesniffer and PHPStan (to level 9) to ensure full compliance with PSR-12 coding standards. This collaborative approach resulted in a high-quality, thoroughly-tested, and well-documented package delivered in significantly less time than traditional development methods.
Requirements
- PHP ^8.4
- squizlabs/php_codesniffer ^4.0
- slevomat/coding-standard ^8.25
- dealerdirect/phpcodesniffer-composer-installer ^1.0
Installation
composer require --dev galaxon/coding-standard
The standard is automatically registered with PHP_CodeSniffer via the dealerdirect/phpcodesniffer-composer-installer plugin.
Usage
Create a phpcs.xml file in your project root:
<?xml version="1.0"?> <ruleset name="My Project"> <description>Coding standard for my project</description> <file>src</file> <file>tests</file> <rule ref="Galaxon"/> </ruleset>
Then run:
vendor/bin/phpcs # Check for issues vendor/bin/phpcbf # Auto-fix issues
Included Sniffs
The Galaxon coding standard extends PSR-12 and includes the following additional sniffs.
Links:
- PSR-12 standard
- PHP_CodeSniffer provides the PSR-12, Generic, and Squiz sniffs. Documentation is found in the repository and wiki
- Slevomat coding standard repository and documentation
Base Standard
- PSR-12: Complete PSR-12 coding standard (with exception for multiple classes in test files)
PSR2.Classes.PropertyDeclarationexcluded — replaced byGalaxon.Classes.PropertyDeclarationfor PHP 8.4 property hook support (see Custom Sniffs)Generic.WhiteSpace.ScopeIndentexcluded — replaced byGalaxon.WhiteSpace.ScopeIndentfor PHP 8.4 property hook support (see Custom Sniffs)
Generic Sniffs
- Generic.Arrays.DisallowLongArraySyntax: Requires short array syntax
[]instead ofarray() - Generic.Formatting.SpaceAfterCast: Enforces no space after type casts (e.g.
(int)$value)
Squiz Sniffs
- Squiz.NamingConventions.ValidVariableName: Enforces
$lowerCamelCasefor variables, parameters, and properties - Squiz.Strings.DoubleQuoteUsage.NotRequired: Ensures strings use single quotes unless double quotes are necessary
Slevomat Sniffs
Arrays
- ArrayAccess: Disallows whitespace between array access operators
- DisallowImplicitArrayCreation: Disallows implicit array creation
- DisallowPartiallyKeyed: Requires arrays to have keys for all or none of the values
- SingleLineArrayWhitespace: Checks whitespace in single-line array declarations
Attributes
- AttributeAndTargetSpacing: Enforces spacing between attributes and their targets
- AttributesOrder: Requires attributes to be ordered alphabetically
- DisallowAttributesJoining: Disallows joining multiple attributes with commas
- DisallowMultipleAttributesPerLine: Requires each attribute on its own line
- RequireAttributeAfterDocComment: Requires attributes to appear after doc comments
Classes
- BackedEnumTypeSpacing: Enforces spacing around backed enum types
- ClassConstantVisibility: Requires visibility modifiers on class constants
- ClassMemberSpacing: Enforces spacing between class members
- ConstantSpacing: Enforces spacing around class constants
- DisallowMultiConstantDefinition: Disallows defining multiple constants in one statement
- DisallowMultiPropertyDefinition: Disallows defining multiple properties in one statement
- DisallowStringExpressionPropertyFetch: Disallows string expressions for property access
- EmptyLinesAroundClassBraces: Enforces no empty lines after opening or before closing class braces
- EnumCaseSpacing: Enforces spacing between enum cases
- MethodSpacing: Enforces spacing between methods
- ModernClassNameReference: Requires ::class syntax for class name references
- PropertyDeclaration: Enforces proper property declaration format including promoted properties
- PropertySpacing: Enforces spacing between properties
- RequireMultiLineMethodSignature: Requires multi-line format for long method signatures
- RequireSelfReference: Requires self:: instead of ClassName:: for self-references
- RequireSingleLineMethodSignature: Requires single-line format for short method signatures
- TraitUseDeclaration: Enforces proper trait use declaration format
- TraitUseSpacing: Enforces spacing around trait use statements
Commenting
- AnnotationName: Enforces correct annotation names
- DeprecatedAnnotationDeclaration: Enforces proper @deprecated annotation format
- EmptyComment: Disallows empty comments
- RequireOneLineDocComment: Requires one-line format for short doc comments
- RequireOneLinePropertyDocComment: Requires one-line format for short property doc comments
- UselessInheritDocComment: Disallows useless @inheritDoc comments
Control Structures
- AssignmentInCondition: Disallows assignments in if, elseif, and do-while conditions
- DisallowContinueWithoutIntegerOperandInSwitch: Disallows continue without integer operand in switch
- DisallowTrailingMultiLineTernaryOperator: Requires leading operators in multi-line ternary expressions
- LanguageConstructWithParentheses: Requires parentheses for language constructs
- RequireMultiLineCondition: Requires multi-line format for long conditions with boolean operators
- RequireMultiLineTernaryOperator: Requires multi-line format for long ternary operators
- RequireNullCoalesceEqualOperator: Requires ??= operator when possible
- RequireNullCoalesceOperator: Requires ?? operator when possible
- RequireNullSafeObjectOperator: Requires ?-> operator when possible
- RequireSingleLineCondition: Requires single-line format for short conditions
- RequireShortTernaryOperator: Requires short ternary operator ?: when possible
- RequireTernaryOperator: Requires ternary operator when possible
- DisallowYodaComparison: Disallows Yoda comparisons (constant === $variable)
- UselessIfConditionWithReturn: Disallows useless if conditions returning true or false
Exceptions
- DeadCatch: Disallows empty catch blocks that don't handle exceptions
- RequireNonCapturingCatch: Requires catch without variable when exception is not used
Files
- LineLength: Enforces maximum line length of 120 characters
Functions
- ArrowFunctionDeclaration: Enforces proper arrow function declaration format
- DisallowEmptyFunction: Disallows empty function bodies
- FunctionLength: Enforces maximum function length of 100 lines
- RequireArrowFunction: Requires arrow functions for simple closures
- RequireMultiLineCall: Requires multi-line format for long function calls
- RequireSingleLineCall: Requires single-line format for short function calls
- NamedArgumentSpacing: Enforces spacing around named arguments
- DisallowTrailingCommaInCall: Disallows trailing comma in function calls
- DisallowTrailingCommaInClosureUse: Disallows trailing comma in closure use statements
- DisallowTrailingCommaInDeclaration: Disallows trailing comma in function declarations
- StaticClosure: Requires static keyword for closures that don't use $this
- StrictCall: Enforces strict call_user_func_array and call_user_func usage
- UselessParameterDefaultValue: Disallows useless parameter default values
Namespaces
- AlphabeticallySortedUses: Requires use statements to be alphabetically sorted
- DisallowGroupUse: Disallows group use declarations
- MultipleUsesPerLine: Disallows multiple use statements on one line
- NamespaceDeclaration: Enforces proper namespace declaration format
- NamespaceSpacing: Enforces spacing around namespace declarations
- ReferenceUsedNamesOnly: Requires use statements for all referenced names
- RequireOneNamespaceInFile: Requires exactly one namespace per file
- UnusedUses: Disallows unused use statements including in annotations
- UseDoesNotStartWithBackslash: Disallows leading backslash in use statements
- UseFromSameNamespace: Disallows use statements from the same namespace
- UseSpacing: Enforces spacing between use statements
- UselessAlias: Disallows useless use statement aliases
Operators
- DisallowEqualOperators: Disallows == and != operators, requires === and !==
- NegationOperatorSpacing: Enforces spacing around negation operator
- RequireCombinedAssignmentOperator: Requires combined assignment operators (+=, -=, etc.)
- RequireOnlyStandaloneIncrementAndDecrementOperators: Requires ++ and -- to be used standalone
- SpreadOperatorSpacing: Enforces spacing around spread operator
PHP
- DisallowDirectMagicInvokeCall: Disallows direct __invoke() calls
- ReferenceSpacing: Enforces spacing around reference operator
- ShortList: Requires short list syntax []
- TypeCast: Enforces proper type cast format
- UselessSemicolon: Disallows useless semicolons
Strings
- DisallowVariableParsing: Disallows variable parsing in strings
Type Hints
- DeclareStrictTypes: Requires declare(strict_types=1)
- ParameterTypeHintSpacing: Enforces spacing around parameter type hints
- ReturnTypeHintSpacing: Enforces spacing around return type hints
- LongTypeHints: Requires long type hints (int instead of integer)
- DNFTypeHintFormat: Enforces DNF type format without spaces
- NullableTypeForNullDefaultValue: Requires nullable type for parameters with null default
- ParameterTypeHint: Requires parameter type hints
- PropertyTypeHint: Requires property type hints
- ReturnTypeHint: Requires return type hints
- ClassConstantTypeHint: Requires class constant type hints
Variables
- DisallowVariableVariable: Disallows variable variables ($$var)
- DuplicateAssignmentToVariable: Disallows duplicate assignments to the same variable
- UselessVariable: Disallows useless variables
Variable and Property Naming Convention
Ensures all variables, parameters, and properties use $lowerCamelCase format without leading underscores.
Good:
$userName = 'John'; $orderTotal = 100; $isValid = true;
Bad:
$user_name = 'John'; // snake_case not allowed $_private = 'value'; // leading underscore not allowed $UserName = 'John'; // UpperCamelCase not allowed
PSR-12 and PER 3.0 do not mandate variable naming conventions. Specifically, from PSR-1, Section 4.2 "Properties":
This guide intentionally avoids any recommendation regarding the use of $StudlyCaps, $camelCase, or $under_score property names. Whatever naming convention is used SHOULD be applied consistently within a reasonable scope. That scope may be vendor-level, package-level, class-level, or method-level.
In addition, from PER Coding Style 3.0 Section 4.3 "Properties and Constants":
Property or constant names MUST NOT be prefixed with a single underscore to indicate protected or private visibility. That is, an underscore prefix explicitly has no meaning.
Once upon a time, the convention was to use $lower_snake_case for variable names and properties; however, as the object-oriented features of PHP evolved, it became more common to use $lowerCamelCase, following the coding convention from Java. AI-generated code typically uses $lowerCamelCase, which is indicative of the trend. Therefore, given the requirement to be consistent, this sniff enforces the use of $lowerCamelCase for all variables, class properties, and function parameters.
Similarly, using an underscore prefix to indicate protected or private visibility was common practice in PHP until use of visibility modifiers became the standard. And now, the use of an underscore prefix is generally discouraged or disallowed.
This sniff is compliant with several PHP coding standards:
- Symfony requires
$lowerCamelCase(ref). - Laravel requires
$lowerCamelCase(unofficially). - Drupal variable names may use either
$lowerCamelCaseor$lower_snake_case(ref), as long as one is consistent. Properties should use$lowerCamelCase, and protected or private properties should not use an underscore prefix. (ref).
Therefore, if any of the Galaxon packages are used in projects based on these frameworks, the code should be compliant.
Custom Sniffs
Galaxon.Arrays.ArrayDeclaration
Enforces consistent array formatting based on array type. The sniff differentiates between lists (no array keys appearing in the code), and associative arrays (at least one key appearing in the code). Technically, a list in PHP is any array with sequential integer keys starting from 0, but since we don't want to remove keys if they exist in the code, we treat any array with keys as an associative array and format it as such.
The format is chosen automatically based on array type, content, and length. Lists without nested arrays that fit on a single line (without overflowing the maximum line length and without multiline elements) use a compact single-line format — regardless of whether elements contain function calls or new expressions. Lists that don't fit on one line use grid format for simple values, or one-per-line for complex values (function/method calls, new expressions). Lists containing nested arrays always use one-per-line format. Associative arrays always use one key-value pair per line.
Array indentation defaults to 4 spaces per nesting level; this is configurable. The sniff uses mb_strlen() for proper Unicode character support when aligning arrows and grid padding. Values in associative arrays must start on the same line as the double arrow.
Single-line lists: Lists without nested arrays that fit on one line use a compact format with no trailing comma — including lists with function calls or new expressions.
// Good $colors = ['red', 'green', 'blue']; $results = [strlen('hi'), ucfirst('bye'), trim('x')]; $objects = [new Foo(), new Bar(), new Baz()]; // Bad $colors = [ 'red', 'green', 'blue', ];
Grid format: Lists of simple values too long for a single line are arranged in a grid with uniform padding. A trailing comma is included.
// Good $colors = [ 'red', 'green', 'blue', 'cyan', 'magenta', 'yellow', 'black', 'white', 'orange', 'purple', 'brown', 'pink', 'grey', 'navy', 'teal', ];
One per line: Lists too long for a single line that contain function/method calls or new expressions use one element per line with a trailing comma.
// Good $results = [ strtoupper('red'), strtolower('GREEN'), trim(' blue '), substr('yellow', 0, 3), ];
List of arrays: Lists containing nested arrays always use one element per line, trailing comma required.
// Good $points = [ [1, 2], [3, 4], [5, 6], ];
Associative arrays: One key-value pair per line, arrows aligned, trailing comma required.
// Good $user = [ 'name' => 'John', 'email' => 'john@example.com', 'age' => 30, ]; // Bad $user = ['name' => 'John', 'email' => 'john@example.com', 'age' => 30];
Configuration:
| Property | Type | Default | Description |
|---|---|---|---|
maxLineLength |
int | 120 | Maximum line length before wrapping to grid or one-per-line format. |
indent |
int | 4 | Number of spaces to indent array elements. |
<rule ref="Galaxon.Arrays.ArrayDeclaration"> <properties> <property name="maxLineLength" value="100"/> <property name="indent" value="2"/> </properties> </rule>
Galaxon.Classes.ClassInstantiationNoBrackets
Removes unnecessary parentheses around new expressions when accessing members (PHP 8.4+).
PHP 8.4 introduced the ability to access properties and methods on newly instantiated objects without wrapping the instantiation in parentheses. This sniff enforces that modern syntax.
Good:
new DateTime()->format('Y-m-d'); new Foo()->method(); new Bar()->property;
Bad:
(new DateTime())->format('Y-m-d'); // Unnecessary parentheses (new Foo())->method(); // Unnecessary parentheses (new Bar())->property; // Unnecessary parentheses
Galaxon.Classes.PropertyDeclaration
Verifies that properties are declared correctly. This is a replacement for PSR2.Classes.PropertyDeclaration that properly handles PHP 8.4 property hooks.
Improvements over the PSR2 version:
- Variables inside property hook bodies (e.g.
$this,$value, local variables) are correctly ignored as non-property declarations. - Properties with hooks end with a closing brace
}, not a semicolon;, and are handled correctly. - Supports PHP 8.4 asymmetric visibility (
public private(set)), enforcing that read-visibility comes before write-visibility. - Enforces correct ordering of modifiers:
abstract/finalbefore visibility,static/readonlyafter visibility.
Galaxon.WhiteSpace.ScopeIndent
Checks that control structures and code are indented correctly. This is a fork of Generic.WhiteSpace.ScopeIndent with PHP 8.4 property hook support.
PHP_CodeSniffer's tokenizer does not recognise property hook braces as scope openers/closers, so the built-in ScopeIndent sniff cannot track their indentation. This sniff builds a map of property hook scopes (both the hook container { get ... set ... } and individual hook bodies get { ... }) and tracks them alongside the standard scope stack.
Good:
class User { public string $name { get { return $this->name; } set { $this->name = trim($value); } } }
Configuration:
| Property | Type | Default | Description |
|---|---|---|---|
indent |
int | 4 | Number of spaces per indentation level. |
<rule ref="Galaxon.WhiteSpace.ScopeIndent"> <properties> <property name="indent" value="2"/> </properties> </rule>
License
MIT License - see LICENSE for details
Support
- Issues: https://github.com/mossy2100/PHP-CodingStandard/issues
- Examples: See
phpcs.xmlfiles in other Galaxon packages
For questions or suggestions, please open an issue.
Changelog
See CHANGELOG.md for version history and changes.
