merged branch jaugustin/property-access-add-magic-call (PR #7263)

This PR was merged into the master branch.

Discussion
----------

[PropertyAccess] add support for magic call

Hi,

I add support for magic call with the `PropertyAccess`

the is basic implementation, if no `getter`, `isser`, or `hasser` or `_get` is found and there is `__call` then the PropertyAccess call the getter

the same for setter.

This functionality is disable by default

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | seems OK (failure/errors are the same on master)
| Fixed tickets | #4683, #6413, #5309
| License       | MIT
| Doc PR        | https://github.com/symfony/symfony-docs/pull/2472

- [x] submit changes to the documentation

@bschussek is this ok ?

Commits
-------

a785baa [PropertyAccess] add support for magic call, related to #4683
This commit is contained in:
Fabien Potencier 2013-04-25 10:51:04 +02:00
commit d8ac478b19
9 changed files with 267 additions and 6 deletions

View File

@ -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

View File

@ -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.
*/

View File

@ -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,

View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jeremie.augustin@pixel-cookers.com>
*/
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);
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jeremie.augustin@pixel-cookers.com>
*/
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();
}

View File

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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());
}
}

View File

@ -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)
);

View File

@ -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());
}
}