8000 [PropertyAccess] WIP: Allow customizing which methods get called when… · symfony/symfony@5bd8b55 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5bd8b55

Browse files
committed
[PropertyAccess] WIP: Allow customizing which methods get called when accessing properties
1 parent b868feb commit 5bd8b55

File tree

8 files changed

+292
-46
lines changed

8 files changed

+292
-46
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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
*
17+
* @Annotation
18+
*
19+
* @author Luis Ramón López <lrlopez@gmail.com>
20+
*/
21+
class PropertyAccessor extends ConfigurationAnnotation
22+
{
23+
protected $setter;
24+
25+
protected $getter;
26+
27+
protected $adder;
28+
29+
protected $remover;
30+
31+
public function getSetter()
32+
{
33+
return $this->setter;
34+
}
35+
36+
public function setSetter($setter)
37+
{
38+
$this->setter = $setter;
39+
}
40+
41+
public function getGetter()
42+
{
43+
return $this->getter;
44+
}
45+
46+
public function setGetter($getter)
47+
{
48+
$this->getter = $getter;
49+
}
50+
51+
public function getAdder()
52+
{
53+
return $this->adder;
54+
}
55+
56+
public function setAdder($adder)
57+
{
58+
$this->adder = $adder;
59+
}
60+
61+
public function getRemover()
62+
{
63+
return $this->remover;
64+
}
65+
66+
public function setRemover($remover)
67+
{
68+
$this->remover = $remover;
69+
}
70+
}

src/Symfony/Component/PropertyAccess/PropertyAccessor.php

Lines changed: 93 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,30 @@ private function getReadAccessInfo($object, $property)
460467
if (isset($this->readPropertyCache[$key])) {
461468
$access = $this->readPropertyCache[$key];
462469
} else {
470+
$annotation = null;
471+
463472
$access = array();
464473

465474
$reflClass = new \ReflectionClass($object);
466-
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
475+
$hasProperty = $reflClass->hasProperty($property);
476+
$access[self::ACCESS_HAS_PROPERTY] = $hasProperty;
477+
478+
if ($hasProperty && $this->reader) {
479+
$annotation = $this->reader->getPropertyAnnotation($reflClass->getProperty($property),
480+
'Symfony\Component\PropertyAccess\Annotation\PropertyAccessor');
481+
482+
}
483+
467484
$camelProp = $this->camelize($property);
468485
$getter = 'get'.$camelProp;
469486
$getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
470487
$isser = 'is'.$camelProp;
471488
$hasser = 'has'.$camelProp;
472489

473-
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
490+
if ($annotation && $annotation->getGetter()) {
491+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
492+
$access[self::ACCESS_NAME] = $annotation->getGetter();
493+
} elseif ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
474494
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
475495
$access[self::ACCESS_NAME] = $getter;
476496
} elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
@@ -676,56 +696,83 @@ private function getWriteAccessInfo($object, $property, $value)
676696
if (isset($this->writePropertyCache[$key])) {
677697
$access = $this->writePropertyCache[$key];
678698
} else {
699+
$annotation = null;
700+
679701
$access = array();
680702

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

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

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];
732+
if ($transversable) {
733+
$methods = $this->findAdderAndRemover($reflClass, $singulars);
734+
735+
if (null !== $methods) {
736+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
737+
$access[self::ACCESS_ADDER] = $methods[0];
738+
$access[self::ACCESS_REMOVER] = $methods[1];
739+
}
693740
}
694-
}
695741

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-
);
742+
if (!isset($access[self::ACCESS_TYPE])) {
743+
$setter = 'set'.$camelized;
744+
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
745+
746+
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
747+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
748+
$access[self::ACCESS_NAME] = $setter;
749+
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
750+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
751+
$access[self::ACCESS_NAME] = $getsetter;
752+
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
753+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
754+
$access[self::ACCESS_NAME] = $property;
755+
} elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
756+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
757+
$access[self::ACCESS_NAME] = $property;
758+
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
759+
// we call the getter and hope the __call do the job
760+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
761+
$access[self::ACCESS_NAME] = $setter;
762+
} else {
763+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
764+
$access[self::ACCESS_NAME] = sprintf(
765+
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
766+
'"__set()" or "__call()" exist and have public access in class "%s".',
767+
$property,
768+
implode('', array_map(function ($singular) {
769+
return '"add'.$singular.'()"/"remove'.$singular.'()",  341A 9;;
770+
}, $singulars)),
771+
$setter,
772+
$getsetter,
773+
$reflClass->name
774+
);
775+
}
729776
}
730777
}
731778

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
}

0 commit comments

Comments
 (0)
0