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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ See individual rule documentation for detailed configuration examples. A [full c
- [Classname Must Match Pattern Rule](docs/rules/Classname-Must-Match-Pattern-Rule.md)
- [Dependency Constraints Rule](docs/rules/Dependency-Constraints-Rule.md) *(deprecated, use Forbidden Dependencies Rule)*
- [Forbidden Accessors Rule](docs/rules/Forbidden-Accessors-Rule.md)
- [Forbidden Business Logic Rule](docs/rules/Forbidden-Business-Logic-Rule.md)
- [Forbidden Dependencies Rule](docs/rules/Forbidden-Dependencies-Rule.md)
- [Forbidden Date Time Comparison Rule](docs/rules/Forbidden-Date-Time-Comparison-Rule.md)
- [Forbidden Namespaces Rule](docs/rules/Forbidden-Namespaces-Rule.md)
- [Forbidden Static Methods Rule](docs/rules/Forbidden-Static-Methods-Rule.md)
- [Method Must Return Type Rule](docs/rules/Method-Must-Return-Type-Rule.md)
Expand All @@ -39,6 +41,7 @@ See individual rule documentation for detailed configuration examples. A [full c
### Clean Code Rules

- [Control Structure Nesting Rule](docs/rules/Control-Structure-Nesting-Rule.md)
- [Forbidden Else Statements Rule](docs/rules/Forbidden-Else-Statements-Rule.md)
- [Too Many Arguments Rule](docs/rules/Too-Many-Arguments-Rule.md)
- [Max Line Length Rule](docs/rules/Max-Line-Length-Rule.md)

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

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.

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/).
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/).

## Why PHPStan to enforce Architectural Rules?

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

declare(strict_types=1);

namespace App\ForbiddenBusinessLogicRule;

final class EmptyGlobalFixture
{
public function onlyIf(): void
{
if (true) {
}
}

public function unmatchedHasIf(): void
{
if (true) {
}
}
}
14 changes: 14 additions & 0 deletions data/ForbiddenBusinessLogicRule/MinimalDefaults.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace App\ForbiddenBusinessLogicRule;

final class MinimalDefaults
{
public function m(): void
{
if (true) {
}
}
}
51 changes: 51 additions & 0 deletions data/ForbiddenBusinessLogicRule/ScenarioFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace App\ForbiddenBusinessLogicRule;

/**
* Fixture for ForbiddenBusinessLogicRuleTest.
*/
final class ScenarioFixture
{
public function onlyGlobalMatch(): void
{
if (true) {
}
}

public function onlyIf(): void
{
if (true) {
}
foreach ([] as $_) {
}
}

public function noOverrideX(): void
{
if (true) {
}
}

public function lastWinsM(): void
{
if (true) {
}
for ($i = 0; $i < 1; $i++) {
}
switch (1) {
default:
break;
}
}

public function unknownNamesN(): void
{
if (true) {
}
foreach ([] as $_) {
}
}
}
12 changes: 12 additions & 0 deletions data/ForbiddenDateTimeComparison/GlobalFunctionViolations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace App\ForbiddenDateTimeComparison;

use DateTimeInterface;

function global_datetime_compare(DateTimeInterface $left, DateTimeInterface $right): bool
{
return $left === $right;
}
30 changes: 30 additions & 0 deletions data/ForbiddenDateTimeComparison/GlobalViolations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace App\ForbiddenDateTimeComparison;

use DateTimeInterface;

final class GlobalViolations
{
public function identicalDates(DateTimeInterface $a, DateTimeInterface $b): bool
{
return $a === $b;
}

public function notIdenticalDates(DateTimeInterface $a, DateTimeInterface $b): bool
{
return $a !== $b;
}

public function looseOk(DateTimeInterface $a, DateTimeInterface $b): bool
{
return $a == $b;
}

public function mixedWithObject(object $a, DateTimeInterface $b): bool
{
return $a === $b;
}
}
20 changes: 20 additions & 0 deletions data/ForbiddenDateTimeComparison/ScopedViolations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\ForbiddenDateTimeComparison;

use DateTimeInterface;

final class ScopedViolations
{
public function matchedMethod(DateTimeInterface $a, DateTimeInterface $b): bool
{
return $a === $b;
}

public function unmatchedMethod(DateTimeInterface $a, DateTimeInterface $b): bool
{
return $a === $b;
}
}
38 changes: 38 additions & 0 deletions data/ForbiddenElseStatementsRuleFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace App\ElseRules;

class Matched
{
public function matchedMethod(bool $x): void
{
if ($x) {
return;
} else {
return;
}
}

public function anotherMatched(bool $x): void
{
if ($x) {
return;
} else {
return;
}
}
}

class Unmatched
{
public function any(bool $x): void
{
if ($x) {
return;
} else {
return;
}
}
}
46 changes: 46 additions & 0 deletions docs/Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,24 @@ Forbids public and/or protected getters and setters on classes matching specifie

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

## Forbidden Business Logic Rule

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

See [Forbidden Business Logic Rule documentation](rules/Forbidden-Business-Logic-Rule.md) for detailed information.

## Forbidden Dependencies Rule

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.

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

## Forbidden Date Time Comparison Rule

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

See [Forbidden Date Time Comparison Rule documentation](rules/Forbidden-Date-Time-Comparison-Rule.md) for detailed information.

## Forbidden Static Methods Rule

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.
Expand All @@ -40,6 +52,12 @@ Ensures that classes matching specified patterns have properties with expected n

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

## Forbidden Else Statements Rule

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.

See [Forbidden Else Statements Rule documentation](rules/Forbidden-Else-Statements-Rule.md) for detailed information.

## Full Configuration Example

Here is a full example for a modular monolith with clean architecture rules.
Expand Down Expand Up @@ -246,6 +264,25 @@ services:
tags:
- phpstan.rules.rule

# Forbid selected control structures (global + optional per-regex overrides)
-
class: Phauthentic\PHPStanRules\Architecture\ForbiddenBusinessLogicRule
arguments:
forbiddenStatements:
- if
- for
- foreach
- while
- switch
patterns:
-
pattern: '/^App\\Capability\\.*\\Domain\\.*::/'
forbiddenStatements:
- if
- switch
tags:
- phpstan.rules.rule

# Forbid accessors on domain entities
-
class: Phauthentic\PHPStanRules\Architecture\ForbiddenAccessorsRule
Expand All @@ -268,4 +305,13 @@ services:
maxNestingLevel: 3
tags:
- phpstan.rules.rule

# Forbid else in selected class methods (regex on Fqcn::methodName)
-
class: Phauthentic\PHPStanRules\CleanCode\ForbiddenElseStatementsRule
arguments:
patterns:
- '/^App\\Capability\\.*\\Presentation\\Http\\.*Controller::(handle|__invoke)$/'
tags:
- phpstan.rules.rule
```
55 changes: 55 additions & 0 deletions docs/rules/Forbidden-Business-Logic-Rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Forbidden Business Logic Rule

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.

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.

## Global list and per-pattern overrides

- **`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.
- **`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).

The default constructor uses all five constructs globally and an empty `patterns` list, which forbids those constructs everywhere in class methods.

## Legacy `patterns` format

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

## Configuration Example

```neon
-
class: Phauthentic\PHPStanRules\Architecture\ForbiddenBusinessLogicRule
arguments:
forbiddenStatements:
- if
- for
- foreach
- while
- switch
patterns:
-
pattern: '/^App\\\\Domain\\\\.*::/'
forbiddenStatements:
- if
- switch
-
pattern: '/^App\\\\Application\\\\.*Workflow::run$/'
forbiddenStatements:
- foreach
tags:
- phpstan.rules.rule
```

## Parameters

- `forbiddenStatements` (`list<string>`): Global list of forbidden construct names: `if`, `for`, `foreach`, `while`, `switch`.
- `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).

## Class-wide patterns

Match every method on a class with a regex on the `::` prefix, for example `/^App\\\\Module\\\\Foo::/` or `/^App\\\\Module\\\\Foo::.+$/`.

## Ignoring violations

Use PHPStan baseline or inline `@phpstan-ignore` with a short justification when a rare exception is required.
Loading
Loading