Skip to content
Merged
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
1 change: 1 addition & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
->exclude('bin')
->exclude('overrides')
->exclude('vendor')
->notPath('tests/Foundation/fixtures/fake-compiled-view.php')
->in(__DIR__)
)
->setUsingCache(false);
186 changes: 186 additions & 0 deletions src/foundation/src/Concerns/ResolvesDumpSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

declare(strict_types=1);

namespace Hypervel\Foundation\Concerns;

use Throwable;

trait ResolvesDumpSource
{
/**
* All of the href formats for common editors.
*
* @var array<string, string>
*/
protected array $editorHrefs = [
'atom' => 'atom://core/open/file?filename={file}&line={line}',
'cursor' => 'cursor://file/{file}:{line}',
'emacs' => 'emacs://open?url=file://{file}&line={line}',
'idea' => 'idea://open?file={file}&line={line}',
'macvim' => 'mvim://open/?url=file://{file}&line={line}',
'netbeans' => 'netbeans://open/?f={file}:{line}',
'nova' => 'nova://core/open/file?filename={file}&line={line}',
'phpstorm' => 'phpstorm://open?file={file}&line={line}',
'sublime' => 'subl://open?url=file://{file}&line={line}',
'textmate' => 'txmt://open?url=file://{file}&line={line}',
'vscode' => 'vscode://file/{file}:{line}',
'vscode-insiders' => 'vscode-insiders://file/{file}:{line}',
'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/{file}:{line}',
'vscode-remote' => 'vscode://vscode-remote/{file}:{line}',
'vscodium' => 'vscodium://file/{file}:{line}',
'xdebug' => 'xdebug://{file}@{line}',
];

/**
* Files that require special trace handling and their levels.
*
* @var array<string, int>
*/
protected static array $adjustableTraces = [
'symfony/var-dumper/Resources/functions/dump.php' => 1,
];

/**
* The source resolver.
*
* @var null|(callable(): (null|array{0: string, 1: string, 2: null|int}))|false
*/
protected static $dumpSourceResolver;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add type declaration and default value


/**
* Resolve the source of the dump call.
*
* @return null|array{0: string, 1: string, 2: null|int}
*/
public function resolveDumpSource(): ?array
{
if (static::$dumpSourceResolver === false) {
return null;
}

if (static::$dumpSourceResolver) {
return call_user_func(static::$dumpSourceResolver);
}

$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20);

$sourceKey = null;

foreach ($trace as $traceKey => $traceFile) {
if (! isset($traceFile['file'])) {
continue;
}

foreach (self::$adjustableTraces as $name => $key) {
if (str_ends_with(
$traceFile['file'],
str_replace('/', DIRECTORY_SEPARATOR, $name)
)) {
$sourceKey = $traceKey + $key;
break;
}
}

if (! is_null($sourceKey)) {
break;
}
}

if (is_null($sourceKey)) {
return null;
}

$file = $trace[$sourceKey]['file'] ?? null;
$line = $trace[$sourceKey]['line'] ?? null;

if (is_null($file) || is_null($line)) {
return null;
}

$relativeFile = $file;

if ($this->isCompiledViewFile($file)) {
$file = $this->getOriginalFileForCompiledView($file);
$line = null;
}

if (str_starts_with($file, $this->basePath)) {
$relativeFile = substr($file, strlen($this->basePath) + 1);
}

return [$file, $relativeFile, $line];
}

/**
* Determine if the given file is a view compiled.
*/
protected function isCompiledViewFile(string $file): bool
{
return str_starts_with($file, $this->compiledViewPath) && str_ends_with($file, '.php');
}

/**
* Get the original view compiled file by the given compiled file.
*/
protected function getOriginalFileForCompiledView(string $file): string
{
preg_match('/\/\*\*PATH\s(.*)\sENDPATH/', file_get_contents($file), $matches);

if (isset($matches[1])) {
$file = $matches[1];
}

return $file;
}

/**
* Resolve the source href, if possible.
*
* @return null|string|void
*/
protected function resolveSourceHref(string $file, ?int $line)
{
try {
$editor = config('app.editor');
} catch (Throwable) {
// ..
}

if (! isset($editor)) {
return;
}

$href = is_array($editor) && isset($editor['href'])
? $editor['href']
: ($this->editorHrefs[$editor['name'] ?? $editor] ?? sprintf('%s://open?file={file}&line={line}', $editor['name'] ?? $editor));

if ($basePath = $editor['base_path'] ?? false) {
$file = str_replace($this->basePath, $basePath, $file);
}

return str_replace(
['{file}', '{line}'],
[$file, is_null($line) ? 1 : $line],
$href,
);
}

/**
* Set the resolver that resolves the source of the dump call.
*
* @param null|(callable(): (null|array{0: string, 1: string, 2: null|int})) $callable
*/
public static function resolveDumpSourceUsing(?callable $callable): void
{
static::$dumpSourceResolver = $callable;
}

/**
* Don't include the location / file of the dump in dumps.
*/
public static function dontIncludeSource(): void
{
static::$dumpSourceResolver = false;
}
}
103 changes: 103 additions & 0 deletions src/foundation/src/Console/CliDumper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace Hypervel\Foundation\Console;

use Hypervel\Foundation\Concerns\ResolvesDumpSource;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper as BaseCliDumper;
use Symfony\Component\VarDumper\VarDumper;

class CliDumper extends BaseCliDumper
{
use ResolvesDumpSource;

/**
* If the dumper is currently dumping.
*/
protected bool $dumping = false;

/**
* Create a new CLI dumper instance.
*
* @param OutputInterface $output
*/
public function __construct(
protected mixed $output,
protected string $basePath,
protected ?string $compiledViewPath,
) {
parent::__construct();

$this->setColors($this->supportsColors());
}

/**
* Create a new CLI dumper instance and register it as the default dumper.
*
* @param string $basePath
* @param string $compiledViewPath
*/
public static function register($basePath, $compiledViewPath): void
{
$cloner = tap(new VarCloner())->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);

$dumper = new static(new ConsoleOutput(), $basePath, $compiledViewPath);

VarDumper::setHandler(fn ($value) => $dumper->dumpWithSource($cloner->cloneVar($value)));
}

/**
* Dump a variable with its source file / line.
*/
public function dumpWithSource(Data $data): void
{
if ($this->dumping) {
$this->dump($data);

return;
}

$this->dumping = true;

$output = (string) $this->dump($data, true);
$lines = explode("\n", $output);

$lines[array_key_last($lines) - 1] .= $this->getDumpSourceContent();

$this->output->write(implode("\n", $lines));

$this->dumping = false;
}

/**
* Get the dump's source console content.
*/
protected function getDumpSourceContent(): string
{
if (is_null($dumpSource = $this->resolveDumpSource())) {
return '';
}

[$file, $relativeFile, $line] = $dumpSource;

$href = $this->resolveSourceHref($file, $line);

return sprintf(
' <fg=gray>// <fg=gray%s>%s%s</></>',
is_null($href) ? '' : ";href={$href}",
$relativeFile,
is_null($line) ? '' : ":{$line}"
);
}

protected function supportsColors(): bool
{
return $this->output->isDecorated();
}
}
Loading