8000 Add accessor and mutator extractor interface and implementation on re… · symfony/symfony@03052ee · GitHub
[go: up one dir, main page]

Skip to content

Commit 03052ee

Browse files
committed
Add accessor and mutator extractor interface and implementation on reflection
1 parent 76260e7 commit 03052ee

File tree

7 files changed

+447
-1
lines changed

7 files changed

+447
-1
lines changed

src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
1717
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
1818
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
19+
use Symfony\Component\PropertyInfo\ReadAccessor;
20+
use Symfony\Component\PropertyInfo\ReadAccessorExtractorInterface;
1921
use Symfony\Component\PropertyInfo\Type;
22+
use Symfony\Component\PropertyInfo\WriteMutator;
23+
use Symfony\Component\PropertyInfo\WriteMutatorExtractorInterface;
2024

2125
/**
2226
* Extracts data using the reflection API.
@@ -25,7 +29,7 @@
2529
*
2630
* @final
2731
*/
28-
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface
32+
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, ReadAccessorExtractorInterface, WriteMutatorExtractorInterface
2933
{
3034
/**
3135
* @internal
@@ -185,6 +189,120 @@ public function isInitializable(string $class, string $property, array $context
185189
return false;
186190
}
187191

192+
/**
193+
* {@inheritdoc}
194+
*/
195+
public function getReadAccessor(string $class, string $property): ?ReadAccessor
196+
{
197+
try {
198+
$reflClass = new \ReflectionClass($class);
199+
} catch (\ReflectionException $e) {
200+
return null;
201+
}
202+
203+
$hasProperty = $reflClass->hasProperty($property);
204+
$camelProp = $this->camelize($property);
205+
$getter = 'get'.$camelProp;
206+
$getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
207+
$isser = 'is'.$camelProp;
208+
$hasser = 'has'.$camelProp;
209+
$accessPrivate = false;
210+
$accessStatic = false;
211+
212+
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
213+
$accessType = ReadAccessor::TYPE_METHOD;
214+
$accessName = $getter;
215+
$accessStatic = $reflClass->getMethod($getter)->isStatic();
216+
} elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
217+
$accessType = ReadAccessor::TYPE_METHOD;
218+
$accessName = $getsetter;
219+
$accessStatic = $reflClass->getMethod($getsetter)->isStatic();
220+
} elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
221+
$accessType = ReadAccessor::TYPE_METHOD;
222+
$accessName = $isser;
223+
$accessStatic = $reflClass->getMethod($isser)->isStatic();
224+
} elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
225+
$accessType = ReadAccessor::TYPE_METHOD;
226+
$accessName = $hasser;
227+
$accessStatic = $reflClass->getMethod($hasser)->isStatic();
228+
} elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
229+
$accessType = ReadAccessor::TYPE_PROPERTY;
230+
$accessName = $property;
231+
} elseif ($hasProperty) {
232+
$accessType = ReadAccessor::TYPE_PROPERTY;
233+
$accessName = $property;
234+
$accessStatic = $reflClass->getProperty($property)->isStatic();
235+
$accessPrivate = !$reflClass->getProperty($property)->isPublic();
236+
} else {
237+
return null;
238+
}
239+
240+
return new ReadAccessor($accessType, $accessName, $accessPrivate, $accessStatic);
241+
}
242+
243+
/**
244+
* {@inheritdoc}
245+
*/
246+
public function getWriteMutator(string $class, string $property, bool $allowConstruct = true): ?WriteMutator
247+
{
248+
try {
249+
$reflClass = new \ReflectionClass($class);
250+
} catch (\ReflectionException $e) {
251+
return null;
252+
}
253+
254+
$hasProperty = $reflClass->hasProperty($property);
255+
$camelized = $this->camelize($property);
256+
$accessParameter = null;
257+
$accessName = null;
258+
$adderAccessName = null;
259+
$removerAccessName = null;
260+
$accessType = null;
261+
$accessPrivate = false;
262+
$accessStatic = false;
263+
$constructor = $reflClass->getConstructor();
264+
265+
if (null !== $constructor) {
266+
foreach ($constructor->getParameters() as $parameter) {
267+
if ($parameter->getName() === $property) {
268+
$accessParameter = $parameter;
269+
}
270+
}
271+
}
272+
273+
$setter = 'set'.$camelized;
274+
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
275+
276+
if (null !== $accessParameter && $allowConstruct) {
277+
$accessType = WriteMutator::TYPE_CONSTRUCTOR;
278+
$accessName = $property;
279+
} elseif ($this->isMethodAccessible($reflClass, $setter, 1)) {
280+
$accessType = WriteMutator::TYPE_METHOD;
281+
$accessName = $setter;
282+
$accessStatic = $reflClass->getMethod($setter)->isStatic();
283+
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
284+
$accessType = WriteMutator::TYPE_METHOD;
285+
F438 $accessName = $getsetter;
286+
$accessStatic = $reflClass->getMethod($getsetter)->isStatic();
287+
} elseif (null !== $methods = $this->findAdderAndRemover($reflClass, (array) Inflector::singularize($camelized))) {
288+
$accessType = WriteMutator::TYPE_ADDER_REMOVER_METHOD;
289+
$adderAccessName = $methods[0];
290+
$removerAccessName = $methods[1];
291+
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
292+
$accessType = WriteMutator::TYPE_PROPERTY;
293+
$accessName = $property;
294+
} elseif ($hasProperty) {
295+
$accessType = WriteMutator::TYPE_PROPERTY;
296+
$accessName = $property;
297+
$accessPrivate = !$reflClass->getProperty($property)->isPublic();
298+
$accessStatic = $reflClass->getProperty($property)->isStatic();
299+
} else {
300+
return null;
301+
}
302+
303+
return new WriteMutator($accessType, $accessName, $adderAccessName, $removerAccessName, $accessPrivate, $accessStatic, $accessParameter);
304+
}
305+
188306
/**
189307
* @return Type[]|null
190308
*/
@@ -415,4 +533,53 @@ private function getPropertyName(string $methodName, array $reflectionProperties
415533

416534
return null;
417535
}
536+
537+
/**
538+
* Searches for add and remove methods.
539+
*
540+
* @param \ReflectionClass $reflClass The reflection class for the given object
541+
* @param array $singulars The singular form of the property name or null
542+
*
543+
* @return array|null An array containing the adder and remover when found, null otherwise
544+
*/
545+
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
546+
{
547+
foreach ($singulars as $singular) {
548+
$addMethod = 'add'.$singular;
549+
$removeMethod = 'remove'.$singular;
550+
551+
$addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1);
552+
$removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1);
553+
554+
if ($addMethodFound && $removeMethodFound) {
555+
return [$addMethod, $removeMethod];
556+
}
557+
}
558+
}
559+
560+
/**
561+
* Returns whether a method is public and has the number of required parameters.
562+
*/
563+
private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): bool
564+
{
565+
if ($class->hasMethod($methodName)) {
566+
$method = $class->getMethod($methodName);
567+
568+
if ($method->isPublic()
569+
&& $method->getNumberOfRequiredParameters() <= $parameters
570+
&& $method->getNumberOfParameters() >= $parameters) {
571+
return true;
572+
}
573+
}
574+
575+
return false;
576+
}
577+
578+
/**
579+
* Camelizes a given string.
580+
*/
581+
private function camelize(string $string): string
582+
{
583+
return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
584+
}
418585
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\PropertyInfo;
13+
14+
/**
15+
* Read accessor tell how to read from a property.
16+
*
17+
* @author Joel Wurtz <jwurtz@jolicode.com>
18+
*/
19+
final class ReadAccessor
20+
{
21+
public const TYPE_METHOD = 1;
22+
public const TYPE_PROPERTY = 2;
23+
public const TYPE_ARRAY_DIMENSION = 3;
24+
public const TYPE_SOURCE = 4;
25+
26+
private $type;
27+
28+
private $name;
29+
30+
private $private;
31+
32+
private $static;
33+
34+
public function __construct(int $type, string $name, bool $private = false, bool $static = false)
35+
{
36+
$this->type = $type;
37+
$this->name = $name;
38+
$this->private = $private;
39+
$this->static = $static;
40+
}
41+
42+
public function getType(): int
43+
{
44+
return $this->type;
45+
}
46+
47+
public function getName(): string
48+
{
49+
return $this->name;
50+
}
51+
52+
public function isPrivate(): bool
53+
{
54+
return $this->private;
55+
}
56+
57+
public function isStatic(): bool
58+
{
59+
return $this->static;
60+
}
61+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\PropertyInfo;
13+
14+
/**
15+
* Extract read accessor for property of a class.
16+
*
17+
* @author Joel Wurtz <jwurtz@jolicode.com>
18+
*/
19+
interface ReadAccessorExtractorInterface
20+
{
21+
public function getReadAccessor(string $class, string $property): ?ReadAccessor;
22+
}

src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
16+
use Symfony\Component\PropertyInfo\ReadAccessor;
1617
use Symfony\Component\PropertyInfo\Tests\Fixtures\AdderRemoverDummy;
1718
use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue;
19+
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
1820
use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable;
1921
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy;
22+
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended;
2023
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended2;
2124
use Symfony\Component\PropertyInfo\Type;
25+
use Symfony\Component\PropertyInfo\WriteMutator;
2226

2327
/**
2428
* @author Kévin Dunglas <dunglas@gmail.com>
@@ -338,4 +342,90 @@ public function constructorTypesProvider(): array
338342
[DefaultValue::class, 'foo', null],
339343
];
340344
}
345+
346+
/**
347+
* @dataProvider readAccessorProvider
348+
*/
349+
public function testGetReadAccessor($class, $property, $found, $type, $name, $private, $static)
350+
{
351+
$readAcessor = $this->extractor->getReadAccessor($class, $property);
352+
353+
if (!$found) {
354+
$this->assertNull($readAcessor);
355+
356+
return;
357+
}
358+
359+
$this->assertNotNull($readAcessor);
360+
$this->assertSame($type, $readAcessor->getType());
361+
$this->assertSame($name, $readAcessor->getName());
362+
$this->assertSame($private, $readAcessor->isPrivate());
363+
$this->assertSame($static, $readAcessor->isStatic());
364+
}
365+
366+
public function readAccessorProvider(): array
367+
{
368+
return [
369+
[Dummy::class, 'bar', true, ReadAccessor::TYPE_PROPERTY, 'bar', true, false],
370+
[Dummy::class, 'baz', true, ReadAccessor::TYPE_PROPERTY, 'baz', true, false],
371+
[Dummy::class, 'bal', true, ReadAccessor::TYPE_PROPERTY, 'bal', false, false],
372+
[Dummy::class, 'parent', true, ReadAccessor::TYPE_PROPERTY, 'parent', false, false],
373+
[Dummy::class, 'static', true, ReadAccessor::TYPE_METHOD, 'getStatic', false, true],
374+
[Dummy::class, 'foo', true, ReadAccessor::TYPE_PROPERTY, 'foo', false, false],
375+
[Php71Dummy::class, 'foo', true, ReadAccessor::TYPE_METHOD, 'getFoo', false, false],
376+
[Php71Dummy::class, 'buz', true, ReadAccessor::TYPE_METHOD, 'getBuz', false, false],
377+
];
378+
}
379+
380+
/**
381+
* @dataProvider writeMutatorProvider
382+
*/
383+
public function testGetWriteMurator($class, $property, $allowConstruct, $found, $type, $name, $addName, $removeName, $private, $static, $hasParameter)
384+
{
385+
$writeMutator = $this->extractor->getWriteMutator($class, $property, $allowConstruct);
386+
387+
if (!$found) {
388+
$this->assertNull($writeMutator);
389+
390+
return;
391+
}
392+
393+
$this->assertNotNull($writeMutator);
394+
$this->assertSame($type, $writeMutator->getType());
395+
$this->assertSame($name, $writeMutator->getName());
396+
$this->assertSame($addName, $writeMutator->getAdderName());
397+
$this->assertSame($removeName, $writeMutator->getRemoverName());
398+
$this->assertSame($private, $writeMutator->isPrivate());
399+
$this->assertSame($static, $writeMutator->isStatic());
400+
401+
if ($hasParameter) {
402+
$this->assertNotNull($writeMutator->getConstructorParameter());
403+
} else {
404+
$this->assertNull($writeMutator->getConstructorParameter());
405+
}
406+
}
407+
408+
public function writeMutatorProvider(): array
409+
{
410+
return [
411+
[Dummy::class, 'bar', false, true, ReadAccessor::TYPE_PROPERTY, 'bar', null, null, true, false, false],
412+
[Dummy::class, 'baz', false, true, ReadAccessor::TYPE_PROPERTY, 'baz', null, null, true, false, false],
413+
[Dummy::class, 'bal', false, true, ReadAccessor::TYPE_PROPERTY, 'bal', null, null, false, false, false],
414+
[Dummy::class, 'parent', false, true, ReadAccessor::TYPE_PROPERTY, 'parent', null, null, false, false, false],
415+
[Dummy::class, 'staticSetter', false, true, ReadAccessor::TYPE_METHOD, 'staticSetter', null, null, false, true, false],
416+
[Dummy::class, 'foo', false, true, ReadAccessor::TYPE_PROPERTY, 'foo', null, null, false, false, false],
417+
[Php71Dummy::class, 'bar', false, true, WriteMutator::TYPE_METHOD, 'setBar', null, null, false, false, false],
418+
[Php71Dummy::class, 'string', false, false, '', '', null, null, false, false, false],
419+
[Php71Dummy::class, 'string', true, true, WriteMutator::TYPE_CONSTRUCTOR, 'string', null, null, false, false, true],
420+
[Php71Dummy::class, 'baz', false, true, WriteMutator::TYPE_ADDER_REMOVER_METHOD, null, 'addBaz', 'removeBaz', false, false, false],
421+
[Php71DummyExtended::class, 'bar', false, true, WriteMutator::TYPE_METHOD, 'setBar', null, null, false, false, false],
422+
[Php71DummyExtended::class, 'string', false, false, -1, '', null, null, false, false, false],
423+
[Php71DummyExtended::class, 'string', true, true, WriteMutator::TYPE_CONSTRUCTOR, 'string', null, null, false, false, true],
424+
[Php71DummyExtended::class, 'baz', false, true, WriteMutator::TYPE_ADDER_REMOVER_METHOD, null, 'addBaz', 'removeBaz', false, false, false],
425+
[Php71DummyExtended2::class, 'bar', false, true, WriteMutator::TYPE_METHOD, 'setBar', null, null, false, false, false],
426+
[Php71DummyExtended2::class, 'string', false, false, '', '', null, null, false, false, false],
427+
[Php71DummyExtended2::class, 'string', true, false, '', '', null, null, false, false, false],
428+
[Php71DummyExtended2::class, 'baz', false, true, WriteMutator::TYPE_ADDER_REMOVER_METHOD, null, 'addBaz', 'removeBaz', false, false, false],
429+
];
430+
}
341431
}

src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php71Dummy.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ public function setBar(?int $bar)
3535
public function addBaz(string $baz)
3636
{
3737
}
38+
39+
public function removeBaz(string $baz)
40+
{
41+
}
3842
}
3943

4044
class Php71DummyExtended extends Php71Dummy

0 commit comments

Comments
 (0)
0