Skip to content

[DowngradePhp82] Add DowngradeReflectionMethodHasPrototypeRector - #398

Open
roxblnfk wants to merge 1 commit into
rectorphp:mainfrom
roxblnfk:feature/downgrade-reflection-has-prototype
Open

[DowngradePhp82] Add DowngradeReflectionMethodHasPrototypeRector#398
roxblnfk wants to merge 1 commit into
rectorphp:mainfrom
roxblnfk:feature/downgrade-reflection-has-prototype

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🔍 What was changed

New DowngradeReflectionMethodHasPrototypeRector (PHP 8.2 → 8.1) that rewrites $reflectionMethod->hasPrototype() into an emulation based on getPrototype().

$reflectionMethod->hasPrototype() becomes:

(function (\ReflectionMethod $reflectionMethod): bool {
    try {
        $reflectionMethod->getPrototype();
        return true;
    } catch (\ReflectionException) {
        return false;
    }
})($reflectionMethod)

The rule bails out on first-class callables, on non-ReflectionMethod receivers, and on any other method name.

Why?

ReflectionMethod::hasPrototype() was added in PHP 8.2 and does not exist below it, so the usual method_exists() ternary idiom used by other reflection downgrade rules would always evaluate to false — semantically wrong. getPrototype() (available long before) throws ReflectionException exactly when there is no prototype, which is precisely what hasPrototype() reports, so it is a faithful substitute.

The result must be usable in any expression context (if (...), assignment, argument), but try/catch is a statement — hence the immediately invoked closure. The receiver is passed as an argument instead of captured via use, so a complex receiver expression keeps working without a separate variable.

Chained downgrades below 8.0 are already covered: the emitted non-capturing catch (\ReflectionException) is lowered by the existing DowngradeNonCapturingCatchesRector.

Checklist

  • How was this tested:
    • Unit tests added

Fixtures cover the plain return case, the if ($m->hasPrototype()) { $m = $m->getPrototype(); } pattern, and two skip cases (non-ReflectionMethod receiver, first-class callable). Locally green: PHPUnit, PHPStan, ECS, rector process --dry-run, class-leak.

ReflectionMethod::hasPrototype() was added in PHP 8.2 and cannot be polyfilled, since the method simply does not exist below 8.2. Emulate it by calling getPrototype() inside a try/catch: it returns the prototype on 8.2- when one exists and throws ReflectionException otherwise. Because the replacement needs statements, wrap it in an immediately invoked closure so the call can be downgraded in any expression context; the receiver is passed as an argument rather than captured, so complex receiver expressions keep working.

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant