From 46ae6327cdb2915dde2812b07222d1f96cd6a680 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 25 Nov 2023 22:10:28 +0400 Subject: [PATCH 1/9] fix windows bin file --- bin/trap.bat | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/trap.bat b/bin/trap.bat index b592f2d1..28fcb590 100644 --- a/bin/trap.bat +++ b/bin/trap.bat @@ -5,7 +5,6 @@ set BIN_PATH=%~dp0 if "%PHP_COMMAND%" == "" set PHP_COMMAND=php -cd %BIN_PATH% "%PHP_COMMAND%" "%BIN_PATH%trap" %* @endlocal From 5097ee1b0dd35f3acf8af54cd3fb556248309784 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 25 Nov 2023 22:12:46 +0400 Subject: [PATCH 2/9] Create the Trap handle returned from a `trap()` call --- src/Client/TrapHandle.php | 98 +++++++++++++++++++++++++++++++++++++++ src/functions.php | 50 +++----------------- 2 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 src/Client/TrapHandle.php diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php new file mode 100644 index 00000000..bf3831b2 --- /dev/null +++ b/src/Client/TrapHandle.php @@ -0,0 +1,98 @@ +values = $array; + return $new; + } + + public function __destruct() + { + $this->haveToSend and $this->sendUsingDump(); + } + + private function sendUsingDump(): void + { + \class_exists(VarDumper::class) or throw new \RuntimeException( + 'VarDumper is not installed. Please install symfony/var-dumper package.' + ); + + // Set default values if not set + if (!isset($_SERVER['VAR_DUMPER_FORMAT'], $_SERVER['VAR_DUMPER_SERVER'])) { + $_SERVER['VAR_DUMPER_FORMAT'] = 'server'; + // todo use the config file in the future + $_SERVER['VAR_DUMPER_SERVER'] = '127.0.0.1:9912'; + } + + // If there are no values - stack trace + if ($this->values === []) { + VarDumper::dump([ + 'cwd' => \getcwd(), + 'trace' => new TraceStub(($this->stackTrace(\getcwd()))), + ]); + return; + } + + // Dump single value + if (\array_keys($this->values) === [0]) { + VarDumper::dump($this->values[0]); + return; + } + + // Dump sequence of values + foreach ($this->values as $key => $value) { + /** @psalm-suppress TooManyArguments */ + VarDumper::dump($value, $key); + } + } + + /** + * @param string $baseDir Base directory for relative paths + * @return array, + * file?: non-empty-string, + * class?: class-string, + * object?: object, + * type?: non-empty-string, + * args?: array + * }> + */ + private function stackTrace(string $baseDir): array + { + $dir = \getcwd() . \DIRECTORY_SEPARATOR; + $cwdLen = \strlen($dir); + // Replace paths with relative paths + $stack = []; + foreach (\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { + if (($frame['class'] ?? null) === __CLASS__) { + continue; + } + + // Convert absolute paths to relative ones + isset($frame['file']) && \str_starts_with($frame['file'], $dir) + and $frame['file'] = '.' . \DIRECTORY_SEPARATOR . \substr($frame['file'], $cwdLen); + + $stack[] = $frame; + } + + return $stack; + } +} diff --git a/src/functions.php b/src/functions.php index 764279f1..1850597e 100644 --- a/src/functions.php +++ b/src/functions.php @@ -2,65 +2,27 @@ declare(strict_types=1); +use Buggregator\Trap\Client\TrapHandle; use Buggregator\Trap\Support\Caster\EnumValue; use Buggregator\Trap\Support\ProtobufCaster; use Google\Protobuf\Internal\MapField; use Google\Protobuf\Internal\Message; use Google\Protobuf\Internal\RepeatedField; -use Symfony\Component\VarDumper\Caster\TraceStub; use Symfony\Component\VarDumper\Cloner\AbstractCloner; -use Symfony\Component\VarDumper\VarDumper; -if (!\function_exists('trap')) { +try { /** * Configure VarDumper to dump values to the local server. * If there are no values - dump stack trace. * * @param mixed ...$values */ - function trap(mixed ...$values): void + function trap(mixed ...$values): TrapHandle { - if (!\class_exists(VarDumper::class)) { - throw new \RuntimeException('VarDumper is not installed. Please install symfony/var-dumper package.'); - } - - // Set default values if not set - if (!isset($_SERVER['VAR_DUMPER_FORMAT'], $_SERVER['VAR_DUMPER_SERVER'])) { - $_SERVER['VAR_DUMPER_FORMAT'] = 'server'; - // todo use the config file in the future - $_SERVER['VAR_DUMPER_SERVER'] = '127.0.0.1:9912'; - } - - // If there are no values - stack trace - if ($values === []) { - // VarDumper::dump(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); - $cwd = \getcwd(); - $stack = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - // Replace paths with relative paths - foreach ($stack as $i => $frame) { - if (isset($frame['file'])) { - $stack[$i]['file'] = \str_replace($cwd, '.', $frame['file']); - } - } - VarDumper::dump([ - 'cwd' => $cwd, - 'trace' => new TraceStub($stack) - ]); - return; - } - - // Dump single value - if (\array_keys($values) === [0]) { - VarDumper::dump($values[0]); - return; - } - - // Dump sequence of values - foreach ($values as $key => $value) { - /** @psalm-suppress TooManyArguments */ - VarDumper::dump($value, $key); - } + return TrapHandle::fromArray($values); } +} catch (\Throwable $e) { + // do nothing } /** From bba2d9c0523e0c3c3249e642e4fa2e755821f57c Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 25 Nov 2023 22:14:28 +0400 Subject: [PATCH 3/9] TrapHandle: add `if(condition)` method to dump only if the condition is true. --- src/Client/TrapHandle.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php index bf3831b2..1fb9bcae 100644 --- a/src/Client/TrapHandle.php +++ b/src/Client/TrapHandle.php @@ -23,6 +23,19 @@ public static function fromArray(array $array): self return $new; } + /** + * Dump only if the condition is true. + */ + public function if(bool|callable $condition): self + { + if (\is_callable($condition)) { + $condition = $condition(); + } + + $this->haveToSend = $condition; + return $this; + } + public function __destruct() { $this->haveToSend and $this->sendUsingDump(); From 87c136110ec98d511fcb9ffdfdceda1693664edf Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 26 Nov 2023 01:04:03 +0400 Subject: [PATCH 4/9] TrapHandle: add `once()` and `times()` method to limit count of dumps; --- src/Client/TrapHandle.php | 66 +++++++++++++++++++++++++------ src/Client/TrapHandle/Counter.php | 26 ++++++++++++ 2 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 src/Client/TrapHandle/Counter.php diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php index 1fb9bcae..d3132f79 100644 --- a/src/Client/TrapHandle.php +++ b/src/Client/TrapHandle.php @@ -4,27 +4,27 @@ namespace Buggregator\Trap\Client; +use Buggregator\Trap\Client\TrapHandle\Counter; use Symfony\Component\VarDumper\Caster\TraceStub; use Symfony\Component\VarDumper\VarDumper; /** * @internal - * @psalm-internal Buggregator\Trap */ final class TrapHandle { - private array $values; private bool $haveToSend = true; + private int $times = 0; + private string $timesCounterKey = ''; public static function fromArray(array $array): self { - $new = new self(); - $new->values = $array; - return $new; + return new self($array); } /** * Dump only if the condition is true. + * The check is performed immediately upon declaration. */ public function if(bool|callable $condition): self { @@ -36,9 +36,36 @@ public function if(bool|callable $condition): self return $this; } + /** + * Dump only $times times. + * The counter isn't incremented if the dump is not sent (any other condition is not met). + * It might be useful for debugging in loops, recursive or just multiple function calls. + * + * @param positive-int $times + * @param bool $fullStack If true, the counter is incremented for each stack trace, not for the line. + */ + public function times(int $times, bool $fullStack = false): self + { + $this->times = $times; + $this->timesCounterKey = (\serialize( + $fullStack + ? $this->stackTrace() + : $this->stackTrace()[0] + )); + return $this; + } + + /** + * Dump values only once. + */ + public function once(): self + { + return $this->times(1); + } + public function __destruct() { - $this->haveToSend and $this->sendUsingDump(); + $this->haveToSend() and $this->sendUsingDump(); } private function sendUsingDump(): void @@ -88,19 +115,18 @@ private function sendUsingDump(): void * args?: array * }> */ - private function stackTrace(string $baseDir): array + private function stackTrace(string $baseDir = ''): array { - $dir = \getcwd() . \DIRECTORY_SEPARATOR; + $dir = $baseDir . \DIRECTORY_SEPARATOR; $cwdLen = \strlen($dir); - // Replace paths with relative paths $stack = []; foreach (\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { - if (($frame['class'] ?? null) === __CLASS__) { + if (($frame['file'] ?? null) === __FILE__) { continue; } // Convert absolute paths to relative ones - isset($frame['file']) && \str_starts_with($frame['file'], $dir) + $cwdLen > 1 && isset($frame['file']) && \str_starts_with($frame['file'], $dir) and $frame['file'] = '.' . \DIRECTORY_SEPARATOR . \substr($frame['file'], $cwdLen); $stack[] = $frame; @@ -108,4 +134,22 @@ private function stackTrace(string $baseDir): array return $stack; } + + private function __construct( + private array $values, + ) { + } + + private function haveToSend(): bool + { + if (!$this->haveToSend) { + return false; + } + + if ($this->times > 0) { + return Counter::checkAndIncrement($this->timesCounterKey, $this->times); + } + + return true; + } } diff --git a/src/Client/TrapHandle/Counter.php b/src/Client/TrapHandle/Counter.php new file mode 100644 index 00000000..42920389 --- /dev/null +++ b/src/Client/TrapHandle/Counter.php @@ -0,0 +1,26 @@ +> */ + private static array $counters = []; + + /** + * Returns true if the counter of related stack trace is less than $times. In this case, the counter is incremented. + */ + public static function checkAndIncrement(string $key, int $times): bool + { + self::$counters[$key] ??= 0; + + if (self::$counters[$key] < $times) { + self::$counters[$key]++; + return true; + } + + return false; + } +} From 5dc40c68a770cc8d34ef9d9e67fedf75f3196916 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 26 Nov 2023 18:32:30 +0400 Subject: [PATCH 5/9] TrapHandle: add `depth()` method to limit the dump depth. Fix the detection of the dumping source --- src/Client/TrapHandle.php | 65 ++++----- .../TrapHandle/ContextProvider/Source.php | 123 ++++++++++++++++++ src/Client/TrapHandle/Dumper.php | 107 +++++++++++++++ src/Client/TrapHandle/StackTrace.php | 51 ++++++++ 4 files changed, 304 insertions(+), 42 deletions(-) create mode 100644 src/Client/TrapHandle/ContextProvider/Source.php create mode 100644 src/Client/TrapHandle/Dumper.php create mode 100644 src/Client/TrapHandle/StackTrace.php diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php index d3132f79..3798ce45 100644 --- a/src/Client/TrapHandle.php +++ b/src/Client/TrapHandle.php @@ -5,8 +5,9 @@ namespace Buggregator\Trap\Client; use Buggregator\Trap\Client\TrapHandle\Counter; +use Buggregator\Trap\Client\TrapHandle\Dumper as VarDumper; +use Buggregator\Trap\Client\TrapHandle\StackTrace; use Symfony\Component\VarDumper\Caster\TraceStub; -use Symfony\Component\VarDumper\VarDumper; /** * @internal @@ -16,6 +17,7 @@ final class TrapHandle private bool $haveToSend = true; private int $times = 0; private string $timesCounterKey = ''; + private int $depth = 0; public static function fromArray(array $array): self { @@ -36,6 +38,17 @@ public function if(bool|callable $condition): self return $this; } + /** + * Set max depth for the dump. + * + * @param int<0, max> $depth If 0 - no limit. + */ + public function depth(int $depth): self + { + $this->depth = $depth; + return $this; + } + /** * Dump only $times times. * The counter isn't incremented if the dump is not sent (any other condition is not met). @@ -47,10 +60,10 @@ public function if(bool|callable $condition): self public function times(int $times, bool $fullStack = false): self { $this->times = $times; - $this->timesCounterKey = (\serialize( + $this->timesCounterKey = \sha1(\serialize( $fullStack - ? $this->stackTrace() - : $this->stackTrace()[0] + ? StackTrace::stackTrace() + : StackTrace::stackTrace()[0] )); return $this; } @@ -65,10 +78,10 @@ public function once(): self public function __destruct() { - $this->haveToSend() and $this->sendUsingDump(); + $this->haveToSend() and $this->sendDump(); } - private function sendUsingDump(): void + private function sendDump(): void { \class_exists(VarDumper::class) or throw new \RuntimeException( 'VarDumper is not installed. Please install symfony/var-dumper package.' @@ -85,56 +98,24 @@ private function sendUsingDump(): void if ($this->values === []) { VarDumper::dump([ 'cwd' => \getcwd(), - 'trace' => new TraceStub(($this->stackTrace(\getcwd()))), - ]); + 'trace' => new TraceStub((StackTrace::stackTrace(\getcwd()))), + ], depth: $this->depth); return; } // Dump single value if (\array_keys($this->values) === [0]) { - VarDumper::dump($this->values[0]); + VarDumper::dump($this->values[0], depth: $this->depth); return; } // Dump sequence of values foreach ($this->values as $key => $value) { /** @psalm-suppress TooManyArguments */ - VarDumper::dump($value, $key); + VarDumper::dump($value, label: $key, depth: $this->depth); } } - /** - * @param string $baseDir Base directory for relative paths - * @return array, - * file?: non-empty-string, - * class?: class-string, - * object?: object, - * type?: non-empty-string, - * args?: array - * }> - */ - private function stackTrace(string $baseDir = ''): array - { - $dir = $baseDir . \DIRECTORY_SEPARATOR; - $cwdLen = \strlen($dir); - $stack = []; - foreach (\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { - if (($frame['file'] ?? null) === __FILE__) { - continue; - } - - // Convert absolute paths to relative ones - $cwdLen > 1 && isset($frame['file']) && \str_starts_with($frame['file'], $dir) - and $frame['file'] = '.' . \DIRECTORY_SEPARATOR . \substr($frame['file'], $cwdLen); - - $stack[] = $frame; - } - - return $stack; - } - private function __construct( private array $values, ) { diff --git a/src/Client/TrapHandle/ContextProvider/Source.php b/src/Client/TrapHandle/ContextProvider/Source.php new file mode 100644 index 00000000..001fb68d --- /dev/null +++ b/src/Client/TrapHandle/ContextProvider/Source.php @@ -0,0 +1,123 @@ + + * @author Maxime Steinhausser + * + * todo: rewrite and decompose + */ +final class Source implements ContextProviderInterface +{ + private int $limit; + private ?string $charset; + private ?string $projectDir; + private ?FileLinkFormatter $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 = StackTrace::stackTrace((string)$this->projectDir); + + $file = $trace[0]['file']; + $line = $trace[0]['line']; + $name = '-' === $file || 'Standard input code' === $file ? 'Standard input code' : false; + $fileExcerpt = false; + + for ($i = 0; $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'] ?? $file; + $line = $trace[$i]['line'] ?? $line; + + while (++$i < $this->limit) { + if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && !str_starts_with($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 (str_starts_with($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/src/Client/TrapHandle/Dumper.php b/src/Client/TrapHandle/Dumper.php new file mode 100644 index 00000000..22e2ff08 --- /dev/null +++ b/src/Client/TrapHandle/Dumper.php @@ -0,0 +1,107 @@ + + */ + private static function registerHandler(): Closure + { + $cloner = new VarCloner(); + $cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); + + $format = $_SERVER['VAR_DUMPER_FORMAT'] ?? null; + switch (true) { + case 'html' === $format: + $dumper = new HtmlDumper(); + break; + case 'cli' === $format: + $dumper = new CliDumper(); + break; + case 'server' === $format: + case $format && 'tcp' === parse_url($format, \PHP_URL_SCHEME): + $host = 'server' === $format ? $_SERVER['VAR_DUMPER_SERVER'] ?? '127.0.0.1:9912' : $format; + $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliDumper() : new HtmlDumper(); + $dumper = new ServerDumper($host, $dumper, self::getContextProviders()); + break; + default: + $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliDumper() : new HtmlDumper(); + } + + if (!$dumper instanceof ServerDumper) { + $dumper = new ContextualizedDumper($dumper, [new SourceContextProvider()]); + } + + return self::$handler = static function ($var, string|int|null $label = null, int $depth = 0) use ($cloner, $dumper): void { + $var = $cloner->cloneVar($var); + + $label === null or $var = $var->withContext(['label' => $label]); + $depth > 0 and $var = $var->withMaxDepth($depth); + + $dumper->dump($var); + }; + } + + /** + * @return array The context providers + * @author Nicolas Grekas + */ + private static function getContextProviders(): array + { + $contextProviders = []; + + if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && \class_exists(Request::class)) { + $requestStack = new RequestStack(); + $requestStack->push(Request::createFromGlobals()); + $contextProviders['request'] = new RequestContextProvider($requestStack); + } + + $fileLinkFormatter = \class_exists(FileLinkFormatter::class) ? new FileLinkFormatter(null, $requestStack ?? null) : null; + + return $contextProviders + [ + 'cli' => new CliContextProvider(), + 'source' => new ContextProvider\Source(null, null, $fileLinkFormatter), + ]; + } +} diff --git a/src/Client/TrapHandle/StackTrace.php b/src/Client/TrapHandle/StackTrace.php new file mode 100644 index 00000000..3d82a554 --- /dev/null +++ b/src/Client/TrapHandle/StackTrace.php @@ -0,0 +1,51 @@ +, + * file?: non-empty-string, + * class?: class-string, + * object?: object, + * type?: non-empty-string, + * args?: array + * }> + */ + public static function stackTrace(string $baseDir = ''): array + { + $dir = $baseDir . \DIRECTORY_SEPARATOR; + $cwdLen = \strlen($dir); + $stack = []; + $internal = false; + foreach (\debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { + if (\str_starts_with($frame['class'] ?? '', 'Buggregator\\Trap\\Client\\')) { + $internal = true; + $stack = []; + continue; + } + + if ($internal) { + // todo check the NoStackTrace attribute + + $internal = false; + } + + // Convert absolute paths to relative ones + $cwdLen > 1 && isset($frame['file']) && \str_starts_with($frame['file'], $dir) + and $frame['file'] = '.' . \DIRECTORY_SEPARATOR . \substr($frame['file'], $cwdLen); + + $stack[] = $frame; + } + + return $stack; + } +} From 5db1b684026c171a21f424480398f71b4cba24d9 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 26 Nov 2023 18:36:56 +0400 Subject: [PATCH 6/9] Move the `symfony/var-dumper` dependency into the `require` section from the `require-dev` --- composer.json | 7 ++----- src/Client/TrapHandle.php | 4 ---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 1c999455..a243f33d 100644 --- a/composer.json +++ b/composer.json @@ -60,16 +60,13 @@ "nyholm/psr7": "^1.8", "php-http/message": "^1.15", "psr/http-message": "^1.1 || ^2", - "symfony/console": "^5.4 || ^6 || ^7" - }, - "suggest": { - "symfony/var-dumper": "To use the trap() function" + "symfony/console": "^5.4 || ^6 || ^7", + "symfony/var-dumper": "^6 || ^7" }, "require-dev": { "dereuromark/composer-prefer-lowest": "^0.1.10", "google/protobuf": "^3.23", "phpunit/phpunit": "^10.4", - "symfony/var-dumper": "^6 || ^7", "vimeo/psalm": "^5.11" } } diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php index 3798ce45..8ee17295 100644 --- a/src/Client/TrapHandle.php +++ b/src/Client/TrapHandle.php @@ -83,10 +83,6 @@ public function __destruct() private function sendDump(): void { - \class_exists(VarDumper::class) or throw new \RuntimeException( - 'VarDumper is not installed. Please install symfony/var-dumper package.' - ); - // Set default values if not set if (!isset($_SERVER['VAR_DUMPER_FORMAT'], $_SERVER['VAR_DUMPER_SERVER'])) { $_SERVER['VAR_DUMPER_FORMAT'] = 'server'; From f8d4c8d91942c5cdf289769790ccebef731c0f92 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 26 Nov 2023 18:45:53 +0400 Subject: [PATCH 7/9] Add samples in the readme --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48aba03a..357eb991 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,21 @@ Additionally, you can manually set traps in the code. Use the `trap()` function, which works almost the same as Symfony's `dump()`, but configures the dumper to send dumps to the local server, unless other user settings are provided. +Also, the `trap()` has a lot of useful options: + +```php +// Limit the depth of the dumped structure +trap($veryDeepArray)->depth(3); + +foreach ($veryLargeArray as $item) { + // We don't need to dump more than 3 items + trap($item)->times(3); +} + +// Dump only if the condition is true +trap($animal)->once()->if($var instanceof Anumal\Cat); +``` + --- We care about the quality of our products' codebase and strive to provide the best user experience. @@ -108,8 +123,8 @@ Then just call the `trap()` function in your code: ```php trap(); // dump the current stack trace -trap($var); // dump a variable -trap($var, foo: $far, bar: $bar); // dump a variables sequence +trap($var)->depth(4); // dump a variable with a depth limit +trap($var, foo: $far, bar: $bar); // dump a named variables sequence ``` > **Note**: From 4a5fe414eabdcdb638c94c0b1f062ef991bf5a58 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 26 Nov 2023 19:33:12 +0400 Subject: [PATCH 8/9] Fix psalm issues --- src/Client/TrapHandle.php | 12 +++++++- .../TrapHandle/ContextProvider/Source.php | 10 +++++-- src/Client/TrapHandle/Counter.php | 6 ++-- src/Client/TrapHandle/Dumper.php | 29 ++++++++++++------- src/Client/TrapHandle/StackTrace.php | 14 ++++----- src/functions.php | 1 + 6 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php index 8ee17295..003e8e4b 100644 --- a/src/Client/TrapHandle.php +++ b/src/Client/TrapHandle.php @@ -31,7 +31,13 @@ public static function fromArray(array $array): self public function if(bool|callable $condition): self { if (\is_callable($condition)) { - $condition = $condition(); + try { + $condition = (bool)$condition(); + } catch (\Throwable $e) { + $this->values[] = $e; + + return $this; + } } $this->haveToSend = $condition; @@ -106,6 +112,10 @@ private function sendDump(): void } // Dump sequence of values + /** + * @var string|int $key + * @var mixed $value + */ foreach ($this->values as $key => $value) { /** @psalm-suppress TooManyArguments */ VarDumper::dump($value, label: $key, depth: $this->depth); diff --git a/src/Client/TrapHandle/ContextProvider/Source.php b/src/Client/TrapHandle/ContextProvider/Source.php index 001fb68d..146413db 100644 --- a/src/Client/TrapHandle/ContextProvider/Source.php +++ b/src/Client/TrapHandle/ContextProvider/Source.php @@ -1,7 +1,5 @@ * @author Maxime Steinhausser * + * @link https://github.com/symfony/var-dumper/blob/7.0/Dumper/ContextProvider/SourceContextProvider.php + * @link https://github.com/symfony/var-dumper/blob/6.3/Dumper/ContextProvider/SourceContextProvider.php + * + * @psalm-suppress all + * * todo: rewrite and decompose */ final class Source implements ContextProviderInterface @@ -27,6 +30,9 @@ final class Source implements ContextProviderInterface private ?string $projectDir; private ?FileLinkFormatter $fileLinkFormatter; + /** + * @psalm-suppress UndefinedClass + */ public function __construct(string $charset = null, string $projectDir = null, FileLinkFormatter $fileLinkFormatter = null, int $limit = 9) { $this->charset = $charset; diff --git a/src/Client/TrapHandle/Counter.php b/src/Client/TrapHandle/Counter.php index 42920389..9c3f8644 100644 --- a/src/Client/TrapHandle/Counter.php +++ b/src/Client/TrapHandle/Counter.php @@ -6,18 +6,20 @@ final class Counter { - /** @var array> */ + /** @var array> */ private static array $counters = []; /** * Returns true if the counter of related stack trace is less than $times. In this case, the counter is incremented. + * + * @param int<0, max> $times */ public static function checkAndIncrement(string $key, int $times): bool { self::$counters[$key] ??= 0; if (self::$counters[$key] < $times) { - self::$counters[$key]++; + ++self::$counters[$key]; return true; } diff --git a/src/Client/TrapHandle/Dumper.php b/src/Client/TrapHandle/Dumper.php index 22e2ff08..678f2abd 100644 --- a/src/Client/TrapHandle/Dumper.php +++ b/src/Client/TrapHandle/Dumper.php @@ -22,33 +22,35 @@ /** * @internal * @psalm-internal Buggregator\Trap\Client - * @psalm-type DumperHandler = Closure(mixed $var, string|int|null $label, int $depth): void */ final class Dumper { - /** @var DumperHandler|null */ + /** @var null|Closure(mixed, string|null, int): mixed */ private static ?Closure $handler; - public static function dump(mixed $var, string|int|null $label = null, int $depth = 0) + public static function dump(mixed $var, string|int|null $label = null, int $depth = 0): mixed { - return (self::$handler ??= self::registerHandler())($var, $label, $depth); + return (self::$handler ??= self::registerHandler())($var, empty($label) ? null : (string)$label, $depth); } /** - * @return DumperHandler|null The previous handler if any + * @return null|callable(mixed, string|null, int): mixed + * @psalm-suppress MixedInferredReturnType, MixedPropertyTypeCoercion, MismatchingDocblockReturnType */ public static function setHandler(callable $callable = null): ?Closure { - return ([$callable, self::$handler] = [self::$handler, $callable(...)])[0]; + return ([$callable, self::$handler] = [self::$handler, $callable === null ? null : $callable(...)])[0]; } /** - * @return DumperHandler + * @return Closure(mixed, string|null, int): mixed + * * @author Nicolas Grekas */ private static function registerHandler(): Closure { $cloner = new VarCloner(); + /** @psalm-suppress InvalidArgument */ $cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); $format = $_SERVER['VAR_DUMPER_FORMAT'] ?? null; @@ -73,19 +75,22 @@ private static function registerHandler(): Closure $dumper = new ContextualizedDumper($dumper, [new SourceContextProvider()]); } - return self::$handler = static function ($var, string|int|null $label = null, int $depth = 0) use ($cloner, $dumper): void { + return self::$handler = static function (mixed $var, string|null $label = null, int $depth = 0) + use ($cloner, $dumper): ?string { $var = $cloner->cloneVar($var); $label === null or $var = $var->withContext(['label' => $label]); $depth > 0 and $var = $var->withMaxDepth($depth); - $dumper->dump($var); + return $dumper->dump($var); }; } /** * @return array The context providers * @author Nicolas Grekas + * + * @psalm-suppress UndefinedClass */ private static function getContextProviders(): array { @@ -93,11 +98,15 @@ private static function getContextProviders(): array if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && \class_exists(Request::class)) { $requestStack = new RequestStack(); + /** @psalm-suppress MixedMethodCall */ $requestStack->push(Request::createFromGlobals()); $contextProviders['request'] = new RequestContextProvider($requestStack); } - $fileLinkFormatter = \class_exists(FileLinkFormatter::class) ? new FileLinkFormatter(null, $requestStack ?? null) : null; + /** @var null|FileLinkFormatter $fileLinkFormatter */ + $fileLinkFormatter = \class_exists(FileLinkFormatter::class) + ? new FileLinkFormatter(null, $requestStack ?? null) + : null; return $contextProviders + [ 'cli' => new CliContextProvider(), diff --git a/src/Client/TrapHandle/StackTrace.php b/src/Client/TrapHandle/StackTrace.php index 3d82a554..24e3d613 100644 --- a/src/Client/TrapHandle/StackTrace.php +++ b/src/Client/TrapHandle/StackTrace.php @@ -4,20 +4,18 @@ namespace Buggregator\Trap\Client\TrapHandle; -use Buggregator\Trap\Client\TrapHandle; - final class StackTrace { /** * @param string $baseDir Base directory for relative paths - * @return array, - * file?: non-empty-string, + * @return list * }> */ public static function stackTrace(string $baseDir = ''): array diff --git a/src/functions.php b/src/functions.php index 1850597e..713171b9 100644 --- a/src/functions.php +++ b/src/functions.php @@ -19,6 +19,7 @@ */ function trap(mixed ...$values): TrapHandle { + /** @psalm-suppress InternalMethod */ return TrapHandle::fromArray($values); } } catch (\Throwable $e) { From d6055795a9e88053e68c41685c0d6c2de4696022 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 26 Nov 2023 19:44:17 +0400 Subject: [PATCH 9/9] Fix typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 357eb991..0b00d129 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,8 @@ foreach ($veryLargeArray as $item) { trap($item)->times(3); } -// Dump only if the condition is true -trap($animal)->once()->if($var instanceof Anumal\Cat); +// Dump once if the condition is true +trap($animal)->once()->if($var instanceof Animal\Cat); ``` ---