Skip to content

[Php80] Skip promoting a property the parent declares without a native type - #8232

Merged
TomasVotruba merged 1 commit into
rectorphp:mainfrom
TomasVotruba:skip-promotion-of-untyped-parent-property
Jul 30, 2026
Merged

[Php80] Skip promoting a property the parent declares without a native type#8232
TomasVotruba merged 1 commit into
rectorphp:mainfrom
TomasVotruba:skip-promotion-of-untyped-parent-property

Conversation

@TomasVotruba

Copy link
Copy Markdown
Member

Promoting a property that a parent declares without a native type produces code PHP refuses to load.

A child may redeclare an inherited untyped property, but it may not add a type to it. Promotion moves the parameter type onto the property, so it does exactly that:

abstract class ParentClass
{
    /**
     * @var SomeModel|null
     */
    protected $model;
}

final class ChildClass extends ParentClass
{
    /**
     * @var SomeModel|null
     */
    protected $model;

    public function __construct(SomeModel $model)
    {
        $this->model = $model;
    }
}

becomes

final class ChildClass extends ParentClass
{
    public function __construct(protected SomeModel $model)
    {
    }
}

which fatals on load:

PHP Fatal error: Type of ChildClass::$model must not be defined (as in class ParentClass)

This is not caught by the existing skips: the property is typed neither in the child nor via a conflicting docblock, so nothing signals the clash — the constraint lives entirely in the ancestor.

The fix

Skip promotion when the nearest ancestor declaring the property has no native type on it:

private function shouldSkipUntypedParentProperty(ClassReflection $classReflection, string $propertyName): bool
{
    foreach ($classReflection->getParents() as $parentClassReflection) {
        if (! $parentClassReflection->hasNativeProperty($propertyName)) {
            continue;
        }

        return ! $parentClassReflection->getNativeProperty($propertyName)
            ->hasNativeType();
    }

    return false;
}

It returns on the first ancestor that declares the property, since that is the declaration the child must stay compatible with. A parent property that is natively typed keeps promoting as before — the child simply has to repeat the same type, which promotion already does.

Test

skip_untyped_parent_property.php.inc with an UntypedPropertyParent source class. Verified it reproduces the bug: with the rule reverted the fixture fails —

Failed on fixture file "skip_untyped_parent_property.php.inc"
Tests: 56, Assertions: 58, Failures: 1

— and passes with the fix: OK (56 tests, 57 assertions).

Found while running Rector over a codebase where API controllers inherit an untyped protected $model from a shared base controller; every promoted child became unloadable.

@TomasVotruba
TomasVotruba merged commit b5b0f88 into rectorphp:main Jul 30, 2026
64 of 65 checks passed
@TomasVotruba
TomasVotruba deleted the skip-promotion-of-untyped-parent-property branch July 30, 2026 20:37
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