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

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 9 additions & 51 deletions src/Symfony/Component/Form/FormFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
use Symfony\Component\Form\Flow\FormFlowInterface;
use Symfony\Component\Form\Flow\FormFlowTypeInterface;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\TypeIdentifier;

class FormFactory implements FormFactoryInterface
{
private ?ReflectionExtractor $writeInfoExtractor = null;
private ?ReflectionExtractor $reflectionExtractor = null;

public function __construct(
private FormRegistryInterface $registry,
Expand Down Expand Up @@ -149,16 +150,16 @@ private function addEmptyDataGuess(string $class, string $property, string $type
return $options;
}

$writeTargetType = $this->getWriteTargetType($class, $property);
$writeTargetType = ($this->reflectionExtractor ??= new ReflectionExtractor())->getTypeFromWriteTarget($class, $property);

if (!$writeTargetType instanceof \ReflectionNamedType || $writeTargetType->allowsNull()) {
if (!$writeTargetType instanceof BuiltinType || $writeTargetType->isNullable()) {
return $options;
}

$emptyData = match ($writeTargetType->getName()) {
'string' => '',
'int', 'float' => '0',
'bool' => false,
$emptyData = match ($writeTargetType->getTypeIdentifier()) {
TypeIdentifier::STRING => '',
TypeIdentifier::INT, TypeIdentifier::FLOAT => '0',
TypeIdentifier::BOOL => false,
default => null,
};

Expand All @@ -169,49 +170,6 @@ private function addEmptyDataGuess(string $class, string $property, string $type
return $options;
}

/**
* Resolves the type of the target that PropertyAccessor writes the mapped value to.
*
* Only a publicly writable mutator method or property is a write target. The type of an accessor
* or of a constructor argument says nothing about what the value is written through.
*/
private function getWriteTargetType(string $class, string $property): ?\ReflectionType
{
$this->writeInfoExtractor ??= new ReflectionExtractor();

$writeInfo = $this->writeInfoExtractor->getWriteInfo($class, $property, [
'enable_getter_setter_extraction' => true,
'enable_constructor_extraction' => false,
'enable_adder_remover_extraction' => false,
]);

if (null === $writeInfo) {
return null;
}

try {
// PropertyWriteInfo names the target without describing it, so the target is reflected here
$target = match ($writeInfo->getType()) {
PropertyWriteInfo::TYPE_METHOD => (new \ReflectionMethod($class, $writeInfo->getName()))->getParameters()[0] ?? null,
PropertyWriteInfo::TYPE_PROPERTY => new \ReflectionProperty($class, $writeInfo->getName()),
default => null,
};
} catch (\ReflectionException) {
return null;
}

// the visibility is carried by the write info only, the target does not tell whether it can be written to
if (null === $target || PropertyWriteInfo::VISIBILITY_PUBLIC !== $writeInfo->getVisibility()) {
return null;
}

if ($target instanceof \ReflectionProperty) {
$target = $target->getHook(\PropertyHookType::Set)?->getParameters()[0] ?? $target;
}

return $target->getType();
}

/**
* Tells whether the type maps a scalar to a single control, so that a scalar "empty_data" is meaningful for it.
*/
Expand Down
3 changes: 2 additions & 1 deletion src/Symfony/Component/Form/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
"symfony/polyfill-intl-icu": "^1.21",
"symfony/polyfill-mbstring": "^1.0",
"symfony/property-access": "^7.4|^8.0",
"symfony/property-info": "^7.4|^8.0",
"symfony/property-info": "^8.2",
"symfony/service-contracts": "^2.5|^3",
"symfony/type-info": "^7.4.7|^8.0.7",
"symfony/var-exporter": "^8.1"
},
"require-dev": {
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/PropertyInfo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
8.2
---

* Add `ReflectionExtractor::getTypeFromWriteTarget()` to get the type of the target a property is written through
* Add `PropertyInfoBundle`, which provides the `property_info` configuration and the services previously provided by `FrameworkBundle` under `framework.property_info`
* Allow defining accessors and mutators via a `#[WithAccessors]` attribute
* Gather data from property hooks in ReflectionExtractor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,55 @@ public function getTypeFromConstructor(string $class, string $property): ?Type
}
}

/**
* Gets 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.
*
* "enable_getter_setter_extraction" defaults to true and "enable_adder_remover_extraction" to false; an adder
* and remover pair yields null. A constructor is not a write target, so "enable_constructor_extraction" has no effect.
*
* @return Type|null The type, or null when there is no publicly writable target or when it declares no type
*/
public function getTypeFromWriteTarget(string $class, string $property, array $context = []): ?Type
{
$context['enable_getter_setter_extraction'] ??= true;
$context['enable_adder_remover_extraction'] ??= false;
$context['enable_constructor_extraction'] = false;

$writeInfo = $this->getWriteInfo($class, $property, $context);

if (null === $writeInfo || !\in_array($writeInfo->getType(), [PropertyWriteInfo::TYPE_METHOD, PropertyWriteInfo::TYPE_PROPERTY], true) || PropertyWriteInfo::VISIBILITY_PUBLIC !== $writeInfo->getVisibility()) {
return null;
}

$refClass = new \ReflectionClass($class);
$name = $writeInfo->getName();

if (PropertyWriteInfo::TYPE_METHOD === $writeInfo->getType()) {
// the named method is missing or not callable with one argument when the write goes through __call()
if (!$this->isMethodAccessible($refClass, $name, 1)[0]) {
return null;
}

$target = $refClass->getMethod($name)->getParameters()[0];
} else {
// the named property is missing or not allowed when the write goes through __set()
if (!$refClass->hasProperty($name) || !(($target = $refClass->getProperty($name))->getModifiers() & $this->propertyReflectionFlags)) {
return null;
}

if ($writeInfo->hasHook()) {
$target = $target->getHook(\PropertyHookType::Set)->getParameters()[0];
}
}

try {
return $this->typeResolver->resolve($target);
} catch (UnsupportedException) {
return null;
}
}

private function getReflectionParameterFromConstructor(string $property, \ReflectionMethod $reflectionConstructor): ?\ReflectionParameter
{
foreach ($reflectionConstructor->getParameters() as $reflectionParameter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
use Symfony\Component\PropertyInfo\Tests\Fixtures\WithAccessors\InvalidMapping;
use Symfony\Component\PropertyInfo\Tests\Fixtures\WithAccessors\JustAdderAndRemover;
use Symfony\Component\PropertyInfo\Tests\Fixtures\WithAccessors\JustGetterOrSetter;
use Symfony\Component\PropertyInfo\Tests\Fixtures\WriteTypeDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\WriteTypeMagicDummy;
use Symfony\Component\TypeInfo\Type;

/**
Expand Down Expand Up @@ -812,6 +814,38 @@ public function testMappingExceptionOnInvalidAccessorMethod()
$extractor->isReadable(InvalidMapping::class, 'prop');
}

#[DataProvider('provideWriteTargetTypes')]
public function testGetTypeFromWriteTarget(string $class, string $property, ?Type $expected, array $context = [])
{
$this->assertEquals($expected, $this->extractor->getTypeFromWriteTarget($class, $property, $context));
}

public static function provideWriteTargetTypes(): iterable
{
yield 'setter over the property' => [WriteTypeDummy::class, 'setterWins', Type::string()];
yield 'nullable setter over a non-nullable property' => [WriteTypeDummy::class, 'nullableSetter', Type::nullable(Type::string())];
yield 'property without a setter' => [WriteTypeDummy::class, 'propertyOnly', Type::int()];
yield 'set hook' => [WriteTypeDummy::class, 'hooked', Type::nullable(Type::string())];
yield 'getter only' => [WriteTypeDummy::class, 'getterOnly', Type::nullable(Type::string())];
yield 'constructor argument' => [WriteTypeDummy::class, 'constructed', Type::nullable(Type::int())];
yield 'constructor argument with constructor extraction enabled' => [WriteTypeDummy::class, 'constructed', Type::nullable(Type::int()), ['enable_constructor_extraction' => true]];
yield 'adder and remover with a public property' => [WriteTypeDummy::class, 'items', Type::array()];
yield 'adder and remover extraction enabled' => [WriteTypeDummy::class, 'items', null, ['enable_adder_remover_extraction' => true]];
yield 'setter named by the attribute' => [WriteTypeDummy::class, 'named', Type::nullable(Type::string())];
yield 'private(set) property' => [WriteTypeDummy::class, 'privateSet', null];
yield 'protected(set) property' => [WriteTypeDummy::class, 'protectedSet', null];
yield 'virtual property without a set hook' => [WriteTypeDummy::class, 'virtual', null];
yield 'readonly property' => [WriteTypeDummy::class, 'readonly', null];
yield 'non-public setter' => [WriteTypeDummy::class, 'privateSetter', null];
yield 'private property' => [WriteTypeDummy::class, 'plainPrivate', null];
yield 'magic __set' => [WriteTypeMagicDummy::class, 'anything', null];
yield 'private property behind a magic __set' => [WriteTypeMagicDummy::class, 'shadowed', null];
yield 'magic __call' => [WriteTypeMagicDummy::class, 'anything', null, ['enable_magic_methods_extraction' => ReflectionExtractor::ALLOW_MAGIC_CALL]];
yield 'private setter behind a magic __call' => [WriteTypeMagicDummy::class, 'guarded', null, ['enable_magic_methods_extraction' => ReflectionExtractor::ALLOW_MAGIC_CALL]];
yield 'unknown property' => [WriteTypeDummy::class, 'unknown', null];
yield 'unknown class' => ['Symfony\Component\PropertyInfo\Tests\Fixtures\DoesNotExist', 'foo', null];
}

public function testGetWriteInfoReadonlyProperties()
{
$writeMutatorConstructor = $this->extractor->getWriteInfo(Php81Dummy::class, 'foo', ['enable_constructor_extraction' => true]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\PropertyInfo\Tests\Fixtures;

use Symfony\Component\PropertyInfo\Attribute\WithAccessors;

class WriteTypeDummy
{
public int $propertyOnly = 0;
public string $nullableSetter = '';
public ?string $getterOnly = null;
public ?int $constructed = null;
public array $items = [];

public string $hooked = '' {
set(?string $value) => $value ?? '';
}

public string $virtual {
get => $this->propertyOnly ? 'yes' : 'no';
}

public private(set) string $privateSet = '';
public protected(set) string $protectedSet = '';
public readonly string $readonly;

private ?string $setterWins = null;
private string $privateSetter = '';
private string $plainPrivate = '';

#[WithAccessors(setter: 'rename')]
private string $named = '';

public function __construct(int $constructed = 0)
{
$this->constructed = $constructed;
$this->readonly = 'fixed';
}

public function setSetterWins(string $setterWins): void
{
$this->setterWins = $setterWins;
}

public function setNullableSetter(?string $nullableSetter): void
{
$this->nullableSetter = $nullableSetter ?? '';
}

public function getGetterOnly(): string
{
return $this->getterOnly ?? '';
}

public function addItem(string $item): void
{
$this->items[] = $item;
}

public function removeItem(string $item): void
{
$this->items = array_diff($this->items, [$item]);
}

public function rename(?string $named): void
{
$this->named = $named ?? '';
}

public function setNamed(int $named): void
{
$this->named = (string) $named;
}

private function setPrivateSetter(string $privateSetter): void
{
$this->privateSetter = $privateSetter;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\PropertyInfo\Tests\Fixtures;

class WriteTypeMagicDummy
{
private string $shadowed = '';
private string $guarded = '';

public function __set(string $name, mixed $value): void
{
}

public function __call(string $name, array $arguments): mixed
{
return null;
}

private function setGuarded(string $guarded): void
{
$this->guarded = $guarded;
}
}
Loading