Close the two routes around the property guard - #45
Merged
Conversation
`guardPropertyAccess()` is `private`, but the four magic methods that reach it were not `final`: a subclass overriding one to give itself magic properties dropped the guard with no diagnostic, and `$this->errors = [...]` went back to landing where nothing reads it. `init()` was the same shape — `protected` so `FileList::__construct()` can call it, but an override that skipped the parent left `guardAgainstReplacedMembers()` unrun and `isValid()` with no snapshot to reset to. Neither method existed in 3.1.0, so this is not a break to announce; the facts go where 4.0.0 already describes them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
guardPropertyAccess()isprivate, but the four magic methods that reach it were notfinal. A subclass overriding one for an ordinary reason of its own — a property bag, a lazy accessor — dropped the guard silently and restored the 3.x bypass:Before this PR that class loads,
reject()raises no diagnostic at all,getErrors()answers with none, andanyFailed()answerstrue— which is the collection reporting a failure that nothing can read.guardAgainstReplacedMembers()does not cover it: it catches a declared property, and a magic-property scheme declares nothing.init()is the same shape. It isprotectedonly soFileList::__construct()can call it, and its docblock already said it is not an extension seam — an override that skips the parent leavesguardAgainstReplacedMembers()unrun andisValid()with no snapshot to reset to, both without a word.finalon all five makes each a fatal error when the subclass loads, for the reasonisValid(),upload()anduploadValid()are alreadyfinal: the check does not stop running quietly.The cost, which is the part worth arguing about.
Fileis now a class no subclass can give magic properties to at all. I think that is right —guardPropertyAccess()already throws for every nameFiledeclares, so any such scheme was full of holes it could not see — but it is a judgement call, not a free win.No breaking-change entry. Neither
init()nor the four magic methods exists in3.1.0, so there is nothing here to report as a regression. The facts go where 4.0.0 already describes each method: the$errors/$constructorErrorsbullet inCHANGELOG.md, and theFile::init()signature entry.UPGRADE.mdgets a paragraph beside theSize::scale()one rather than a table row, since that table's left column is "if you overrode…" and nobody overrode these.Testing instructions
testAMethodThatIsNotASeamIsFinal()covers the five, with the reason per method in the assertion message.testTheEntryPointsCannotBeOverridden()is untouched — its fact is distinct and already shipped.Both holes reproduce on
mainand are fatal at load here:Locally: 724 tests / 1457 assertions with the one expected skip, plus
composer lint,composer phpstanandcomposer check-syntax.composer i18n:potregenerates byte-identical.🤖 Generated with Claude Code