Skip to content
This repository was archived by the owner on Jan 2, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src/Action/DisplayDumpLocation/DisplayDumpLocationAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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();
}

}
126 changes: 126 additions & 0 deletions src/Action/DisplayDumpLocation/SourceContextProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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 <[email protected]>
* @author Maxime Steinhausser <[email protected]>
*/
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[] = '<li'.($i === $line ? ' class="selected"' : '').'><code>'.$this->htmlEncode($src[$i - 1]).'</code></li>';
}

$fileExcerpt = '<ol start="'.max($line - 3, 1).'">'.implode("\n", $fileExcerpt).'</ol>';
}
}
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}