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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 0.1.2 Under development

- feat(log): render multi-line log messages with preserved whitespace, `print_r()` dumps as a tree, and `var_export()` values highlighted.
- feat(log): render `var_export()` values and PSR-3 context as typed dump trees; rename the `PrintR*` tree model and renderer to `Dump*`.

## 0.1.1 October 28, 2026

Expand Down
2 changes: 1 addition & 1 deletion resources/assets/dist/css/debug.min.css

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions resources/src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -2626,20 +2626,25 @@
word-break: break-all;
}

.yii-debug-queue-tree-value-string {
/* Typed values of the queue payload tree and the log dump tree. */
.yii-debug-queue-tree-value-string,
.yii-debug-log-dump-value-string {
color: var(--yii-debug-success);
}

.yii-debug-queue-tree-value-number {
.yii-debug-queue-tree-value-number,
.yii-debug-log-dump-value-number {
color: var(--yii-debug-info);
}

.yii-debug-queue-tree-value-bool {
.yii-debug-queue-tree-value-bool,
.yii-debug-log-dump-value-bool {
color: var(--yii-debug-warning);
font-weight: 600;
}

.yii-debug-queue-tree-value-null {
.yii-debug-queue-tree-value-null,
.yii-debug-log-dump-value-null {
color: var(--yii-debug-panel-muted);
font-style: italic;
}
Expand Down
6 changes: 0 additions & 6 deletions src/Helper/PhpHighlighter.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ public static function highlight(mixed $value): string
*
* The `<pre>` wrapper scrolls horizontally, so it carries `tabindex="0"` to stay reachable by keyboard.
*
* Usage example:
* ```php
* $html = PhpHighlighter::highlightCode("array (\n 'id' => 7,\n)");
* // '<pre tabindex="0"><code style="color: #000000"><span style="color: #007700">array (' . "\n" . ' </span>...'
* ```
*
* @param string $code PHP source of an expression, without the opening tag.
*
* @return string Highlighted markup in a focusable `<pre>`, without the opening tag PHP needs to recognize the
Expand Down
12 changes: 0 additions & 12 deletions src/Helper/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@ public static function camel2id(string $name): string
* {@see PhpHighlighter::highlightCode()} output; {@see self::preserveLineBreaks()} escapes raw text first. `\r\n`
* and a lone `\r` become one line break, as the HTML parser reads them.
*
* Usage example:
* ```php
* $html = Text::preserveHtmlLineBreaks("<span>'a\r\nb'</span>");
* // "<span>'a&#10;b'</span>"
* ```
*
* @param string $html Escaped HTML markup.
*
* @return string Markup without raw line breaks.
Expand All @@ -67,12 +61,6 @@ public static function preserveHtmlLineBreaks(string $html): string
* normalization and still renders as a line break under `white-space: pre-wrap`. `\r\n` and a lone `\r` become one
* line break, as the HTML parser reads them.
*
* Usage example:
* ```php
* $html = Pre::tag()->html(Text::preserveLineBreaks("a\n\n<b>"))->render();
* // "<pre>\na&#10;&#10;&lt;b&gt;\n</pre>"
* ```
*
* @param string $text Raw text to render.
*
* @return string Escaped HTML content without raw line breaks.
Expand Down
10 changes: 4 additions & 6 deletions src/Panel/Log/PrintREntry.php → src/Panel/Log/DumpEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,20 @@
namespace PHPForge\Debug\Panel\Log;

/**
* Represents one `[key] => value` line of a parsed `print_r()` block.
* Represents one key and its value inside an array, object, or enum block of a dump tree.
*
* Inside object and enum blocks the key is split into the property name, its visibility, and the class that declares
* a private property; array keys and the dynamic property names of a `stdClass` object stay verbatim.
* Only the property keys of a `print_r()` object or enum dump, `stdClass` excluded, carry a visibility.
*/
final readonly class PrintREntry
final readonly class DumpEntry
{
/**
* @param string $key Array key or property name.
* @param PrintRNode $value Value of the entry.
* @param string $visibility `protected`, `private`, or `''` for a public property or an array key.
* @param string $declaringClass Class declaring a private property; `''` otherwise.
*/
public function __construct(
public string $key,
public PrintRNode $value,
public DumpNode $value,
public string $visibility = '',
public string $declaringClass = '',
) {}
Expand Down
63 changes: 63 additions & 0 deletions src/Panel/Log/DumpKind.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Panel\Log;

/**
* Represents the kinds of value a dump tree distinguishes.
*
* A `print_r()` dump yields only blocks, recursion markers, and untyped {@see self::SCALAR} text.
*/
enum DumpKind
{
/**
* Array block.
*/
case ARRAY;

/**
* Boolean, with its text as printed.
*/
case BOOLEAN;

/**
* Enum block, or a `var_export()` case reference with the case name as text and no entries.
*/
case ENUM;

/**
* Float, with its text as printed.
*/
case FLOAT;

/**
* Integer, with its text as printed.
*/
case INTEGER;

/**
* `null`, with its text as printed.
*/
case NULL;

/**
* Object block.
*/
case OBJECT;

/**
* Recursion marker `print_r()` prints instead of an array or object block.
*/
case RECURSION;

/**
* Untyped `print_r()` text, empty for `''`, `null`, and `false`.
*/
case SCALAR;

/**
* String, with its decoded value as text.
*/
case STRING;
}
169 changes: 169 additions & 0 deletions src/Panel/Log/DumpNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Panel\Log;

use PHPForge\Debug\Helper\Dump;

use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function var_export;

/**
* Represents one value of a dump tree: a scalar, an enum case reference, a recursion marker, or a block with entries.
*
* {@see PrintRParser} and {@see PhpExportParser} build trees from log messages; {@see DumpTreeRenderer} renders them.
*/
final readonly class DumpNode
{
/**
* @param string $label Class name of an object, enum, or recursive object; `''` otherwise.
* @param string $text Scalar text, enum case name, or free text preceding a root block header.
* @param list<DumpEntry> $entries Block entries.
*/
private function __construct(
public DumpKind $kind,
public string $label,
public string $text,
public array $entries,
) {}

/**
* Creates an array, object, or enum block.
*
* @param string $label Class name of an object or enum; `''` for an array.
* @param list<DumpEntry> $entries Entries in dump order.
* @param string $prefix Free text preceding a root block header, such as `Request:`.
*/
public static function block(DumpKind $kind, string $label, array $entries, string $prefix = ''): self
{
return new self($kind, $label, $prefix, $entries);
}

/**
* Creates a boolean from its printed text.
*/
public static function boolean(string $text): self
{
return new self(DumpKind::BOOLEAN, '', $text, []);
}

/**
* Creates the enum case reference that `var_export()` prints as `\Class::Case`.
*
* @param string $class Enum class name, without the leading backslash.
*/
public static function enumCase(string $class, string $case): self
{
return new self(DumpKind::ENUM, $class, $case, []);
}

/**
* Creates a float from its printed text.
*/
public static function float(string $text): self
{
return new self(DumpKind::FLOAT, '', $text, []);
}

/**
* Creates the tree of a context value normalized by {@see LogContext::normalize()}.
*
* A value that is neither an array, a scalar, nor `null` becomes an untyped scalar.
*
* @return self|null Tree, or `null` over the {@see PrintRParser} depth or value bound.
*/
public static function fromValue(mixed $value): self|null
{
$nodes = 0;

return self::fromNormalized($value, 1, $nodes);
}

/**
* Creates an integer from its printed text.
*/
public static function integer(string $text): self
{
return new self(DumpKind::INTEGER, '', $text, []);
}

/**
* Creates a `null` from its printed text.
*/
public static function null(string $text): self
{
return new self(DumpKind::NULL, '', $text, []);
}

/**
* Creates the marker `print_r()` prints instead of an array or object it is already printing.
*
* @param string $label Class name of a recursive object; `''` for a recursive array.
*/
public static function recursion(string $label): self
{
return new self(DumpKind::RECURSION, $label, '', []);
}

/**
* Creates an untyped `print_r()` scalar.
*
* @param string $text Text as printed, continuation lines included.
*/
public static function scalar(string $text): self
{
return new self(DumpKind::SCALAR, '', $text, []);
}

/**
* Creates a string from its decoded value.
*/
public static function string(string $value): self
{
return new self(DumpKind::STRING, '', $value, []);
}

/**
* Builds one value of {@see self::fromValue()}, counting it in `$nodes`.
*/
private static function fromNormalized(mixed $value, int $depth, int &$nodes): self|null
{
if (++$nodes > PrintRParser::MAX_NODES) {
return null;
}

if (is_array($value) === false) {
return match (true) {
is_string($value) => self::string($value),
is_int($value) => self::integer(var_export($value, true)),
is_float($value) => self::float(var_export($value, true)),
is_bool($value) => self::boolean(var_export($value, true)),
$value === null => self::null('NULL'),
default => self::scalar(Dump::asString($value)),
};
}

if ($depth > PrintRParser::MAX_DEPTH) {
return null;
}

$entries = [];

foreach ($value as $key => $item) {
$node = self::fromNormalized($item, $depth + 1, $nodes);

if ($node === null) {
return null;
}

$entries[] = new DumpEntry((string) $key, $node);
}

return self::block(DumpKind::ARRAY, '', $entries);
}
}
Loading
Loading