feature #32133 [PropertyAccess] Allow to disable magic __get & __set (ogizanagi)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[PropertyAccess] Allow to disable magic __get & __set

| Q             | A
| ------------- | ---
| Branch?       | master <!-- see below -->
| Bug fix?      | no
| New feature?  | yes <!-- please update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | yes <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes   <!-- please add some, will be required by reviewers -->
| Fixed tickets | N/A   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | TODO

Working with some legacy code having annoying `__get` & `__set` methods, this would have been useful the same way `__call` can be enabled/disabled.

Commits
-------

11b7bf316e [PropertyAccess] Allow to disable magic __get & __set
This commit is contained in:
Fabien Potencier 2020-08-19 19:12:12 +02:00
commit d6980e5f38
22 changed files with 329 additions and 45 deletions

View File

@ -17,6 +17,17 @@ Mime
* Deprecated `Address::fromString()`, use `Address::create()` instead
PropertyAccess
--------------
* Deprecated passing a boolean as the first argument of `PropertyAccessor::__construct()`.
Pass a combination of bitwise flags instead.
PropertyInfo
------------
* Dropped the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()` in favor of `enable_magic_methods_extraction`.
TwigBundle
----------

View File

@ -108,6 +108,17 @@ PhpUnitBridge
* Removed support for `@expectedDeprecation` annotations, use the `ExpectDeprecationTrait::expectDeprecation()` method instead.
PropertyAccess
--------------
* Dropped support of a boolean as the first argument of `PropertyAccessor::__construct()`.
Pass a combination of bitwise flags instead.
PropertyInfo
------------
* Dropped the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()` in favor of `enable_magic_methods_extraction`.
Routing
-------

View File

@ -949,6 +949,8 @@ class Configuration implements ConfigurationInterface
->info('Property access configuration')
->children()
->booleanNode('magic_call')->defaultFalse()->end()
->booleanNode('magic_get')->defaultTrue()->end()
->booleanNode('magic_set')->defaultTrue()->end()
->booleanNode('throw_exception_on_invalid_index')->defaultFalse()->end()
->booleanNode('throw_exception_on_invalid_property_path')->defaultTrue()->end()
->end()

View File

@ -1429,9 +1429,14 @@ class FrameworkExtension extends Extension
$loader->load('property_access.php');
$magicMethods = PropertyAccessor::DISALLOW_MAGIC_METHODS;
$magicMethods |= $config['magic_call'] ? PropertyAccessor::MAGIC_CALL : 0;
$magicMethods |= $config['magic_get'] ? PropertyAccessor::MAGIC_GET : 0;
$magicMethods |= $config['magic_set'] ? PropertyAccessor::MAGIC_SET : 0;
$container
->getDefinition('property_accessor')
->replaceArgument(0, $config['magic_call'])
->replaceArgument(0, $magicMethods)
->replaceArgument(1, $config['throw_exception_on_invalid_index'])
->replaceArgument(3, $config['throw_exception_on_invalid_property_path'])
->replaceArgument(4, new Reference(PropertyReadInfoExtractorInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE))

View File

@ -18,7 +18,7 @@ return static function (ContainerConfigurator $container) {
$container->services()
->set('property_accessor', PropertyAccessor::class)
->args([
abstract_arg('magicCall, set by the extension'),
abstract_arg('magic methods allowed, set by the extension'),
abstract_arg('throwExceptionOnInvalidIndex, set by the extension'),
service('cache.property_access')->ignoreOnInvalid(),
abstract_arg('throwExceptionOnInvalidPropertyPath, set by the extension'),

View File

@ -236,6 +236,8 @@
<xsd:complexType name="property_access">
<xsd:attribute name="magic-call" type="xsd:boolean" />
<xsd:attribute name="magic-get" type="xsd:boolean" />
<xsd:attribute name="magic-set" type="xsd:boolean" />
<xsd:attribute name="throw-exception-on-invalid-index" type="xsd:boolean" />
<xsd:attribute name="throw-exception-on-invalid-property-path" type="xsd:boolean" />
</xsd:complexType>

View File

@ -415,6 +415,8 @@ class ConfigurationTest extends TestCase
],
'property_access' => [
'magic_call' => false,
'magic_get' => true,
'magic_set' => true,
'throw_exception_on_invalid_index' => false,
'throw_exception_on_invalid_property_path' => true,
],

View File

@ -3,6 +3,8 @@
$container->loadFromExtension('framework', [
'property_access' => [
'magic_call' => true,
'magic_get' => true,
'magic_set' => false,
'throw_exception_on_invalid_index' => true,
'throw_exception_on_invalid_property_path' => false,
],

View File

@ -7,6 +7,6 @@
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:property-access magic-call="true" throw-exception-on-invalid-index="true" throw-exception-on-invalid-property-path="false"/>
<framework:property-access magic-call="true" magic-get="true" magic-set="false" throw-exception-on-invalid-index="true" throw-exception-on-invalid-property-path="false"/>
</framework:config>
</container>

View File

@ -1,5 +1,7 @@
framework:
property_access:
magic_call: true
magic_get: true
magic_set: false
throw_exception_on_invalid_index: true
throw_exception_on_invalid_property_path: false

View File

@ -86,7 +86,7 @@ abstract class FrameworkExtensionTest extends TestCase
$container = $this->createContainerFromFile('full');
$def = $container->getDefinition('property_accessor');
$this->assertFalse($def->getArgument(0));
$this->assertSame(PropertyAccessor::MAGIC_SET | PropertyAccessor::MAGIC_GET, $def->getArgument(0));
$this->assertFalse($def->getArgument(1));
$this->assertTrue($def->getArgument(3));
}
@ -95,7 +95,7 @@ abstract class FrameworkExtensionTest extends TestCase
{
$container = $this->createContainerFromFile('property_accessor');
$def = $container->getDefinition('property_accessor');
$this->assertTrue($def->getArgument(0));
$this->assertSame(PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_CALL, $def->getArgument(0));
$this->assertTrue($def->getArgument(1));
$this->assertFalse($def->getArgument(3));
}

View File

@ -83,6 +83,7 @@
"symfony/messenger": "<4.4",
"symfony/mime": "<4.4",
"symfony/property-info": "<4.4",
"symfony/property-access": "<5.2",
"symfony/serializer": "<5.2",
"symfony/stopwatch": "<4.4",
"symfony/translation": "<5.0",

View File

@ -1,6 +1,12 @@
CHANGELOG
=========
5.2.0
-----
* deprecated passing a boolean as the first argument of `PropertyAccessor::__construct()`, expecting a combination of bitwise flags instead
* added the ability to disable usage of the magic `__get` & `__set` methods
5.1.0
-----

View File

@ -38,6 +38,15 @@ use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
*/
class PropertyAccessor implements PropertyAccessorInterface
{
/** @var int Allow none of the magic methods */
public const DISALLOW_MAGIC_METHODS = ReflectionExtractor::DISALLOW_MAGIC_METHODS;
/** @var int Allow magic __get methods */
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 __call methods */
public const MAGIC_CALL = ReflectionExtractor::ALLOW_MAGIC_CALL;
private const VALUE = 0;
private const REF = 1;
private const IS_REF_CHAINED = 2;
@ -45,10 +54,7 @@ class PropertyAccessor implements PropertyAccessorInterface
private const CACHE_PREFIX_WRITE = 'w';
private const CACHE_PREFIX_PROPERTY_PATH = 'p';
/**
* @var bool
*/
private $magicCall;
private $magicMethodsFlags;
private $ignoreInvalidIndices;
private $ignoreInvalidProperty;
@ -68,7 +74,6 @@ class PropertyAccessor implements PropertyAccessorInterface
* @var PropertyWriteInfoExtractorInterface
*/
private $writeInfoExtractor;
private $readPropertyCache = [];
private $writePropertyCache = [];
private static $resultProto = [self::VALUE => null];
@ -76,10 +81,20 @@ class PropertyAccessor implements PropertyAccessorInterface
/**
* Should not be used by application code. Use
* {@link PropertyAccess::createPropertyAccessor()} instead.
*
* @param int $magicMethods A bitwise combination of the MAGIC_* constants
* to specify the allowed magic methods (__get, __set, __call)
* or self::DISALLOW_MAGIC_METHODS for none
*/
public function __construct(bool $magicCall = false, bool $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null, bool $throwExceptionOnInvalidPropertyPath = true, PropertyReadInfoExtractorInterface $readInfoExtractor = null, PropertyWriteInfoExtractorInterface $writeInfoExtractor = null)
public function __construct(/*int */$magicMethods = self::MAGIC_GET | self::MAGIC_SET, bool $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null, bool $throwExceptionOnInvalidPropertyPath = true, PropertyReadInfoExtractorInterface $readInfoExtractor = null, PropertyWriteInfoExtractorInterface $writeInfoExtractor = null)
{
$this->magicCall = $magicCall;
if (\is_bool($magicMethods)) {
trigger_deprecation('symfony/property-info', '5.2', 'Passing a boolean to "%s()" first argument is deprecated since 5.1 and expect a combination of bitwise flags instead (i.e an integer).', __METHOD__);
$magicMethods = ($magicMethods ? self::MAGIC_CALL : 0) | self::MAGIC_GET | self::MAGIC_SET;
}
$this->magicMethodsFlags = $magicMethods;
$this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex;
$this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value
$this->ignoreInvalidProperty = !$throwExceptionOnInvalidPropertyPath;
@ -472,7 +487,7 @@ class PropertyAccessor implements PropertyAccessorInterface
$accessor = $this->readInfoExtractor->getReadInfo($class, $property, [
'enable_getter_setter_extraction' => true,
'enable_magic_call_extraction' => $this->magicCall,
'enable_magic_methods_extraction' => $this->magicMethodsFlags,
'enable_constructor_extraction' => false,
]);
@ -592,7 +607,7 @@ class PropertyAccessor implements PropertyAccessorInterface
$mutator = $this->writeInfoExtractor->getWriteInfo($class, $property, [
'enable_getter_setter_extraction' => true,
'enable_magic_call_extraction' => $this->magicCall,
'enable_magic_methods_extraction' => $this->magicMethodsFlags,
'enable_constructor_extraction' => false,
'enable_adder_remover_extraction' => $useAdderAndRemover,
]);

View File

@ -22,7 +22,8 @@ use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
*/
class PropertyAccessorBuilder
{
private $magicCall = false;
/** @var int */
private $magicMethods = PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_SET;
private $throwExceptionOnInvalidIndex = false;
private $throwExceptionOnInvalidPropertyPath = true;
@ -41,6 +42,26 @@ class PropertyAccessorBuilder
*/
private $writeInfoExtractor;
/**
* Enables the use of all magic methods by the PropertyAccessor.
*/
public function enableMagicMethods(): self
{
$this->magicMethods = PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_SET | PropertyAccessor::MAGIC_CALL;
return $this;
}
/**
* Disable the use of all magic methods by the PropertyAccessor.
*/
public function disableMagicMethods(): self
{
$this->magicMethods = PropertyAccessor::DISALLOW_MAGIC_METHODS;
return $this;
}
/**
* Enables the use of "__call" by the PropertyAccessor.
*
@ -48,7 +69,27 @@ class PropertyAccessorBuilder
*/
public function enableMagicCall()
{
$this->magicCall = true;
$this->magicMethods |= PropertyAccessor::MAGIC_CALL;
return $this;
}
/**
* Enables the use of "__get" by the PropertyAccessor.
*/
public function enableMagicGet(): self
{
$this->magicMethods |= PropertyAccessor::MAGIC_GET;
return $this;
}
/**
* Enables the use of "__set" by the PropertyAccessor.
*/
public function enableMagicSet(): self
{
$this->magicMethods |= PropertyAccessor::MAGIC_SET;
return $this;
}
@ -60,7 +101,27 @@ class PropertyAccessorBuilder
*/
public function disableMagicCall()
{
$this->magicCall = false;
$this->magicMethods ^= PropertyAccessor::MAGIC_CALL;
return $this;
}
/**
* Disables the use of "__get" by the PropertyAccessor.
*/
public function disableMagicGet(): self
{
$this->magicMethods ^= PropertyAccessor::MAGIC_GET;
return $this;
}
/**
* Disables the use of "__set" by the PropertyAccessor.
*/
public function disableMagicSet(): self
{
$this->magicMethods ^= PropertyAccessor::MAGIC_SET;
return $this;
}
@ -70,7 +131,23 @@ class PropertyAccessorBuilder
*/
public function isMagicCallEnabled()
{
return $this->magicCall;
return (bool) ($this->magicMethods & PropertyAccessor::MAGIC_CALL);
}
/**
* @return bool whether the use of "__get" by the PropertyAccessor is enabled
*/
public function isMagicGetEnabled(): bool
{
return $this->magicMethods & PropertyAccessor::MAGIC_GET;
}
/**
* @return bool whether the use of "__set" by the PropertyAccessor is enabled
*/
public function isMagicSetEnabled(): bool
{
return $this->magicMethods & PropertyAccessor::MAGIC_SET;
}
/**
@ -206,6 +283,6 @@ class PropertyAccessorBuilder
*/
public function getPropertyAccessor()
{
return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool, $this->throwExceptionOnInvalidPropertyPath, $this->readInfoExtractor, $this->writeInfoExtractor);
return new PropertyAccessor($this->magicMethods, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool, $this->throwExceptionOnInvalidPropertyPath, $this->readInfoExtractor, $this->writeInfoExtractor);
}
}

View File

@ -45,7 +45,21 @@ class PropertyAccessorBuilderTest extends TestCase
$this->assertSame($this->builder, $this->builder->disableMagicCall());
}
public function testIsMagicCallEnable()
public function testTogglingMagicGet()
{
$this->assertTrue($this->builder->isMagicGetEnabled());
$this->assertFalse($this->builder->disableMagicGet()->isMagicCallEnabled());
$this->assertTrue($this->builder->enableMagicGet()->isMagicGetEnabled());
}
public function testTogglingMagicSet()
{
$this->assertTrue($this->builder->isMagicSetEnabled());
$this->assertFalse($this->builder->disableMagicSet()->isMagicSetEnabled());
$this->assertTrue($this->builder->enableMagicSet()->isMagicSetEnabled());
}
public function testTogglingMagicCall()
{
$this->assertFalse($this->builder->isMagicCallEnabled());
$this->assertTrue($this->builder->enableMagicCall()->isMagicCallEnabled());

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
@ -130,7 +131,7 @@ class PropertyAccessorTest extends TestCase
public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchIndexException');
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, true);
$this->propertyAccessor->getValue($objectOrArray, $path);
}
@ -217,6 +218,15 @@ class PropertyAccessorTest extends TestCase
$this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'magicProperty'));
}
public function testGetValueIgnoresMagicGet()
{
$this->expectException(NoSuchPropertyException::class);
$propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS);
$propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'magicProperty');
}
public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath()
{
$object = new \ArrayObject();
@ -243,7 +253,7 @@ class PropertyAccessorTest extends TestCase
public function testGetValueNotModifyObjectException()
{
$propertyAccessor = new PropertyAccessor(false, true);
$propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, true);
$object = new \stdClass();
$object->firstName = ['Bernhard'];
@ -261,17 +271,28 @@ class PropertyAccessorTest extends TestCase
$this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty');
}
public function testGetValueReadsMagicCallIfEnabled()
/**
* @group legacy
* @expectedDeprecation Since symfony/property-info 5.2: Passing a boolean to "Symfony\Component\PropertyAccess\PropertyAccessor::__construct()" first argument is deprecated since 5.1 and expect a combination of bitwise flags instead (i.e an integer).
*/
public function testLegacyGetValueReadsMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testGetValueReadsMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_SET | PropertyAccessor::MAGIC_CALL);
$this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
// https://github.com/symfony/symfony/pull/4450
public function testGetValueReadsMagicCallThatReturnsConstant()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL);
$this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'constantMagicCallProperty'));
}
@ -320,7 +341,7 @@ class PropertyAccessorTest extends TestCase
*/
public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, true);
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
@ -343,6 +364,16 @@ class PropertyAccessorTest extends TestCase
$this->assertEquals('Updated', $author->__get('magicProperty'));
}
public function testSetValueIgnoresMagicSet()
{
$this->expectException(NoSuchPropertyException::class);
$propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS);
$author = new TestClassMagicGet('Bernhard');
$propertyAccessor->setValue($author, 'magicProperty', 'Updated');
}
public function testSetValueThrowsExceptionIfThereAreMissingParameters()
{
$this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException');
@ -359,7 +390,11 @@ class PropertyAccessorTest extends TestCase
$this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated');
}
public function testSetValueUpdatesMagicCallIfEnabled()
/**
* @group legacy
* @expectedDeprecation Since symfony/property-info 5.2: Passing a boolean to "Symfony\Component\PropertyAccess\PropertyAccessor::__construct()" first argument is deprecated since 5.1 and expect a combination of bitwise flags instead (i.e an integer).
*/
public function testLegacySetValueUpdatesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
@ -370,6 +405,17 @@ class PropertyAccessorTest extends TestCase
$this->assertEquals('Updated', $author->__call('getMagicCallProperty', []));
}
public function testSetValueUpdatesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL);
$author = new TestClassMagicCall('Bernhard');
$this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated');
$this->assertEquals('Updated', $author->__call('getMagicCallProperty', []));
}
/**
* @dataProvider getPathsWithUnexpectedType
*/
@ -382,7 +428,7 @@ class PropertyAccessorTest extends TestCase
public function testGetValueWhenArrayValueIsNull()
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, true);
$this->assertNull($this->propertyAccessor->getValue(['index' => ['nullable' => null]], '[index][nullable]'));
}
@ -416,7 +462,7 @@ class PropertyAccessorTest extends TestCase
*/
public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, true);
// When exceptions are enabled, non-existing indices cannot be read
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
@ -432,13 +478,24 @@ class PropertyAccessorTest extends TestCase
$this->assertFalse($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testIsReadableRecognizesMagicCallIfEnabled()
/**
* @group legacy
* @expectedDeprecation Since symfony/property-info 5.2: Passing a boolean to "Symfony\Component\PropertyAccess\PropertyAccessor::__construct()" first argument is deprecated since 5.1 and expect a combination of bitwise flags instead (i.e an integer).
*/
public function testLegacyIsReadableRecognizesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testIsReadableRecognizesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL);
$this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
/**
* @dataProvider getPathsWithUnexpectedType
*/
@ -477,7 +534,7 @@ class PropertyAccessorTest extends TestCase
*/
public function testIsWritableReturnsTrueIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, true);
// Non-existing indices can be written even if exceptions are enabled
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path));
@ -493,13 +550,24 @@ class PropertyAccessorTest extends TestCase
$this->assertFalse($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testIsWritableRecognizesMagicCallIfEnabled()
/**
* @group legacy
* @expectedDeprecation Since symfony/property-info 5.2: Passing a boolean to "Symfony\Component\PropertyAccess\PropertyAccessor::__construct()" first argument is deprecated since 5.1 and expect a combination of bitwise flags instead (i.e an integer).
*/
public function testLegacyIsWritableRecognizesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testIsWritableRecognizesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL);
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
/**
* @dataProvider getPathsWithUnexpectedType
*/
@ -650,7 +718,7 @@ class PropertyAccessorTest extends TestCase
{
$obj = new TestClass('foo');
$propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter());
$propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, false, new ArrayAdapter());
$this->assertEquals('foo', $propertyAccessor->getValue($obj, 'publicGetSetter'));
$propertyAccessor->setValue($obj, 'publicGetSetter', 'bar');
$propertyAccessor->setValue($obj, 'publicGetSetter', 'baz');
@ -664,7 +732,7 @@ class PropertyAccessorTest extends TestCase
$obj->{'a/b'} = '1';
$obj->{'a%2Fb'} = '2';
$propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter());
$propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, false, new ArrayAdapter());
$this->assertSame('bar', $propertyAccessor->getValue($obj, '@foo'));
$this->assertSame('1', $propertyAccessor->getValue($obj, 'a/b'));
$this->assertSame('2', $propertyAccessor->getValue($obj, 'a%2Fb'));
@ -685,7 +753,7 @@ class PropertyAccessorTest extends TestCase
$obj = $this->generateAnonymousClass($value);
$propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter());
$propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, false, new ArrayAdapter());
$this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo'));
}
@ -713,7 +781,7 @@ class PropertyAccessorTest extends TestCase
$obj = $this->generateAnonymousClass('');
$propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter());
$propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, false, new ArrayAdapter());
$propertyAccessor->setValue($obj, 'foo', $value);
$this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo'));

View File

@ -17,8 +17,9 @@
],
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-php80": "^1.15",
"symfony/property-info": "^5.1.1"
"symfony/property-info": "^5.2"
},
"require-dev": {
"symfony/cache": "^4.4|^5.0"

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
5.2.0
-----
* deprecated the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()`. in favor of `enable_magic_methods_extraction`
5.1.0
-----

View File

@ -51,6 +51,15 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
public const ALLOW_PROTECTED = 2;
public const ALLOW_PUBLIC = 4;
/** @var int Allow none of the magic methods */
public const DISALLOW_MAGIC_METHODS = 0;
/** @var int Allow magic __get methods */
public const ALLOW_MAGIC_GET = 1 << 0;
/** @var int Allow magic __set methods */
public const ALLOW_MAGIC_SET = 1 << 1;
/** @var int Allow magic __call methods */
public const ALLOW_MAGIC_CALL = 1 << 2;
private const MAP_TYPES = [
'integer' => Type::BUILTIN_TYPE_INT,
'boolean' => Type::BUILTIN_TYPE_BOOL,
@ -62,6 +71,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
private $arrayMutatorPrefixes;
private $enableConstructorExtraction;
private $methodReflectionFlags;
private $magicMethodsFlags;
private $propertyReflectionFlags;
private $inflector;
@ -73,7 +83,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
* @param string[]|null $accessorPrefixes
* @param string[]|null $arrayMutatorPrefixes
*/
public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null, bool $enableConstructorExtraction = true, int $accessFlags = self::ALLOW_PUBLIC, InflectorInterface $inflector = null)
public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null, bool $enableConstructorExtraction = true, int $accessFlags = self::ALLOW_PUBLIC, InflectorInterface $inflector = null, int $magicMethodsFlags = self::ALLOW_MAGIC_GET | self::ALLOW_MAGIC_SET)
{
$this->mutatorPrefixes = null !== $mutatorPrefixes ? $mutatorPrefixes : self::$defaultMutatorPrefixes;
$this->accessorPrefixes = null !== $accessorPrefixes ? $accessorPrefixes : self::$defaultAccessorPrefixes;
@ -81,6 +91,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
$this->enableConstructorExtraction = $enableConstructorExtraction;
$this->methodReflectionFlags = $this->getMethodsFlags($accessFlags);
$this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags);
$this->magicMethodsFlags = $magicMethodsFlags;
$this->inflector = $inflector ?? new EnglishInflector();
$this->arrayMutatorPrefixesFirst = array_merge($this->arrayMutatorPrefixes, array_diff($this->mutatorPrefixes, $this->arrayMutatorPrefixes));
@ -230,7 +241,15 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
}
$allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false;
$allowMagicCall = $context['enable_magic_call_extraction'] ?? false;
$magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags;
$allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL);
$allowMagicGet = (bool) ($magicMethods & self::ALLOW_MAGIC_GET);
if (isset($context['enable_magic_call_extraction'])) {
trigger_deprecation('symfony/property-info', '5.2', 'Using the "enable_magic_call_extraction" context option in "%s()" is deprecated. Use "enable_magic_methods_extraction" instead.', __METHOD__);
$allowMagicCall = $context['enable_magic_call_extraction'] ?? false;
}
$hasProperty = $reflClass->hasProperty($property);
$camelProp = $this->camelize($property);
@ -258,7 +277,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($reflProperty), $reflProperty->isStatic(), true);
}
if ($reflClass->hasMethod('__get') && ($reflClass->getMethod('__get')->getModifiers() & $this->methodReflectionFlags)) {
if ($allowMagicGet && $reflClass->hasMethod('__get') && ($reflClass->getMethod('__get')->getModifiers() & $this->methodReflectionFlags)) {
return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, false);
}
@ -281,7 +300,16 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
}
$allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false;
$allowMagicCall = $context['enable_magic_call_extraction'] ?? false;
$magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags;
$allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL);
$allowMagicSet = (bool) ($magicMethods & self::ALLOW_MAGIC_SET);
if (isset($context['enable_magic_call_extraction'])) {
trigger_deprecation('symfony/property-info', '5.2', 'Using the "enable_magic_call_extraction" context option in "%s()" is deprecated. Use "enable_magic_methods_extraction" instead.', __METHOD__);
$allowMagicCall = $context['enable_magic_call_extraction'] ?? false;
}
$allowConstruct = $context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction;
$allowAdderRemover = $context['enable_adder_remover_extraction'] ?? true;
@ -347,12 +375,14 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic());
}
[$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__set', 2);
if ($accessible) {
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false);
}
if ($allowMagicSet) {
[$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__set', 2);
if ($accessible) {
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false);
}
$errors = array_merge($errors, $methodAccessibleErrors);
$errors = array_merge($errors, $methodAccessibleErrors);
}
if ($allowMagicCall) {
[$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__call', 2);

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\PropertyInfo\Tests\Extractor;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyReadInfo;
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
@ -30,6 +31,8 @@ use Symfony\Component\PropertyInfo\Type;
*/
class ReflectionExtractorTest extends TestCase
{
use ExpectDeprecationTrait;
/**
* @var ReflectionExtractor
*/
@ -518,4 +521,30 @@ class ReflectionExtractorTest extends TestCase
[Php71DummyExtended2::class, 'baz', false, true, PropertyWriteInfo::TYPE_ADDER_AND_REMOVER, null, 'addBaz', 'removeBaz', PropertyWriteInfo::VISIBILITY_PUBLIC, false],
];
}
/**
* @group legacy
*/
public function testGetReadInfoDeprecatedEnableMagicCallExtractionInContext()
{
$this->expectDeprecation('Since symfony/property-info 5.2: Using the "enable_magic_call_extraction" context option in "Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor::getReadInfo()" is deprecated. Use "enable_magic_methods_extraction" instead.');
$extractor = new ReflectionExtractor();
$extractor->getReadInfo(\stdClass::class, 'foo', [
'enable_magic_call_extraction' => true,
]);
}
/**
* @group legacy
*/
public function testGetWriteInfoDeprecatedEnableMagicCallExtractionInContext()
{
$this->expectDeprecation('Since symfony/property-info 5.2: Using the "enable_magic_call_extraction" context option in "Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor::getWriteInfo()" is deprecated. Use "enable_magic_methods_extraction" instead.');
$extractor = new ReflectionExtractor();
$extractor->getWriteInfo(\stdClass::class, 'foo', [
'enable_magic_call_extraction' => true,
]);
}
}

View File

@ -24,6 +24,7 @@
],
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-php80": "^1.15",
"symfony/string": "^5.1"
},