phpnomad / phpstan-rules
PHPStan rules for PHPNomad framework conventions
Package info
github.com/phpnomad/phpstan-rules
Type:phpstan-extension
pkg:composer/phpnomad/phpstan-rules
Requires
- php: ^8.2
- phpstan/phpstan: ^2.0
Requires (Dev)
- phpstan/extension-installer: ^1.0
- phpunit/phpunit: ^10.0 || ^11.0
README
Custom PHPStan rules that enforce PHPNomad framework conventions.
These rules catch architectural anti-patterns at static analysis time, including models with inline serialization, service locator usage, raw SQL, singleton abuse, missing interface implementations, database scope violations, cache misuse, and more.
Installation
composer require --dev phpnomad/phpstan-rules
If you use phpstan/extension-installer, the rules activate automatically.
Otherwise, add to your phpstan.neon:
includes: - vendor/phpnomad/phpstan-rules/extension.neon
Rules
Models
| Identifier | Description |
|---|---|
phpnomad.model.notFinal |
Models implementing DataModel must be declared final. |
phpnomad.model.setter |
Models must not have setter methods (set*). Models are immutable. |
phpnomad.model.arrayMethod |
Models must not have toArray() or fromArray(). Use a separate adapter class. |
Adapters
| Identifier | Description |
|---|---|
phpnomad.adapter.notImplementing |
Classes named *Adapter or in Adapters namespaces must implement ModelAdapter. Exempts MutationAdapter implementations. |
Events
| Identifier | Description |
|---|---|
phpnomad.event.notImplementing |
Classes in Events namespaces or named *Event must implement PHPNomad\Events\Interfaces\Event. |
phpnomad.event.notFinal |
Event classes should be declared final. |
phpnomad.event.notReadonly |
Event properties and promoted constructor parameters should be readonly. |
phpnomad.listener.notImplementing |
Classes in Listeners namespaces or named *Listener must implement CanHandle. |
DI / Container
| Identifier | Description |
|---|---|
phpnomad.di.serviceLocator |
InstanceProvider::get() must not be called inside business classes. Use constructor injection. Calls inside initializers and bootstrappers are allowed. |
phpnomad.di.missingTrait |
Classes implementing CanSetContainer must use the HasSettableContainer trait. |
Initializers
| Identifier | Description |
|---|---|
phpnomad.initializer.useInterface |
Initializers should use Has* interfaces (e.g., HasListeners, HasTaskHandlers) instead of manually calling registration methods like EventStrategy::attach(). |
Facades
| Identifier | Description |
|---|---|
phpnomad.facade.notExtending |
Classes in Facades namespaces or named *Facade must extend PHPNomad\Facade\Abstracts\Facade. |
phpnomad.facade.noAbstractInstance |
Concrete Facade subclasses must implement abstractInstance(). |
Database
| Identifier | Description |
|---|---|
phpnomad.database.rawSql |
Raw SQL strings (SELECT ... FROM, INSERT INTO, etc.) must not appear in code. Use PHPNomad datastore patterns. |
phpnomad.database.scope |
QueryBuilder, ClauseBuilder, and QueryStrategy must only be used inside Datastore classes. |
phpnomad.database.concreteTableHint |
Constructor parameters should type-hint the Datastore interface, not concrete Table subclasses. |
Controllers
| Identifier | Description |
|---|---|
phpnomad.controller.noStatus |
Controller getResponse() must explicitly set an HTTP status code via setStatus() or setError(). |
Console
| Identifier | Description |
|---|---|
phpnomad.command.notImplementing |
Classes in Commands namespaces or named *Command must implement PHPNomad\Console\Interfaces\Command. |
phpnomad.command.handleReturnType |
Command handle() methods must declare an int return type. |
Tasks
| Identifier | Description |
|---|---|
phpnomad.task.notImplementing |
Classes in Tasks namespaces or named *Task must implement PHPNomad\Tasks\Interfaces\Task. |
phpnomad.taskHandler.notImplementing |
Task handler classes must implement PHPNomad\Tasks\Interfaces\CanHandleTask. |
phpnomad.task.directHandle |
Task handlers must be dispatched via TaskStrategy::dispatch(), not by calling handle() directly. |
Cache
| Identifier | Description |
|---|---|
phpnomad.cache.directStrategy |
Business classes should not inject CacheStrategy directly. Use CacheableService or a Facade. |
phpnomad.cache.uncaughtException |
CacheStrategy::get() throws CachedItemNotFoundException. Ensure the call is wrapped in a try/catch. |
phpnomad.cache.hardcodedTtl |
Do not hardcode TTL values as integer literals. Use a CachePolicy or configuration constant. |
phpnomad.cache.stringConcatKey |
Use the HasCacheKey interface for cache key generation instead of string concatenation. |
General
| Identifier | Description |
|---|---|
phpnomad.general.singletonInBusiness |
::instance() singleton calls must not be used outside of Facade classes. Use dependency injection. |
phpnomad.general.globalKeyword |
The global keyword must not be used. Use dependency injection. |
Stack Elevator (layer purity)
Enforces the Stack Elevator doctrine (Novatorius coding standards KB:
initiatives-nomadic-development-coding-standards-the-stack-elevator): code is layered Core (pure business
concepts) → Service (implementation logic) → Platform (system integration), and dependencies only
point upward (Platform → Service → Core). Layers are marked by namespace segments; because a layer
name can also appear as a domain name (e.g. Novatorius\Quartermaster\Platform\Core\...), the
last matching segment in a namespace determines its layer.
| Identifier | Description |
|---|---|
phpnomad.stackElevator.coreDependsOnService |
Core-layer code must not import or reference Service-layer symbols. |
phpnomad.stackElevator.coreDependsOnPlatform |
Core-layer code must not import or reference Platform-layer symbols. |
phpnomad.stackElevator.coreDenylist |
Core-layer code must not reference denylisted third-party namespaces. |
phpnomad.stackElevator.serviceDependsOnPlatform |
Service-layer code must not import or reference Platform-layer symbols. |
phpnomad.stackElevator.serviceDenylist |
Service-layer code must not reference denylisted third-party namespaces. |
Checked references: use statements, extends/implements, trait uses, property/parameter/return
types, instantiations, static calls and constant fetches, instanceof, catch types, attributes,
and fully qualified function calls. Doc-block-only references are not checked.
Adopting layer purity on its own
The Stack Elevator rules live in a self-contained stack-elevator.neon. To enforce layer purity
without the rest of the convention ruleset (useful when first onboarding an existing codebase),
disable the auto-loaded extension for this package and include only the layer-purity file:
"extra": { "phpstan/extension-installer": { "ignore": ["phpnomad/phpstan-rules"] } }
includes: - vendor/phpnomad/phpstan-rules/stack-elevator.neon
The full extension.neon already includes stack-elevator.neon, so the default (all rules)
behavior is unchanged.
All settings are configurable via phpstan.neon (defaults shown):
parameters: phpNomadStackElevator: checkCoreLayer: true checkServiceLayer: true namespaceRoots: # vendor roots the convention applies to - Novatorius - PHPNomad coreSegments: [Core] # namespace segments marking each layer serviceSegments: [Service] platformSegments: [Platform, WordPress, Integration] excludeNamespaces: [] # grandfathered namespace prefixes ("*" wildcards allowed) coreDenylist: [] # third-party prefixes Core may never reference serviceDenylist: [] # third-party prefixes Service may never reference
Existing repos will have violations when first enabling these rules. Grandfather them with
excludeNamespaces (e.g. a composition root such as Novatorius\Quartermaster\Service, whose job
is to wire all layers together) or a PHPStan baseline, then shrink the list over time.
Suppressing Rules
Use PHPStan's built-in ignore syntax:
// @phpstan-ignore phpnomad.model.notFinal class User implements DataModel { // ... }
Or suppress in phpstan.neon via baseline:
vendor/bin/phpstan --generate-baseline
Requirements
- PHP 8.2+
- PHPStan 2.0+
License
MIT