Skip to content

refactor(rules): migrate deprecated isSubclassOf(string) → isSubclassOfClass (queue #112) - #56

Merged
Goosterhof merged 1 commit into
mainfrom
armorer/queue112-issubclassof-deprecation
Jul 16, 2026
Merged

refactor(rules): migrate deprecated isSubclassOf(string) → isSubclassOfClass (queue #112)#56
Goosterhof merged 1 commit into
mainfrom
armorer/queue112-issubclassof-deprecation

Conversation

@Goosterhof

Copy link
Copy Markdown
Contributor

What

ClassReflection::isSubclassOf(string) is @deprecated in the vendored PHPStan (2.2+) and removed in PHPStan 3 — a latent break for every consumer territory's static analysis the day this package targets 3.x. Migrated all 3 string-form call sites to isSubclassOfClass(ClassReflection):

  • EnforceFormRequestToDtoRule (formRequestBaseClass)
  • EnforceResourceDataValidatorOptInRule (resourceDataBaseClass)
  • EnforceAuditModelProtectionsRule (Model::class — the queue named only the first two; the WR-0195 audit rule's fixed-class site is the third, swept in the same pass)

Each rule now injects ReflectionProvider and resolves its base FQCN via hasClass()/getClass() before calling isSubclassOfClass().

The load-bearing no-op is preserved exactly

The deprecated string form's own body is if (!hasClass($fqcn)) return false; return isSubclassOfClass(getClass($fqcn)); (confirmed by reading the vendored PHPStan phar). The migration is a 1:1 inline of that, so the "consumers analysing non-Laravel trees are unaffected" guarantee — a tree with no configured base class never fires the rule — is reproduced byte-for-byte. Pinned by 3 new base-class-absent no-op tests.

Verification

  • composer test 176/176 (256 assertions, +3 new) · composer phpstan [OK] No errors (deprecation gone) · Pint clean · grep proof: every ->isSubclassOf( is now ->isSubclassOfClass( (remaining hits are docblock prose).
  • No extension.neon change needed — Nette autowires ReflectionProvider; existing container-resolution tests prove wiring. First rule here to inject a PHPStan service (standard PHPStan-core pattern) — worth a review glance.
  • CHANGELOG under [Unreleased] → ### Changed (PATCH; pairs with the pending WR-0438 release cut — not cut here).

🤖 General-dispatched Armorer (warpath).

…OfClass (queue #112)

isSubclassOf(string) is @deprecated in PHPStan 2.2.2 and removed in 3.x — a
latent break for every consumer's static analysis. Resolve the configured/known
base FQCN via ReflectionProvider->hasClass()/getClass() and call
isSubclassOfClass(ClassReflection), preserving the unknown-base-class no-op that
keeps non-Laravel consumers unaffected. Pinned by a base-class-absent fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kMpRQSwwg8zM5kYzbNArQ
@Goosterhof
Goosterhof requested a review from a team as a code owner July 16, 2026 07:40
@Goosterhof Goosterhof added the Agent Review Requested Requesting review of specialized AI review agents. label Jul 16, 2026

@Goosterhof Goosterhof left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: clean. Verified independently, not just read.

This is a textbook deprecation migration and I went looking for the classic failure modes of a stringClassReflection API swap — none present.

What I checked, not just what the PR claims:

  1. Byte-for-byte equivalence claim — verified against the actual vendored PHPStan source, not taken on faith. Extracted vendor/phpstan/phpstan/phpstan.phar and read ClassReflection::isSubclassOf() directly:

    public function isSubclassOf(string $className): bool
    {
        if (!$this->reflectionProvider->hasClass($className)) {
            return false;
        }
        return $this->isSubclassOfClass($this->reflectionProvider->getClass($className));
    }

    This matches the PR body's claimed body exactly, and all three migrated call sites (EnforceAuditModelProtectionsRule, EnforceFormRequestToDtoRule, EnforceResourceDataValidatorOptInRule) reproduce this hasClass-guard-then-isSubclassOfClass shape 1:1. No signature-mismatch risk (no raw string passed where a ClassReflection is required), no inverted subclass direction, no dropped existence guard.

  2. Completenessgrep -rn -- "isSubclassOf(" src/ (excluding isSubclassOfClass) returns zero live call sites; the only remaining string hits are docblock prose. All 3 sites from the queue-#112 scope are migrated, plus the audit-rule's fixed-class site the queue didn't explicitly name — correctly swept in the same pass rather than left half-migrated.

  3. Ran the suite myself, not trusting the PR's reported numbers: composer test → 176/176 passed (256 assertions), composer phpstan[OK] No errors, deprecation warning gone. Matches the PR body's claimed counts exactly.

  4. DI wiringReflectionProvider is constructor-injected in all three rules with no extension.neon argument added, and extension.neon confirms no arguments: block for any of the three rules — correct, since PHPStan/Nette autowires ReflectionProvider as a container service. testRuleResolvesFromExtensionNeonAndFires (which pulls each rule out of the real container) is in the green 176, which is the actual proof the autowiring works, not just a claim.

  5. No silent enforcement weakening — each rule gained a new no-op test (base-class-absent / not-a-subclass) rather than a fixture change on an existing positive-case test, so I don't have to take "behavior unchanged" on faith for the tests that already existed; they're literally untouched except for the new constructor arg being threaded through ruleOverride calls.

Dashboard note (not a finding): check (8.4) / check (8.5) were still pending on GitHub Actions at review time — both are required contexts on main's branch protection. Worth a green-check confirmation before merge; my own local run of the same commands (composer test + composer phpstan) was clean, so I don't expect a surprise, but I haven't watched the Action itself complete.

No blockers, no majors, no minors. This is the correct, complete, test-pinned form of this migration — approve-quality work, posting as COMMENT only because GitHub blocks self-approval on your own PR.

Automated war-room agent review — posted because this PR carries the Agent Review Requested label.

@Goosterhof
Goosterhof enabled auto-merge July 16, 2026 07:45

@dmooibroek dmooibroek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean at 14715d7 — confirm the-general. Grep: zero live isSubclassOf( call sites, only isSubclassOfClass in src/Rules/*.php (stray hits are comments/test names). Each rule DIs ReflectionProvider, guards hasClass() before isSubclassOfClass(getClass(...)) — reproduces deprecated form's no-op, pinned by 3 base-class-absent tests. Static review only here (no vendor in worktree); the-general already ran composer test 176/176 + phpstan clean. No gated findings.

@Goosterhof
Goosterhof merged commit d27d7cb into main Jul 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Agent Review Requested Requesting review of specialized AI review agents.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants