Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[PropertyAccess] Reset object property to either null or uninitialized state#44880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
filiplikavcan wants to merge2 commits intosymfony:7.4
base:7.4
Choose a base branch
Loading
frombratiask:6.1
Open
Show file tree
Hide file tree
Changes fromall 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
91 changes: 85 additions & 6 deletionssrc/Symfony/Component/PropertyAccess/PropertyAccessor.php
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyNullableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyReadInfo;
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
Expand All@@ -44,6 +45,8 @@ class PropertyAccessor implements PropertyAccessorInterface
public const MAGIC_GET = ReflectionExtractor::ALLOW_MAGIC_GET;
/** @var int Allow magic __set methods */
public const MAGIC_SET = ReflectionExtractor::ALLOW_MAGIC_SET;
/** @var int Allow magic __set methods */
public const MAGIC_UNSET = ReflectionExtractor::ALLOW_MAGIC_UNSET;
/** @var int Allow magic __call methods */
public const MAGIC_CALL = ReflectionExtractor::ALLOW_MAGIC_CALL;

Expand All@@ -58,6 +61,13 @@ class PropertyAccessor implements PropertyAccessorInterface
private const CACHE_PREFIX_WRITE = 'w';
private const CACHE_PREFIX_PROPERTY_PATH = 'p';

private const RESET_WITH_NOTHING = 0;
private const RESET_WITH_UNSET = 1;
private const RESET_WITH_NULL = 2;

private const PROPERTY_NULLABLE = ReflectionExtractor::PROPERTY_NULLABLE;
private const SETTER_NULLABLE = ReflectionExtractor::SETTER_NULLABLE;

private $magicMethodsFlags;
private $ignoreInvalidIndices;
private $ignoreInvalidProperty;
Expand DownExpand Up@@ -516,12 +526,14 @@ private function writeProperty(array $zval, string $property, mixed $value)
if (PropertyWriteInfo::TYPE_NONE !== $mutator->getType()) {
$type = $mutator->getType();

if (PropertyWriteInfo::TYPE_METHOD === $type) {
$object->{$mutator->getName()}($value);
} elseif (PropertyWriteInfo::TYPE_PROPERTY === $type) {
$object->{$mutator->getName()} = $value;
} elseif (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $type) {
$this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo());
if (!(null === $value && $this->resetProperty($object, $property, $type))) {
if (PropertyWriteInfo::TYPE_METHOD === $type) {
$object->{$mutator->getName()}($value);
} elseif (PropertyWriteInfo::TYPE_PROPERTY === $type) {
$object->{$mutator->getName()} = $value;
} elseif (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $type) {
$this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo());
}
}
} elseif ($object instanceof \stdClass && property_exists($object, $property)) {
$object->$property = $value;
Expand All@@ -534,6 +546,73 @@ private function writeProperty(array $zval, string $property, mixed $value)
}
}

private function resetProperty(object $object, string $property, string $type): bool
{
switch ($this->getResetMode($object::class, $property, $type)) {
case self::RESET_WITH_NULL:
try {
$propertyRefl = new \ReflectionProperty($object::class, $property);
$propertyRefl->setAccessible(true);
$propertyRefl->setValue($object, null);
}
catch (\ReflectionException $e) {
return false;
}
break;
case self::RESET_WITH_UNSET:
unset($object->$property);
break;
default:
return false;
}

return true;
}

private function getResetMode(string $class, string $property, string $type): int
{
if (false === (bool) ($this->magicMethodsFlags & self::MAGIC_UNSET)) {
return self::RESET_WITH_NOTHING;
}

switch ($type) {
case PropertyWriteInfo::TYPE_METHOD:
case PropertyWriteInfo::TYPE_PROPERTY:
// intentionally blank
break;
default:
return self::RESET_WITH_NOTHING;
}

if (!$this->writeInfoExtractor instanceof PropertyNullableExtractorInterface) {
return self::RESET_WITH_NOTHING;
}

$nullableInfo = $this->writeInfoExtractor->getNullableInfo($class, $property);

// property & setter nullable
if ($nullableInfo === (self::PROPERTY_NULLABLE | self::SETTER_NULLABLE)) {
return self::RESET_WITH_NOTHING;
}

// property nullable & setter not nullable
if (true === (bool) ($nullableInfo & self::PROPERTY_NULLABLE)) {
return self::RESET_WITH_NULL;
}

if (PropertyWriteInfo::TYPE_METHOD === $type) {
try {
if (false === (new \ReflectionClass($class))->hasMethod('__unset')) {
return self::RESET_WITH_NOTHING;
}
} catch (\ReflectionException $e) {
return self::RESET_WITH_NOTHING;
}
}

return self::RESET_WITH_UNSET;
}

/**
* Adjusts a collection-valued property by calling add*() and remove*() methods.
*/
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,6 +100,18 @@ public function enableMagicSet(): static
return $this;
}

/**
* Enables the use of "__unset" by the PropertyAccessor.
*
* @return $this
*/
public function enableMagicUnset(): static
{
$this->magicMethods |= PropertyAccessor::MAGIC_UNSET;

return $this;
}

/**
* Disables the use of "__call" by the PropertyAccessor.
*
Expand DownExpand Up@@ -136,6 +148,18 @@ public function disableMagicSet(): static
return $this;
}

/**
* Disables the use of "__unset" by the PropertyAccessor.
*
* @return $this
*/
public function disableMagicUnset(): static
{
$this->magicMethods &= ~PropertyAccessor::MAGIC_UNSET;

return $this;
}

/**
* @return bool whether the use of "__call" by the PropertyAccessor is enabled
*/
Expand All@@ -160,6 +184,14 @@ public function isMagicSetEnabled(): bool
return $this->magicMethods & PropertyAccessor::MAGIC_SET;
}

/**
* @return bool whether the use of "__unset" by the PropertyAccessor is enabled
*/
public function isMagicUnsetEnabled(): bool
{
return $this->magicMethods & PropertyAccessor::MAGIC_UNSET;
}

/**
* Enables exceptions when reading a non-existing index.
*
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
<?php

namespace Symfony\Component\PropertyAccess\Tests\Constraint;

use PHPUnit\Framework\Constraint\Constraint;

final class ObjectHasInitializedProperty extends Constraint
{
private string $attributeName;

public function __construct(string $attributeName)
{
$this->attributeName = $attributeName;
}

public function toString(): string
{
return sprintf(
'has initialized attribute "%s"',
$this->attributeName
);
}

protected function matches($other): bool
{
$propertyRefl = new \ReflectionProperty($other::class, $this->attributeName);
$propertyRefl->setAccessible(true);
return $propertyRefl->isInitialized($other);

// return strpos(var_export($other, true), "'{$this->attributeName}' =>") !== false;
// @codeCoverageIgnoreEnd
}

protected function failureDescription($other): string
{
return sprintf(
'object %s',
$this->toString()
);
}

protected function attributeName(): string
{
return $this->attributeName;
}
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,11 @@

namespace Symfony\Component\PropertyAccess\Tests;


use PHPUnit\Framework\Constraint\LogicalNot;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\PropertyAccess\Tests\Constraint\ObjectHasInitializedProperty;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
Expand DownExpand Up@@ -590,6 +593,143 @@ public function testThrowTypeErrorWithNullArgument()
$this->propertyAccessor->setValue($object, 'date', null);
}

/**
* @dataProvider getNullablePropertyData
*/
public function testSetPropertyToNullWithMagicUnsetAllowedWithoutUnsetMethod(string $property, string $setter)
{
$object = eval(sprintf('return new class() {
private %s;
public function setA(%s) { $this->a = $a; }
public function getA() { return $this->a; }
};',
$property,
$setter
));

$propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_UNSET);

if (strpos($property, 'int') === 0) {
$this->expectException(\TypeError::class);
$this->expectErrorMessageMatches('/Cannot assign null to property .*::\$a of type int/');
$propertyAccessor->setValue($object, 'a', null);
}
else {
$propertyAccessor->setValue($object, 'a', null);
$this->assertNull($propertyAccessor->getValue($object, 'a'));
}
}

/**
* @dataProvider getNullablePropertyData
*/
public function testSetPropertyToNullWithMagicUnsetAllowed(string $property, string $setter)
{
$propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_UNSET);

$objectWithPrivate = eval(sprintf('return new class() {
private %s;
public function setA(%s) { $this->a = $a; }
public function getA() { return $this->a; }
public function __unset($name) { unset($this->$name); }
};',
$property,
$setter
));

$objectWithPublic = eval(sprintf('return new class() {
public %s;
};',
$property
));

$propertyAccessor->setValue($objectWithPrivate, 'a', null);
$propertyAccessor->setValue($objectWithPublic, 'a', null);

if (strpos($property, 'int') === 0) {
$this->assertThat($objectWithPrivate, new LogicalNot(new ObjectHasInitializedProperty('a')));
$this->assertThat($objectWithPublic, new LogicalNot(new ObjectHasInitializedProperty('a')));
}
else {
$this->assertNull($propertyAccessor->getValue($objectWithPrivate, 'a'));
$this->assertNull($propertyAccessor->getValue($objectWithPublic, 'a'));
}
}

/**
* @dataProvider getNullablePropertyData
*/
public function testSetPrivatePropertyToNullWithoutMagicUnsetAllowed(string $property, string $setter)
{
$object = eval(sprintf('return new class() {
private %s;
public function setA(%s) { $this->a = $a; }
public function getA() { return $this->a; }
};',
$property,
$setter
));

if (strpos($setter, 'int') === 0) {
$this->expectException(\TypeError::class);
$this->expectErrorMessageMatches('/Argument #1 \(\$a\) must be of type int, null given/');
$this->propertyAccessor->setValue($object, 'a', null);
}
elseif (strpos($property, 'int') === 0) {
$this->expectException(\TypeError::class);
$this->expectErrorMessageMatches('/Cannot assign null to property .*::\$a of type int/');
$this->propertyAccessor->setValue($object, 'a', null);
}
else {
$this->propertyAccessor->setValue($object, 'a', null);
$this->assertNull($this->propertyAccessor->getValue($object, 'a'));
}
}

/**
* @dataProvider getNullablePropertyData
*/
public function testSetPublicPropertyToNullWithoutMagicUnsetAllowed(string $property, string $setter)
{
$object = eval(sprintf('return new class() {
public %s;
};',
$property
));

if (strpos($property, 'int') === 0) {
$this->expectException(\TypeError::class);
$this->expectErrorMessageMatches('/Cannot assign null to property .*::\$a of type int/');
$this->propertyAccessor->setValue($object, 'a', null);
}
else {
$this->propertyAccessor->setValue($object, 'a', null);
$this->assertNull($this->propertyAccessor->getValue($object, 'a'));
}
}

public function getNullablePropertyData()
{
return [
'typed property' => ['int $a', '$a'],
'nullable setter, typed property' => ['int $a', '?int $a'],
'typed property, default value' => ['int $a = 1', '$a'],
'nullable setter, typed property, default value' => ['int $a = 1', '?int $a'],
'no types' => ['$a', '$a'],
'typed setter' => ['$a', 'int $a'],
'nullable property' => ['?int $a', '$a'],
'nullable setter' => ['$a', '?int $a'],
'nullable property, typed setter' => ['?int $a', 'int $a'],
'nullable property, nullable setter' => ['?int $a', '?int $a'],
'no types, default value' => ['$a = 1', '$a'],
'typed setter, default value' => ['$a = 1', 'int $a'],
'nullable property, default value' => ['?int $a = 1', '$a'],
'nullable setter, default value' => ['$a = 1', '?int $a'],
'nullable property, typed setter, default value' => ['?int $a = 1', 'int $a'],
'nullable property, nullable setter, default value' => ['?int $a = 1', '?int $a'],
];
}

public function testSetTypeHint()
{
$date = new \DateTime();
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp