[PropertyInfo][Form] Add ReflectionExtractor::getTypeFromWriteTarget() and use it for the "empty_data" guess - #66040
Open
nicolas-grekas wants to merge 1 commit into
Open
Conversation
nicolas-grekas
requested review from
dunglas,
xabbuh and
yceruto
as code owners
September 13, 2026 14:19
…) and use it for the "empty_data" guess
nicolas-grekas
force-pushed
the
property-info-write-type
branch
from
September 13, 2026 15:20
73b435d to
9489898
Compare
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.
Follow-up to #66026 and to @yceruto's review there.
The gap
PropertyInfo splits access into read and write:
getReadInfo()andgetWriteInfo()each name the member a value goes through. Types are not split.getType()consults#[WithAccessors], then a mutator, then an accessor, then the constructor, then the property, and returns the first hit. That single answer merges three questions: what a value can be written through, what is read back, and what the constructor takes.Form needs the first one for the
empty_dataguess. @yceruto's review on #66026 showed the merge in action: apublic ?int $qty = nullwith a__construct(int $qty = 0)reportedint, so the guess wrote0wherenullwas accepted. The fix in #66026 calledgetWriteInfo()and then redid by hand what the extractor had already done internally and discarded: it reflected the named method or property again, checked the visibility, resolved thesethook and read the native type.Probe on this branch:
Written through
PropertyAccessor, both properties acceptnull.getType()reports the accessor's and the constructor's type instead.PropertyInfo
ReflectionExtractorgets one public method, next togetTypeFromConstructor():It returns the type of the target that
getWriteInfo()resolves for the same$context: the first parameter of a mutator method, or the property itself, through the parameter of itssethook when it has one. It returnsnullwhen there is no publicly writable target, or when the target declares no type. Two defaults differ fromgetWriteInfo():enable_getter_setter_extractiondefaults totrueandenable_adder_remover_extractiontofalse, which is whatPropertyAccessorpasses for a scalar write; an adder and remover pair yieldsnull.enable_constructor_extractionhas no effect: a constructor is not a write target. A target reached through__set()or__call()yieldsnull, since the magic method carries no type for the property.No interface is extracted. The method has one consumer,
FormFactory, which already instantiatesReflectionExtractoritself and never goes through theproperty_infocomposite. An interface, a tag and a cache layer can be added when a second consumer exists.Form
FormFactory::getWriteTargetType()is replaced by one call togetTypeFromWriteTarget().addEmptyDataGuess()matches on the returnedType: not aBuiltinType, or nullable, means no guess; otherwisestringgives'',intandfloatgive'0',boolgivesfalse.FormFactory.phpgoes from 256 to 214 lines (+9/-51). Every test inEmptyDataGuessingTestpasses unchanged.Form consumes
Typedirectly, sosymfony/type-infois back in itsrequire, with the constraintsymfony/property-infouses.symfony/property-infois bumped to^8.2, where the method exists.Not in this PR
The constructor branch of
getType()is a 9.0 topic. In theproperty_infocomposite it is already shadowed:ConstructorExtractoris tagged as a type extractor at priority -999 andReflectionExtractorat -1002, so the branch only serves standalone users ofReflectionExtractor.The constructor branch of
getWriteInfo(), which returnsPropertyWriteInfo::TYPE_CONSTRUCTOR, is not deprecated either. Nothing in the tree compares against that constant, andPropertyAccessoropts out of it, but the Serializer relies on it: without a class metadata factory,AbstractNormalizer::instantiateObject()asksisAllowedAttribute()for each constructor parameter, which reachesgetWriteInfo()with the flag at its default, and that is what makes a constructor-only parameter an allowed attribute. Deprecating the branch needs that call site to switch toisInitializable()first.The Serializer is untouched.
AbstractObjectNormalizer::denormalizeParameter()wants a constructor-scoped type, a different scope from the write type, and its attribute path depends on the phpdoc collection types that a reflection-only write type would drop.Checks
./phpunit src/Symfony/Component/PropertyInfo: 725 tests, 1183 assertions, green (703 tests on the base). The new provider has 22 cases: setter over property, nullable setter, property only,sethook, getter only, constructor argument with and withoutenable_constructor_extraction, adder and remover with and withoutenable_adder_remover_extraction,#[WithAccessors(setter:)],private(set),protected(set), virtual withoutsethook, readonly, non-public setter, private property,__set, private property behind__set,__call, private setter behind__call, unknown property, unknown class. All 22 error with "Call to undefined method" when the method is removed../phpunit src/Symfony/Component/Form: 5193 tests, 10927 assertions, 384 skipped, 8 incomplete, identical to the base.src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection: 334 tests;src/Symfony/Bridge/Doctrine/Tests/Form: 143;src/Symfony/Component/PropertyAccess: 526;src/Symfony/Component/Serializer: 1660; all identical to the base.php-cs-fixer --dry-run: clean on the touched files.Documentation
ReflectionExtractor::getTypeFromWriteTarget()next togetType()andgetTypeFromConstructor()in the PropertyInfo reference, with the difference shown by the example above.