Skip to content

Commit

Permalink
feature #19826 [VarDumper] Add ClassStub for clickable & shorter PHP …
Browse files Browse the repository at this point in the history
…identifiers (nicolas-grekas)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[VarDumper] Add ClassStub for clickable & shorter PHP identifiers

| Q             | A
| ------------- | ---
| Branch?       | master
| New feature?  | yes
| Tests pass?   | yes
| License       | MIT
| Doc PR        | symfony/symfony-docs#6946

Tells dumpers when PHP identifiers are used so that they can shorten the namespace and create IDE links to the source.

![capture du 2016-09-02 17-07-06](https://cloud.githubusercontent.com/assets/243674/18208461/df2c9684-712f-11e6-9fea-de13e21f86c3.png)

(PR also embeds some fixes/cleanups)

Commits
-------

788f7e8 [VarDumper] Add ClassStub for clickable & shorter PHP identifiers
  • Loading branch information
fabpot committed Sep 2, 2016
2 parents 3521105 + 788f7e8 commit 8ca77f9
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 43 deletions.
75 changes: 75 additions & 0 deletions src/Symfony/Component/VarDumper/Caster/ClassStub.php
@@ -0,0 +1,75 @@
<?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\Caster;

/**
* Represents a PHP class identifier.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ClassStub extends ConstStub
{
/**
* Constructor.
*
* @param string A PHP identifier, e.g. a class, method, interface, etc. name
* @param callable The callable targeted by the identifier when it is ambiguous or not a real PHP identifier
*/
public function __construct($identifier, $callable = null)
{
$this->value = $identifier;

if (0 < $i = strrpos($identifier, '\\')) {
$this->attr['ellipsis'] = strlen($identifier) - $i;
}

if (null !== $callable) {
if ($callable instanceof \Closure) {
$r = new \ReflectionFunction($callable);
} elseif (is_object($callable)) {
$r = new \ReflectionMethod($callable, '__invoke');
} elseif (is_array($callable)) {
$r = new \ReflectionMethod($callable[0], $callable[1]);
} elseif (false !== $i = strpos($callable, '::')) {
$r = new \ReflectionMethod(substr($callable, 0, $i), substr($callable, 2 + $i));
} else {
$r = new \ReflectionFunction($callable);
}
} elseif (false !== $i = strpos($identifier, '::')) {
$r = new \ReflectionMethod(substr($identifier, 0, $i), substr($identifier, 2 + $i));
} else {
$r = new \ReflectionClass($identifier);
}

if ($f = $r->getFileName()) {
$this->attr['file'] = $f;
$this->attr['line'] = $r->getStartLine() - substr_count($r->getDocComment(), "\n");
}
}

public static function wrapCallable($callable)
{
if (is_object($callable) || !is_callable($callable)) {
return $callable;
}

if (!is_array($callable)) {
$callable = new static($callable);
} elseif (is_string($callable[0])) {
$callable[0] = new static($callable[0]);
} else {
$callable[1] = new static($callable[1], $callable);
}

return $callable;
}
}
5 changes: 5 additions & 0 deletions src/Symfony/Component/VarDumper/Caster/ConstStub.php
Expand Up @@ -25,4 +25,9 @@ public function __construct($name, $value)
$this->class = $name;
$this->value = $value;
}

public function __toString()
{
return (string) $this->value;
}
}
29 changes: 14 additions & 15 deletions src/Symfony/Component/VarDumper/Caster/LinkStub.php
Expand Up @@ -18,30 +18,29 @@
*/
class LinkStub extends ConstStub
{
public function __construct($file, $line = 0)
public function __construct($label, $line = 0, $href = null)
{
$this->value = $file;
$this->value = $label;

if (is_string($file)) {
$this->type = self::TYPE_STRING;
$this->class = preg_match('//u', $file) ? self::STRING_UTF8 : self::STRING_BINARY;

if (0 === strpos($file, 'file://')) {
$file = substr($file, 7);
} elseif (false !== strpos($file, '://')) {
$this->attr['href'] = $file;
if (null === $href) {
$href = $label;
}
if (is_string($href)) {
if (0 === strpos($href, 'file://')) {
$href = substr($href, 7);
} elseif (false !== strpos($href, '://')) {
$this->attr['href'] = $href;

return;
}
if (file_exists($file)) {
if (file_exists($href)) {
if ($line) {
$this->attr['line'] = $line;
}
$this->attr['file'] = realpath($file);
$this->attr['file'] = realpath($href);

if ($this->attr['file'] === $file) {
$ellipsis = explode(DIRECTORY_SEPARATOR, $file);
$this->attr['ellipsis'] = 3 < count($ellipsis) ? 2 + strlen(implode(array_slice($ellipsis, -2))) : 0;
if ($this->attr['file'] === $href && 3 < count($ellipsis = explode(DIRECTORY_SEPARATOR, $href))) {
$this->attr['ellipsis'] = 2 + strlen(implode(array_slice($ellipsis, -2)));
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Symfony/Component/VarDumper/Caster/PdoCaster.php
Expand Up @@ -77,6 +77,12 @@ public static function castPdo(\PDO $c, array $a, Stub $stub, $isNested)
} catch (\Exception $e) {
}
}
if (isset($attr[$k = 'STATEMENT_CLASS'][1])) {
if ($attr[$k][1]) {
$attr[$k][1] = new ArgsStub($attr[$k][1], '__construct', $attr[$k][0]);
}
$attr[$k][0] = new ClassStub($attr[$k][0]);
}

$prefix = Caster::PREFIX_VIRTUAL;
$a += array(
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/VarDumper/Caster/RedisCaster.php
Expand Up @@ -69,7 +69,7 @@ public static function castRedisArray(\RedisArray $c, array $a, Stub $stub, $isN

return $a + array(
$prefix.'hosts' => $c->_hosts(),
$prefix.'function' => $c->_function(),
$prefix.'function' => ClassStub::wrapCallable($c->_function()),
);
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/VarDumper/Caster/SplCaster.php
Expand Up @@ -36,7 +36,7 @@ public static function castArrayObject(\ArrayObject $c, array $a, Stub $stub, $i
$b = array(
$prefix.'flag::STD_PROP_LIST' => (bool) ($flags & \ArrayObject::STD_PROP_LIST),
$prefix.'flag::ARRAY_AS_PROPS' => (bool) ($flags & \ArrayObject::ARRAY_AS_PROPS),
$prefix.'iteratorClass' => $c->getIteratorClass(),
$prefix.'iteratorClass' => new ClassStub($c->getIteratorClass()),
$prefix.'storage' => $c->getArrayCopy(),
);

Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/VarDumper/Caster/StubCaster.php
Expand Up @@ -30,6 +30,11 @@ public static function castStub(Stub $c, array $a, Stub $stub, $isNested)
$stub->cut = $c->cut;
$stub->attr = $c->attr;

if (Stub::TYPE_REF === $c->type && !$c->class && is_string($c->value) && !preg_match('//u', $c->value)) {
$stub->type = self::TYPE_STRING;
$stub->class = self::STRING_BINARY;
}

return array();
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php
Expand Up @@ -177,6 +177,9 @@ protected function echoLine($line, $depth, $indentPad)
*/
protected function utf8Encode($s)
{
if (preg_match('//u', $s)) {
return $s;
}
if (false !== $c = @iconv($this->charset, 'UTF-8', $s)) {
return $c;
}
Expand Down
8 changes: 3 additions & 5 deletions src/Symfony/Component/VarDumper/Dumper/CliDumper.php
Expand Up @@ -148,8 +148,8 @@ public function dumpScalar(Cursor $cursor, $type, $value)
break;

default:
$attr += array('value' => isset($value[0]) && !preg_match('//u', $value) ? $this->utf8Encode($value) : $value);
$value = isset($type[0]) && !preg_match('//u', $type) ? $this->utf8Encode($type) : $type;
$attr += array('value' => $this->utf8Encode($value));
$value = $this->utf8Encode($type);
break;
}

Expand Down Expand Up @@ -249,9 +249,7 @@ public function enterHash(Cursor $cursor, $type, $class, $hasChild)
{
$this->dumpKey($cursor);

if (!preg_match('//u', $class)) {
$class = $this->utf8Encode($class);
}
$class = $this->utf8Encode($class);
if (Cursor::HASH_OBJECT === $type) {
$prefix = $class && 'stdClass' !== $class ? $this->style('note', $class).' {' : '{';
} elseif (Cursor::HASH_RESOURCE === $type) {
Expand Down
8 changes: 4 additions & 4 deletions src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php
Expand Up @@ -481,9 +481,9 @@ protected function style($style, $value, $attr = array())
} elseif ('protected' === $style) {
$style .= ' title="Protected property"';
} elseif ('meta' === $style && isset($attr['title'])) {
$style .= sprintf(' title="%s"', esc($attr['title']));
$style .= sprintf(' title="%s"', esc($this->utf8Encode($attr['title'])));
} elseif ('private' === $style) {
$style .= sprintf(' title="Private property defined in class:&#10;`%s`"', esc($attr['class']));
$style .= sprintf(' title="Private property defined in class:&#10;`%s`"', esc($this->utf8Encode($attr['class'])));
}
$map = static::$controlCharsMap;
$style = "<span class=sf-dump-{$style}>";
Expand Down Expand Up @@ -515,9 +515,9 @@ protected function style($style, $value, $attr = array())
$v .= '</span>';
}
if (isset($attr['file'])) {
$v = sprintf('<a data-file="%s" data-line="%d">%s</a>', esc($attr['file']), isset($attr['line']) ? $attr['line'] : 1, $v);
$v = sprintf('<a data-file="%s" data-line="%d">%s</a>', esc($this->utf8Encode($attr['file'])), isset($attr['line']) ? $attr['line'] : 1, $v);
} elseif (isset($attr['href'])) {
$v = sprintf('<a href="%s">%s</a>', esc($attr['href']), $v);
$v = sprintf('<a href="%s">%s</a>', esc($this->utf8Encode($attr['href'])), $v);
}
if (isset($attr['lang'])) {
$v = sprintf('<code class="%s">%s</code>', esc($attr['lang']), $v);
Expand Down
40 changes: 23 additions & 17 deletions src/Symfony/Component/VarDumper/Tests/Caster/PdoCasterTest.php
Expand Up @@ -13,12 +13,15 @@

use Symfony\Component\VarDumper\Caster\PdoCaster;
use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\VarDumper\Test\VarDumperTestTrait;

/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class PdoCasterTest extends \PHPUnit_Framework_TestCase
{
use VarDumperTestTrait;

/**
* @requires extension pdo_sqlite
*/
Expand All @@ -36,22 +39,25 @@ public function testCastPdo()
$this->assertSame('NATURAL', $attr['CASE']->class);
$this->assertSame('BOTH', $attr['DEFAULT_FETCH_MODE']->class);

$xCast = array(
"\0~\0inTransaction" => $pdo->inTransaction(),
"\0~\0attributes" => array(
'CASE' => $attr['CASE'],
'ERRMODE' => $attr['ERRMODE'],
'PERSISTENT' => false,
'DRIVER_NAME' => 'sqlite',
'ORACLE_NULLS' => $attr['ORACLE_NULLS'],
'CLIENT_VERSION' => $pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION),
'SERVER_VERSION' => $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION),
'STATEMENT_CLASS' => array('PDOStatement'),
'DEFAULT_FETCH_MODE' => $attr['DEFAULT_FETCH_MODE'],
),
);
unset($cast["\0~\0attributes"]['STATEMENT_CLASS'][1]);

$this->assertSame($xCast, $cast);
$xDump = <<<'EODUMP'
array:2 [
"\x00~\x00inTransaction" => false
"\x00~\x00attributes" => array:9 [
"CASE" => NATURAL
"ERRMODE" => SILENT
"PERSISTENT" => false
"DRIVER_NAME" => "sqlite"
"ORACLE_NULLS" => NATURAL
"CLIENT_VERSION" => "%s"
"SERVER_VERSION" => "%s"
"STATEMENT_CLASS" => array:%d [
0 => "PDOStatement"%A
]
"DEFAULT_FETCH_MODE" => BOTH
]
]
EODUMP;

$this->assertDumpMatchesFormat($xDump, $cast);
}
}
24 changes: 24 additions & 0 deletions src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php
Expand Up @@ -12,7 +12,11 @@
namespace Symfony\Component\VarDumper\Tests\Caster;

use Symfony\Component\VarDumper\Caster\ArgsStub;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Symfony\Component\VarDumper\Test\VarDumperTestTrait;
use Symfony\Component\VarDumper\Tests\Fixtures\FooInterface;

class StubCasterTest extends \PHPUnit_Framework_TestCase
{
Expand Down Expand Up @@ -80,4 +84,24 @@ public function testArgsStubWithClosure()

$this->assertDumpMatchesFormat($expectedDump, $args);
}

public function testClassStub()
{
$var = array(new ClassStub('hello', array(FooInterface::class, 'foo')));

$cloner = new VarCloner();
$dumper = new HtmlDumper();
$dumper->setDumpHeader('<foo></foo>');
$dumper->setDumpBoundaries('<bar>', '</bar>');
$dump = $dumper->dump($cloner->cloneVar($var), true);

$expectedDump = <<<'EODUMP'
<foo></foo><bar><span class=sf-dump-note>array:1</span> [<samp>
<span class=sf-dump-index>0</span> => "<a data-file="%sFooInterface.php" data-line="8"><span class=sf-dump-str title="5 characters">hello</span></a>"
</samp>]
</bar>
EODUMP;

$this->assertStringMatchesFormat($expectedDump, $dump);
}
}
11 changes: 11 additions & 0 deletions src/Symfony/Component/VarDumper/Tests/Fixtures/FooInterface.php
@@ -0,0 +1,11 @@
<?php

namespace Symfony\Component\VarDumper\Tests\Fixtures;

interface FooInterface
{
/**
* Hello.
*/
public function foo();
}

0 comments on commit 8ca77f9

Please sign in to comment.