Skip to content

[9.x] Add Laravel-specific casting to the var dumper #44408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
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
22 changes: 2 additions & 20 deletions src/Illuminate/Foundation/Console/CliDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
namespace Illuminate\Foundation\Console;

use Illuminate\Foundation\Concerns\ResolvesDumpSource;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Illuminate\Foundation\VarDumper\Concerns\HandlesDumps;
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 HandlesDumps;
use ResolvesDumpSource;

/**
Expand Down Expand Up @@ -59,22 +57,6 @@ public function __construct($output, $basePath, $compiledViewPath)
$this->compiledViewPath = $compiledViewPath;
}

/**
* Create a new CLI dumper instance and register it as the default dumper.
*
* @param string $basePath
* @param string $compiledViewPath
* @return void
*/
public static function register($basePath, $compiledViewPath)
{
$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.
*
Expand Down
21 changes: 2 additions & 19 deletions src/Illuminate/Foundation/Http/HtmlDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
namespace Illuminate\Foundation\Http;

use Illuminate\Foundation\Concerns\ResolvesDumpSource;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Illuminate\Foundation\VarDumper\Concerns\HandlesDumps;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\HtmlDumper as BaseHtmlDumper;
use Symfony\Component\VarDumper\VarDumper;

class HtmlDumper extends BaseHtmlDumper
{
use HandlesDumps;
use ResolvesDumpSource;

/**
Expand Down Expand Up @@ -63,22 +62,6 @@ public function __construct($basePath, $compiledViewPath)
$this->compiledViewPath = $compiledViewPath;
}

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

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

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

/**
* Dump a variable with its source file / line.
*
Expand Down
25 changes: 16 additions & 9 deletions src/Illuminate/Foundation/Providers/FoundationServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Illuminate\Testing\LoggedExceptionCollection;
use Illuminate\Testing\ParallelTestingServiceProvider;
use Illuminate\Validation\ValidationException;
use Symfony\Component\Console\Output\ConsoleOutput;

class FoundationServiceProvider extends AggregateServiceProvider
{
Expand Down Expand Up @@ -68,24 +69,30 @@ public function register()
}

/**
* Register an var dumper (with source) to debug variables.
* Register a var dumper (with source and Laravel-specific casters) to debug variables.
*
* @return void
*/
public function registerDumper()
{
$basePath = $this->app->basePath();
$this->app->singleton(HtmlDumper::class, fn () => new HtmlDumper(
$this->app->basePath(),
$this->app['config']->get('view.compiled')
));

$compiledViewPath = $this->app['config']->get('view.compiled');
$this->app->singleton(CliDumper::class, fn () => new CliDumper(
new ConsoleOutput(),
$this->app->basePath(),
$this->app['config']->get('view.compiled')
));

$format = $_SERVER['VAR_DUMPER_FORMAT'] ?? null;

match (true) {
'html' == $format => HtmlDumper::register($basePath, $compiledViewPath),
'cli' == $format => CliDumper::register($basePath, $compiledViewPath),
'server' == $format => null,
$format && 'tcp' == parse_url($format, PHP_URL_SCHEME) => null,
default => in_array(PHP_SAPI, ['cli', 'phpdbg']) ? CliDumper::register($basePath, $compiledViewPath) : HtmlDumper::register($basePath, $compiledViewPath),
match ($format) {
'server', 'tcp' === parse_url((string) $format, PHP_URL_SCHEME) => null,
'html' => $this->app->make(HtmlDumper::class)->register(),
'cli', in_array(PHP_SAPI, ['cli', 'phpdbg']) => $this->app->make(CliDumper::class)->register(),
default => $this->app->make(HtmlDumper::class)->register(),
};
}

Expand Down
95 changes: 95 additions & 0 deletions src/Illuminate/Foundation/VarDumper/Casters/BuilderCaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Illuminate\Foundation\VarDumper\Casters;

use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Foundation\VarDumper\Properties;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use PDO;
use RuntimeException;
use Throwable;

class BuilderCaster extends Caster
{
/**
* @inheritdoc
*/
protected function cast($target, $properties, $stub, $isNested, $filter = 0)
{
$result = new Properties();

$result->putVirtual('sql', $this->formatSql($target));
$result->putProtected('connection', $target->getConnection());

if ($target instanceof EloquentBuilder) {
$result->copyAndCutProtected('model', $properties);
$result->copyProtected('eagerLoad', $properties);
}

if ($target instanceof Relation) {
$result->copyAndCutProtected('parent', $properties);
$result->copyAndCutProtected('related', $properties);
}

$result->applyCutsToStub($stub, $properties);

return $result->all();
}

/**
* Merge the bindings into the SQL statement for easier debugging.
*
* @param \Illuminate\Contracts\Database\Query\Builder $builder
* @return string|array
*/
protected function formatSql($builder)
{
$sql = $builder->toSql();
$bindings = Arr::flatten($builder->getBindings());

try {
$pdo = $this->getPdoFromBuilder($builder);

$formatted = preg_replace_callback('/(?<!\?)\?(?!\?)/', function () use ($pdo, &$bindings) {
if (0 === count($bindings)) {
throw new RuntimeException('Too few bindings.');
}

return $pdo->quote(array_shift($bindings));
}, $sql);

if (count($bindings)) {
throw new RuntimeException('Too many bindings.');
}

return $formatted;
} catch (Throwable) {
return compact('sql', 'bindings');
}
}

/**
* Get the underlying PDO connection from the Builder instance.
*
* @param \Illuminate\Contracts\Database\Query\Builder $builder
* @return \PDO
*/
protected function getPdoFromBuilder($builder)
{
$connection = $builder->getConnection();

if (! method_exists($connection, 'getPdo')) {
throw new InvalidArgumentException('Connection does not provide access to PDO connection.');
}

$pdo = $connection->getPdo();

if (! ($pdo instanceof PDO)) {
throw new InvalidArgumentException('Connection does not have a PDO connection.');
}

return $pdo;
}
}
42 changes: 42 additions & 0 deletions src/Illuminate/Foundation/VarDumper/Casters/CarbonCaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Illuminate\Foundation\VarDumper\Casters;

class CarbonCaster extends Caster
{
/**
* @inheritdoc
*/
protected function cast($target, $properties, $stub, $isNested, $filter = 0)
{
return $properties
->putVirtual('date', $target->format($this->getFormat($target)))
->when($isNested, fn ($properties) => $properties->only('date'))
->except(['constructedObjectId', 'dumpProperties'])
->filter()
->reorder(['date', '*'])
->applyCutsToStub($stub, $properties)
->all();
}

/**
* Dynamically create the debug format based on what timestamp data exists.
*
* @param \Carbon\CarbonInterface $target
* @return string
*/
protected function getFormat($target): string
{
// Only include microseconds if we have it
$microseconds = '000000' === $target->format('u')
? ''
: '.u';

// Only include timezone name ("America/New_York") if we have it
$timezone = $target->getTimezone()->getLocation()
? ' e (P)'
: ' P';

return 'Y-m-d H:i:s'.$microseconds.$timezone;
}
}
64 changes: 64 additions & 0 deletions src/Illuminate/Foundation/VarDumper/Casters/Caster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace Illuminate\Foundation\VarDumper\Casters;

use Illuminate\Foundation\VarDumper\Properties;

abstract class Caster
{
/**
* Whether this caster is enabled.
*
* @var bool
*/
protected static $enabled = true;

/**
* Cast the target for dumping.
*
* @param mixed $target
* @param \Illuminate\Foundation\VarDumper\Properties $properties
* @param \Symfony\Component\VarDumper\Cloner\Stub $stub
* @param bool $isNested
* @param int $filter
* @return array
*/
abstract protected function cast($target, $properties, $stub, $isNested, $filter = 0);

/**
* Invoke the caster.
*
* @param mixed $target
* @param array $properties
* @param \Symfony\Component\VarDumper\Cloner\Stub $stub
* @param bool $isNested
* @param int $filter
* @return array
*/
public function __invoke($target, $properties, $stub, $isNested, $filter = 0)
{
return self::$enabled
? $this->cast($target, new Properties($properties), $stub, $isNested, $filter)
: $properties;
}

/**
* Disable this caster.
*
* @return void
*/
public static function disable()
{
self::$enabled = false;
}

/**
* Enable this caster.
*
* @return void
*/
public static function enable()
{
self::$enabled = true;
}
}
31 changes: 31 additions & 0 deletions src/Illuminate/Foundation/VarDumper/Casters/ContainerCaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Illuminate\Foundation\VarDumper\Casters;

class ContainerCaster extends Caster
{
/**
* @inheritdoc
*/
protected function cast($target, $properties, $stub, $isNested, $filter = 0)
{
if ($isNested) {
$stub->cut += $properties->count();

return [];
}

$keep = [
'bindings',
'aliases',
'resolved',
'extenders',
];

return $properties
->only($keep)
->reorder($keep)
->applyCutsToStub($stub, $properties)
->all();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Illuminate\Foundation\VarDumper\Casters;

use Illuminate\Foundation\VarDumper\Key;

class DatabaseConnectionCaster extends Caster
{
/**
* @inheritdoc
*/
protected function cast($target, $properties, $stub, $isNested, $filter = 0)
{
if (! is_array($config = $properties->getProtected('config'))) {
return $properties->all();
}

$stub->cut += count($properties);

return [
Key::virtual('name') => $config['name'],
Key::virtual('database') => $config['database'],
Key::virtual('driver') => $config['driver'],
];
}
}
Loading