Skip to content

[PropertyAccess] add support for magic call #7263

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 25, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Symfony/Component/PropertyAccess/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/Symfony/Component/PropertyAccess/PropertyAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ public static function getPropertyAccessor()
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.
*/
Expand Down
17 changes: 13 additions & 4 deletions src/Symfony/Component/PropertyAccess/PropertyAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -221,10 +224,13 @@ private function &readProperty(&$object, $property)
// 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,
Expand Down Expand Up @@ -348,10 +354,13 @@ private function writeProperty(&$object, $property, $singular, $value)
// 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,
Expand Down
61 changes: 61 additions & 0 deletions src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php
Original file line number Diff line number Diff line change
@@ -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()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not call these methods enable__Call() etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Drak dirty name, isn't it ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also prefer the current one over @Drak's version

{
$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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ public function noAdderRemoverData()
$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)
);
Expand All @@ -313,7 +313,7 @@ public function noAdderRemoverData()
$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)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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
{
Expand Down Expand Up @@ -331,4 +332,52 @@ public function testSetValueThrowsExceptionIfEmpty()

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

}