Skip to content

Commit

Permalink
Merge pull request #29: add trap() options: if(), once(), `time…
Browse files Browse the repository at this point in the history
…s()`, `depth()`
  • Loading branch information
roxblnfk authored Nov 26, 2023
2 parents dd3f145 + d605579 commit 0eb5d12
Show file tree
Hide file tree
Showing 9 changed files with 490 additions and 52 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 once if the condition is true
trap($animal)->once()->if($var instanceof Animal\Cat);
```

---

We care about the quality of our products' codebase and strive to provide the best user experience.
Expand All @@ -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**:
Expand Down
1 change: 0 additions & 1 deletion bin/trap.bat
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ set BIN_PATH=%~dp0

if "%PHP_COMMAND%" == "" set PHP_COMMAND=php

cd %BIN_PATH%
"%PHP_COMMAND%" "%BIN_PATH%trap" %*

@endlocal
7 changes: 2 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
142 changes: 142 additions & 0 deletions src/Client/TrapHandle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

declare(strict_types=1);

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;

/**
* @internal
*/
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
{
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
{
if (\is_callable($condition)) {
try {
$condition = (bool)$condition();
} catch (\Throwable $e) {
$this->values[] = $e;

return $this;
}
}

$this->haveToSend = $condition;
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).
* 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 = \sha1(\serialize(
$fullStack
? StackTrace::stackTrace()
: StackTrace::stackTrace()[0]
));
return $this;
}

/**
* Dump values only once.
*/
public function once(): self
{
return $this->times(1);
}

public function __destruct()
{
$this->haveToSend() and $this->sendDump();
}

private function sendDump(): void
{
// 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((StackTrace::stackTrace(\getcwd()))),
], depth: $this->depth);
return;
}

// Dump single value
if (\array_keys($this->values) === [0]) {
VarDumper::dump($this->values[0], depth: $this->depth);
return;
}

// 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);
}
}

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;
}
}
129 changes: 129 additions & 0 deletions src/Client/TrapHandle/ContextProvider/Source.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Buggregator\Trap\Client\TrapHandle\ContextProvider;

use Buggregator\Trap\Client\TrapHandle\StackTrace;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
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 <p@tchwork.com>
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*
* @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
{
private int $limit;
private ?string $charset;
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;
$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[] = '<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 (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);
}
}
28 changes: 28 additions & 0 deletions src/Client/TrapHandle/Counter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Buggregator\Trap\Client\TrapHandle;

final class Counter
{
/** @var array<string, int<0, max>> */
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];
return true;
}

return false;
}
}
Loading

0 comments on commit 0eb5d12

Please sign in to comment.