Skip to content

Commit e461527

Browse files
committed
[PropertyAccessor] WIP: Allow customizing which methods get called
1 parent b868feb commit e461527

File tree

7 files changed

+241
-47
lines changed

7 files changed

+241
-47
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<service id="property_accessor" class="Symfony\Component\PropertyAccess\PropertyAccessor" >
99
<argument /> <!-- magicCall, set by the extension -->
1010
<argument /> <!-- throwExceptionOnInvalidIndex, set by the extension -->
11+
<argument type="service" id="annotation_reader" />
1112
</service>
1213
</services>
1314
</container>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyAccess\Annotation;
13+
14+
/**
15+
* Base configuration annotation.
16+
*
17+
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
18+
*/
19+
abstract class ConfigurationAnnotation
20+
{
21+
public function __construct(array $values)
22+
{
23+
foreach ($values as $k => $v) {
24+
if (!method_exists($this, $name = 'set'.$k)) {
25+
throw new \RuntimeException(sprintf('Unknown key "%s" for annotation "@%s".', $k, get_class($this)));
26+
}
27+
28+
$this->$name($v);
29+
}
30+
}
31+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyAccess\Annotation;
13+
14+
/**
15+
* Property accessor configuration annotation.
16+
* @Annotation
17+
* @author Luis Ramón López <lrlopez@gmail.com>
18+
*/
19+
class PropertyAccessor extends ConfigurationAnnotation
20+
{
21+
protected $setter;
22+
23+
protected $getter;
24+
25+
protected $adder;
26+
27+
protected $remover;
28+
29+
public function getSetter()
30+
{
31+
return $this->setter;
32+
}
33+
34+
public function setSetter($setter)
35+
{
36+
$this->setter = $setter;
37+
}
38+
39+
public function getGetter()
40+
{
41+
return $this->getter;
42+
}
43+
44+
public function setGetter($getter)
45+
{
46+
$this->getter = $getter;
47+
}
48+
49+
public function getAdder()
50+
{
51+
return $this->adder;
52+
}
53+
54+
public function setAdder($adder)
55+
{
56+
$this->adder = $adder;
57+
}
58+
59+
public function getRemover()
60+
{
61+
return $this->remover;
62+
}
63+
64+
public function setRemover($remover)
65+
{
66+
$this->remover = $remover;
67+
}
68+
}

src/Symfony/Component/PropertyAccess/PropertyAccessor.php

Lines changed: 99 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\PropertyAccess;
1313

14+
use Doctrine\Common\Annotations\AnnotationReader;
1415
use Symfony\Component\PropertyAccess\Exception\AccessException;
1516
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
1617
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
@@ -114,17 +115,23 @@ class PropertyAccessor implements PropertyAccessorInterface
114115
*/
115116
private $writePropertyCache = array();
116117

118+
/**
119+
* @var AnnotationReader
120+
*/
121+
private $reader;
122+
117123
/**
118124
* Should not be used by application code. Use
119125
* {@link PropertyAccess::createPropertyAccessor()} instead.
120126
*
121127
* @param bool $magicCall
122128
* @param bool $throwExceptionOnInvalidIndex
123129
*/
124-
public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false)
130+
public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false, AnnotationReader $reader = null)
125131
{
126132
$this->magicCall = $magicCall;
127133
$this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex;
134+
$this->reader = $reader;
128135
}
129136

130137
/**
@@ -460,17 +467,33 @@ private function getReadAccessInfo($object, $property)
460467
if (isset($this->readPropertyCache[$key])) {
461468
$access = $this->readPropertyCache[$key];
462469
} else {
470+
/**
471+
* @var \Symfony\Component\PropertyAccess\Annotation\PropertyAccessor
472+
*/
473+
$annotation = null;
474+
463475
$access = array();
464476

465477
$reflClass = new \ReflectionClass($object);
466-
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
478+
$hasProperty = $reflClass->hasProperty($property);
479+
$access[self::ACCESS_HAS_PROPERTY] = $hasProperty;
480+
481+
if ($hasProperty && $this->reader) {
482+
$annotation = $this->reader->getPropertyAnnotation($reflClass->getProperty($property),
483+
'Symfony\Component\PropertyAccess\Annotation\PropertyAccessor');
484+
485+
}
486+
467487
$camelProp = $this->camelize($property);
468488
$getter = 'get'.$camelProp;
469489
$getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
470490
$isser = 'is'.$camelProp;
471491
$hasser = 'has'.$camelProp;
472492

473-
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
493+
if ($annotation && $annotation->getGetter()) {
494+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
495+
$access[self::ACCESS_NAME] = $annotation->getGetter();
496+
} elseif ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
474497
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
475498
$access[self::ACCESS_NAME] = $getter;
476499
} elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
@@ -676,56 +699,86 @@ private function getWriteAccessInfo($object, $property, $value)
676699
if (isset($this->writePropertyCache[$key])) {
677700
$access = $this->writePropertyCache[$key];
678701
} else {
702+
/**
703+
* @var \Symfony\Component\PropertyAccess\Annotation\PropertyAccessor
704+
*/
705+
$annotation = null;
706+
679707
$access = array();
680708

681709
$reflClass = new \ReflectionClass($object);
682-
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
683-
$camelized = $this->camelize($property);
684-
$singulars = (array) StringUtil::singularify($camelized);
710+
$hasProperty = $reflClass->hasProperty($property);
711+
$access[self::ACCESS_HAS_PROPERTY] = $hasProperty;
712+
713+
$transversable = is_array($value) || $value instanceof \Traversable;
714+
$done = false;
715+
716+
if ($hasProperty && $this->reader) {
717+
$annotation = $this->reader->getPropertyAnnotation($reflClass->getProperty($property),
718+
'Symfony\Component\PropertyAccess\Annotation\PropertyAccessor');
719+
720+
if ($annotation) {
721+
if ($transversable && $annotation->getAdder() && $annotation->getRemover()) {
722+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
723+
$access[self::ACCESS_ADDER] = $annotation->getAdder();
724+
$access[self::ACCESS_REMOVER] = $annotation->getRemover();
725+
$done = true;
726+
} elseif ($annotation->getSetter()) {
727+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
728+
$access[self::ACCESS_NAME] = $annotation->getSetter();
729+
$done = true;
730+
}
731+
}
732+
}
685733

686-
if (is_array($value) || $value instanceof \Traversable) {
687-
$methods = $this->findAdderAndRemover($reflClass, $singulars);
734+
if (!$done) {
735+
$camelized = $this->camelize($property);
736+
$singulars = (array)StringUtil::singularify($camelized);
688737

689-
if (null !== $methods) {
690-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
691-
$access[self::ACCESS_ADDER] = $methods[0];
692-
$access[self::ACCESS_REMOVER] = $methods[1];
738+
if ($transversable) {
739+
$methods = $this->findAdderAndRemover($reflClass, $singulars);
740+
741+
if (null !== $methods) {
742+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
743+
$access[self::ACCESS_ADDER] = $methods[0];
744+
$access[self::ACCESS_REMOVER] = $methods[1];
745+
}
693746
}
694-
}
695747

696-
if (!isset($access[self::ACCESS_TYPE])) {
697-
$setter = 'set'.$camelized;
698-
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
699-
700-
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
701-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
702-
$access[self::ACCESS_NAME] = $setter;
703-
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
704-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
705-
$access[self::ACCESS_NAME] = $getsetter;
706-
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
707-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
708-
$access[self::ACCESS_NAME] = $property;
709-
} elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
710-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
711-
$access[self::ACCESS_NAME] = $property;
712-
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
713-
// we call the getter and hope the __call do the job
714-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
715-
$access[self::ACCESS_NAME] = $setter;
716-
} else {
717-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
718-
$access[self::ACCESS_NAME] = sprintf(
719-
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
720-
'"__set()" or "__call()" exist and have public access in class "%s".',
721-
$property,
722-
implode('', array_map(function ($singular) {
723-
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
724-
}, $singulars)),
725-
$setter,
726-
$getsetter,
727-
$reflClass->name
728-
);
748+
if (!isset($access[self::ACCESS_TYPE])) {
749+
$setter = 'set' . $camelized;
750+
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
751+
752+
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
753+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
754+
$access[self::ACCESS_NAME] = $setter;
755+
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
756+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
757+
$access[self::ACCESS_NAME] = $getsetter;
758+
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
759+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
760+
$access[self::ACCESS_NAME] = $property;
761+
} elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
762+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
763+
$access[self::ACCESS_NAME] = $property;
764+
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
765+
// we call the getter and hope the __call do the job
766+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
767+
$access[self::ACCESS_NAME] = $setter;
768+
} else {
769+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
770+
$access[self::ACCESS_NAME] = sprintf(
771+
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", ' .
772+
'"__set()" or "__call()" exist and have public access in class "%s".',
773+
$property,
774+
implode('', array_map(function ($singular) {
775+
return '"add' . $singular . '()"/"remove' . $singular . '()", ';
776+
}, $singulars)),
777+
$setter,
778+
$getsetter,
779+
$reflClass->name
780+
);
781+
}
729782
}
730783
}
731784

src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
1313

14+
use Doctrine\ORM\Mapping\Column;
15+
use Symfony\Component\PropertyAccess\Annotation\PropertyAccessor;
16+
1417
class TestClass
1518
{
1619
public $publicProperty;
@@ -28,6 +31,11 @@ class TestClass
2831
private $publicGetter;
2932
private $date;
3033

34+
/**
35+
* @PropertyAccessor(getter="customGetterTest", setter="customSetterTest")
36+
*/
37+
private $customGetterSetter;
38+
3139
public function __construct($value)
3240
{
3341
$this->publicProperty = $value;
@@ -40,6 +48,7 @@ public function __construct($value)
4048
$this->publicIsAccessor = $value;
4149
$this->publicHasAccessor = $value;
4250
$this->publicGetter = $value;
51+
$this->customGetterSetter = $value;
4352
}
4453

4554
public function setPublicAccessor($value)
@@ -184,4 +193,14 @@ public function getDate()
184193
{
185194
return $this->date;
186195
}
196+
197+
public function customGetterTest()
198+
{
199+
return $this->customGetterSetter;
200+
}
201+
202+
public function customSetterTest($value)
203+
{
204+
$this->customGetterSetter = $value;
205+
}
187206
}

src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\PropertyAccess\Tests;
1313

14+
use Doctrine\Common\Annotations\AnnotationReader;
15+
use Doctrine\Common\Annotations\AnnotationRegistry;
1416
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
1517
use Symfony\Component\PropertyAccess\PropertyAccessor;
1618
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass;
@@ -187,6 +189,13 @@ public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p
187189
$this->propertyAccessor->getValue($objectOrArray, $path);
188190
}
189191

192+
public function testGetWithCustomGetter()
193+
{
194+
AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__ . '/../../../..');
195+
$this->propertyAccessor = new PropertyAccessor(false, false, new AnnotationReader());
196+
$this->assertSame('webmozart', $this->propertyAccessor->getValue(new TestClass('webmozart'), 'customGetterSetter'));
197+
}
198+
190199
/**
191200
* @dataProvider getValidPropertyPaths
192201
*/
@@ -283,6 +292,18 @@ public function testSetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p
283292
$this->propertyAccessor->setValue($objectOrArray, $path, 'value');
284293
}
285294

295+
public function testSetValueWithCustomSetter()
296+
{
297+
AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__ . '/../../../..');
298+
$this->propertyAccessor = new PropertyAccessor(false, false, new AnnotationReader());
299+
300+
$custom = new TestClass('webmozart');
301+
302+
$this->propertyAccessor->setValue($custom, 'customGetterSetter', 'it works!');
303+
304+
$this->assertEquals('it works!', $custom->customGetterTest());
305+
}
306+
286307
public function testGetValueWhenArrayValueIsNull()
287308
{
288309
$this->propertyAccessor = new PropertyAccessor(false, true);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy