Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Additional rules for PHPStan, mostly focused on Clean Code and architecture conventions.

The rules help with enforcing certain method signatures, return types and dependency constraints in your codebase.

All controllers in your application should be `readonly`, no method should have more than 3 arguments, and no class should have more than 2 nested control structures.

## Usage

```bash
Expand All @@ -12,13 +16,16 @@ composer require phauthentic/phpstan-rules --dev

See [Rules documentation](docs/Rules.md) for a list of available rules and configuration examples.

**Available Rules:**
- [Control Structure Nesting Rule](docs/Rules.md#control-structure-nesting-rule)
- [Too Many Arguments Rule](docs/Rules.md#too-many-arguments-rule)
- [Readonly Class Rule](docs/Rules.md#readonly-class-rule)
- [Dependency Constraints Rule](docs/Rules.md#dependency-constraints-rule)
- [Final Class Rule](docs/Rules.md#final-class-rule)
- [Namespace Class Pattern Rule](docs/Rules.md#namespace-class-pattern-rule)
- Architecture Rules:
- [Dependency Constraints Rule](docs/Rules.md#dependency-constraints-rule)
- [Readonly Class Rule](docs/Rules.md#readonly-class-rule)
- [Final Class Rule](docs/Rules.md#final-class-rule)
- [Namespace Class Pattern Rule](docs/Rules.md#namespace-class-pattern-rule)
- [Method Signature Must Match Rule](docs/Rules.md#method-signature-must-match-rule)
- [Method Must Return Type Rule](docs/Rules.md#method-must-return-type-rule)
- Clean Code Rules:
- [Control Structure Nesting Rule](docs/Rules.md#control-structure-nesting-rule)
- [Too Many Arguments Rule](docs/Rules.md#too-many-arguments-rule)

### Using Regex in Rules

Expand Down
12 changes: 12 additions & 0 deletions data/MethodMustReturnType/ReturnTypeTestClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

class ReturnTypeTestClass
{
public function mustReturnInt(): void {}
public function mustReturnNullableString(): string {}
public function mustReturnVoid(): int {}
public function mustReturnSpecificObject(): OtherObject {}
}

class SomeObject {}
class OtherObject {}
8 changes: 8 additions & 0 deletions data/MethodSignatureMustMatch/TestClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

class TestClass
{
public function testMethod(int $a)
{
}
}
19 changes: 19 additions & 0 deletions data/MethodsReturningBooleMustFollowNamingConvention/TestClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

class TestClass
{
public function flag(): bool
{
return true;
}

public function isActive(): bool
{
return true;
}

public function hasPermission(): bool
{
return false;
}
}
66 changes: 65 additions & 1 deletion docs/Rules.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Rules

Add them to your `phpstan.neon` configuration file under the section `services`.
Expand Down Expand Up @@ -99,3 +98,68 @@ Ensures that classes inside namespaces matching a given regex must have names ma
tags:
- phpstan.rules.rule
```

## Method Signature Must Match Rule

Ensures that methods matching a class and method name pattern have a specific signature, including parameter types, names, and count.

**Configuration Example:**
```neon
-
class: Phauthentic\PhpstanRules\Architecture\MethodSignatureMustMatchRule
arguments:
signaturePatterns:
-
pattern: '/^MyClass::myMethod$/'
minParameters: 2
maxParameters: 2
signature:
-
type: 'int'
pattern: '/^id$/'
-
type: 'string'
pattern: '/^name$/'
tags:
- phpstan.rules.rule
```
- `pattern`: Regex for `ClassName::methodName`.
- `minParameters`/`maxParameters`: Minimum/maximum number of parameters.
- `signature`: List of expected parameter types and (optionally) name patterns.

## Method Must Return Type Rule

Ensures that methods matching a class and method name pattern have a specific return type, nullability, or are void.

**Configuration Example:**
```neon
-
class: Phauthentic\PhpstanRules\Architecture\MethodMustReturnTypeRule
arguments:
returnTypePatterns:
-
pattern: '/^MyClass::getId$/'
type: 'int'
nullable: false
void: false
objectTypePattern: null
-
pattern: '/^MyClass::findUser$/'
type: 'object'
nullable: true
void: false
objectTypePattern: '/^App\\\\Entity\\\\User$/'
-
pattern: '/^MyClass::reset$/'
type: 'void'
nullable: false
void: true
objectTypePattern: null
tags:
- phpstan.rules.rule
```
- `pattern`: Regex for `ClassName::methodName`.
- `type`: Expected return type (`int`, `string`, `object`, etc.).
- `nullable`: Whether the return type must be nullable.
- `void`: Whether the method must return void.
- `objectTypePattern`: Regex for object return types (if `type` is `object`).
29 changes: 15 additions & 14 deletions src/Architecture/ClassMustBeFinalRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
use PHPStan\ShouldNotHappenException;

/**
* Specification:
* - Checks if a class matches a given regex pattern.
* - Checks if the class is declared as final.
*
* @implements Rule<Class_>
*/
class ClassMustBeFinalRule implements Rule
Expand All @@ -20,19 +24,13 @@ class ClassMustBeFinalRule implements Rule

private const IDENTIFIER = 'phauthentic.architecture.classMustBeFinal';

/**
* @var array<string> An array of regex patterns to match against class names.
* e.g., ['#^App\\Domain\\.*#', '#^App\\Service\\.*#']
*/
protected array $patterns;

/**
* @param array<string> $patterns An array of regex patterns to match against class names.
* Each pattern should be a valid PCRE regex.
*/
public function __construct(array $patterns)
{
$this->patterns = $patterns;
public function __construct(
protected array $patterns
) {
}

public function getNodeType(): string
Expand All @@ -55,14 +53,17 @@ public function processNode(Node $node, Scope $scope): array

foreach ($this->patterns as $pattern) {
if (preg_match($pattern, $fullClassName) && !$node->isFinal()) {
return [
RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $fullClassName))
->identifier(self::IDENTIFIER)
->build(),
];
return [$this->buildRuleError($fullClassName)];
}
}

return [];
}

private function buildRuleError(string $fullClassName)
{
return RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $fullClassName))
->identifier(self::IDENTIFIER)
->build();
}
}
2 changes: 2 additions & 0 deletions src/Architecture/ClassMustBeReadonlyRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use PHPStan\Rules\RuleErrorBuilder;

/**
* Specification:
* - A matching class must be declared as readonly.
* @implements Rule<Class_>
*/
class ClassMustBeReadonlyRule implements Rule
Expand Down
16 changes: 6 additions & 10 deletions src/Architecture/ClassnameMustMatchPatternRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\ShouldNotHappenException;

/**
* Specification:
* PHPStan rule to ensure that classes inside namespaces matching a given regex
* must have names matching at least one of the provided patterns.
*
Expand All @@ -24,17 +25,12 @@ class ClassnameMustMatchPatternRule implements Rule

private const IDENTIFIER = 'phauthentic.architecture.classnameMustMatchPattern';

/**
* @var array{namespace: string, classPatterns: string[]}[]
*/
private array $namespaceClassPatterns;

/**
* @param array{namespace: string, classPatterns: string[]}[] $namespaceClassPatterns
*/
public function __construct(array $namespaceClassPatterns)
{
$this->namespaceClassPatterns = $namespaceClassPatterns;
public function __construct(
protected array $namespaceClassPatterns
) {
}

public function getNodeType(): string
Expand Down Expand Up @@ -124,8 +120,8 @@ public function buildRuleError(
): array {
$fqcn = $namespaceName ? $namespaceName . '\\' . $className : $className;
$errors[] = RuleErrorBuilder::message(
$this->buildErrorMessage($fqcn, $namespaceName, $classPatterns)
)
$this->buildErrorMessage($fqcn, $namespaceName, $classPatterns)
)
->line($stmt->getLine())
->identifier(self::IDENTIFIER)
->build();
Expand Down
4 changes: 4 additions & 0 deletions src/Architecture/DependencyConstraintsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
* This rule checks the `use` statements in your PHP code and ensures that
* certain namespaces do not depend on other namespaces as specified in the
* configuration.
*
* Specification:
* - A class in a namespace matching a given regex is not allowed to depend on any namespace defined by a set of
* other regexes.
*/
class DependencyConstraintsRule implements Rule
{
Expand Down
Loading