From a785baaa64e9b93d6f0419c49f6dec8c512e3c2b Mon Sep 17 00:00:00 2001 From: jaugustin Date: Mon, 4 Mar 2013 23:59:45 +0100 Subject: [PATCH] [PropertyAccess] add support for magic call, related to #4683 add a new propertyAccessorBuilder to enable / disable the use of __call by the PropertyAccessor --- .../Component/PropertyAccess/CHANGELOG.md | 2 + .../PropertyAccess/PropertyAccess.php | 10 +++ .../PropertyAccess/PropertyAccessor.php | 17 ++++-- .../PropertyAccessorBuilder.php | 61 +++++++++++++++++++ .../PropertyAccessorBuilderInterface.php | 46 ++++++++++++++ .../Tests/Fixtures/MagicianCall.php | 29 +++++++++ .../Tests/PropertyAccessorBuilderTest.php | 55 +++++++++++++++++ .../Tests/PropertyAccessorCollectionTest.php | 4 +- .../Tests/PropertyAccessorTest.php | 49 +++++++++++++++ 9 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php create mode 100644 src/Symfony/Component/PropertyAccess/PropertyAccessorBuilderInterface.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/MagicianCall.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index d81f412fe8..e8d11f95b6 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 2.3.0 ------ + * added PropertyAccessorBuilder, to enable or disable the support of "__call" + * added support for "__call" in the PropertyAccessor (disabled by default) * [BC BREAK] changed PropertyAccessor to continue its search for a property or method even if a non-public match was found. Before, a PropertyAccessDeniedException was thrown in this case. Class PropertyAccessDeniedException was removed diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccess.php b/src/Symfony/Component/PropertyAccess/PropertyAccess.php index eeefc549e5..a01c9923b4 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccess.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccess.php @@ -28,6 +28,16 @@ final class PropertyAccess return new PropertyAccessor(); } + /** + * Creates a property accessor builder. + * + * @return PropertyAccessorBuilder The new property accessor builder + */ + public static function getPropertyAccessorBuilder() + { + return new PropertyAccessorBuilder(); + } + /** * This class cannot be instantiated. */ diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index db204f1f7c..c65d919e11 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -24,12 +24,15 @@ class PropertyAccessor implements PropertyAccessorInterface const VALUE = 0; const IS_REF = 1; + private $magicCall; + /** * Should not be used by application code. Use * {@link PropertyAccess::getPropertyAccessor()} instead. */ - public function __construct() + public function __construct($magicCall = false) { + $this->magicCall = $magicCall; } /** @@ -221,10 +224,13 @@ class PropertyAccessor implements PropertyAccessorInterface // fatal error. $result[self::VALUE] =& $object->$property; $result[self::IS_REF] = true; + } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { + // we call the getter and hope the __call do the job + $result[self::VALUE] = $object->$getter(); } else { throw new NoSuchPropertyException(sprintf( 'Neither the property "%s" nor one of the methods "%s()", '. - '"%s()", "%s()" or "__get()" exist and have public access in '. + '"%s()", "%s()", "__get()" or "__call()" exist and have public access in '. 'class "%s".', $property, $getter, @@ -348,10 +354,13 @@ class PropertyAccessor implements PropertyAccessorInterface // returns true, consequently the following line will result in a // fatal error. $object->$property = $value; + } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { + // we call the getter and hope the __call do the job + $object->$setter($value); } else { throw new NoSuchPropertyException(sprintf( - 'Neither the property "%s" nor one of the methods %s"%s()" or '. - '"__set()" exist and have public access in class "%s".', + 'Neither the property "%s" nor one of the methods %s"%s()", '. + '"__set()" or "__call()" exist and have public access in class "%s".', $property, $guessedAdders, $setter, diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php new file mode 100644 index 0000000000..f1302b79d4 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * The default implementation of {@link PropertyAccessorBuilderInterface}. + * + * @author Jérémie Augustin + */ +class PropertyAccessorBuilder implements PropertyAccessorBuilderInterface +{ + /** + * @var Boolean + */ + private $magicCall = false; + + /** + * {@inheritdoc} + */ + public function enableMagicCall() + { + $this->magicCall = true; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function disableMagicCall() + { + $this->magicCall = false; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isMagicCallEnabled() + { + return $this->magicCall; + } + + /** + * {@inheritdoc} + */ + public function getPropertyAccessor() + { + return new PropertyAccessor($this->magicCall); + } +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilderInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilderInterface.php new file mode 100644 index 0000000000..e338cb8627 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilderInterface.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * A configurable builder for PropertyAccessorInterface objects. + * + * @author Jérémie Augustin + */ +interface PropertyAccessorBuilderInterface +{ + /** + * Enable the use of "__call" by the ProperyAccessor. + * + * @return PropertyAccessorBuilderInterface The builder object. + */ + public function enableMagicCall(); + + /** + * Disable the use of "__call" by the ProperyAccessor. + * + * @return PropertyAccessorBuilderInterface The builder object. + */ + public function disableMagicCall(); + + /** + * @return Boolean true if the use of "__call" by the ProperyAccessor is enable. + */ + public function isMagicCallEnabled(); + + /** + * Builds and returns a new propertyAccessor object. + * + * @return PropertyAccessorInterface The built propertyAccessor. + */ + public function getPropertyAccessor(); +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/MagicianCall.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/MagicianCall.php new file mode 100644 index 0000000000..26ee6fdc9a --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/MagicianCall.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class MagicianCall +{ + private $foobar; + + public function __call($name, $args) + { + $property = lcfirst(substr($name, 3)); + if ('get' === substr($name, 0, 3)) { + + return isset($this->$property) ? $this->$property : null; + } elseif ('set' === substr($name, 0, 3)) { + $value = 1 == count($args) ? $args[0] : null; + $this->$property = $value; + } + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php new file mode 100644 index 0000000000..6e33e5fa50 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use Symfony\Component\PropertyAccess\PropertyAccessorBuilder; + +class PropertyAccessorBuilderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var PropertyAccessorBuilderInterface + */ + protected $builder; + + protected function setUp() + { + $this->builder = new PropertyAccessorBuilder(); + } + + protected function tearDown() + { + $this->builder = null; + } + + public function testEnableMagicCall() + { + $this->assertSame($this->builder, $this->builder->enableMagicCall()); + } + + public function testDisableMagicCall() + { + $this->assertSame($this->builder, $this->builder->disableMagicCall()); + } + + public function testIsMagicCallEnable() + { + $this->assertFalse($this->builder->isMagicCallEnabled()); + $this->assertTrue($this->builder->enableMagicCall()->isMagicCallEnabled()); + $this->assertFalse($this->builder->disableMagicCall()->isMagicCallEnabled()); + } + + public function testGetPropertyAccessor() + { + $this->assertInstanceOf('Symfony\Component\PropertyAccess\PropertyAccessor', $this->builder->getPropertyAccessor()); + $this->assertInstanceOf('Symfony\Component\PropertyAccess\PropertyAccessor', $this->builder->enableMagicCall()->getPropertyAccessor()); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index 0113a47f4b..b0f75aa366 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -289,7 +289,7 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas $propertyPath = 'axes'; $expectedMessage = sprintf( 'Neither the property "axes" nor one of the methods "addAx()", '. - '"addAxe()", "addAxis()", "setAxes()" or "__set()" exist and have '. + '"addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have '. 'public access in class "%s".', get_class($car) ); @@ -313,7 +313,7 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas $propertyPath = 'axes'; $expectedMessage = sprintf( 'Neither the property "axes" nor one of the methods "addAx()", '. - '"addAxe()", "addAxis()", "setAxes()" or "__set()" exist and have '. + '"addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have '. 'public access in class "%s".', get_class($car) ); diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index a15deb5637..e22ca097be 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 Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\Tests\Fixtures\Author; use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician; +use Symfony\Component\PropertyAccess\Tests\Fixtures\MagicianCall; class PropertyAccessorTest extends \PHPUnit_Framework_TestCase { @@ -331,4 +332,52 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase $this->propertyAccessor->setValue($value, 'foobar', 'bam'); } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testSetValueFailsIfMagicCallDisabled() + { + $value = new MagicianCall(); + + $this->propertyAccessor->setValue($value, 'foobar', 'bam'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testGetValueFailsIfMagicCallDisabled() + { + $value = new MagicianCall(); + + $this->propertyAccessor->getValue($value, 'foobar', 'bam'); + } + + public function testGetValueReadsMagicCall() + { + $propertyAccessor = new PropertyAccessor(true); + $object = new MagicianCall(); + $object->setMagicProperty('foobar'); + + $this->assertSame('foobar', $propertyAccessor->getValue($object, 'magicProperty')); + } + + public function testGetValueReadsMagicCallThatReturnsConstant() + { + $propertyAccessor = new PropertyAccessor(true); + $object = new MagicianCall(); + + $this->assertNull($propertyAccessor->getValue($object, 'MagicProperty')); + } + + public function testSetValueUpdatesMagicCall() + { + $propertyAccessor = new PropertyAccessor(true); + $object = new MagicianCall(); + + $propertyAccessor->setValue($object, 'magicProperty', 'foobar'); + + $this->assertEquals('foobar', $object->getMagicProperty()); + } + }