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,@initializer,@disallows, sealed trait methods, and configurable disallowed function calls
The conventions here prioritize ergonomics over PSR alignment.
- PHP 8.5+
- Composer
- dPrint CLI (optional, for formatting)
- PHPStan
2.2+
composer require --dev northrook/php-csAdd the package, then run the setup script from your project root:
composer require --dev northrook/php-cs
vendor/bin/php-cs-config
composer updateThe script writes a project dprint.json that extends the package standard, 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 phpstandprint.json is always rewritten so the formatting standard stays locked to the package. Pass --force to overwrite an existing phpstan.neon or refresh composer.json values that were already set.
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.neonparameters: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 phpstanInstall the dPrint CLI.
The setup script always writes a thin project dprint.json that extends the package canonical config (plugin options are locked):
{
"extends": "vendor/northrook/php-cs/dprint.json"
}Format PHP files:
dprint fmtDeclare members that implementing or extending types must provide, using standard PHPDoc tags.
Checked on concrete classes (and skipped for abstract classes). On interfaces, only @const must be declared natively — @method and @property* are implementor contracts enforced on concrete classes.
| 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 for implementors.
@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 create(string $id) * @property string $name */interface NamedFactory {}Mark members on abstract classes or traits that every descendant must redeclare — including intermediate abstract classes.
abstractclass Base
{
/** @abstract */publicconststringLABEL = 'base';
/** @abstract */protectedstring$name = 'base';
/** @abstract */publicfunctionlabel(): string
{
returnself::LABEL;
}
}Each class in the hierarchy must declare its own versions of these members; inheritance alone is not enough.
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
{
privatefunction__construct() {}
publicstaticfunctionchecksum(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.
Mark a class (or trait) as a singleton façade. It must extend Northrook\Singleton (from northrook/core-contracts).
/** * @singleton */abstractclass Facade extends Singleton {}
finalclass Debug extends Facade
{
// ...
}Subclasses inherit the constraint from a tagged parent. A @singleton trait imposes the rule on every class that uses it — including via nested traits or parents that use the trait.
Reported with the singleton.missingBase identifier.
Mark a method that initializes readonly or typed properties on behalf of __construct — common when a trait helper snapshots state the constructor assigns.
PHPStan treats tagged methods as additional constructors, clearing false positives such as property.uninitializedReadonly and property.readOnlyAssignNotInConstructor when init lives in a trait helper instead of literally in __construct.
Tag the init method (own line, no arguments). A trait tag propagates to every class that uses it.
trait ExceptionTrait
{
protectedreadonlyarray$context;
/** * @initializer */finalprotectedfunction_context_snapshot(?array$context = null): void
{
$this->context = Snapshot::context($context ?? []);
}
}
finalclass RuntimeException extends \RuntimeException
{
use ExceptionTrait;
publicfunction__construct(?array$context = null)
{
$this->_context_snapshot($context);
}
}Rules:
- Tagged methods must be called from
__construct(or another@initializermethod). - Methods newly introduced by a class or a trait it uses (that no parent already exposes) must be invoked directly as
$this->method()from__construct. Calls inside closures do not count. - Do not tag
__constructitself or static methods.
Reported identifiers:
initializer.calledOutsideConstructorinitializer.notCalledFromConstructorinitializer.staticMethodinitializer.redundant
Mark methods that the annotated type — and every consumer — must not end up with. Useful when declaring a method would change engine behaviour (e.g. __toString() → Stringable) or when a façade must not expose certain entry points.
/** * @disallows __clone(), __toString(), static get() */class Redactor
{
// no stubs — consumers must not introduce these either
}- Applies to classes, interfaces, and traits (enums when they compose a tagged type).
- Specs are comma-separated;
staticis part of the identity (static get()≠ instanceget());()is optional. - Collected from self, parents, interfaces, and traits — including nested traits and traits used by parents.
- Errors if the analysed type has the method via any inheritance path (own body, parent, trait, or interface).
Reported with the disallows.methodPresent identifier.
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
{
finalpublicfunctionrun(): string
{
return'sealed';
}
}
finalclass Broken
{
use Sealed;
// finalTraitMethod.overriddenpublicfunctionrun(): 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- fixturesSet testDirectories to an empty list to enforce the seal everywhere.
Errors when calling a function listed in disallowedFunctionCalls. Names are absolute (var_export ⇒ \var_export, Northrook\Contracts\foo ⇒ \Northrook\Contracts\foo). Trailing () and a leading \ are optional in config.
Optional message is shown as a tip; defaults to {function}() is disallowed.
Optional exceptIn (string or list of class/interface names) skips the error when the call is in a class that is, extends, or implements that type. Trait methods follow the using class.
parameters:disallowedFunctionCalls:-function:'var_export()'message:'Use Serializer/Snapshot/VarExporter; native var_export leaks object props.'exceptIn:'\Northrook\Contracts\Exportable'finalclass Broken
{
publicfunctionrun(mixed$value): string
{
// disallowedFunctionCalls.varExportreturn\var_export($value, true);
}
}
finalclass Snapshot implements \Northrook\Contracts\Exportable
{
publicfunctionexport(mixed$value): string
{
return\var_export($value, true);
}
}Dynamic calls ($fn()), method calls, and static method calls are not checked. A namespaced function that shares a banned global's name is not flagged.
The package ships .phpstorm.meta.php.
PhpStorm recognizes @const, @abstract, @static, @singleton, @initializer, and @disallows in docblocks (in addition to the built-in @method and @property support).
In this repository:
composer check # phpstan + phpunit + collision
composer phpstan
composer test
composer collision