Skip to content

Commit 9af22fc

Browse files
floriankraemerFlorian Krämer
andauthored
Enhance documentation for extending rules feature (#28)
- Added a new section in README.md to explain how to create domain-specific, self-documenting rules by extending base rules. - Introduced a new documentation file, ExtendingRules.md, detailing the benefits, examples, and best practices for implementing custom rules in PHPStan. - Included code examples for creating and registering custom rules, emphasizing clarity and maintainability. This update aims to improve user understanding and encourage best practices in rule customization. Co-authored-by: Florian Krämer <f.kraemer@clipmyhorse.tv>
1 parent ac328f8 commit 9af22fc

2 files changed

Lines changed: 218 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ See individual rule documentation for detailed configuration examples. A [full c
3737
- [Too Many Arguments Rule](docs/rules/Too-Many-Arguments-Rule.md)
3838
- [Max Line Length Rule](docs/rules/Max-Line-Length-Rule.md)
3939

40+
### Domain Specific Rules / Extending Rules
41+
42+
The rules in this package can be extended at project level to create self-documenting, domain-specific rules. Instead of configuring complex regex patterns in your neon file, you can create custom rule classes that encapsulate the configuration. For example, a class named `DomainClassesMustBeFinalRule` immediately communicates its purpose. See the [Extending Rules](docs/ExtendingRules.md) documentation for examples and best practices.
43+
4044
### Using Regex in Rules
4145

4246
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.

docs/ExtendingRules.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Extending Rules
2+
3+
This package provides flexible base rules that can be extended in your project to create domain-specific, self-documenting rules. Instead of configuring complex regex patterns directly in your `phpstan.neon` file, you can create custom rule classes that encapsulate the configuration.
4+
5+
## Benefits of Extending Rules
6+
7+
- **Self-Documenting Code**: A class named `ImmutableDateTimeIsForbiddenInModulesRule` immediately communicates its purpose, unlike a generic configuration block.
8+
- **Reusability**: The rule can be shared across multiple projects or teams without copying configuration.
9+
- **IDE Support**: Full autocompletion and refactoring support in your IDE.
10+
- **Testability**: Custom rules can be unit tested independently.
11+
- **Single Responsibility**: Each rule class has one clear purpose, making maintenance easier.
12+
- **Cleaner Configuration**: Your `phpstan.neon` stays minimal and readable.
13+
14+
## Examples
15+
16+
### Example 1: Forbid DateTime in Module Namespaces
17+
18+
This rule forbids the usage of PHP's mutable `DateTime` class in your module namespaces, encouraging the use of `DateTimeImmutable` or domain-specific value objects.
19+
20+
**Create the rule class:**
21+
22+
```php
23+
<?php
24+
25+
declare(strict_types=1);
26+
27+
namespace App\PHPStan\Rules;
28+
29+
use Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule;
30+
31+
/**
32+
* Prevents usage of mutable DateTime in module namespaces.
33+
* Use DateTimeImmutable or domain-specific date value objects instead.
34+
*/
35+
class ImmutableDateTimeIsForbiddenInModulesRule extends ForbiddenDependenciesRule
36+
{
37+
public function __construct()
38+
{
39+
parent::__construct(
40+
forbiddenDependencies: [
41+
'/^App\\\\Module\\\\.*/' => [
42+
'/^DateTime$/'
43+
]
44+
],
45+
checkFqcn: true
46+
);
47+
}
48+
}
49+
```
50+
51+
**Register in `phpstan.neon`:**
52+
53+
```neon
54+
services:
55+
-
56+
class: App\PHPStan\Rules\ImmutableDateTimeIsForbiddenInModulesRule
57+
tags:
58+
- phpstan.rules.rule
59+
```
60+
61+
### Example 2: Domain Classes Must Be Final
62+
63+
This rule ensures all classes in your domain layer are declared as `final` to prevent inheritance and encourage composition over inheritance.
64+
65+
**Create the rule class:**
66+
67+
```php
68+
<?php
69+
70+
declare(strict_types=1);
71+
72+
namespace App\PHPStan\Rules;
73+
74+
use Phauthentic\PHPStanRules\Architecture\ClassMustBeFinalRule;
75+
76+
/**
77+
* Enforces that all domain classes are final.
78+
* This promotes composition over inheritance in the domain layer.
79+
*/
80+
class DomainClassesMustBeFinalRule extends ClassMustBeFinalRule
81+
{
82+
public function __construct()
83+
{
84+
parent::__construct(
85+
patterns: [
86+
'/^App\\\\Domain\\\\.*/'
87+
],
88+
ignoreAbstractClasses: true
89+
);
90+
}
91+
}
92+
```
93+
94+
**Register in `phpstan.neon`:**
95+
96+
```neon
97+
services:
98+
-
99+
class: App\PHPStan\Rules\DomainClassesMustBeFinalRule
100+
tags:
101+
- phpstan.rules.rule
102+
```
103+
104+
### Example 3: Forbid Legacy Namespace Creation
105+
106+
This rule prevents developers from creating classes in deprecated or legacy namespaces, helping to enforce architectural boundaries during refactoring efforts.
107+
108+
**Create the rule class:**
109+
110+
```php
111+
<?php
112+
113+
declare(strict_types=1);
114+
115+
namespace App\PHPStan\Rules;
116+
117+
use Phauthentic\PHPStanRules\Architecture\ForbiddenNamespacesRule;
118+
119+
/**
120+
* Prevents creation of classes in legacy namespaces.
121+
* All new code should use the App\Module\* namespace structure.
122+
*/
123+
class LegacyNamespaceForbiddenRule extends ForbiddenNamespacesRule
124+
{
125+
public function __construct()
126+
{
127+
parent::__construct(
128+
forbiddenNamespaces: [
129+
'/^App\\\\Legacy\\\\.*/',
130+
'/^App\\\\Old\\\\.*/',
131+
'/^App\\\\Deprecated\\\\.*/'
132+
]
133+
);
134+
}
135+
}
136+
```
137+
138+
**Register in `phpstan.neon`:**
139+
140+
```neon
141+
services:
142+
-
143+
class: App\PHPStan\Rules\LegacyNamespaceForbiddenRule
144+
tags:
145+
- phpstan.rules.rule
146+
```
147+
148+
## Complete Configuration Example
149+
150+
Here's how your `phpstan.neon` might look with multiple custom rules:
151+
152+
```neon
153+
parameters:
154+
level: 8
155+
paths:
156+
- src
157+
158+
services:
159+
-
160+
class: App\PHPStan\Rules\ImmutableDateTimeIsForbiddenInModulesRule
161+
tags:
162+
- phpstan.rules.rule
163+
164+
-
165+
class: App\PHPStan\Rules\DomainClassesMustBeFinalRule
166+
tags:
167+
- phpstan.rules.rule
168+
169+
-
170+
class: App\PHPStan\Rules\LegacyNamespaceForbiddenRule
171+
tags:
172+
- phpstan.rules.rule
173+
```
174+
175+
## Customizing Error Messages and Identifiers
176+
177+
If you need custom error messages, you can override the protected constants:
178+
179+
```php
180+
<?php
181+
182+
declare(strict_types=1);
183+
184+
namespace App\PHPStan\Rules;
185+
186+
use Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule;
187+
188+
class ImmutableDateTimeIsForbiddenInModulesRule extends ForbiddenDependenciesRule
189+
{
190+
protected const ERROR_MESSAGE = 'Mutable DateTime is not allowed in modules. Use DateTimeImmutable instead. Found dependency from `%s` to `%s`.';
191+
192+
protected const IDENTIFIER = 'app.architecture.immutableDateTimeInModules';
193+
194+
public function __construct()
195+
{
196+
parent::__construct(
197+
forbiddenDependencies: [
198+
'/^App\\\\Module\\\\.*/' => [
199+
'/^DateTime$/'
200+
]
201+
],
202+
checkFqcn: true
203+
);
204+
}
205+
}
206+
```
207+
208+
## Best Practices
209+
210+
1. **Use descriptive class names** that clearly communicate the rule's purpose.
211+
2. **Add PHPDoc comments** explaining why the rule exists and what alternatives developers should use.
212+
3. **Place rules in a dedicated namespace** like `App\PHPStan\Rules` or `App\Infrastructure\PHPStan`.
213+
4. **Consider creating a base rule class** for your project if you have common patterns across multiple rules.
214+
5. **Write tests** for your custom rules to ensure they catch the intended violations.

0 commit comments

Comments
 (0)