Skip to content

Commit 4f01ef9

Browse files
Merge pull request #32 from Phauthentic/rules
Adding more Rules
2 parents 35d68c7 + 3fa03c8 commit 4f01ef9

28 files changed

Lines changed: 1171 additions & 16 deletions

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ See individual rule documentation for detailed configuration examples. A [full c
2727
- [Classname Must Match Pattern Rule](docs/rules/Classname-Must-Match-Pattern-Rule.md)
2828
- [Dependency Constraints Rule](docs/rules/Dependency-Constraints-Rule.md) *(deprecated, use Forbidden Dependencies Rule)*
2929
- [Forbidden Accessors Rule](docs/rules/Forbidden-Accessors-Rule.md)
30+
- [Forbidden Business Logic Rule](docs/rules/Forbidden-Business-Logic-Rule.md)
3031
- [Forbidden Dependencies Rule](docs/rules/Forbidden-Dependencies-Rule.md)
32+
- [Forbidden Date Time Comparison Rule](docs/rules/Forbidden-Date-Time-Comparison-Rule.md)
3133
- [Forbidden Namespaces Rule](docs/rules/Forbidden-Namespaces-Rule.md)
3234
- [Forbidden Static Methods Rule](docs/rules/Forbidden-Static-Methods-Rule.md)
3335
- [Method Must Return Type Rule](docs/rules/Method-Must-Return-Type-Rule.md)
@@ -39,6 +41,7 @@ See individual rule documentation for detailed configuration examples. A [full c
3941
### Clean Code Rules
4042

4143
- [Control Structure Nesting Rule](docs/rules/Control-Structure-Nesting-Rule.md)
44+
- [Forbidden Else Statements Rule](docs/rules/Forbidden-Else-Statements-Rule.md)
4245
- [Too Many Arguments Rule](docs/rules/Too-Many-Arguments-Rule.md)
4346
- [Max Line Length Rule](docs/rules/Max-Line-Length-Rule.md)
4447

@@ -50,7 +53,7 @@ The rules in this package can be extended at project level to create self-docume
5053

5154
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.
5255

53-
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/).
56+
If you struggle to write the regex patterns you need, you can use AI agents 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/).
5457

5558
## Why PHPStan to enforce Architectural Rules?
5659

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ForbiddenBusinessLogicRule;
6+
7+
final class EmptyGlobalFixture
8+
{
9+
public function onlyIf(): void
10+
{
11+
if (true) {
12+
}
13+
}
14+
15+
public function unmatchedHasIf(): void
16+
{
17+
if (true) {
18+
}
19+
}
20+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ForbiddenBusinessLogicRule;
6+
7+
final class MinimalDefaults
8+
{
9+
public function m(): void
10+
{
11+
if (true) {
12+
}
13+
}
14+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ForbiddenBusinessLogicRule;
6+
7+
/**
8+
* Fixture for ForbiddenBusinessLogicRuleTest.
9+
*/
10+
final class ScenarioFixture
11+
{
12+
public function onlyGlobalMatch(): void
13+
{
14+
if (true) {
15+
}
16+
}
17+
18+
public function onlyIf(): void
19+
{
20+
if (true) {
21+
}
22+
foreach ([] as $_) {
23+
}
24+
}
25+
26+
public function noOverrideX(): void
27+
{
28+
if (true) {
29+
}
30+
}
31+
32+
public function lastWinsM(): void
33+
{
34+
if (true) {
35+
}
36+
for ($i = 0; $i < 1; $i++) {
37+
}
38+
switch (1) {
39+
default:
40+
break;
41+
}
42+
}
43+
44+
public function unknownNamesN(): void
45+
{
46+
if (true) {
47+
}
48+
foreach ([] as $_) {
49+
}
50+
}
51+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ForbiddenDateTimeComparison;
6+
7+
use DateTimeInterface;
8+
9+
function global_datetime_compare(DateTimeInterface $left, DateTimeInterface $right): bool
10+
{
11+
return $left === $right;
12+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ForbiddenDateTimeComparison;
6+
7+
use DateTimeInterface;
8+
9+
final class GlobalViolations
10+
{
11+
public function identicalDates(DateTimeInterface $a, DateTimeInterface $b): bool
12+
{
13+
return $a === $b;
14+
}
15+
16+
public function notIdenticalDates(DateTimeInterface $a, DateTimeInterface $b): bool
17+
{
18+
return $a !== $b;
19+
}
20+
21+
public function looseOk(DateTimeInterface $a, DateTimeInterface $b): bool
22+
{
23+
return $a == $b;
24+
}
25+
26+
public function mixedWithObject(object $a, DateTimeInterface $b): bool
27+
{
28+
return $a === $b;
29+
}
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ForbiddenDateTimeComparison;
6+
7+
use DateTimeInterface;
8+
9+
final class ScopedViolations
10+
{
11+
public function matchedMethod(DateTimeInterface $a, DateTimeInterface $b): bool
12+
{
13+
return $a === $b;
14+
}
15+
16+
public function unmatchedMethod(DateTimeInterface $a, DateTimeInterface $b): bool
17+
{
18+
return $a === $b;
19+
}
20+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ElseRules;
6+
7+
class Matched
8+
{
9+
public function matchedMethod(bool $x): void
10+
{
11+
if ($x) {
12+
return;
13+
} else {
14+
return;
15+
}
16+
}
17+
18+
public function anotherMatched(bool $x): void
19+
{
20+
if ($x) {
21+
return;
22+
} else {
23+
return;
24+
}
25+
}
26+
}
27+
28+
class Unmatched
29+
{
30+
public function any(bool $x): void
31+
{
32+
if ($x) {
33+
return;
34+
} else {
35+
return;
36+
}
37+
}
38+
}

docs/Rules.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,24 @@ Forbids public and/or protected getters and setters on classes matching specifie
2222

2323
See [Forbidden Accessors Rule documentation](rules/Forbidden-Accessors-Rule.md) for detailed information.
2424

25+
## Forbidden Business Logic Rule
26+
27+
Forbids configured control structures (`if`, `for`, `foreach`, `while`, `switch`) inside class methods. A global `forbiddenStatements` list applies to all such methods; optional `patterns` (regex on `Fqcn::methodName`) may supply `forbiddenStatements` per match, which replace the effective list for that method (last matching override wins).
28+
29+
See [Forbidden Business Logic Rule documentation](rules/Forbidden-Business-Logic-Rule.md) for detailed information.
30+
2531
## Forbidden Dependencies Rule
2632

2733
Enforces dependency constraints between namespaces by checking `use` statements and optionally fully qualified class names (FQCNs). This rule prevents classes in one namespace from depending on classes in another, helping enforce architectural boundaries like layer separation.
2834

2935
See [Forbidden Dependencies Rule documentation](rules/Forbidden-Dependencies-Rule.md) for detailed information.
3036

37+
## Forbidden Date Time Comparison Rule
38+
39+
Forbids `===` and `!==` when both sides are definitely `DateTimeInterface`, because strict equality is object identity, not same instant. Optional `patterns` (regex on `Fqcn::methodName`) limit the rule to matching class methods; an empty `patterns` list applies globally (unlike Forbidden Else Statements Rule, where empty `patterns` disables the rule).
40+
41+
See [Forbidden Date Time Comparison Rule documentation](rules/Forbidden-Date-Time-Comparison-Rule.md) for detailed information.
42+
3143
## Forbidden Static Methods Rule
3244

3345
Forbids specific static method calls matching regex patterns. Supports namespace-level, class-level, and method-level granularity. The rule resolves `self`, `static`, and `parent` keywords to actual class names.
@@ -40,6 +52,12 @@ Ensures that classes matching specified patterns have properties with expected n
4052

4153
See [Property Must Match Rule documentation](rules/Property-Must-Match-Rule.md) for detailed information.
4254

55+
## Forbidden Else Statements Rule
56+
57+
Forbids plain `else` in class methods whose `Full\Class\Name::methodName` matches any configured regex, using the same `Fqcn::methodName` convention as other method-scoped rules. Empty `patterns` disables the rule.
58+
59+
See [Forbidden Else Statements Rule documentation](rules/Forbidden-Else-Statements-Rule.md) for detailed information.
60+
4361
## Full Configuration Example
4462

4563
Here is a full example for a modular monolith with clean architecture rules.
@@ -246,6 +264,25 @@ services:
246264
tags:
247265
- phpstan.rules.rule
248266

267+
# Forbid selected control structures (global + optional per-regex overrides)
268+
-
269+
class: Phauthentic\PHPStanRules\Architecture\ForbiddenBusinessLogicRule
270+
arguments:
271+
forbiddenStatements:
272+
- if
273+
- for
274+
- foreach
275+
- while
276+
- switch
277+
patterns:
278+
-
279+
pattern: '/^App\\Capability\\.*\\Domain\\.*::/'
280+
forbiddenStatements:
281+
- if
282+
- switch
283+
tags:
284+
- phpstan.rules.rule
285+
249286
# Forbid accessors on domain entities
250287
-
251288
class: Phauthentic\PHPStanRules\Architecture\ForbiddenAccessorsRule
@@ -268,4 +305,13 @@ services:
268305
maxNestingLevel: 3
269306
tags:
270307
- phpstan.rules.rule
308+
309+
# Forbid else in selected class methods (regex on Fqcn::methodName)
310+
-
311+
class: Phauthentic\PHPStanRules\CleanCode\ForbiddenElseStatementsRule
312+
arguments:
313+
patterns:
314+
- '/^App\\Capability\\.*\\Presentation\\Http\\.*Controller::(handle|__invoke)$/'
315+
tags:
316+
- phpstan.rules.rule
271317
```
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Forbidden Business Logic Rule
2+
3+
Forbids selected imperative control structures inside class methods: `if`, `for`, `foreach`, `while`, and `switch`. Typical use cases include pushing branching and iteration behind domain objects or policy objects in specific layers.
4+
5+
The rule evaluates the current scope as `Full\Class\Name::methodName` (from PHPStan’s class and function reflections). Anonymous functions and global functions are out of scope when the class or function reflection is missing.
6+
7+
## Global list and per-pattern overrides
8+
9+
- **`forbiddenStatements`**: Default set of construct names to forbid in every class method. Names are case-insensitive. Unknown names are ignored. If this list is empty, nothing is forbidden unless a matching pattern entry supplies a non-empty `forbiddenStatements` list.
10+
- **`patterns`**: Ordered list of entries. Each entry has a **`pattern`** (regex matched against `Fqcn::methodName`). Optionally **`forbiddenStatements`**: if present on an entry whose pattern matches, it **replaces** the effective forbidden list entirely for that method. If several entries match, the **last** matching entry that defines `forbiddenStatements` wins. Entries that match but omit `forbiddenStatements` do not change the effective list (it stays as set by global and earlier matches).
11+
12+
The default constructor uses all five constructs globally and an empty `patterns` list, which forbids those constructs everywhere in class methods.
13+
14+
## Legacy `patterns` format
15+
16+
You may pass plain regex strings; each is normalised to `{ pattern: "..." }` without an override, so only the global list applies for methods that do not receive a later matching override with `forbiddenStatements`.
17+
18+
## Configuration Example
19+
20+
```neon
21+
-
22+
class: Phauthentic\PHPStanRules\Architecture\ForbiddenBusinessLogicRule
23+
arguments:
24+
forbiddenStatements:
25+
- if
26+
- for
27+
- foreach
28+
- while
29+
- switch
30+
patterns:
31+
-
32+
pattern: '/^App\\\\Domain\\\\.*::/'
33+
forbiddenStatements:
34+
- if
35+
- switch
36+
-
37+
pattern: '/^App\\\\Application\\\\.*Workflow::run$/'
38+
forbiddenStatements:
39+
- foreach
40+
tags:
41+
- phpstan.rules.rule
42+
```
43+
44+
## Parameters
45+
46+
- `forbiddenStatements` (`list<string>`): Global list of forbidden construct names: `if`, `for`, `foreach`, `while`, `switch`.
47+
- `patterns` (`list<array{pattern: string, forbiddenStatements?: list<string>}> | list<string>`): Regex entries against `Fqcn::methodName`, optionally with per-entry `forbiddenStatements` that replace the effective list when that pattern matches (last such match wins).
48+
49+
## Class-wide patterns
50+
51+
Match every method on a class with a regex on the `::` prefix, for example `/^App\\\\Module\\\\Foo::/` or `/^App\\\\Module\\\\Foo::.+$/`.
52+
53+
## Ignoring violations
54+
55+
Use PHPStan baseline or inline `@phpstan-ignore` with a short justification when a rare exception is required.

0 commit comments

Comments
 (0)