northrook / php-cs
Custom PHP Coding Standards for Northrook projects.
Requires
- php: >=8.4
- phpstan/phpstan: ^2.2
Requires (Dev)
- phpunit/phpunit: ^13.2
README
Shared formatting and static analysis configuration.
This package provides:
- dPrint formatting via a shared
dprint.json - PHPStan at level
9, with custom rules for native PHPDoc member contracts (@method,@property,@const),@abstract,@static,@singleton, and sealed trait methods
The conventions here prioritize ergonomics over PSR alignment.
Requirements
- PHP 8.4+
- Composer
- dPrint CLI (optional, for formatting)
- PHPStan
2.2+
Installation
composer require --dev northrook/php-cs
Quick start
Add the package, then run the setup script from your project root:
composer require --dev northrook/php-cs vendor/bin/php-cs-config composer update
The script copies the shared dprint.json, generates a project phpstan.neon, and updates composer.json:
require-devphpstan/phpstanscripts.phpstanvendor/bin/phpstan analysescripts.php-cs-configvendor/bin/php-cs-configscripts.collisionvendor/bin/collision-check
After setup, these run as Composer scripts from the project root:
composer php-cs-config composer collision composer phpstan
Pass --force to overwrite existing config files or refresh values that were already set.
PHPStan
The custom rules and the enforced level 9 live in the package's canonical extension.neon.
The setup script generates a thin project phpstan.neon that includes that extension.neon and declares the analysed paths:
includes: - vendor/northrook/php-cs/extension.neon parameters: paths: - src - tests
- the source directory (
src, falling back tophp) tests, when present
Add any project-specific overrides (paths, excludePaths, ignoreErrors, a different level) to that generated phpstan.neon.
Run PHPStan from the project root:
composer phpstan
dPrint
Install the dPrint CLI.
The setup script copies the shared config into the project.
Format PHP files:
dprint fmt
Custom PHPStan rules
Native PHPDoc member contracts
Declare members that implementing or extending types must provide, using standard PHPDoc tags.
Checked on concrete classes, and on interfaces themselves.
| Tag | Example |
|---|---|
@const |
@const STATUS_CODE or @const string STATUS_CODE |
@property |
@property string $name |
@method |
@method string run() or @method static static register() |
@property-read and @property-write are treated like @property.
@method can require static. Types are checked for @method, @property, and @const.
Visibility is not part of standard @method / @property syntax and is not validated.
On concrete classes, mismatches are reported with stable identifiers (e.g. requiresMember.method.TypeMissing).
Unexpected-but-compatible modifiers/types produce ignorable warnings.
Requirements are collected from the class itself, its parents, interfaces, and traits — including nested traits and traits used by parents.
/** * @method static static register() */ abstract class ContractSingleton { final protected static function getInstance(): static { return self::$instance ??= self::register(); } }
@abstract tag
Mark members on abstract classes or traits that every descendant must redeclare — including intermediate abstract classes.
abstract class Base { /** @abstract */ public const string LABEL = 'base'; /** @abstract */ protected string $name = 'base'; /** @abstract */ public function label(): string { return self::LABEL; } }
Each class in the hierarchy must declare its own versions of these members; inheritance alone is not enough.
@static tag
Mark a class (or trait) as a static utility type: it must have a non-public constructor (private or protected). final is not required.
/** * @static */ class Hash { private function __construct() {} public static function checksum(string $value): string { /* ... */ } }
Subclasses must follow the same constructor rule. A @static trait imposes the rule on every class that uses it — including via nested traits or parents that use the trait.
Reported with the staticClass.publicConstructor identifier.
@singleton tag
Mark a class as a singleton façade. It must implement \Northrook\Contracts\Interfaces\SingletonInterface. This is intentionally only an interface check — extending Northrook\Contracts\Singleton is the usual way to satisfy the pattern, but is not required by the rule.
/** * @singleton */ final class Debug extends Singleton { // ... }
Reported with the singleton.missingInterface identifier.
Sealed trait methods
Errors when a class, trait, or enum body redeclares a final method sealed by a trait — including traits used by parents and nested traits.
PHP silently lets the using type override a trait's final method, defeating the intended seal (PHP only fatals when a subclass overrides an inherited final trait method).
trait Sealed { final public function run(): string { return 'sealed'; } } final class Broken { use Sealed; // finalTraitMethod.overridden public function run(): string { return 'overridden'; } }
Reported with the finalTraitMethod.overridden identifier.
Overrides in test directories are allowed by default. Configure path segments via finalTraitMethod.testDirectories (defaults to tests):
parameters: finalTraitMethod: testDirectories: - tests - fixtures
Set testDirectories to an empty list to enforce the seal everywhere.
PhpStorm
The package ships .phpstorm.meta.php.
PhpStorm recognizes @const, @abstract, @static, and @singleton in docblocks (in addition to the built-in @method and @property support).
Validation
In this repository:
composer check # phpstan + phpunit + collision composer phpstan composer test composer collision