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
7 changes: 7 additions & 0 deletions skills/php-modernization/checkpoints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,13 @@ mechanical:
severity: error
desc: "readonly must not be applied to Doctrine entities or mapped-superclasses (Doctrine hydrates them via reflection-bypassing the constructor). Embeddables are a nuanced case — see references/doctrine-modernization-edges.md"

# === MECHANICAL REFACTOR SAFETY ===
- id: PM-44
type: command
command: 'D=""; for d in src Classes tests Tests; do [ -d "$d" ] && D="$D $d"; done; [ -z "$D" ] || ! grep -rqP ''const\s+(\w+)\s*=\s*self::\1\b'' $D'
severity: error
desc: "Self-referencing class constant (const X = self::X) — a fatal error at class-load time, produced by replace-all literal-to-constant extraction that also rewrites the declaration site. Static analysis does not catch it; only executing the file does. Exclude the declaration line when extracting a repeated literal."

llm_reviews:
# === READONLY CLASS STATIC PROPERTY CHECK ===
- id: PM-32
Expand Down
33 changes: 33 additions & 0 deletions skills/php-modernization/references/migration-strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,39 @@ return $this->legacyParser->parse($input);

## Anti-Patterns to Avoid

### Replace-All Literal Extraction That Rewrites Its Own Declaration

Bulk-extracting a repeated literal into a constant (the usual fix for SonarQube
`php:S1192`) is a natural scripted transform: find every `'…'`, replace with
`self::NAME`. Run naively, the replacement also hits the line that *defines* the
constant:

```php
// ❌ what a blind replace-all produces
private const SMALL_PAYLOAD = self::SMALL_PAYLOAD; // Fatal: self-referencing constant

// ✅ intended
private const SMALL_PAYLOAD = '{"a":1}';
```

**Static analysis does not catch this.** PHPStan at level 10 reports nothing —
the error is raised by the engine at class-load time, so only *executing* a file
that loads the class surfaces it. A change set that passes every analyzer can
still be fatally broken.

Two rules when scripting this class of refactor:

1. **Exclude the declaration line.** Insert the `const` declaration *after*
rewriting the usages, or skip any line already matching
`const\s+NAME\s*=`.
2. **Verify by executing, not by analyzing.** Run the test suite (or at minimum
`php -l` plus a load of each touched class); a green PHPStan run is not
evidence here.

Checkpoint `PM-44` detects the resulting pattern. It generalizes beyond PHP —
any "extract repeated value to a named constant" transform in any language has
the same declaration-site hazard.

### Premature Backwards Compatibility

Don't add backwards compatibility code for features or versions that haven't been released yet:
Expand Down
Loading