Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 5bd8b55

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

File tree

8 files changed

+292
-46
lines changed
Filter options

8 files changed

+292
-46
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml
+1Lines 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>
+31Lines 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+
}
+70Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+93-46Lines 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.'()", ';
770+
}, $singulars)),
771+
$setter,
772+
$getsetter,
773+
$reflClass->name
774+
);
775+
}
729776
}
730777
}
731778

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php
+19Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.