diff --git a/UPGRADE-5.2.md b/UPGRADE-5.2.md index b563d441e8..3e44ed362a 100644 --- a/UPGRADE-5.2.md +++ b/UPGRADE-5.2.md @@ -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 ---------- diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index 195d644fd5..9f1f9a78f0 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -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 ------- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 63a53861cc..4e47cbf75d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -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() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 316a8ccd33..f922448df7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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)) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php index da2933d77e..00d8f66b5a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php @@ -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'), diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index ab0da8c848..9a03ede460 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -236,6 +236,8 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 127baddddb..e47f19aea8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -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, ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php index 8f431f8735..dc6954fe89 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php @@ -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, ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml index 07e33ae3e8..9406919e92 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml @@ -7,6 +7,6 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml index ea527c9821..931b50383f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml @@ -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 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 3bfd523f2e..878b2592f6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -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)); } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index fd1659b10f..46b3c5db65 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -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", diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index f6a167f859..e46378210d 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index af7331d52b..18004da4e6 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -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, ]); diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php index 41853abbfe..fcda6ce2e5 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php @@ -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); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php index eb46d300da..ce125855dc 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php @@ -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()); diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 89e02f334d..c3dac41fdb 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -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')); diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index f7001b837e..533c6f65b0 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -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" diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index 2925a37a94..75621067ad 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 10fa472882..442552724e 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -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); diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 353a41b829..b9bdafbd33 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -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, + ]); + } } diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json index c1ce2b757c..9e872695ba 100644 --- a/src/Symfony/Component/PropertyInfo/composer.json +++ b/src/Symfony/Component/PropertyInfo/composer.json @@ -24,6 +24,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-php80": "^1.15", "symfony/string": "^5.1" },