Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[PropertyInfo][Form] Add ReflectionExtractor::getTypeFromWriteTarget() and use it for the "empty_data" guess - #66040

Open
nicolas-grekas wants to merge 1 commit into
symfony:8.2from
nicolas-grekas:property-info-write-type
Open

[PropertyInfo][Form] Add ReflectionExtractor::getTypeFromWriteTarget() and use it for the "empty_data" guess#66040
nicolas-grekas wants to merge 1 commit into
symfony:8.2from
nicolas-grekas:property-info-write-type

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Sep 13, 2026

Copy link
Copy Markdown
Member
Q A
Branch? 8.2
Bug fix? no
New feature? yes
Deprecations? no
Issues -
License MIT

Follow-up to #66026 and to @yceruto's review there.

The gap

PropertyInfo splits access into read and write: getReadInfo() and getWriteInfo() 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_data guess. @yceruto's review on #66026 showed the merge in action: a public ?int $qty = null with a __construct(int $qty = 0) reported int, so the guess wrote 0 where null was accepted. The fix in #66026 called getWriteInfo() 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 the set hook and read the native type.

Probe on this branch:

class Article
{
    public ?string $title = null;
    public ?int $rank = null;

    public function __construct(int $rank = 0)
    {
        $this->rank = $rank;
    }

    public function getTitle(): string
    {
        return $this->title ?? '';
    }
}
title  getType(): string   getTypeFromWriteTarget(): null|string
rank   getType(): int      getTypeFromWriteTarget(): int|null

Written through PropertyAccessor, both properties accept null. getType() reports the accessor's and the constructor's type instead.

PropertyInfo

ReflectionExtractor gets one public method, next to getTypeFromConstructor():

public function getTypeFromWriteTarget(string $class, string $property, array $context = []): ?Type

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 its set hook when it has one. It returns null when there is no publicly writable target, or when the target declares no type. Two defaults differ from getWriteInfo(): enable_getter_setter_extraction defaults to true and enable_adder_remover_extraction to false, which is what PropertyAccessor passes for a scalar write; an adder and remover pair yields null. enable_constructor_extraction has no effect: a constructor is not a write target. A target reached through __set() or __call() yields null, since the magic method carries no type for the property.

No interface is extracted. The method has one consumer, FormFactory, which already instantiates ReflectionExtractor itself and never goes through the property_info composite. An interface, a tag and a cache layer can be added when a second consumer exists.

Form

FormFactory::getWriteTargetType() is replaced by one call to getTypeFromWriteTarget(). addEmptyDataGuess() matches on the returned Type: not a BuiltinType, or nullable, means no guess; otherwise string gives '', int and float give '0', bool gives false. FormFactory.php goes from 256 to 214 lines (+9/-51). Every test in EmptyDataGuessingTest passes unchanged.

Form consumes Type directly, so symfony/type-info is back in its require, with the constraint symfony/property-info uses. symfony/property-info is bumped to ^8.2, where the method exists.

Not in this PR

The constructor branch of getType() is a 9.0 topic. In the property_info composite it is already shadowed: ConstructorExtractor is tagged as a type extractor at priority -999 and ReflectionExtractor at -1002, so the branch only serves standalone users of ReflectionExtractor.

The constructor branch of getWriteInfo(), which returns PropertyWriteInfo::TYPE_CONSTRUCTOR, is not deprecated either. Nothing in the tree compares against that constant, and PropertyAccessor opts out of it, but the Serializer relies on it: without a class metadata factory, AbstractNormalizer::instantiateObject() asks isAllowedAttribute() for each constructor parameter, which reaches getWriteInfo() 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 to isInitializable() 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, set hook, getter only, constructor argument with and without enable_constructor_extraction, adder and remover with and without enable_adder_remover_extraction, #[WithAccessors(setter:)], private(set), protected(set), virtual without set hook, 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.
  • Net diff: +215/-52 (production +61/-52, tests +154/-0); the first version of this PR was +320/-58.

Documentation

  • ReflectionExtractor::getTypeFromWriteTarget() next to getType() and getTypeFromConstructor() in the PropertyInfo reference, with the difference shown by the example above.

@carsonbot carsonbot added this to the 8.2 milestone Sep 13, 2026
@carsonbot carsonbot changed the title [PropertyInfo][Form] Add PropertyWriteTypeExtractorInterface and use it for the "empty_data" guess [Form][PropertyInfo] Add PropertyWriteTypeExtractorInterface and use it for the "empty_data" guess Sep 13, 2026
@nicolas-grekas
nicolas-grekas force-pushed the property-info-write-type branch from 73b435d to 9489898 Compare September 13, 2026 15:20
@nicolas-grekas nicolas-grekas changed the title [Form][PropertyInfo] Add PropertyWriteTypeExtractorInterface and use it for the "empty_data" guess [PropertyInfo][Form] Add ReflectionExtractor::getTypeFromWriteTarget() and use it for the "empty_data" guess Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants