diff --git a/src/Illuminate/Foundation/Console/CliDumper.php b/src/Illuminate/Foundation/Console/CliDumper.php index beed2f2af9f4..cb60962064f9 100644 --- a/src/Illuminate/Foundation/Console/CliDumper.php +++ b/src/Illuminate/Foundation/Console/CliDumper.php @@ -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; /** @@ -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. * diff --git a/src/Illuminate/Foundation/Http/HtmlDumper.php b/src/Illuminate/Foundation/Http/HtmlDumper.php index 2df09013fe65..90249d888d8e 100644 --- a/src/Illuminate/Foundation/Http/HtmlDumper.php +++ b/src/Illuminate/Foundation/Http/HtmlDumper.php @@ -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; /** @@ -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. * diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index df55911aabad..baae373e605c 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -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 { @@ -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(), }; } diff --git a/src/Illuminate/Foundation/VarDumper/Casters/BuilderCaster.php b/src/Illuminate/Foundation/VarDumper/Casters/BuilderCaster.php new file mode 100644 index 000000000000..1ecb6eed5000 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/BuilderCaster.php @@ -0,0 +1,95 @@ +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('/(?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; + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Casters/CarbonCaster.php b/src/Illuminate/Foundation/VarDumper/Casters/CarbonCaster.php new file mode 100644 index 000000000000..dcdd161a62b6 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/CarbonCaster.php @@ -0,0 +1,42 @@ +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; + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Casters/Caster.php b/src/Illuminate/Foundation/VarDumper/Casters/Caster.php new file mode 100644 index 000000000000..3fc704040032 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/Caster.php @@ -0,0 +1,64 @@ +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; + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Casters/ContainerCaster.php b/src/Illuminate/Foundation/VarDumper/Casters/ContainerCaster.php new file mode 100644 index 000000000000..a48006ba071b --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/ContainerCaster.php @@ -0,0 +1,31 @@ +cut += $properties->count(); + + return []; + } + + $keep = [ + 'bindings', + 'aliases', + 'resolved', + 'extenders', + ]; + + return $properties + ->only($keep) + ->reorder($keep) + ->applyCutsToStub($stub, $properties) + ->all(); + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Casters/DatabaseConnectionCaster.php b/src/Illuminate/Foundation/VarDumper/Casters/DatabaseConnectionCaster.php new file mode 100644 index 000000000000..d8484750fe96 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/DatabaseConnectionCaster.php @@ -0,0 +1,26 @@ +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'], + ]; + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Casters/HeaderBagCaster.php b/src/Illuminate/Foundation/VarDumper/Casters/HeaderBagCaster.php new file mode 100644 index 000000000000..888698ef0013 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/HeaderBagCaster.php @@ -0,0 +1,27 @@ +all()) + ->map(function (array $headers) { + return 1 === count($headers) + ? $headers[0] + : $headers; + }) + ->mapWithKeys(fn ($value, $key) => [Key::virtual($key) => $value]) + ->all(); + + $result[Key::protected('cacheControl')] = $properties[Key::protected('cacheControl')]; + + return $result; + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Casters/ModelCaster.php b/src/Illuminate/Foundation/VarDumper/Casters/ModelCaster.php new file mode 100644 index 000000000000..79553c74971c --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/ModelCaster.php @@ -0,0 +1,37 @@ +only($keep) + ->reorder($keep) + ->applyCutsToStub($stub, $properties) + ->all(); + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Casters/ParameterBagCaster.php b/src/Illuminate/Foundation/VarDumper/Casters/ParameterBagCaster.php new file mode 100644 index 000000000000..043d838b3712 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/ParameterBagCaster.php @@ -0,0 +1,18 @@ +all()) + ->mapWithKeys(fn ($value, $key) => [Key::virtual($key) => $value]) + ->all(); + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Casters/RequestCaster.php b/src/Illuminate/Foundation/VarDumper/Casters/RequestCaster.php new file mode 100644 index 000000000000..bc821a685bd0 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/RequestCaster.php @@ -0,0 +1,18 @@ +except(['userResolver', 'routeResolver']) + ->filter() + ->applyCutsToStub($stub, $properties) + ->all(); + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Casters/ResponseCaster.php b/src/Illuminate/Foundation/VarDumper/Casters/ResponseCaster.php new file mode 100644 index 000000000000..14171d32e2e2 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Casters/ResponseCaster.php @@ -0,0 +1,17 @@ +filter() + ->applyCutsToStub($stub, $properties) + ->all(); + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Concerns/HandlesDumps.php b/src/Illuminate/Foundation/VarDumper/Concerns/HandlesDumps.php new file mode 100644 index 000000000000..00d9efe2ccf0 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Concerns/HandlesDumps.php @@ -0,0 +1,99 @@ + $this->handle($value)); + } + + /** + * Set the cloner instance. + * + * @param VarCloner $cloner + * @return $this + */ + public function setCloner($cloner) + { + $this->cloner = $cloner; + + return $this; + } + + /** + * Get the default cloner with all casters registered. + * + * @return VarCloner + */ + public function getDefaultCloner() + { + $builderCaster = new BuilderCaster(); + + return tap(new VarCloner())->addCasters([ + Closure::class => [ReflectionCaster::class, 'unsetClosureFileInfo'], + Container::class => new ContainerCaster(), + ConnectionInterface::class => new DatabaseConnectionCaster(), + BaseBuilder::class => $builderCaster, + EloquentBuilder::class => $builderCaster, + Relation::class => $builderCaster, + CarbonInterface::class => new CarbonCaster(), + HeaderBag::class => new HeaderBagCaster(), + Model::class => new ModelCaster(), + ParameterBag::class => new ParameterBagCaster(), + Request::class => new RequestCaster(), + Response::class => new ResponseCaster(), + ]); + } + + /** + * Clone and dump a variable. + * + * @param mixed $value + * @return void + */ + public function handle($value) + { + $this->cloner ??= $this->getDefaultCloner(); + + $this->dumpWithSource($this->cloner->cloneVar($value)); + } +} diff --git a/src/Illuminate/Foundation/VarDumper/Key.php b/src/Illuminate/Foundation/VarDumper/Key.php new file mode 100644 index 000000000000..7e9555dd6066 --- /dev/null +++ b/src/Illuminate/Foundation/VarDumper/Key.php @@ -0,0 +1,41 @@ +cut += ($original->count() - $this->count()); + + return $this; + } + + /** + * Cut a property out. + * + * @param string $key + * @param mixed $default + * @return \Symfony\Component\VarDumper\Caster\CutStub + */ + public function cut($key, $default = null): CutStub + { + return new CutStub($this->get($key, $default)); + } + + /** + * Cut a protected property out. + * + * @param string $key + * @param mixed $default + * @return \Symfony\Component\VarDumper\Caster\CutStub + */ + public function cutProtected($key, $default = null): CutStub + { + return $this->cut(Key::protected($key), $default); + } + + /** + * Cut a virtual property out. + * + * @param string $key + * @param mixed $default + * @return \Symfony\Component\VarDumper\Caster\CutStub + */ + public function cutVirtual($key, $default = null): CutStub + { + return $this->cut(Key::virtual($key), $default); + } + + /** + * Cut a dynamic property out. + * + * @param string $key + * @param mixed $default + * @return \Symfony\Component\VarDumper\Caster\CutStub + */ + public function cutDynamic($key, $default = null): CutStub + { + return $this->cut(Key::dynamic($key), $default); + } + + /** + * Get a property value. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) + { + $missing = new stdClass(); + foreach ($this->addPrefixes($key) as $prefixed_key) { + $parameter = parent::get($prefixed_key, $missing); + if ($missing !== $parameter) { + return $parameter; + } + } + + return $default; + } + + /** + * Get a protected property value. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getProtected($key, $default = null) + { + return $this->get(Key::protected($key), $default); + } + + /** + * Get a virtual property value. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getVirtual($key, $default = null) + { + return $this->get(Key::virtual($key), $default); + } + + /** + * Get a dynamic property value. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getDynamic($key, $default = null) + { + return $this->get(Key::dynamic($key), $default); + } + + /** + * Check whether a property/properties exists. + * + * @param string|array $key + * @return bool + */ + public function has($key) + { + if (! is_array($key)) { + $key = func_get_args(); + } + + foreach ($key as $value) { + if (! $this->hasAny($this->addPrefixes($value))) { + return false; + } + } + + return true; + } + + /** + * Check whether any provided property/properties exists. + * + * @param string|array $key + * @return bool + */ + public function hasAny($key) + { + if ($this->isEmpty()) { + return false; + } + + if (! is_array($key)) { + $key = func_get_args(); + } + + foreach ($key as $value) { + if (array_key_exists($value, $this->items)) { + return true; + } + } + + return false; + } + + /** + * Add a protected property. + * + * @param string $key + * @param mixed $value + * @return $this + */ + public function putProtected($key, $value) + { + return $this->put(Key::protected($key), $value); + } + + /** + * Add a virtual property. + * + * @param string $key + * @param mixed $value + * @return $this + */ + public function putVirtual($key, $value) + { + return $this->put(Key::virtual($key), $value); + } + + /** + * Add a dynamic property. + * + * @param string $key + * @param mixed $value + * @return $this + */ + public function putDynamic($key, $value) + { + return $this->put(Key::dynamic($key), $value); + } + + /** + * Copy a property from one collection to this. + * + * @param string $key + * @param \Illuminate\Foundation\VarDumper\Properties $from + * @param mixed $default + * @return $this + */ + public function copy($key, $from, $default = null) + { + return $this->put($key, $from->get($key, $default)); + } + + /** + * Copy a protected property from one collection to this. + * + * @param string $key + * @param \Illuminate\Foundation\VarDumper\Properties $from + * @param mixed $default + * @return $this + */ + public function copyProtected($key, $from, $default = null) + { + return $this->copy(Key::protected($key), $from, $default); + } + + /** + * Copy a virtual property from one collection to this. + * + * @param string $key + * @param \Illuminate\Foundation\VarDumper\Properties $from + * @param mixed $default + * @return $this + */ + public function copyVirtual($key, $from, $default = null) + { + return $this->copy(Key::virtual($key), $from, $default); + } + + /** + * Copy a dynamic property from one collection to this. + * + * @param string $key + * @param \Illuminate\Foundation\VarDumper\Properties $from + * @param mixed $default + * @return $this + */ + public function copyDynamic($key, $from, $default = null) + { + return $this->copy(Key::dynamic($key), $from, $default); + } + + /** + * Cut a property from one collection and copy it to this. + * + * @param string $key + * @param \Illuminate\Foundation\VarDumper\Properties $from + * @param mixed $default + * @return $this + */ + public function copyAndCut($key, $from, $default = null) + { + return $this->put($key, $from->cut($key, $default)); + } + + /** + * Cut a protected property from one collection and copy it to this. + * + * @param string $key + * @param \Illuminate\Foundation\VarDumper\Properties $from + * @param mixed $default + * @return $this + */ + public function copyAndCutProtected($key, $from, $default = null) + { + return $this->copyAndCut(Key::protected($key), $from, $default); + } + + /** + * Cut a virtual property from one collection and copy it to this. + * + * @param string $key + * @param \Illuminate\Foundation\VarDumper\Properties $from + * @param mixed $default + * @return $this + */ + public function copyAndCutVirtual($key, $from, $default = null) + { + return $this->copyAndCut(Key::virtual($key), $from, $default); + } + + /** + * Cut a dynamic property from one collection and copy it to this. + * + * @param string $key + * @param \Illuminate\Foundation\VarDumper\Properties $from + * @param mixed $default + * @return $this + */ + public function copyAndCutDynamic($key, $from, $default = null) + { + return $this->copyAndCut(Key::dynamic($key), $from, $default); + } + + /** + * Get only the specified properties. + * + * @param string|array $keys + * @return \Illuminate\Foundation\VarDumper\Properties + */ + public function only($keys) + { + return $this->filter(function ($value, $key) use ($keys) { + return Str::is($keys, $key) || Str::is($keys, $this->stripPrefix($key)); + }); + } + + /** + * Get all properties except the specified ones. + * + * @param string|array $keys + * @return \Illuminate\Foundation\VarDumper\Properties + */ + public function except($keys) + { + return $this->reject(function ($value, $key) use ($keys) { + return Str::is($keys, $key) || Str::is($keys, $this->stripPrefix($key)); + }); + } + + /** + * Filter the properties. If no callback is provided, empty properties are cut. + * + * @param callable|null $callback + * @return \Illuminate\Foundation\VarDumper\Properties + */ + public function filter(callable $callback = null) + { + if (null === $callback) { + $callback = static function ($property) { + if (is_array($property)) { + return count($property); + } + + if ($property instanceof Enumerable) { + return $property->isNotEmpty(); + } + + return null !== $property; + }; + } + + return parent::filter($callback); + } + + /** + * Reorder the properties by a set of rules. + * + * @param array $rules + * @return \Illuminate\Foundation\VarDumper\Properties + */ + public function reorder(array $rules) + { + return $this->sortBy($this->getReorderCallback($rules)); + } + + /** + * Convert sorting rules to a 'sortBy' callback function. + * + * @param array $rules + * @return \Closure + */ + protected function getReorderCallback(array $rules) + { + $map = $this->createReorderMapFromRules($rules); + + return function ($value, $key) use ($map) { + $result = Arr::pull($map, '*'); + + foreach ($map as $pattern => $position) { + if ($key === $pattern || Str::is($pattern, $this->stripPrefix($key))) { + $result = $position; + } + } + + return $result; + }; + } + + /** + * Build a map of patterns to sort position for reordering. + * + * @param array $rules + * @return array + */ + protected function createReorderMapFromRules(array $rules): array + { + $rules = array_values($rules); + $map = array_combine($rules, array_keys($rules)); + + // Ensure that there's always a '*' pattern, defaulting to the end + $map['*'] ??= count($map); + + return $map; + } + + /** + * Strip any VarCloner prefixes from a key. + * + * @param string $key + * @return string + */ + protected function stripPrefix($key) + { + return str_replace($this->prefixes, '', $key); + } + + /** + * Get all possible matching prefixed keys for comparison. + * + * @param string $key + * @return array + */ + protected function addPrefixes($key) + { + if (Str::startsWith($key, $this->prefixes)) { + return [$key]; + } + + return array_merge([$key], array_map(fn ($prefix) => $prefix.$key, $this->prefixes)); + } +} diff --git a/tests/Foundation/Console/CliDumperTest.php b/tests/Foundation/Console/CliDumperTest.php index 5777b04aa2c2..7b8ef460cd51 100644 --- a/tests/Foundation/Console/CliDumperTest.php +++ b/tests/Foundation/Console/CliDumperTest.php @@ -2,16 +2,22 @@ namespace Illuminate\Tests\Foundation\Console; +use Illuminate\Container\Container; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Console\CliDumper; +use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Illuminate\Support\Carbon; use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; use Symfony\Component\Console\Output\BufferedOutput; -use Symfony\Component\VarDumper\Caster\ReflectionCaster; -use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Test\VarDumperTestTrait; class CliDumperTest extends TestCase { + use VarDumperTestTrait; + protected function setUp(): void { CliDumper::resolveDumpSourceUsing(function () { @@ -108,6 +114,163 @@ public function testNull() $this->assertSame($expected, $output); } + public function testContainer() + { + $container = new Container(); + + $container->bind(static::class, fn () => $this); + $container->alias(static::class, 'bar'); + $container->extend('bar', fn () => $this); + $container->make('bar'); + + $fqcn = static::class; + $expected = << array:2 [ + "concrete" => Closure() { + class: "{$fqcn}" + this: {$fqcn} {#1 …} + } + "shared" => false + ] + ] + #aliases: array:1 [ + "bar" => "{$fqcn}" + ] + #resolved: array:1 [ + "{$fqcn}" => true + ] + #extenders: array:1 [ + "{$fqcn}" => array:1 [ + 0 => Closure() { + class: "{$fqcn}" + this: {$fqcn} {#1 …} + } + ] + ] + …%d + } + EOD; + + $this->assertDumpMatchesFormat($expected, $container); + } + + public function testCarbonDate() + { + $carbon = Carbon::parse('2022-01-18 19:44:02.572622', 'America/New_York'); + + $expected = <<assertDumpMatchesFormat($expected, $carbon); + } + + public function testRequest() + { + $request = Request::create('/1'); + + $expected = <<assertDumpMatchesFormat($expected, $request); + } + + public function testResponse() + { + $response = new Response('Hello world.'); + + $expected = <<assertDumpMatchesFormat($expected, $response); + } + + public function testModel() + { + $model = new CliDumperModel(); + + $expected = <<assertDumpMatchesFormat($expected, $model); + } + public function testWhenIsFileViewIsNotViewCompiled() { $file = '/my-work-directory/routes/console.php'; @@ -221,15 +384,42 @@ protected function dump($value) '/my-work-directory/storage/framework/views', ); - $cloner = tap(new VarCloner())->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); - - $dumper->dumpWithSource($cloner->cloneVar($value)); + $dumper->handle($value); return $output->fetch(); } + protected function getDump($data, $key = null, int $filter = 0): ?string + { + $dumper = new CliDumper( + $output = new BufferedOutput(), + '/my-work-directory', + '/my-work-directory/storage/framework/views', + ); + + $cloner = $dumper->getDefaultCloner(); + $cloner->setMaxItems(-1); + + $data = $cloner->cloneVar($data, $filter)->withRefHandles(false); + + if (null !== $key) { + $data = $data->seek($key); + if (null === $data) { + return null; + } + } + + $dumper->dumpWithSource($data); + + return rtrim($output->fetch()); + } + protected function tearDown(): void { CliDumper::resolveDumpSourceUsing(null); } } + +class CliDumperModel extends Model +{ +} diff --git a/tests/Foundation/Http/HtmlDumperTest.php b/tests/Foundation/Http/HtmlDumperTest.php index 26dbcebf0b94..eb37cfef1d73 100644 --- a/tests/Foundation/Http/HtmlDumperTest.php +++ b/tests/Foundation/Http/HtmlDumperTest.php @@ -8,8 +8,6 @@ use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; -use Symfony\Component\VarDumper\Caster\ReflectionCaster; -use Symfony\Component\VarDumper\Cloner\VarCloner; class HtmlDumperTest extends TestCase { @@ -94,6 +92,39 @@ public function testNull() $this->assertStringContainsString($expected, $output); } + public function testContainer() + { + $container = new Container(); + + $output = $this->dump($container); + + $expectations = [ + ' // app/routes/console.php:18', + '#bindings: []', + '#aliases: []', + '#resolved: []', + '#extenders: []', + // '…15', TODO: sometimes … + ]; + + foreach ($expectations as $expected) { + $this->assertStringContainsString($expected, $output); + } + + $missing = [ + 'methodBindings', + 'instances', + 'scopedInstances', + 'abstractAliases', + 'tags', + 'buildStack', + ]; + + foreach ($missing as $needle) { + $this->assertStringNotContainsString($needle, $output); + } + } + public function testUnresolvableSource() { HtmlDumper::resolveDumpSourceUsing(fn () => null); @@ -271,9 +302,7 @@ protected function dump($value) $dumper->setOutput($outputFile); - $cloner = tap(new VarCloner())->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); - - $dumper->dumpWithSource($cloner->cloneVar($value)); + $dumper->handle($value); return tap(file_get_contents($outputFile), fn () => @unlink($outputFile)); } diff --git a/tests/Foundation/VarDumper/PropertiesTest.php b/tests/Foundation/VarDumper/PropertiesTest.php new file mode 100644 index 000000000000..267b24cecab8 --- /dev/null +++ b/tests/Foundation/VarDumper/PropertiesTest.php @@ -0,0 +1,178 @@ +parameters = new Properties([ + Key::protected('protected') => 1, + Key::virtual('virtual') => 1, + Key::dynamic('dynamic') => 1, + 'prefix_b' => 1, + 'prefix_a' => 1, + 'b_suffix' => 1, + 'a_suffix' => 1, + 'other' => 1, + ]); + } + + public function testReorderingParameters() + { + $rules = [ + 'prefix_*', + 'dynamic', + 'virtual', + '*', + 'protected', + '*_suffix', + ]; + + $reordered = $this->parameters->reorder($rules)->all(); + + $this->assertEquals([ + 'prefix_b' => 1, + 'prefix_a' => 1, + Key::dynamic('dynamic') => 1, + Key::virtual('virtual') => 1, + 'other' => 1, + Key::protected('protected') => 1, + 'b_suffix' => 1, + 'a_suffix' => 1, + ], $reordered); + } + + public function testOnlyKeepingSpecificParameters() + { + $subset = $this->parameters->only(['dynamic', '*_suffix'])->all(); + + $this->assertEquals([ + Key::dynamic('dynamic') => 1, + 'b_suffix' => 1, + 'a_suffix' => 1, + ], $subset); + } + + public function testExcludingSpecificParameters() + { + $subset = $this->parameters->except(['dynamic', '*_suffix'])->all(); + + $this->assertEquals([ + Key::protected('protected') => 1, + Key::virtual('virtual') => 1, + 'prefix_b' => 1, + 'prefix_a' => 1, + 'other' => 1, + ], $subset); + } + + public function testHasMethod() + { + $this->assertTrue($this->parameters->has('protected')); + $this->assertTrue($this->parameters->has(Key::protected('protected'))); + $this->assertTrue($this->parameters->has(Key::protected('protected'), 'prefix_b')); + $this->assertTrue($this->parameters->has([Key::protected('protected'), 'prefix_b'])); + + $this->assertFalse($this->parameters->has(Key::virtual('protected'))); + $this->assertFalse($this->parameters->has(Key::protected('protected'), 'foo')); + $this->assertFalse($this->parameters->has([Key::protected('protected'), 'foo'])); + + $this->assertTrue($this->parameters->hasAny(Key::protected('protected'), 'foo')); + $this->assertTrue($this->parameters->hasAny([Key::protected('protected'), 'foo'])); + } + + public function testGetMethods() + { + $this->assertEquals(1, $this->parameters->getProtected('protected')); + $this->assertNull($this->parameters->getProtected('virtual')); + + $this->assertEquals(1, $this->parameters->getVirtual('virtual')); + $this->assertNull($this->parameters->getVirtual('protected')); + + $this->assertEquals(1, $this->parameters->getDynamic('dynamic')); + $this->assertNull($this->parameters->getDynamic('protected')); + + $this->assertEquals(1, $this->parameters->get('protected')); + $this->assertEquals(1, $this->parameters->get('virtual')); + $this->assertEquals(1, $this->parameters->get('dynamic')); + $this->assertEquals(1, $this->parameters->get('prefix_b')); + $this->assertNull($this->parameters->get('missing param')); + } + + public function testCutMethods() + { + $this->assertEquals(1, $this->parameters->cutProtected('protected')->value); + $this->assertNull($this->parameters->cutProtected('virtual')->value); + + $this->assertEquals(1, $this->parameters->cutVirtual('virtual')->value); + $this->assertNull($this->parameters->cutVirtual('protected')->value); + + $this->assertEquals(1, $this->parameters->cutDynamic('dynamic')->value); + $this->assertNull($this->parameters->cutDynamic('protected')->value); + + $this->assertEquals(1, $this->parameters->cut('protected')->value); + $this->assertEquals(1, $this->parameters->cut('virtual')->value); + $this->assertEquals(1, $this->parameters->cut('dynamic')->value); + $this->assertEquals(1, $this->parameters->cut('prefix_b')->value); + $this->assertNull($this->parameters->cut('missing param')->value); + } + + public function testCopyMethods() + { + $destination = new Properties(); + + $destination->copyProtected('protected', $this->parameters); + $destination->copyVirtual('virtual', $this->parameters); + $destination->copyDynamic('dynamic', $this->parameters); + $destination->copy('prefix_b', $this->parameters); + + $this->assertEquals(1, $destination->get('protected')); + $this->assertEquals(1, $destination->get('virtual')); + $this->assertEquals(1, $destination->get('dynamic')); + $this->assertEquals(1, $destination->get('prefix_b')); + } + + public function testCopyAndCutMethods() + { + $destination = new Properties(); + + $destination->copyAndCutProtected('protected', $this->parameters); + $destination->copyAndCutVirtual('virtual', $this->parameters); + $destination->copyAndCutDynamic('dynamic', $this->parameters); + + $this->assertInstanceOf(CutStub::class, $destination->get('protected')); + $this->assertInstanceOf(CutStub::class, $destination->get('virtual')); + $this->assertInstanceOf(CutStub::class, $destination->get('dynamic')); + + $this->assertEquals(1, $destination->get('protected')->value); + $this->assertEquals(1, $destination->get('virtual')->value); + $this->assertEquals(1, $destination->get('dynamic')->value); + } + + public function testDefaultFiltering() + { + $properties = new Properties([ + 'null' => null, + 'empty array' => [], + 'empty collection' => new Collection(), + 'false' => false, + 'value' => 1, + ]); + + $this->assertEquals(['false' => false, 'value' => 1], $properties->filter()->all()); + } +}