Skip to content

Commit 2ea6ba5

Browse files
author
Florian Krämer
committed
Merge branch 'master' of github.com:Phauthentic/phpstan-rules
2 parents 37abef7 + ed003ed commit 2ea6ba5

16 files changed

+150
-85
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,25 @@ See [Rules documentation](docs/Rules.md) for a list of available rules and confi
2222

2323
### Using Regex in Rules
2424

25-
A lot of the rules use regex patterns to match things. Many people are not good in writing them but thankfully there is AI today.
25+
A lot of the rules use regex patterns to match things. Many people are not good at writing them but thankfully there is AI today.
2626

2727
If you struggle to write the regex patterns you need, you can use AI tools like [ChatGPT](https://chat.openai.com/) to help you generate them. Just describe what you want to match, and it can provide you with a regex pattern that fits your needs. The regex can be tested using online tools like [regex101](https://regex101.com/).
2828

29+
## Why PHPStan to enforce Architectural Rules?
30+
31+
Because PHPStan is a widely used static analysis tool in the PHP community. It already provides a solid foundation for code quality checks, and adding custom rules allows you to enforce specific coding standards and architectural constraints is just a logical choice. You won't need more 3rd party tools to enforce your architectural constraints.
32+
33+
It is also more or less easy to write your own rules if you need to enforce something specific that is not covered by the existing rules.
34+
35+
### Alternative Tools
36+
37+
If you don't like this library, you can also check out other tools. Some of them provide a fluent interface instead of a Regex. If this feels more comfortable for you, you might want to check them out:
38+
39+
* [Deptrac](https://github.com/deptrac/deptrac) - Checks dependencies between namespaces and classes.
40+
* [PHP Architecture Tester](https://www.phpat.dev/) - A tool to enforce architectural rules in PHP applications.
41+
* [PHPArkitect](https://github.com/phparkitect/arkitect) - A tool to enforce architectural rules in PHP applications.
42+
* [PHPMD](https://phpmd.org/) - A tool that scans PHP source code and looks for potential problems.
43+
2944
## License
3045

3146
This library is under the MIT license.

composer.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
},
99
"autoload": {
1010
"psr-4": {
11-
"Phauthentic\\PhpstanRules\\": "src/"
11+
"Phauthentic\\PHPStanRules\\": "src/"
12+
}
13+
},
14+
"autoload-dev": {
15+
"psr-4": {
16+
"Phauthentic\\PHPStanRules\\Tests\\": "tests/"
1217
}
1318
},
1419
"authors": [

docs/Rules.md

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,48 @@
33

44
Add them to your `phpstan.neon` configuration file under the section `services`.
55

6-
---
7-
86
## Control Structure Nesting Rule
97

108
Ensures that the nesting level of `if` and `try-catch` statements does not exceed a specified limit.
119

1210
**Configuration Example:**
1311
```neon
1412
-
15-
class: Phauthentic\PhpstanRules\ControlStructureNestingRule
13+
class: Phauthentic\PhpstanRules\CleanCode\ControlStructureNestingRule
1614
arguments:
1715
maxNestingLevel: 2
1816
tags:
1917
- phpstan.rules.rule
2018
```
2119

22-
---
23-
2420
## Too Many Arguments Rule
2521

2622
Checks that methods do not have more than a specified number of arguments.
2723

2824
**Configuration Example:**
2925
```neon
3026
-
31-
class: Phauthentic\PhpstanRules\TooManyArgumentsRule
27+
class: Phauthentic\PhpstanRules\CleanCode\TooManyArgumentsRule
3228
arguments:
3329
maxArguments: 3
3430
tags:
3531
- phpstan.rules.rule
3632
```
3733

38-
---
39-
4034
## Readonly Class Rule
4135

4236
Ensures that classes matching specified patterns are declared as `readonly`.
4337

4438
**Configuration Example:**
4539
```neon
4640
-
47-
class: Phauthentic\PhpstanRules\ReadonlyClassRule
41+
class: Phauthentic\PhpstanRules\Architecture\ReadonlyClassRule
4842
arguments:
4943
patterns: ['/^App\\Controller\\/']
5044
tags:
5145
- phpstan.rules.rule
5246
```
5347

54-
---
55-
5648
## Dependency Constraints Rule
5749

5850
Enforces dependency constraints between namespaces by checking `use` statements.
@@ -64,7 +56,7 @@ In the example below nothing from `App\Domain` can depend on anything from `App\
6456
**Configuration Example:**
6557
```neon
6658
-
67-
class: Phauthentic\PhpstanRules\DependencyConstraintsRule
59+
class: Phauthentic\PhpstanRules\Architecture\DependencyConstraintsRule
6860
arguments:
6961
forbiddenDependencies: [
7062
'/^App\\Domain(?:\\\w+)*$/': ['/^App\\Controller\\/']
@@ -73,37 +65,35 @@ In the example below nothing from `App\Domain` can depend on anything from `App\
7365
- phpstan.rules.rule
7466
```
7567

76-
---
77-
7868
## Final Class Rule
7969

8070
Ensures that classes matching specified patterns are declared as `final`.
8171

8272
**Configuration Example:**
8373
```neon
8474
-
85-
class: Phauthentic\PhpstanRules\FinalClassRule
75+
class: Phauthentic\PhpstanRules\Architecture\FinalClassRule
8676
arguments:
8777
patterns: ['/^App\\Service\\/']
8878
tags:
8979
- phpstan.rules.rule
9080
```
9181

92-
---
93-
9482
## Namespace Class Pattern Rule
9583

9684
Ensures that classes inside namespaces matching a given regex must have names matching at least one of the provided patterns.
9785

9886
**Configuration Example:**
9987
```neon
10088
-
101-
class: Phauthentic\PhpstanRules\NamespaceClassPatternRule
89+
class: Phauthentic\PhpstanRules\Architecture\NamespaceClassPatternRule
10290
arguments:
10391
namespaceClassPatterns: [
10492
[
10593
namespace: '/^App\\Service$/',
106-
classPatterns: ['/Class$/']
94+
classPatterns: [
95+
'/Class$/'
96+
]
10797
]
10898
]
10999
tags:

phpstan.neon

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,39 @@ parameters:
77

88
services:
99
-
10-
class: Phauthentic\PhpstanRules\ControlStructureNestingRule
10+
class: Phauthentic\PhpstanRules\CleanCode\ControlStructureNestingRule
1111
arguments:
1212
maxNestingLevel: 2
1313
tags:
1414
- phpstan.rules.rule
1515
-
16-
class: Phauthentic\PhpstanRules\TooManyArgumentsRule
16+
class: Phauthentic\PhpstanRules\CleanCode\TooManyArgumentsRule
1717
arguments:
1818
maxArguments: 3
1919
tags:
2020
- phpstan.rules.rule
2121
-
22-
class: Phauthentic\PhpstanRules\ReadonlyClassRule
22+
class: Phauthentic\PhpstanRules\Architecture\ReadonlyClassRule
2323
arguments:
2424
patterns: ['/^App\\Controller\\/']
2525
tags:
2626
- phpstan.rules.rule
2727
-
28-
class: Phauthentic\PhpstanRules\DependencyConstraintsRule
28+
class: Phauthentic\PhpstanRules\Architecture\DependencyConstraintsRule
2929
arguments:
3030
forbiddenDependencies: [
3131
'/^App\\Domain(?:\\\w+)*$/': ['/^App\\Controller\\/']
3232
]
3333
tags:
3434
- phpstan.rules.rule
3535
-
36-
class: Phauthentic\PhpstanRules\FinalClassRule
36+
class: Phauthentic\PhpstanRules\Architecture\FinalClassRule
3737
arguments:
3838
patterns: ['/^App\\Service\\/']
3939
tags:
4040
- phpstan.rules.rule
4141
-
42-
class: Phauthentic\PhpstanRules\NamespaceClassPatternRule
42+
class: Phauthentic\PhpstanRules\Architecture\NamespaceClassPatternRule
4343
arguments:
4444
namespaceClassPatterns: [
4545
[

src/FinalClassRule.php renamed to src/Architecture/ClassMustBeFinalRule.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,24 @@
22

33
declare(strict_types=1);
44

5-
namespace Phauthentic\PhpstanRules;
5+
namespace Phauthentic\PHPStanRules\Architecture;
66

77
use PhpParser\Node;
88
use PhpParser\Node\Stmt\Class_;
99
use PHPStan\Analyser\Scope;
1010
use PHPStan\Rules\Rule;
1111
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\ShouldNotHappenException;
1213

1314
/**
1415
* @implements Rule<Class_>
1516
*/
16-
class FinalClassRule implements Rule
17+
class ClassMustBeFinalRule implements Rule
1718
{
1819
private const ERROR_MESSAGE = 'Class %s must be final.';
1920

21+
private const IDENTIFIER = 'phauthentic.architecture.classMustBeFinal';
22+
2023
/**
2124
* @var array<string> An array of regex patterns to match against class names.
2225
* e.g., ['#^App\\Domain\\.*#', '#^App\\Service\\.*#']
@@ -37,6 +40,9 @@ public function getNodeType(): string
3740
return Class_::class;
3841
}
3942

43+
/**
44+
* @throws ShouldNotHappenException
45+
*/
4046
public function processNode(Node $node, Scope $scope): array
4147
{
4248
if (!$node instanceof Class_ || !isset($node->name)) {
@@ -51,6 +57,7 @@ public function processNode(Node $node, Scope $scope): array
5157
if (preg_match($pattern, $fullClassName) && !$node->isFinal()) {
5258
return [
5359
RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $fullClassName))
60+
->identifier(self::IDENTIFIER)
5461
->build(),
5562
];
5663
}

src/ReadonlyClassRule.php renamed to src/Architecture/ClassMustBeReadonlyRule.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Phauthentic\PhpstanRules;
5+
namespace Phauthentic\PHPStanRules\Architecture;
66

77
use PhpParser\Node;
88
use PhpParser\Node\Stmt\Class_;
@@ -13,10 +13,12 @@
1313
/**
1414
* @implements Rule<Class_>
1515
*/
16-
class ReadonlyClassRule implements Rule
16+
class ClassMustBeReadonlyRule implements Rule
1717
{
1818
private const ERROR_MESSAGE = 'Class %s must be readonly.';
1919

20+
private const IDENTIFIER = 'phauthentic.architecture.classMustBeReadonly';
21+
2022
/**
2123
* @var string[]
2224
*/
@@ -49,6 +51,7 @@ public function processNode(Node $node, Scope $scope): array
4951
if (preg_match($pattern, $fullClassName) && !$node->isReadonly()) {
5052
return [
5153
RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $fullClassName))
54+
->identifier(self::IDENTIFIER)
5255
->build(),
5356
];
5457
}

src/NamespaceClassPatternRule.php renamed to src/Architecture/ClassnameMustMatchPatternRule.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,28 @@
22

33
declare(strict_types=1);
44

5-
namespace Phauthentic\PhpstanRules;
5+
namespace Phauthentic\PHPStanRules\Architecture;
66

77
use PhpParser\Node;
8-
use PhpParser\Node\Stmt\Namespace_;
98
use PhpParser\Node\Stmt\Class_;
9+
use PhpParser\Node\Stmt\Namespace_;
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\ShouldNotHappenException;
1314

1415
/**
1516
* PHPStan rule to ensure that classes inside namespaces matching a given regex
1617
* must have names matching at least one of the provided patterns.
1718
*
1819
* @implements Rule<Namespace_>
1920
*/
20-
class NamespaceClassPatternRule implements Rule
21+
class ClassnameMustMatchPatternRule implements Rule
2122
{
23+
private const ERROR_MESSAGE = 'Class %s in namespace %s does not match any of the required patterns:';
24+
25+
private const IDENTIFIER = 'phauthentic.architecture.classnameMustMatchPattern';
26+
2227
/**
2328
* @var array{namespace: string, classPatterns: string[]}[]
2429
*/
@@ -91,7 +96,7 @@ private function classNameMatches(string $className, array $classPatterns): bool
9196
private function buildErrorMessage(string $fqcn, string $namespace, array $patterns): string
9297
{
9398
$lines = [
94-
sprintf('Class %s in namespace %s does not match any of the required patterns:', $fqcn, $namespace),
99+
sprintf(self::ERROR_MESSAGE, $fqcn, $namespace),
95100
];
96101

97102
foreach ($patterns as $pattern) {
@@ -108,13 +113,22 @@ private function buildErrorMessage(string $fqcn, string $namespace, array $patte
108113
* @param Class_ $stmt
109114
* @param array $errors
110115
* @return array
116+
* @throws ShouldNotHappenException
111117
*/
112-
public function buildRuleError(string $namespaceName, string $className, $classPatterns, Class_ $stmt, array $errors): array
113-
{
118+
public function buildRuleError(
119+
string $namespaceName,
120+
string $className,
121+
$classPatterns,
122+
Class_ $stmt,
123+
array $errors
124+
): array {
114125
$fqcn = $namespaceName ? $namespaceName . '\\' . $className : $className;
115126
$errors[] = RuleErrorBuilder::message(
116-
$this->buildErrorMessage($fqcn, $namespaceName, $classPatterns)
117-
)->line($stmt->getLine())->build();
127+
$this->buildErrorMessage($fqcn, $namespaceName, $classPatterns)
128+
)
129+
->line($stmt->getLine())
130+
->identifier(self::IDENTIFIER)
131+
->build();
118132

119133
return $errors;
120134
}

src/DependencyConstraintsRule.php renamed to src/Architecture/DependencyConstraintsRule.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
declare(strict_types=1);
44

5-
namespace Phauthentic\PhpstanRules;
5+
namespace Phauthentic\PHPStanRules\Architecture;
66

77
use PhpParser\Node;
88
use PhpParser\Node\Stmt\Use_;
99
use PHPStan\Analyser\Scope;
1010
use PHPStan\Rules\Rule;
1111
use PHPStan\Rules\RuleError;
1212
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\ShouldNotHappenException;
1314

1415
/**
1516
* A PHPStan rule to enforce dependency constraints between namespaces.
@@ -22,6 +23,8 @@ class DependencyConstraintsRule implements Rule
2223
{
2324
private const ERROR_MESSAGE = 'Dependency violation: A class in namespace `%s` is not allowed to depend on `%s`.';
2425

26+
private const IDENTIFIER = 'phauthentic.architecture.dependencyConstraints';
27+
2528
/**
2629
* @var array<string, array<string>>
2730
* An array where the key is a regex for the source namespace and the value is
@@ -43,6 +46,9 @@ public function getNodeType(): string
4346
return Use_::class;
4447
}
4548

49+
/**
50+
* @throws ShouldNotHappenException
51+
*/
4652
public function processNode(Node $node, Scope $scope): array
4753
{
4854
$currentNamespace = $scope->getNamespace();
@@ -69,7 +75,7 @@ public function processNode(Node $node, Scope $scope): array
6975
* @param string $currentNamespace
7076
* @param array<RuleError> $errors
7177
* @return array<RuleError>
72-
* @throws \PHPStan\ShouldNotHappenException
78+
* @throws ShouldNotHappenException
7379
*/
7480
public function validateUseStatements(Node $node, array $disallowedDependencyPatterns, string $currentNamespace, array $errors): array
7581
{
@@ -82,6 +88,7 @@ public function validateUseStatements(Node $node, array $disallowedDependencyPat
8288
$currentNamespace,
8389
$usedClassName
8490
))
91+
->identifier(self::IDENTIFIER)
8592
->line($use->getStartLine())
8693
->build();
8794
}

0 commit comments

Comments
 (0)