feature #38167 [VarDumper] Support for ReflectionAttribute (derrabus)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[VarDumper] Support for ReflectionAttribute

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | N/A
| License       | MIT
| Doc PR        | not needed

VarDumper currently does not understand that certain reflection objects might have attributes attached to it. Dumping a `ReflectionAttribute` just yields `ReflectionAttribute {#4711}` which is not really helpful. This PR attempts to fix this.

```
ReflectionAttribute {#4711
  name: "App\MyAttribute"
  arguments: array:2 [
    0 => "one"
    "extra" => "hello"
  ]
}
```

While working on this, I noticed that class constants (which can be reflected on since PHP 7.1) are just dumped as plain values, so I've also added a caster for `ReflectionClasConstant` as bonus.

The full output for the `LotsOfAttributes` fixture class that is included with is PR looks like this:

<details>

```
^ ReflectionClass {#7
  +name: "Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes"
  modifiers: "final"
  implements: []
  constants: array:1 [
    0 => ReflectionClassConstant {#20
      +name: "SOME_CONSTANT"
      +class: "Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes"
      modifiers: "public"
      value: "some value"
      attributes: array:2 [
        0 => ReflectionAttribute {#33
          name: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute"
          arguments: array:1 [
            0 => "one"
          ]
        }
        1 => ReflectionAttribute {#34
          name: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute"
          arguments: array:1 [
            0 => "two"
          ]
        }
      ]
    }
  ]
  properties: array:1 [
    "someProperty" => ReflectionProperty {#19
      +name: "someProperty"
      +class: "Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes"
      modifiers: "private"
      attributes: array:1 [
        0 => ReflectionAttribute {#30
          name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute"
          arguments: array:2 [
            0 => "one"
            "extra" => "hello"
          ]
        }
      ]
    }
  ]
  methods: array:1 [
    "someMethod" => ReflectionMethod {#21
      +name: "someMethod"
      +class: "Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes"
      returnType: "void"
      parameters: {
        $someParameter: ReflectionParameter {#28
          +name: "someParameter"
          position: 0
          attributes: array:1 [
            0 => ReflectionAttribute {#42
              name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute"
              arguments: array:1 [
                0 => "three"
              ]
            }
          ]
          typeHint: "string"
        }
      }
      attributes: array:1 [
        0 => ReflectionAttribute {#27
          name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute"
          arguments: array:1 [
            0 => "two"
          ]
        }
      ]
      modifiers: "public"
    }
  ]
  attributes: array:1 [
    0 => ReflectionAttribute {#22
      name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute"
      arguments: []
    }
  ]
  extra: {
    file: "./src/Symfony/Component/VarDumper/Tests/Fixtures/LotsOfAttributes.php"
    line: "15 to 28"
    isUserDefined: true
  }
}
```

</details>

Commits
-------

34dbf01618 [VarDumper] Support for ReflectionAttribute.
This commit is contained in:
Fabien Potencier 2020-09-19 07:56:36 +02:00
commit 772547ec02
7 changed files with 285 additions and 6 deletions

View File

@ -35,8 +35,11 @@ foreach ($loader->getClassMap() as $class => $file) {
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php'):
case false !== strpos($file, '/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures'):
case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'):
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/LotsOfAttributes.php'):
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/MyAttribute.php'):
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'):
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/Php74.php') && \PHP_VERSION_ID < 70400:
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/RepeatableAttribute.php'):
continue 2;
}

View File

@ -105,6 +105,16 @@ class ReflectionCaster
return $a;
}
public static function castAttribute(\ReflectionAttribute $c, array $a, Stub $stub, bool $isNested)
{
self::addMap($a, $c, [
'name' => 'getName',
'arguments' => 'getArguments',
]);
return $a;
}
public static function castReflectionGenerator(\ReflectionGenerator $c, array $a, Stub $stub, bool $isNested)
{
$prefix = Caster::PREFIX_VIRTUAL;
@ -151,7 +161,7 @@ class ReflectionCaster
self::addMap($a, $c, [
'extends' => 'getParentClass',
'implements' => 'getInterfaceNames',
'constants' => 'getConstants',
'constants' => 'getReflectionConstants',
]);
foreach ($c->getProperties() as $n) {
@ -162,6 +172,8 @@ class ReflectionCaster
$a[$prefix.'methods'][$n->name] = $n;
}
self::addAttributes($a, $c, $prefix);
if (!($filter & Caster::EXCLUDE_VERBOSE) && !$isNested) {
self::addExtra($a, $c);
}
@ -206,6 +218,8 @@ class ReflectionCaster
$a[$prefix.'parameters'] = new EnumStub($a[$prefix.'parameters']);
}
self::addAttributes($a, $c, $prefix);
if (!($filter & Caster::EXCLUDE_VERBOSE) && $v = $c->getStaticVariables()) {
foreach ($v as $k => &$v) {
if (\is_object($v)) {
@ -225,6 +239,16 @@ class ReflectionCaster
return $a;
}
public static function castClassConstant(\ReflectionClassConstant $c, array $a, Stub $stub, bool $isNested)
{
$a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers()));
$a[Caster::PREFIX_VIRTUAL.'value'] = $c->getValue();
self::addAttributes($a, $c);
return $a;
}
public static function castMethod(\ReflectionMethod $c, array $a, Stub $stub, bool $isNested)
{
$a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers()));
@ -243,6 +267,8 @@ class ReflectionCaster
'allowsNull' => 'allowsNull',
]);
self::addAttributes($a, $c, $prefix);
if ($v = $c->getType()) {
$a[$prefix.'typeHint'] = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v;
}
@ -271,6 +297,8 @@ class ReflectionCaster
public static function castProperty(\ReflectionProperty $c, array $a, Stub $stub, bool $isNested)
{
$a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers()));
self::addAttributes($a, $c);
self::addExtra($a, $c);
return $a;
@ -377,7 +405,7 @@ class ReflectionCaster
}
}
private static function addMap(array &$a, \Reflector $c, array $map, string $prefix = Caster::PREFIX_VIRTUAL)
private static function addMap(array &$a, object $c, array $map, string $prefix = Caster::PREFIX_VIRTUAL)
{
foreach ($map as $k => $m) {
if (\PHP_VERSION_ID >= 80000 && 'isDisabled' === $k) {
@ -389,4 +417,13 @@ class ReflectionCaster
}
}
}
private static function addAttributes(array &$a, \Reflector $c, string $prefix = Caster::PREFIX_VIRTUAL): void
{
if (\PHP_VERSION_ID >= 80000) {
foreach ($c->getAttributes() as $n) {
$a[$prefix.'attributes'][] = $n;
}
}
}
}

View File

@ -32,8 +32,10 @@ abstract class AbstractCloner implements ClonerInterface
'Closure' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castClosure'],
'Generator' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castGenerator'],
'ReflectionType' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castType'],
'ReflectionAttribute' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castAttribute'],
'ReflectionGenerator' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castReflectionGenerator'],
'ReflectionClass' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castClass'],
'ReflectionClassConstant' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castClassConstant'],
'ReflectionFunctionAbstract' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castFunctionAbstract'],
'ReflectionMethod' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castMethod'],
'ReflectionParameter' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castParameter'],

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Test\VarDumperTestTrait;
use Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo;
use Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes;
use Symfony\Component\VarDumper\Tests\Fixtures\NotLoadableClass;
/**
@ -36,9 +37,24 @@ ReflectionClass {
0 => "Reflector"
%A]
constants: array:3 [
"IS_IMPLICIT_ABSTRACT" => 16
"IS_EXPLICIT_ABSTRACT" => %d
"IS_FINAL" => %d
0 => ReflectionClassConstant {
+name: "IS_IMPLICIT_ABSTRACT"
+class: "ReflectionClass"
modifiers: "public"
value: 16
}
1 => ReflectionClassConstant {
+name: "IS_EXPLICIT_ABSTRACT"
+class: "ReflectionClass"
modifiers: "public"
value: %d
}
2 => ReflectionClassConstant {
+name: "IS_FINAL"
+class: "ReflectionClass"
modifiers: "public"
value: %d
}
]
properties: array:%d [
"name" => ReflectionProperty {
@ -75,7 +91,7 @@ Closure($x) {
$b: & 123
}
file: "%sReflectionCasterTest.php"
line: "68 to 68"
line: "84 to 84"
}
EOTXT
, $var
@ -242,6 +258,135 @@ EODUMP;
$this->assertDumpMatchesFormat($expectedDump, $generator);
}
/**
* @requires PHP 8
*/
public function testReflectionClassWithAttribute()
{
$var = new \ReflectionClass(LotsOfAttributes::class);
$this->assertDumpMatchesFormat(<<< 'EOTXT'
ReflectionClass {
+name: "Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes"
%A attributes: array:1 [
0 => ReflectionAttribute {
name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute"
arguments: []
}
]
%A
}
EOTXT
, $var);
}
/**
* @requires PHP 8
*/
public function testReflectionMethodWithAttribute()
{
$var = new \ReflectionMethod(LotsOfAttributes::class, 'someMethod');
$this->assertDumpMatchesFormat(<<< 'EOTXT'
ReflectionMethod {
+name: "someMethod"
+class: "Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes"
%A attributes: array:1 [
0 => ReflectionAttribute {
name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute"
arguments: array:1 [
0 => "two"
]
}
]
%A
}
EOTXT
, $var);
}
/**
* @requires PHP 8
*/
public function testReflectionPropertyWithAttribute()
{
$var = new \ReflectionProperty(LotsOfAttributes::class, 'someProperty');
$this->assertDumpMatchesFormat(<<< 'EOTXT'
ReflectionProperty {
+name: "someProperty"
+class: "Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes"
%A attributes: array:1 [
0 => ReflectionAttribute {
name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute"
arguments: array:2 [
0 => "one"
"extra" => "hello"
]
}
]
}
EOTXT
, $var);
}
/**
* @requires PHP 8
*/
public function testReflectionClassConstantWithAttribute()
{
$var = new \ReflectionClassConstant(LotsOfAttributes::class, 'SOME_CONSTANT');
$this->assertDumpMatchesFormat(<<< 'EOTXT'
ReflectionClassConstant {
+name: "SOME_CONSTANT"
+class: "Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes"
modifiers: "public"
value: "some value"
attributes: array:2 [
0 => ReflectionAttribute {
name: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute"
arguments: array:1 [
0 => "one"
]
}
1 => ReflectionAttribute {
name: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute"
arguments: array:1 [
0 => "two"
]
}
]
}
EOTXT
, $var);
}
/**
* @requires PHP 8
*/
public function testReflectionParameterWithAttribute()
{
$var = new \ReflectionParameter([LotsOfAttributes::class, 'someMethod'], 'someParameter');
$this->assertDumpMatchesFormat(<<< 'EOTXT'
ReflectionParameter {
+name: "someParameter"
position: 0
attributes: array:1 [
0 => ReflectionAttribute {
name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute"
arguments: array:1 [
0 => "three"
]
}
]
%A
}
EOTXT
, $var);
}
public static function stub(): void
{
}

View File

@ -0,0 +1,28 @@
<?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\VarDumper\Tests\Fixtures;
#[MyAttribute]
final class LotsOfAttributes
{
#[RepeatableAttribute('one'), RepeatableAttribute('two')]
public const SOME_CONSTANT = 'some value';
#[MyAttribute('one', extra: 'hello')]
private string $someProperty;
#[MyAttribute('two')]
public function someMethod(
#[MyAttribute('three')] string $someParameter
): void {
}
}

View File

@ -0,0 +1,34 @@
<?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\VarDumper\Tests\Fixtures;
use Attribute;
#[Attribute]
final class MyAttribute
{
public function __construct(
private string $foo = 'default',
private ?string $extra = null,
) {
}
public function getFoo(): string
{
return $this->foo;
}
public function getExtra(): ?string
{
return $this->extra;
}
}

View File

@ -0,0 +1,30 @@
<?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\VarDumper\Tests\Fixtures;
use Attribute;
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS_CONST | Attribute::TARGET_PROPERTY)]
final class RepeatableAttribute
{
private string $string;
public function __construct(string $string = 'default')
{
$this->string = $string;
}
public function getString(): string
{
return $this->string;
}
}