From 02387b77e91b531b73c848fab000df7f195b9f93 Mon Sep 17 00:00:00 2001 From: Julien JOYE Date: Thu, 13 Jun 2019 18:22:21 +0300 Subject: [PATCH] fix(issue/8): Implement an alternative SourceContextProvider for the DisplayDumpLocationAction --- .../DisplayDumpLocationAction.php | 28 +++- .../SourceContextProvider.php | 126 ++++++++++++++++++ .../DisplayDumpLocationActionTest.php | 5 +- .../DisplayDumpLocationActionTest.php | 24 +++- 4 files changed, 166 insertions(+), 17 deletions(-) create mode 100644 src/Action/DisplayDumpLocation/SourceContextProvider.php diff --git a/src/Action/DisplayDumpLocation/DisplayDumpLocationAction.php b/src/Action/DisplayDumpLocation/DisplayDumpLocationAction.php index 5dda335..3d02228 100644 --- a/src/Action/DisplayDumpLocation/DisplayDumpLocationAction.php +++ b/src/Action/DisplayDumpLocation/DisplayDumpLocationAction.php @@ -15,6 +15,7 @@ use Ekino\Drupal\Debug\Action\EventSubscriberActionInterface; use Ekino\Drupal\Debug\Kernel\Event\DebugKernelEvents; +use Ekino\Drupal\Debug\Action\DisplayDumpLocation\SourceContextProvider as BackPortedSourceContextProvider; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider; @@ -35,16 +36,13 @@ public static function getSubscribedEvents(): array public function process(): void { - if (!\class_exists(SourceContextProvider::class)) { - return; - } - $cloner = new VarCloner(); $dumper = \in_array(\PHP_SAPI, array('cli', 'phpdbg'), true) ? new CliDumper() : new HtmlDumper(); + $sourceContextProvider = $this->getSourceContextProvider(); - VarDumper::setHandler(function ($var) use ($cloner, $dumper): void { - (function (): void { - list('name' => $name, 'file' => $file, 'line' => $line) = (new SourceContextProvider())->getContext(); + VarDumper::setHandler(function ($var) use ($cloner, $dumper, $sourceContextProvider ): void { + (function () use ($sourceContextProvider) : void { + list('name' => $name, 'file' => $file, 'line' => $line) = $sourceContextProvider->getContext(); $attr = array(); if ($this instanceof HtmlDumper) { @@ -66,4 +64,20 @@ public function process(): void $dumper->dump($cloner->cloneVar($var)); }); } + + /** + * Get the Source Context Provider. + * It will return an instance of the SourceContextProvider if existing. + * Otherwise, it will return an instance of + * the BackPortedSourceContextProvider. + */ + private function getSourceContextProvider() + { + if (!\class_exists(SourceContextProvider::class)) { + return new BackPortedSourceContextProvider(); + } + + return new SourceContextProvider(); + } + } diff --git a/src/Action/DisplayDumpLocation/SourceContextProvider.php b/src/Action/DisplayDumpLocation/SourceContextProvider.php new file mode 100644 index 0000000..828ced4 --- /dev/null +++ b/src/Action/DisplayDumpLocation/SourceContextProvider.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ekino\Drupal\Debug\Action\DisplayDumpLocation; + +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; +use Symfony\Component\VarDumper\VarDumper; +use Twig\Template; + +/** + * Tries to provide context from sources (class name, file, line, code excerpt, ...). + * + * @author Nicolas Grekas + * @author Maxime Steinhausser + */ +final class SourceContextProvider +{ + private $limit; + private $charset; + private $projectDir; + private $fileLinkFormatter; + + public function __construct(string $charset = null, string $projectDir = null, FileLinkFormatter $fileLinkFormatter = null, int $limit = 9) + { + $this->charset = $charset; + $this->projectDir = $projectDir; + $this->fileLinkFormatter = $fileLinkFormatter; + $this->limit = $limit; + } + + public function getContext(): ?array + { + $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, $this->limit); + + $file = $trace[1]['file']; + $line = $trace[1]['line']; + $name = false; + $fileExcerpt = false; + + for ($i = 2; $i < $this->limit; ++$i) { + if (isset($trace[$i]['class'], $trace[$i]['function']) + && 'dump' === $trace[$i]['function'] + && VarDumper::class === $trace[$i]['class'] + ) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + while (++$i < $this->limit) { + if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && 0 !== strpos($trace[$i]['function'], 'call_user_func')) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + break; + } elseif (isset($trace[$i]['object']) && $trace[$i]['object'] instanceof Template) { + $template = $trace[$i]['object']; + $name = $template->getTemplateName(); + $src = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getCode() : (method_exists($template, 'getSource') ? $template->getSource() : false); + $info = $template->getDebugInfo(); + if (isset($info[$trace[$i - 1]['line']])) { + $line = $info[$trace[$i - 1]['line']]; + $file = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getPath() : null; + + if ($src) { + $src = explode("\n", $src); + $fileExcerpt = []; + + for ($i = max($line - 3, 1), $max = min($line + 3, \count($src)); $i <= $max; ++$i) { + $fileExcerpt[] = ''.$this->htmlEncode($src[$i - 1]).''; + } + + $fileExcerpt = '
    '.implode("\n", $fileExcerpt).'
'; + } + } + break; + } + } + break; + } + } + + if (false === $name) { + $name = str_replace('\\', '/', $file); + $name = substr($name, strrpos($name, '/') + 1); + } + + $context = ['name' => $name, 'file' => $file, 'line' => $line]; + $context['file_excerpt'] = $fileExcerpt; + + if (null !== $this->projectDir) { + $context['project_dir'] = $this->projectDir; + if (0 === strpos($file, $this->projectDir)) { + $context['file_relative'] = ltrim(substr($file, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); + } + } + + if ($this->fileLinkFormatter && $fileLink = $this->fileLinkFormatter->format($context['file'], $context['line'])) { + $context['file_link'] = $fileLink; + } + + return $context; + } + + private function htmlEncode(string $s): string + { + $html = ''; + + $dumper = new HtmlDumper(function ($line) use (&$html) { $html .= $line; }, $this->charset); + $dumper->setDumpHeader(''); + $dumper->setDumpBoundaries('', ''); + + $cloner = new VarCloner(); + $dumper->dump($cloner->cloneVar($s)); + + return substr(strip_tags($html), 1, -1); + } +} diff --git a/tests/Integration/Action/DisplayDumpLocation/DisplayDumpLocationActionTest.php b/tests/Integration/Action/DisplayDumpLocation/DisplayDumpLocationActionTest.php index ab98310..d7ee265 100644 --- a/tests/Integration/Action/DisplayDumpLocation/DisplayDumpLocationActionTest.php +++ b/tests/Integration/Action/DisplayDumpLocation/DisplayDumpLocationActionTest.php @@ -31,10 +31,7 @@ protected function doTestInitialBehaviorWithDrupalKernel(Client $client): void */ protected function doTestTargetedBehaviorWithDebugKernel(Client $client): void { - $this->assertThat($this->getDumpText($client), $this->logicalOr( - $this->identicalTo("add_dump_die.module on line 5:\n\"fcy\"\n"), - $this->identicalTo("\"fcy\"\n") - )); + $this->assertSame("add_dump_die.module on line 5:\n\"fcy\"\n", $this->getDumpText($client)); } /** diff --git a/tests/Unit/src/Action/DisplayDumpLocation/DisplayDumpLocationActionTest.php b/tests/Unit/src/Action/DisplayDumpLocation/DisplayDumpLocationActionTest.php index 0cf924f..63f5e51 100644 --- a/tests/Unit/src/Action/DisplayDumpLocation/DisplayDumpLocationActionTest.php +++ b/tests/Unit/src/Action/DisplayDumpLocation/DisplayDumpLocationActionTest.php @@ -14,6 +14,7 @@ namespace Ekino\Drupal\Debug\Tests\Unit\Action\DisplayDumpLocation; use Ekino\Drupal\Debug\Action\DisplayDumpLocation\DisplayDumpLocationAction; +use Ekino\Drupal\Debug\Action\DisplayDumpLocation\SourceContextProvider as BackPortedSourceContextProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider; use Symfony\Component\VarDumper\VarDumper; @@ -30,13 +31,24 @@ public function testGetSubscribedEvents(): void public function testProcess(): void { VarDumper::setHandler(null); - (new DisplayDumpLocationAction())->process(); + $this->assertAttributeInstanceOf(\Closure::class, 'handler', VarDumper::class); + } + + public function testGetSourceContextProvider(): void + { + $expectedSourceContextProviderClass = (!\class_exists(SourceContextProvider::class)) ? + BackPortedSourceContextProvider::class : + SourceContextProvider::class; + + $displayDumpLocationAction = new DisplayDumpLocationAction(); + $displayDumpLocationActionReflector = new \ReflectionClass(DisplayDumpLocationAction::class); + + $getSourceContextProviderMethod = $displayDumpLocationActionReflector->getMethod('getSourceContextProvider'); + $getSourceContextProviderMethod->setAccessible( true ); + + $sourceContextProvider = $getSourceContextProviderMethod->invoke($displayDumpLocationAction); - if (!\class_exists(SourceContextProvider::class)) { - $this->assertAttributeInternalType('null', 'handler', VarDumper::class); - } else { - $this->assertAttributeInstanceOf(\Closure::class, 'handler', VarDumper::class); - } + $this->assertInstanceOf($expectedSourceContextProviderClass, $sourceContextProvider); } }