Skip to content

Fix phpstan/phpstan#14063: Readonly property modification through clone() is not reported outside allowed scope#5432

Open
phpstan-bot wants to merge 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-a40ev9e
Open

Fix phpstan/phpstan#14063: Readonly property modification through clone() is not reported outside allowed scope#5432
phpstan-bot wants to merge 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-a40ev9e

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When using clone($obj, ['value' => 'newVal']) on a readonly class from outside the declaring class scope, PHPStan was not reporting an error even though PHP 8.5 throws a fatal error at runtime: "Cannot modify protected(set) readonly property".

Changes

  • Added regression test data file tests/PHPStan/Rules/Properties/data/bug-14063.php with readonly class scenarios for clone-with
  • Added testBug14063() to AccessPropertiesInAssignRuleTest.php verifying the "Assign to protected(set) property" error is reported when modifying readonly class properties via clone() from outside the declaring class
  • Added testBug14063() to ReadOnlyPropertyAssignRuleTest.php verifying no duplicate errors from the readonly property assign rule

Root cause

The bug was in BetterReflection (updated from 6.65 to 6.70 in commit b4c5164). BetterReflection's computeModifiers() method in ReflectionProperty had logic to set the IS_PROTECTED_SET_COMPATIBILITY flag for public readonly properties (making them behave as protected(set) for write visibility checks). However, the check only considered the IS_READONLY_COMPATIBILITY modifier on the property itself, not the getDeclaringClass()->isReadOnly() case. This meant properties in a readonly class that don't have explicit readonly on the property (e.g., constructor-promoted properties like public string $value in a readonly class) were not treated as protected(set), causing AccessPropertiesCheck::canWriteProperty() to incorrectly return true from outside the class scope.

BetterReflection 6.70 added the $this->getDeclaringClass()->isReadOnly() check, which correctly handles readonly class properties.

Test

The regression test covers:

  • final readonly class with constructor-promoted property — clone from global scope (should error)
  • readonly class with constructor-promoted property — clone from function scope (should error)
  • Clone from within the declaring class (should be allowed)
  • Clone from a child class (should be allowed for protected(set))

Fixes phpstan/phpstan#14063

- New test in tests/PHPStan/Rules/Properties/data/bug-14063.php covering readonly class with clone() property overrides
- Tests AccessPropertiesInAssignRule correctly reports "Assign to protected(set) property" when modifying readonly class properties via clone() from outside the declaring class
- Tests ReadOnlyPropertyAssignRule does not double-report (defers to AccessPropertiesInAssignRule)
- The underlying fix was in BetterReflection 6.70 which correctly sets isProtectedSet() for properties in readonly classes

Closes phpstan/phpstan#14063
}

#[RequiresPhp('>= 8.5')]
public function testBug14063(): void
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.

Remove test from ReadOnlyPropertyAssignRuleTest.php‎

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Removed the testBug14063() method from ReadOnlyPropertyAssignRuleTest.php as requested by the reviewer. The test was asserting zero errors, which adds no value since that rule isn't expected to handle this case. Tests and static analysis both pass.

The test was asserting no errors from ReadOnlyPropertyAssignRule, which
adds no value since the rule is not expected to handle this case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

Non-readonly class Bug14063\Child cannot extend readonly class Bug14063\Base

Make Child class readonly since PHP does not allow a non-readonly
class to extend a readonly class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Tests background task also completed successfully (11780 tests passing). Both checks confirmed — the fix has been committed and pushed.

@VincentLanglet VincentLanglet requested a review from staabm April 8, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants