shipmonk/phpstan-rules
40 super-strict PHPStan rules from ShipMonk to plug gaps in extra-strict setups. Install via Composer, include rules.neon, then enable/disable or tune rules per-project. Targets tricky PHP edge cases like unsafe comparisons, casts, arrays, enums and more.
composer require --dev shipmonk/phpstan-rules
phpstan.neon:
includes:
- vendor/shipmonk/phpstan-rules/rules.neon
vendor/bin/phpstan analyse
Start with enforceNativeReturnTypehint and enforceClosureParamNativeTypehint to catch missing type hints in methods and closures. These rules align with modern PHP practices and reduce runtime type errors.
forbidCast or forbidArithmeticOperationOnNonNumber) and fix violations before adding more.classSuffixNaming to enforce naming conventions (e.g., *Test for test classes) and reduce ambiguity in autoloading.enforceEnumMatch: Replace exhaustive if-else chains for enums with match expressions. Configure PHPStan to fail on missing cases:
parameters:
shipmonkRules:
enforceEnumMatch: true
enforceReadonlyPublicProperty: Gradually migrate public properties to readonly (PHP 8.1+). Exclude properties with default values if needed:
parameters:
shipmonkRules:
enforceReadonlyPublicProperty:
excludePropertyWithDefaultValue: true
# .github/workflows/phpstan.yml
jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: composer install
- run: vendor/bin/phpstan analyse --level=max --error-format=github
! to replace default configurations (e.g., whitelist allowed casts):
parameters:
shipmonkRules:
forbidCast:
blacklist!: ['(bool)', '(int)'] # Only allow (bool) and (int) casts
backedEnumGenerics if not using generics:
parameters:
shipmonkRules:
backedEnumGenerics:
enabled: false
reportAnyTypeWideningInVarTag) for lists:
parameters:
reportAnyTypeWideningInVarTag: true
shipmonkRules:
enforceListReturn: true
False Positives in enforceEnumMatch:
elseif chains that are intentionally exhaustive (e.g., for legacy code). Use @phpstan-ignore-next-line to suppress:
// @phpstan-ignore-next-line
elseif ($enum === MyEnum::LegacyCase) { ... }
match or document why exhaustive checks are needed.forbidCheckedExceptionInCallable Overreach:
@param-immediately-invoked-callable to whitelist:
/** @param-immediately-invoked-callable $callback */
public function execute(callable $callback): void { ... }
enforceIteratorToArrayPreserveKeys Breaking Changes:
$preserve_keys in iterator_to_array(). Update all calls, even if you’re confident about the default behavior.BackedEnum Generics Requires Stub Files:
backedEnumGenerics does nothing without a custom stub file. Follow the ShipMonk guide to set up generics.PHP Version Mismatches:
enforceReadonlyPublicProperty or enforceNativeReturnTypehint are ignored on PHP <8.1. Ensure your phpstan.neon targets the correct PHP version:
phpVersion: '8.2'
parameters:
shipmonkRules:
enableAllRules: false
enforceNativeReturnTypehint:
enabled: true
forbidCheckedExceptionInCallable) require additional PHPStan configs (e.g., exceptions.checkedExceptionClasses). Verify your phpstan.neon includes:
parameters:
exceptions:
checkedExceptionClasses:
- App\Exceptions\RuntimeException
--error-format=json: For programmatic analysis, parse JSON output to identify rule-specific issues:
vendor/bin/phpstan analyse --error-format=json | jq '.files[] | select(.ruleSet == "shipmonk/phpstan-rules")'
Custom Rule Logic:
PHPStan\Rules\Rule and overriding methods. Example:
// app/Rules/CustomRule.php
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
class CustomRule implements Rule {
public function getNodeTypeNames(): array { return ['Php\Function_']; }
public function processNode(Node $node): array {
if ($node instanceof Php\Function_) {
return [$this->buildError($node)];
}
return [];
}
private function buildError(Node $node): RuleErrorBuilder {
return RuleErrorBuilder::message('Custom rule violation')
->onNode($node)
->build();
}
}
phpstan.neon:
services:
- App\Rules\CustomRule
Dynamic Rule Configuration:
PHPStan\Analyser\Scope to conditionally enable/disable rules based on class/method names:
use PHPStan\Analyser\Scope;
class DynamicRule implements Rule {
public function processNode(Node $node, Scope $scope): array {
if ($scope->getClassReflection()?->getName() === 'App\Services\LegacyService') {
return []; // Skip for legacy classes
}
return [$this->buildError($node)];
}
}
Whitelisting Exceptions:
forbidCast, use ignoreErrors in phpstan.neon to exclude specific files/classes:
ignoreErrors:
- '#^Class .* contains forbidden cast$#'
- 'app/Legacy/Casts.php'
forbidArithmeticOperationOnNonNumber) before broader ones (e.g., enforceNativeReturnTypehint) to avoid redundant checks.vendor/bin/phpstan clear-cache
How can I help you explore Laravel packages today?