diff --git a/src/Panel/Asset/AssetBundleRow.php b/src/Panel/Asset/AssetBundleRow.php index b1c3113..883c75b 100644 --- a/src/Panel/Asset/AssetBundleRow.php +++ b/src/Panel/Asset/AssetBundleRow.php @@ -47,6 +47,14 @@ public function __construct( public array $depends, ) {} + /** + * Narrows one persisted bundle of the asset payload into a typed row. + * + * @param mixed $data Persisted bundle, expected to be an object carrying the declared shape. + * @param string $path JSON path of the bundle, used to report a malformed payload. + * + * @return self Row carrying every persisted bundle field. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -92,6 +100,8 @@ public static function fromBundle(string $name, array $bundle): self } /** + * Returns the typed row for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Asset/AssetSnapshot.php b/src/Panel/Asset/AssetSnapshot.php index 0c63a9e..ab955ca 100644 --- a/src/Panel/Asset/AssetSnapshot.php +++ b/src/Panel/Asset/AssetSnapshot.php @@ -16,9 +16,17 @@ /** * @param list $bundles */ - public function __construct(private array $bundles, private ViteManifest|null $vite) {} + public function __construct( + private array $bundles, + /** + * Captured Vite bridge snapshot, or `null` when no Vite bridge is registered. + */ + private ViteManifest|null $vite, + ) {} /** + * Returns the asset bundles registered during the request. + * * @return list Registered bundles in registration order. */ public function bundles(): array @@ -26,6 +34,14 @@ public function bundles(): array return $this->bundles; } + /** + * Narrows the persisted asset payload into a typed snapshot. + * + * @param mixed $data Persisted payload, expected to be an object carrying a `bundles` list and a `vite` entry. + * @param string $path JSON path of the payload, used to report a malformed capture. + * + * @return self Snapshot carrying the registered bundles and the Vite manifest. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -48,6 +64,8 @@ public static function fromArray(mixed $data, string $path): self } /** + * Returns the panel snapshot for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Asset/ViteChunk.php b/src/Panel/Asset/ViteChunk.php index 134e92a..f4de776 100644 --- a/src/Panel/Asset/ViteChunk.php +++ b/src/Panel/Asset/ViteChunk.php @@ -34,6 +34,14 @@ public function __construct( public bool $isEntry, ) {} + /** + * Narrows one persisted manifest entry into a typed chunk. + * + * @param mixed $data Persisted manifest entry, expected to be an object carrying the declared shape. + * @param string $path JSON path of the entry, used to report a malformed payload. + * + * @return self Chunk carrying every persisted manifest field. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -57,6 +65,8 @@ public static function fromArray(mixed $data, string $path): self } /** + * Returns the typed row for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Asset/ViteManifest.php b/src/Panel/Asset/ViteManifest.php index 709538b..767c287 100644 --- a/src/Panel/Asset/ViteManifest.php +++ b/src/Panel/Asset/ViteManifest.php @@ -17,13 +17,33 @@ * @param list $chunks */ public function __construct( + /** + * Public base URL the built assets are served from. + */ public string $baseUrl, + /** + * Whether the bridge serves assets from the Vite dev server instead of the build manifest. + */ public bool $devMode, + /** + * Dev server URL, or `null` when the bridge serves built assets. + */ public string|null $devServerUrl, + /** + * Filesystem path of the build manifest the bridge reads. + */ public string $manifestPath, public array $chunks, ) {} + /** + * Narrows the persisted Vite payload into a typed manifest. + * + * @param mixed $data Persisted manifest, expected to be an object carrying the declared shape. + * @param string $path JSON path of the manifest, used to report a malformed payload. + * + * @return self Manifest carrying the bridge configuration and its build chunks. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -53,6 +73,8 @@ public static function fromArray(mixed $data, string $path): self } /** + * Returns the typed manifest for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Db/DbSnapshot.php b/src/Panel/Db/DbSnapshot.php index 684e016..93f7c5b 100644 --- a/src/Panel/Db/DbSnapshot.php +++ b/src/Panel/Db/DbSnapshot.php @@ -39,6 +39,8 @@ public static function capture(array $rows): self } /** + * Returns the captured query rows. + * * @return list Executed statements in capture order. */ public function entries(): array @@ -46,6 +48,14 @@ public function entries(): array return $this->entries; } + /** + * Narrows the persisted database payload into a typed snapshot. + * + * @param mixed $data Persisted payload, expected to be an object carrying an `entries` list. + * @param string $path JSON path of the payload, used to report a malformed capture. + * + * @return self Snapshot carrying the persisted query rows. + */ public static function fromArray(mixed $data, string $path): self { return new self( @@ -56,6 +66,8 @@ public static function fromArray(mixed $data, string $path): self } /** + * Returns the panel snapshot for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Db/NPlusOneFinding.php b/src/Panel/Db/NPlusOneFinding.php index 40147ed..8cfb655 100644 --- a/src/Panel/Db/NPlusOneFinding.php +++ b/src/Panel/Db/NPlusOneFinding.php @@ -15,6 +15,7 @@ * @param float $totalDuration Total group duration in milliseconds. * @param int $firstSequence First query sequence used as the deep-link target. * @param list $sequences Query sequences belonging to the group. + * @param string $representativeQuery Statement shown as the sample of the group. */ public function __construct( public string $fingerprint, @@ -25,6 +26,11 @@ public function __construct( public string $representativeQuery, ) {} + /** + * Returns the stable DOM id the queries grid deep-links the group with. + * + * @return string DOM id derived from the first query sequence of the group. + */ public function id(): string { return "yii-debug-db-n1-{$this->firstSequence}"; diff --git a/src/Panel/Db/QueryRow.php b/src/Panel/Db/QueryRow.php index 022b6be..39416a6 100644 --- a/src/Panel/Db/QueryRow.php +++ b/src/Panel/Db/QueryRow.php @@ -126,6 +126,14 @@ public static function findBySequence(array $rows, string $seq): self|null return null; } + /** + * Narrows one persisted statement of the database payload into a typed row. + * + * @param mixed $data Persisted statement, expected to be an object carrying the declared shape. + * @param string $path JSON path of the statement, used to report a malformed payload. + * + * @return self Row carrying every persisted statement field. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) diff --git a/src/Panel/Db/SqlHighlighter.php b/src/Panel/Db/SqlHighlighter.php index 14a2bde..cd13841 100644 --- a/src/Panel/Db/SqlHighlighter.php +++ b/src/Panel/Db/SqlHighlighter.php @@ -7,12 +7,13 @@ use UIAwesome\Html\Helper\Encode; use function array_reverse; +use function preg_match; use function preg_match_all; use function strlen; use function substr; /** - * Highlights SQL statements as escape-safe HTML for the DB panel and the EXPLAIN view. + * Recognizes SQL statements and highlights them as escape-safe HTML for the DB, Log, and Profiling panels. */ final class SqlHighlighter { @@ -29,6 +30,22 @@ final class SqlHighlighter . '|ORDER|HAVING|LIMIT|OFFSET|UNION|ALL|DISTINCT|INTO|VALUES|SET|CREATE|ALTER|DROP|TABLE|INDEX|VIEW|TRIGGER' . '|SEQUENCE|PRIMARY|FOREIGN|KEY|REFERENCES|CONSTRAINT|DEFAULT|CHECK|UNIQUE|ASC|DESC|WITH|RECURSIVE' . '|RETURNING|CAST|COALESCE|NULLIF|BEGIN|COMMIT|ROLLBACK|TRANSACTION|EXPLAIN|ANALYZE|SHOW|DESCRIBE)\b)~is'; + /** + * Statement shapes that identify raw SQL: an opening verb followed by the clause that verb requires, so prose + * opening with the same word ("Select me", "Update available") stays plain text. + */ + private const string STATEMENT_PATTERN = '~^\s*(?:' + . 'SELECT\b.{0,4096}?\bFROM\b' + . '|INSERT\s+INTO\b' + . '|UPDATE\b.{0,4096}?\bSET\b' + . '|DELETE\s+FROM\b' + . '|REPLACE\s+INTO\b' + . '|WITH\b.{0,4096}?\bSELECT\b' + . '|(?:CREATE|ALTER|DROP|TRUNCATE)\s+(?:TEMPORARY\s+|UNIQUE\s+)?' + . '(?:TABLE|INDEX|VIEW|SCHEMA|DATABASE|SEQUENCE|TRIGGER)\b' + . '|(?:PRAGMA|EXPLAIN|VACUUM|ANALYZE|SHOW)\s+\S' + . '|(?:BEGIN|COMMIT|ROLLBACK)(?:\s+TRANSACTION)?\s*;?\s*$' + . ')~is'; /** * Maps a matched named group to its `` class; an empty class emits the escaped token unwrapped. */ @@ -81,4 +98,19 @@ public static function highlight(string $sql): string return $html . Encode::content(substr($sql, $offset)); } + + /** + * Returns whether the value reads as a raw SQL statement rather than as a log or profiling sentence. + * + * Panels whose rows mix both call this to decide when {@see highlight()} applies, so statements logged under a + * category the panel does not know still render with the token spans of the queries grid. + * + * @param string $value Message or block description to inspect. + * + * @return bool `true` when the value opens a SQL statement; `false` otherwise. + */ + public static function isStatement(string $value): bool + { + return preg_match(self::STATEMENT_PATTERN, $value) === 1; + } } diff --git a/src/Panel/Dump/DumpRow.php b/src/Panel/Dump/DumpRow.php index e3a0389..9ec8552 100644 --- a/src/Panel/Dump/DumpRow.php +++ b/src/Panel/Dump/DumpRow.php @@ -36,6 +36,14 @@ public function __construct( public array $trace, ) {} + /** + * Narrows one persisted dump of the dump payload into a typed row. + * + * @param mixed $data Persisted dump, expected to be an object carrying the declared shape. + * @param string $path JSON path of the dump, used to report a malformed payload. + * + * @return self Row carrying every persisted dump field. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -75,6 +83,8 @@ public static function fromLoggerTuple(array $message): self } /** + * Returns the typed row for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Dump/DumpSnapshot.php b/src/Panel/Dump/DumpSnapshot.php index 543a757..0cb73e3 100644 --- a/src/Panel/Dump/DumpSnapshot.php +++ b/src/Panel/Dump/DumpSnapshot.php @@ -37,6 +37,8 @@ public static function capture(array $messages): self } /** + * Returns the captured dump rows. + * * @return list Captured rows in capture order. */ public function entries(): array @@ -44,6 +46,14 @@ public function entries(): array return $this->entries; } + /** + * Narrows the persisted dump payload into a typed snapshot. + * + * @param mixed $data Persisted payload, expected to be an object carrying an `entries` list. + * @param string $path JSON path of the payload, used to report a malformed capture. + * + * @return self Snapshot carrying the persisted dump rows. + */ public static function fromArray(mixed $data, string $path): self { return new self( @@ -54,6 +64,8 @@ public static function fromArray(mixed $data, string $path): self } /** + * Returns the panel snapshot for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Event/EventCapture.php b/src/Panel/Event/EventCapture.php index daeaadd..e24d057 100644 --- a/src/Panel/Event/EventCapture.php +++ b/src/Panel/Event/EventCapture.php @@ -23,9 +23,11 @@ final class EventCapture { /** + * Bounds and sanitizes the adapter-selected context fields. + * * @param array $fields Adapter-selected fields only. * - * @return array + * @return array At most sixteen sanitized fields, with sensitive values replaced by `[redacted]`. */ public static function context(array $fields): array { @@ -43,11 +45,13 @@ public static function context(array $fields): array } /** + * Bounds and sanitizes the source frames observed when an event fired. + * * @param list> $frames Backtrace acquired with `DEBUG_BACKTRACE_IGNORE_ARGS`. * @param int $limit Maximum frames; hard-limited to sixteen. * @param list $skipFiles Adapter instrumentation files to omit. * - * @return list + * @return list Sanitized `file:line` frames in call order. */ public static function trace(array $frames, int $limit, array $skipFiles = []): array { @@ -74,6 +78,13 @@ public static function trace(array $frames, int $limit, array $skipFiles = []): return $result; } + /** + * Strips NUL-delimited declaration paths and bounds the captured text. + * + * @param string $value Raw diagnostic text. + * + * @return string Text bounded to 2048 bytes, suffixed with `[truncated]` when it was cut. + */ private static function text(string $value): string { // Anonymous class names may contain a NUL-delimited declaration path. diff --git a/src/Panel/Event/EventInspection.php b/src/Panel/Event/EventInspection.php index 8147cf8..d384b61 100644 --- a/src/Panel/Event/EventInspection.php +++ b/src/Panel/Event/EventInspection.php @@ -16,28 +16,60 @@ */ final class EventInspection implements PanelRow { + /** + * Monotonic observation time in seconds, or `null` when no clock reading was captured. + */ private float|null $clock = null; /** + * Explicit adapter-selected scalar fields observed with the event. + * * @var array */ private array $context = []; + /** + * Context capture state: `disabled`, `captured`, `unsupported`, or `failed`. + */ private string $contextStatus = 'disabled'; + /** + * Observed nesting depth of the scope the event belongs to. + */ private int $depth = 0; + /** + * Request-local scope identity correlating lifecycle markers, or `null` when no correlation was observed. + */ private int|null $pairId = null; + /** + * Lifecycle marker: `enter`, `leave`, or empty for ordinary events. + */ private string $phase = ''; /** + * Argument-free source frames observed when the event fired. + * * @var list */ private array $trace = []; + /** + * Trace capture state: `disabled`, `captured`, or `failed`. + */ private string $traceStatus = 'disabled'; + /** + * Hydrates event diagnostics from decoded JSON data. + * + * @param mixed $data Decoded event diagnostics payload. + * @param string $path Payload path used in hydration errors. + * + * @throws HydrationException When a value exceeds its bound or a capture state is unknown. + * + * @return self Hydrated event diagnostics. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path)->shape( @@ -105,52 +137,91 @@ public static function fromArray(mixed $data, string $path): self ->withLifecycle($pairId, $phase, $depth, $clock); } + /** + * Returns the monotonic observation time. + * + * @return float|null Observation time in seconds, or `null` when no clock reading was captured. + */ public function getClock(): float|null { return $this->clock; } /** - * @return array + * Returns the selected context fields. + * + * @return array Explicit adapter-selected scalar fields. */ public function getContext(): array { return $this->context; } + /** + * Returns the context capture state. + * + * @return string One of `disabled`, `captured`, `unsupported`, or `failed`. + */ public function getContextStatus(): string { return $this->contextStatus; } + /** + * Returns the observed nesting depth. + * + * @return int Nesting depth of the scope the event belongs to. + */ public function getDepth(): int { return $this->depth; } + /** + * Returns the request-local scope identity. + * + * @return int|null Scope identity correlating lifecycle markers, or `null` when none was observed. + */ public function getPairId(): int|null { return $this->pairId; } + /** + * Returns the lifecycle marker. + * + * @return string `enter`, `leave`, or empty for ordinary events. + */ public function getPhase(): string { return $this->phase; } /** - * @return list + * Returns the captured source frames. + * + * @return list Argument-free source frames. */ public function getTrace(): array { return $this->trace; } + /** + * Returns the trace capture state. + * + * @return string One of `disabled`, `captured`, or `failed`. + */ public function getTraceStatus(): string { return $this->traceStatus; } + /** + * Returns the diagnostics for JSON serialization. + * + * @return array Serialized context, trace, capture states, and lifecycle correlation. + */ public function jsonSerialize(): array { return [ @@ -170,6 +241,8 @@ public function jsonSerialize(): array * * @param array $context Explicit adapter-selected scalar fields, not an object dump. * @param string $contextStatus One of `disabled`, `captured`, `unsupported`, or `failed`. + * + * @return self Diagnostics with the context and its capture state applied. */ public function withContext(array $context, string $contextStatus): self { @@ -187,6 +260,8 @@ public function withContext(array $context, string $contextStatus): self * @param string $phase Empty for ordinary events; `enter` or `leave` for lifecycle markers. * @param int $depth Observed nesting depth. * @param float|null $clock Monotonic observation time in seconds, not a wall-clock timestamp. + * + * @return self Diagnostics with the lifecycle correlation applied. */ public function withLifecycle(int|null $pairId, string $phase, int $depth, float|null $clock): self { @@ -204,6 +279,8 @@ public function withLifecycle(int|null $pairId, string $phase, int $depth, float * * @param list $trace Argument-free source frames. * @param string $traceStatus One of `disabled`, `captured`, or `failed`. + * + * @return self Diagnostics with the trace and its capture state applied. */ public function withTrace(array $trace, string $traceStatus): self { diff --git a/src/Panel/Event/EventRow.php b/src/Panel/Event/EventRow.php index 8f6c916..b2707a8 100644 --- a/src/Panel/Event/EventRow.php +++ b/src/Panel/Event/EventRow.php @@ -48,6 +48,8 @@ public function __construct( * Returns how many distinct event classes the given rows cover. * * @param list $rows Captured event rows. + * + * @return int Number of distinct event classes across the given rows. */ public static function distinctClassCount(array $rows): int { @@ -62,6 +64,14 @@ public static function distinctClassCount(array $rows): int return count($classes); } + /** + * Hydrates an event row from decoded JSON data. + * + * @param mixed $data Decoded event row payload. + * @param string $path Payload path used in hydration errors. + * + * @return self Hydrated row, enriched with diagnostics when the payload carried them. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -89,13 +99,20 @@ class: $payload->string('class'), : $row; } + /** + * Returns the optional diagnostics attached to the row. + * + * @return EventInspection|null Attached diagnostics, or `null` when the row was captured without them. + */ public function inspection(): EventInspection|null { return $this->inspection; } /** - * @return array + * Returns the row for JSON serialization. + * + * @return array Serialized row fields, including the diagnostics when the row carries them. */ public function jsonSerialize(): array { @@ -113,6 +130,8 @@ public function jsonSerialize(): array * Returns how many of the given rows were triggered statically. * * @param list $rows Captured event rows. + * + * @return int Number of statically triggered rows. */ public static function staticCount(array $rows): int { @@ -129,6 +148,10 @@ public static function staticCount(array $rows): int /** * Returns an enriched copy without changing the constructor or the original captured row. + * + * @param EventInspection $inspection Optional diagnostics to attach. + * + * @return self Row carrying the diagnostics. */ public function withInspection(EventInspection $inspection): self { diff --git a/src/Panel/Event/EventSequence.php b/src/Panel/Event/EventSequence.php index e50bf66..75aaa8a 100644 --- a/src/Panel/Event/EventSequence.php +++ b/src/Panel/Event/EventSequence.php @@ -22,6 +22,8 @@ private array $positions; /** + * Derives the chronology from the complete capture. + * * @param list $rows Original observation order. */ public function __construct(private array $rows) @@ -42,11 +44,25 @@ public function __construct(private array $rows) $this->pairs = $pairs; } + /** + * Returns how long after the first captured event the given row fired. + * + * @param EventRow $row Row to measure. + * + * @return float Offset from the first captured row, in milliseconds. + */ public function elapsed(EventRow $row): float { return ($row->time - ($this->rows[0]->time ?? $row->time)) * 1000; } + /** + * Returns the idle time between the given row and the one observed before it. + * + * @param EventRow $row Row to measure. + * + * @return float|null Milliseconds since the previous row, or `null` for the first row and for unknown rows. + */ public function gap(EventRow $row): float|null { $index = $this->index($row); @@ -56,6 +72,13 @@ public function gap(EventRow $row): float|null return $previous === null ? null : ($row->time - $previous->time) * 1000; } + /** + * Returns the original observation position of the given row. + * + * @param EventRow $row Row to locate. + * + * @return int One-based capture position, or `0` when the row is not part of the capture. + */ public function index(EventRow $row): int { return $this->positions[spl_object_id($row)] ?? 0; @@ -63,6 +86,10 @@ public function index(EventRow $row): int /** * Returns only unambiguous, explicitly correlated inclusive intervals. + * + * @param EventRow $row Lifecycle `enter` marker to measure. + * + * @return float|null Inclusive duration in milliseconds, or `null` when the correlation is absent or ambiguous. */ public function interval(EventRow $row): float|null { diff --git a/src/Panel/Event/EventSnapshot.php b/src/Panel/Event/EventSnapshot.php index 30f851c..8e101be 100644 --- a/src/Panel/Event/EventSnapshot.php +++ b/src/Panel/Event/EventSnapshot.php @@ -14,11 +14,15 @@ final readonly class EventSnapshot implements PanelSnapshot { /** - * @param list $entries + * Creates a snapshot from the captured event rows. + * + * @param list $entries Captured rows in fire order. */ public function __construct(private array $entries) {} /** + * Returns the captured event rows. + * * @return list Captured rows in fire order. */ public function entries(): array @@ -26,6 +30,14 @@ public function entries(): array return $this->entries; } + /** + * Hydrates the Event panel snapshot from decoded JSON data. + * + * @param mixed $data Decoded Event panel payload. + * @param string $path Payload path used in hydration errors. + * + * @return self Hydrated snapshot carrying the typed rows. + */ public static function fromArray(mixed $data, string $path): self { return new self( @@ -36,7 +48,9 @@ public static function fromArray(mixed $data, string $path): self } /** - * @return array + * Returns the snapshot for JSON serialization. + * + * @return array Serialized event rows in fire order. */ public function jsonSerialize(): array { diff --git a/src/Panel/Log/LogCellRenderer.php b/src/Panel/Log/LogCellRenderer.php index 5ed8363..ec7728b 100644 --- a/src/Panel/Log/LogCellRenderer.php +++ b/src/Panel/Log/LogCellRenderer.php @@ -82,16 +82,19 @@ public static function renderLevelCell(LogRow $row): string * Renders the message cell, followed by the optional trace list. * * The row holds the message as a display string (already exported when the source was non-string), so the renderer - * escapes it once — except for DB command entries, whose raw SQL message renders through - * {@see SqlHighlighter::highlight()} with the same token spans as the db panel queries grid. Long messages - * collapse behind the {@see CellMore} clamp. + * escapes it once — except for DB command entries and for any message {@see SqlHighlighter::isStatement()} reads + * as raw SQL, which render through {@see SqlHighlighter::highlight()} with the same token spans as the db panel + * queries grid. Long messages collapse behind the {@see CellMore} clamp. * * @param LogRow $row Typed log record. * @param Closure(array): string $traceLine Renders one backtrace frame as a link line. */ public static function renderMessageCell(LogRow $row, Closure $traceLine): string { - $body = str_starts_with($row->category, self::SQL_CATEGORY_PREFIX) + $isSql = str_starts_with($row->category, self::SQL_CATEGORY_PREFIX) + || SqlHighlighter::isStatement($row->message); + + $body = $isSql ? Div::tag() ->class('yii-debug-db-sql') ->html(SqlHighlighter::highlight($row->message)) diff --git a/src/Panel/Log/LogRow.php b/src/Panel/Log/LogRow.php index a6302a9..6870589 100644 --- a/src/Panel/Log/LogRow.php +++ b/src/Panel/Log/LogRow.php @@ -60,6 +60,14 @@ public function __construct( public array $trace, ) {} + /** + * Narrows one persisted message of the log payload into a typed row. + * + * @param mixed $data Persisted message, expected to be an object carrying the declared shape. + * @param string $path JSON path of the message, used to report a malformed payload. + * + * @return self Row carrying every persisted message field. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -128,6 +136,8 @@ public static function fromLoggerTuple( } /** + * Returns the typed row for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Log/LogSnapshot.php b/src/Panel/Log/LogSnapshot.php index 5876bb9..3dddac3 100644 --- a/src/Panel/Log/LogSnapshot.php +++ b/src/Panel/Log/LogSnapshot.php @@ -64,6 +64,8 @@ public static function capture(array $messages): self } /** + * Returns the captured log rows. + * * @return list Captured rows in capture order. */ public function entries(): array @@ -71,6 +73,14 @@ public function entries(): array return $this->entries; } + /** + * Narrows the persisted log payload into a typed snapshot. + * + * @param mixed $data Persisted payload, expected to be an object carrying an `entries` list. + * @param string $path JSON path of the payload, used to report a malformed capture. + * + * @return self Snapshot carrying the persisted log rows. + */ public static function fromArray(mixed $data, string $path): self { return new self( @@ -81,6 +91,8 @@ public static function fromArray(mixed $data, string $path): self } /** + * Returns the panel snapshot for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/PanelRenderer.php b/src/Panel/PanelRenderer.php index fc3a2ef..064cc42 100644 --- a/src/Panel/PanelRenderer.php +++ b/src/Panel/PanelRenderer.php @@ -4,6 +4,7 @@ namespace PHPForge\Debug\Panel; +use JsonException; use PHPForge\Debug\{ColumnStyle, PanelView}; use PHPForge\Debug\Helper\{Badge, CellMore, Disclosure, EmptyState, ExtensionPill, Format, Icon, Table, Trace}; use PHPForge\Debug\Panel\Db\SqlHighlighter; @@ -583,6 +584,15 @@ private static function pills(PillsBlock $block): string ->render(); } + /** + * Renders an inline value as its JSON text, clamped when it overflows the cell. + * + * @param mixed $value Value to encode. + * + * @throws JsonException When the value cannot be encoded. + * + * @return string Encoded JSON document, clamped behind the cell expander when it is long. + */ private function preview(mixed $value): string { $json = json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); diff --git a/src/Panel/Profile/ProfileCellRenderer.php b/src/Panel/Profile/ProfileCellRenderer.php index abc06e0..3c3b78b 100644 --- a/src/Panel/Profile/ProfileCellRenderer.php +++ b/src/Panel/Profile/ProfileCellRenderer.php @@ -55,9 +55,9 @@ public static function renderDurationCell(ProfileRow $row, float $maxDuration): /** * Renders the info cell with one indentation arrow per nesting level, followed by the info text. * - * DB command blocks carry a raw SQL statement as their info, so they render through - * {@see SqlHighlighter::highlight()} with the same token spans as the db panel queries grid. Long statements - * collapse behind the {@see CellMore} clamp instead of stretching the row past the viewport. + * DB command blocks, and any block whose info {@see SqlHighlighter::isStatement()} reads as raw SQL, render + * through {@see SqlHighlighter::highlight()} with the same token spans as the db panel queries grid. Long + * statements collapse behind the {@see CellMore} clamp instead of stretching the row past the viewport. * * @param ProfileRow $row Typed profile row. */ @@ -68,7 +68,7 @@ public static function renderInfoCell(ProfileRow $row): string ->content('→') ->render(); - $body = self::isSqlCategory($row->category) + $body = self::isSqlCategory($row->category) || SqlHighlighter::isStatement($row->info) ? Div::tag()->class('yii-debug-db-sql') ->html(SqlHighlighter::highlight($row->info)) ->render() @@ -91,6 +91,13 @@ public static function renderTimeCell(ProfileRow $row): string ->render(); } + /** + * Returns whether the category belongs to a DB command block whose info is a raw SQL statement. + * + * @param string $category Profile category to inspect. + * + * @return bool `true` when the category starts with a known DB command prefix; `false` otherwise. + */ private static function isSqlCategory(string $category): bool { foreach (self::SQL_CATEGORY_PREFIXES as $prefix) { diff --git a/src/Panel/Profile/ProfileRow.php b/src/Panel/Profile/ProfileRow.php index 71b9394..28f5a7e 100644 --- a/src/Panel/Profile/ProfileRow.php +++ b/src/Panel/Profile/ProfileRow.php @@ -54,6 +54,14 @@ public function __construct( public array $trace, ) {} + /** + * Narrows one persisted block of the profiling payload into a typed row. + * + * @param mixed $data Persisted block, expected to be an object carrying the declared shape. + * @param string $path JSON path of the block, used to report a malformed payload. + * + * @return self Row carrying every persisted block field. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -106,6 +114,8 @@ public static function fromTiming(array $timing, int $seq): self } /** + * Returns the typed row for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Profile/ProfilingSnapshot.php b/src/Panel/Profile/ProfilingSnapshot.php index 96e227e..387b8d0 100644 --- a/src/Panel/Profile/ProfilingSnapshot.php +++ b/src/Panel/Profile/ProfilingSnapshot.php @@ -27,7 +27,13 @@ * @param list $samples */ public function __construct( + /** + * Peak memory in bytes recorded for the request. + */ public int $memory, + /** + * Request processing duration in seconds. + */ public float $time, private array $entries, private array $samples, @@ -123,6 +129,8 @@ public static function captureCompleted(int $memory, float $time, array $message } /** + * Returns the resolved profile blocks. + * * @return list Resolved profile blocks in capture order. */ public function entries(): array @@ -130,6 +138,14 @@ public function entries(): array return $this->entries; } + /** + * Narrows the persisted profiling payload into a typed snapshot. + * + * @param mixed $data Persisted payload, expected to be an object carrying the declared shape. + * @param string $path JSON path of the payload, used to report a malformed capture. + * + * @return self Snapshot carrying the request metrics, the profile blocks, and the memory samples. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -165,6 +181,8 @@ public static function fromArray(mixed $data, string $path): self } /** + * Returns the panel snapshot for JSON serialization. + * * @return array */ public function jsonSerialize(): array @@ -181,6 +199,8 @@ public function jsonSerialize(): array } /** + * Returns the memory samples that feed the timeline chart. + * * @return list Memory readings recorded alongside each captured profile message. */ public function samples(): array diff --git a/src/Panel/Queue/JobRecord.php b/src/Panel/Queue/JobRecord.php index c0d9577..33d1e01 100644 --- a/src/Panel/Queue/JobRecord.php +++ b/src/Panel/Queue/JobRecord.php @@ -114,6 +114,16 @@ public function __construct( public string $error, ) {} + /** + * Narrows one persisted event of the queue payload into a typed record. + * + * @param mixed $data Persisted event, expected to be an object carrying the declared shape. + * @param string $path JSON path of the event, used to report a malformed payload. + * + * @throws HydrationException When the persisted event type is not one of {@see self::EVENT_TYPES}. + * + * @return self Record carrying every persisted event field. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -194,6 +204,8 @@ public static function fromCapture(array $row): self } /** + * Returns the typed row for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Queue/QueueSnapshot.php b/src/Panel/Queue/QueueSnapshot.php index b827037..0145b56 100644 --- a/src/Panel/Queue/QueueSnapshot.php +++ b/src/Panel/Queue/QueueSnapshot.php @@ -38,6 +38,8 @@ public static function capture(array $records): self } /** + * Returns the captured queue lifecycle events. + * * @return list Captured job events in event order. */ public function entries(): array @@ -45,6 +47,14 @@ public function entries(): array return $this->entries; } + /** + * Narrows the persisted queue payload into a typed snapshot. + * + * @param mixed $data Persisted payload, expected to be an object carrying an `entries` list. + * @param string $path JSON path of the payload, used to report a malformed capture. + * + * @return self Snapshot carrying the persisted job events. + */ public static function fromArray(mixed $data, string $path): self { return new self( @@ -55,6 +65,8 @@ public static function fromArray(mixed $data, string $path): self } /** + * Returns the panel snapshot for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Request/RequestDiagnosticValueRenderer.php b/src/Panel/Request/RequestDiagnosticValueRenderer.php index 3bf1205..79682fd 100644 --- a/src/Panel/Request/RequestDiagnosticValueRenderer.php +++ b/src/Panel/Request/RequestDiagnosticValueRenderer.php @@ -26,6 +26,10 @@ final class RequestDiagnosticValueRenderer { /** * Escapes a diagnostic label while substituting malformed UTF-8 bytes. + * + * @param string $value Diagnostic label to escape. + * + * @return string Escaped label with malformed bytes substituted. */ public static function escape(string $value): string { @@ -34,6 +38,10 @@ public static function escape(string $value): string /** * Renders a header value, preserving repeated header lines as distinct ordered values. + * + * @param mixed $value Captured header value. + * + * @return string Clamped list markup for repeated lines, or the single rendered value. */ public static function header(mixed $value): string { @@ -67,6 +75,10 @@ public static function header(mixed $value): string /** * Renders a captured scalar as readable text and falls back to the diagnostic dumper for structured values. + * + * @param mixed $value Captured diagnostic value. + * + * @return string Clamped markup carrying the rendered value. */ public static function value(mixed $value): string { @@ -77,6 +89,12 @@ public static function value(mixed $value): string } /** + * Determines whether the value is a non-empty list of strings. + * + * @param mixed $value Captured diagnostic value. + * + * @return bool `true` when every entry is a `string`, `false` otherwise. + * * @phpstan-assert-if-true list $value */ private static function isStringList(mixed $value): bool @@ -94,6 +112,13 @@ private static function isStringList(mixed $value): bool return true; } + /** + * Renders a captured string, marking an empty one with a readable placeholder. + * + * @param string $value Captured string value. + * + * @return string Escaped text, or the empty-value placeholder markup. + */ private static function string(string $value): string { if ($value === '') { diff --git a/src/Panel/Request/RequestSnapshot.php b/src/Panel/Request/RequestSnapshot.php index 1146737..bee089d 100644 --- a/src/Panel/Request/RequestSnapshot.php +++ b/src/Panel/Request/RequestSnapshot.php @@ -13,10 +13,22 @@ */ final readonly class RequestSnapshot implements PanelSnapshot { + /** + * Creates a snapshot from the tagged Request payload. + * + * @param DebugArray $data Tagged Request payload captured for the request. + * @param int $statusCode HTTP status code the request completed with. + */ private function __construct(private DebugArray $data, public int $statusCode) {} /** - * @param array $data + * Captures the Request payload together with the status code it must agree with. + * + * @param array $data Raw Request payload captured for the request. + * + * @throws HydrationException When the payload carries no integer `statusCode`. + * + * @return self Snapshot carrying the tagged payload and its status code. */ public static function capture(array $data): self { @@ -30,13 +42,25 @@ public static function capture(array $data): self } /** - * @return array + * Returns the payload restored to plain PHP values. + * + * @return array Payload restored to plain PHP values. */ public function data(): array { return $this->data->values(); } + /** + * Hydrates the Request panel snapshot from decoded JSON data. + * + * @param mixed $data Decoded Request panel payload. + * @param string $path Payload path used in hydration errors. + * + * @throws HydrationException When the stored status code disagrees with the one held in the payload. + * + * @return self Hydrated Request panel snapshot. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -57,7 +81,9 @@ public static function fromArray(mixed $data, string $path): self } /** - * @return array + * Returns the snapshot for JSON serialization. + * + * @return array Tagged payload and the status code it agrees with. */ public function jsonSerialize(): array { diff --git a/src/Panel/Request/Routing/CurrentRouteView.php b/src/Panel/Request/Routing/CurrentRouteView.php index b3382a0..09aad04 100644 --- a/src/Panel/Request/Routing/CurrentRouteView.php +++ b/src/Panel/Request/Routing/CurrentRouteView.php @@ -9,74 +9,136 @@ */ final class CurrentRouteView { + /** + * Handler the request dispatched to, or `null` when the adapter exposes none. + */ private string|null $action = null; + /** + * Route definition the request matched, or `null` when the adapter exposes none. + */ private RouteDefinition|null $definition = null; + /** + * Routing failure reported while resolving the request, or `null` when resolution succeeded. + */ private string|null $error = null; + /** + * Resolution summary shown above the routing trace, or `null` when the adapter captured none. + */ private string|null $message = null; /** + * Parameters bound to the resolved route. + * * @var array */ private array $parameters = []; /** + * Routing rules inspected while resolving the request. + * * @var list */ private array $trace = []; + /** + * @param string $route Route the request resolved to, as the application declares it. + */ public function __construct( private string $route = '', ) {} + /** + * Creates current-route diagnostics ready for immutable enrichment. + * + * @param string $route Route the request resolved to, as the application declares it. + * + * @return self View carrying only the resolved route. + */ public static function create(string $route = ''): self { return new self($route); } + /** + * Returns the handler the request dispatched to. + * + * @return string|null Handler the request dispatched to, or `null` when the adapter exposes none. + */ public function getAction(): string|null { return $this->action; } + /** + * Returns the route definition the request matched. + * + * @return RouteDefinition|null Matched route definition, or `null` when the adapter exposes none. + */ public function getDefinition(): RouteDefinition|null { return $this->definition; } + /** + * Returns the routing failure reported while resolving the request. + * + * @return string|null Routing failure message, or `null` when resolution succeeded. + */ public function getError(): string|null { return $this->error; } + /** + * Returns the resolution summary shown above the routing trace. + * + * @return string|null Resolution summary, or `null` when the adapter captured none. + */ public function getMessage(): string|null { return $this->message; } /** - * @return array + * Returns the parameters bound to the resolved route. + * + * @return array Parameters bound to the resolved route. */ public function getParameters(): array { return $this->parameters; } + /** + * Returns the route the request resolved to. + * + * @return string Route the request resolved to, as the application declares it. + */ public function getRoute(): string { return $this->route; } /** - * @return list + * Returns the routing rules inspected while resolving the request. + * + * @return list Routing rules inspected while resolving the request. */ public function getTrace(): array { return $this->trace; } + /** + * Returns a copy carrying another dispatched handler. + * + * @param string|null $action Handler the request dispatched to, or `null` when the adapter exposes none. + * + * @return self View with the action applied. + */ public function withAction(string|null $action): self { $clone = clone $this; @@ -85,6 +147,13 @@ public function withAction(string|null $action): self return $clone; } + /** + * Returns a copy carrying another route definition. + * + * @param RouteDefinition|null $definition Route definition the request matched, or `null` when unsupported. + * + * @return self View with the definition applied. + */ public function withDefinition(RouteDefinition|null $definition): self { $clone = clone $this; @@ -93,6 +162,13 @@ public function withDefinition(RouteDefinition|null $definition): self return $clone; } + /** + * Returns a copy carrying another routing failure. + * + * @param string|null $error Routing failure reported while resolving the request, or `null` to clear it. + * + * @return self View with the error applied. + */ public function withError(string|null $error): self { $clone = clone $this; @@ -101,6 +177,13 @@ public function withError(string|null $error): self return $clone; } + /** + * Returns a copy carrying another resolution summary. + * + * @param string|null $message Resolution summary shown above the routing trace, or `null` to clear it. + * + * @return self View with the message applied. + */ public function withMessage(string|null $message): self { $clone = clone $this; @@ -110,7 +193,11 @@ public function withMessage(string|null $message): self } /** - * @param array $parameters + * Returns a copy carrying another set of route parameters. + * + * @param array $parameters Parameters bound to the resolved route. + * + * @return self View with the parameters applied. */ public function withParameters(array $parameters): self { @@ -121,7 +208,11 @@ public function withParameters(array $parameters): self } /** - * @param list $trace + * Returns a copy carrying another routing trace. + * + * @param list $trace Routing rules inspected while resolving the request. + * + * @return self View with the trace applied. */ public function withTrace(array $trace): self { diff --git a/src/Panel/Request/Routing/RouteInventoryView.php b/src/Panel/Request/Routing/RouteInventoryView.php index 604016a..09733da 100644 --- a/src/Panel/Request/Routing/RouteInventoryView.php +++ b/src/Panel/Request/Routing/RouteInventoryView.php @@ -28,15 +28,19 @@ final class RouteInventoryView */ private string $source = 'Current application configuration'; + /** + * @param list $routes Route definitions the application declares. + */ public function __construct( - /** - * @var list - */ private array $routes, ) {} /** - * @param list $routes + * Creates a route inventory ready for immutable enrichment. + * + * @param list $routes Route definitions the application declares. + * + * @return self Inventory carrying only the route definitions. */ public static function create(array $routes): self { @@ -44,38 +48,61 @@ public static function create(array $routes): self } /** - * @return list + * Returns the badges derived from the inventory. + * + * @return list Route badges derived from the inventory. */ public function getBadges(): array { return $this->badges; } + /** + * Returns the error generated while building the inventory. + * + * @return string|null Error message, or `null` when no error occurred. + */ public function getError(): string|null { return $this->error; } /** - * @return list + * Returns the route definitions the inventory holds. + * + * @return list Route definitions the application declares. */ public function getRoutes(): array { return $this->routes; } + /** + * Returns the label describing where the inventory data came from. + * + * @return string Source label describing where the inventory data came from. + */ public function getSource(): string { return $this->source; } + /** + * Determines whether the inventory reflects live configuration. + * + * @return bool `true` when the inventory reflects live configuration, `false` for a stored capture. + */ public function isLive(): bool { return $this->live; } /** - * @param list $badges + * Returns a copy carrying another set of badges. + * + * @param list $badges Route badges derived from the inventory. + * + * @return self Inventory with the badges applied. */ public function withBadges(array $badges): self { @@ -85,6 +112,13 @@ public function withBadges(array $badges): self return $clone; } + /** + * Returns a copy carrying another inventory error. + * + * @param string|null $error Error message generated while building the inventory, or `null` to clear it. + * + * @return self Inventory with the error applied. + */ public function withError(string|null $error): self { $clone = clone $this; @@ -93,6 +127,13 @@ public function withError(string|null $error): self return $clone; } + /** + * Returns a copy flagged as live configuration or as a stored capture. + * + * @param bool $live `true` when the inventory reflects live configuration, `false` for a stored capture. + * + * @return self Inventory with the live flag applied. + */ public function withLive(bool $live): self { $clone = clone $this; @@ -101,6 +142,13 @@ public function withLive(bool $live): self return $clone; } + /** + * Returns a copy carrying another source label. + * + * @param string $source Source label describing where the inventory data came from. + * + * @return self Inventory with the source applied. + */ public function withSource(string $source): self { $clone = clone $this; diff --git a/src/Panel/Router/CurrentRouteLogRow.php b/src/Panel/Router/CurrentRouteLogRow.php index 3482041..5ab33fb 100644 --- a/src/Panel/Router/CurrentRouteLogRow.php +++ b/src/Panel/Router/CurrentRouteLogRow.php @@ -31,6 +31,14 @@ public function __construct( public bool $match, ) {} + /** + * Narrows one persisted rule of the routing payload into a typed row. + * + * @param mixed $data Persisted rule, expected to be an object carrying the declared shape. + * @param string $path JSON path of the rule, used to report a malformed payload. + * + * @return self Row carrying the inspected rule, its parent, and the match flag. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path)->shape(['rule', 'parent', 'match']); @@ -64,6 +72,8 @@ public static function fromLogMessage(mixed $message): self|null } /** + * Returns the typed row for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Router/RouterSnapshot.php b/src/Panel/Router/RouterSnapshot.php index e858711..3ec5083 100644 --- a/src/Panel/Router/RouterSnapshot.php +++ b/src/Panel/Router/RouterSnapshot.php @@ -19,8 +19,17 @@ * @param list $entries */ public function __construct( + /** + * Dispatched action descriptor, or `null` when routing resolved none. + */ public string|null $action, + /** + * Route the request resolved to. + */ public string $route, + /** + * Routing trace message emitted by the URL manager, or `null` when it emitted none. + */ public string|null $message, private array $entries, ) {} @@ -66,6 +75,8 @@ public static function capture(string|null $action, array $messages, string $rou } /** + * Returns the URL rules inspected while resolving the route. + * * @return list Rules inspected during routing, in inspection order. */ public function entries(): array @@ -73,6 +84,14 @@ public function entries(): array return $this->entries; } + /** + * Narrows the persisted routing payload into a typed snapshot. + * + * @param mixed $data Persisted payload, expected to be an object carrying the declared shape. + * @param string $path JSON path of the payload, used to report a malformed capture. + * + * @return self Snapshot carrying the resolved route and the inspected rules. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path) @@ -114,6 +133,8 @@ public function hasMatch(): bool } /** + * Returns the panel snapshot for JSON serialization. + * * @return array */ public function jsonSerialize(): array diff --git a/src/Panel/Timeline/TimelineMemoryRenderer.php b/src/Panel/Timeline/TimelineMemoryRenderer.php index e37c43e..009e6db 100644 --- a/src/Panel/Timeline/TimelineMemoryRenderer.php +++ b/src/Panel/Timeline/TimelineMemoryRenderer.php @@ -32,6 +32,13 @@ final class TimelineMemoryRenderer * Renders an SVG memory graph, or `''` when its geometry cannot be resolved. * * @param list $samples Memory samples in any order. + * @param float $start Request start time the horizontal axis is offset from. + * @param float $duration Request duration the horizontal axis spans, in seconds. + * @param int $memory Peak memory the vertical axis scales to, in bytes. + * @param int $width Viewport width, in user units. + * @param int $height Viewport height, in user units. + * + * @return string Inline SVG markup, or an empty string when the geometry cannot be resolved. */ public static function render( array $samples, @@ -81,6 +88,11 @@ public static function render( ->render(); } + /** + * Builds the vertical fill gradient from the configured opacity stops. + * + * @return LinearGradient Gradient the filled area references. + */ private static function gradient(): LinearGradient { $stops = []; @@ -101,6 +113,13 @@ private static function gradient(): LinearGradient ->html(...$stops); } + /** + * Formats a coordinate as a compact decimal without trailing zeros. + * + * @param float|int $value Coordinate to format. + * + * @return string Formatted coordinate. + */ private static function number(float|int $value): string { $rendered = rtrim(sprintf('%.6F', $value), '0'); @@ -109,7 +128,13 @@ private static function number(float|int $value): string } /** - * @param list $points + * Builds the filled area point list, closing the shape along the baseline. + * + * @param list $points Plotted coordinates in ascending `x` order. + * @param int $width Viewport width, in user units. + * @param int $height Viewport height, in user units. + * + * @return string Point list for the area polygon. */ private static function polygonPoints(array $points, int $width, int $height): string { @@ -121,7 +146,13 @@ private static function polygonPoints(array $points, int $width, int $height): s } /** - * @param list $points + * Builds the stroked line point list, extending the last sample to the right edge. + * + * @param list $points Plotted coordinates in ascending `x` order. + * @param int $width Viewport width, in user units. + * @param int $height Viewport height, in user units. + * + * @return string Point list for the trend polyline. */ private static function polylinePoints(array $points, int $width, int $height): string { @@ -134,7 +165,8 @@ private static function polylinePoints(array $points, int $width, int $height): * Traces the sampled points from the baseline, returning the point list and the last plotted `y` coordinate the * polygon and polyline closers extend from. * - * @param list $points + * @param list $points Plotted coordinates in ascending `x` order. + * @param int $height Viewport height, in user units. * * @return array{0: string, 1: float|int} Rendered point list and the last plotted `y` coordinate. */ diff --git a/src/Panel/Timeline/TimelineRenderer.php b/src/Panel/Timeline/TimelineRenderer.php index 27a8d59..0da4fc4 100644 --- a/src/Panel/Timeline/TimelineRenderer.php +++ b/src/Panel/Timeline/TimelineRenderer.php @@ -29,6 +29,11 @@ final class TimelineRenderer * * @param list $rows Positioned timeline spans. * @param array $rulers Ruler offsets keyed by milliseconds. + * @param string $memorySvg Inline memory graph markup, or an empty string to omit the footer. + * @param int $memory Peak memory reported in the footer, in bytes. + * @param int $memoryHeight Memory graph track height, in pixels. + * + * @return string Chart markup, or an empty string when no span survived filtering. */ public static function renderChart( array $rows, @@ -58,6 +63,11 @@ public static function renderChart( /** * Renders the empty-state hint linking to the sortable Profiling panel. + * + * @param bool $hasRows Whether any span survived filtering. + * @param string $profilingUrl URL of the Profiling panel for the same request. + * + * @return string Hint markup, or an empty string when spans are present. */ public static function renderEmptyHint(bool $hasRows, string $profilingUrl): string { @@ -89,7 +99,12 @@ public static function renderEmptyHint(bool $hasRows, string $profilingUrl): str /** * Renders the filter form while preserving adapter-owned route parameters. * + * @param string $action Form action URL owned by the adapter. * @param array $hiddenParams Hidden route and theme parameters. + * @param string $duration Minimum duration filter, in milliseconds. + * @param string $category Category filter matched against span names. + * + * @return string Filter form markup. */ public static function renderFilterForm( string $action, @@ -144,6 +159,12 @@ public static function renderFilterForm( /** * Renders total duration, peak memory, and visible span count. + * + * @param float $duration Total request duration, in milliseconds. + * @param int $memory Peak memory, in bytes. + * @param int $spanCount Number of spans left after filtering. + * + * @return string Summary header markup. */ public static function renderSummary(float $duration, int $memory, int $spanCount): string { @@ -175,6 +196,13 @@ public static function renderSummary(float $duration, int $memory, int $spanCoun ->render(); } + /** + * Builds the accessible row label, prefixing the category only when the tooltip omits it. + * + * @param TimelineSpanRow $row Span to label. + * + * @return string Accessible label for the span row. + */ private static function accessibleRowLabel(TimelineSpanRow $row): string { if ($row->category === '' || str_starts_with($row->tooltip, $row->category . "\n")) { @@ -184,6 +212,13 @@ private static function accessibleRowLabel(TimelineSpanRow $row): string return "{$row->category}\n{$row->tooltip}"; } + /** + * Formats an axis tick in milliseconds below one second and in seconds above it. + * + * @param int $milliseconds Tick offset, in milliseconds. + * + * @return string Tick label carrying its unit. + */ private static function formatTickLabel(int $milliseconds): string { if ($milliseconds < 1000) { @@ -196,7 +231,11 @@ private static function formatTickLabel(int $milliseconds): string } /** + * Renders the ruler axis, positioning each tick at its offset. + * * @param array $rulers Ruler offsets keyed by milliseconds. + * + * @return Header Axis header holding the positioned ticks. */ private static function renderAxis(array $rulers): Header { @@ -212,6 +251,15 @@ private static function renderAxis(array $rulers): Header return Header::tag()->class('yii-debug-tl-axis')->html(...$ticks); } + /** + * Renders the memory footer holding the graph and the peak-memory readout. + * + * @param string $svg Inline memory graph markup. + * @param int $memory Peak memory, in bytes. + * @param int $height Graph track height, in pixels. + * + * @return Footer Memory footer element. + */ private static function renderMemoryFooter(string $svg, int $memory, int $height): Footer { return Footer::tag() @@ -232,6 +280,13 @@ private static function renderMemoryFooter(string $svg, int $memory, int $height ); } + /** + * Renders one span as a labelled row carrying its positioned bar. + * + * @param TimelineSpanRow $row Positioned timeline span. + * + * @return Div Span row element. + */ private static function renderRow(TimelineSpanRow $row): Div { return Div::tag() @@ -272,7 +327,11 @@ private static function renderRow(TimelineSpanRow $row): Div } /** + * Renders the span rows as an accessible list. + * * @param list $rows Positioned timeline spans. + * + * @return Div List element holding the span rows. */ private static function renderRows(array $rows): Div { @@ -288,6 +347,13 @@ private static function renderRows(array $rows): Div ->html(...$rendered); } + /** + * Shortens a category to its class short name, falling back to the full category. + * + * @param string $category Span category. + * + * @return string Short category name, or the placeholder glyph when the category is empty. + */ private static function shortCategoryName(string $category): string { if ($category === '') { diff --git a/src/Panel/Timeline/TimelineSnapshot.php b/src/Panel/Timeline/TimelineSnapshot.php index 4ce701b..6d60569 100644 --- a/src/Panel/Timeline/TimelineSnapshot.php +++ b/src/Panel/Timeline/TimelineSnapshot.php @@ -12,12 +12,22 @@ final readonly class TimelineSnapshot implements PanelSnapshot { /** + * Creates the timing and peak-memory snapshot for the request. + * * @param float $start The start time of the timeline snapshot. * @param float $end The end time of the timeline snapshot. * @param int $memory The peak memory usage at the time of the snapshot. */ public function __construct(public float $start, public float $end, public int $memory) {} + /** + * Hydrates the Timeline panel snapshot from decoded JSON data. + * + * @param mixed $data Decoded Timeline panel payload. + * @param string $path Payload path used in hydration errors. + * + * @return self Hydrated timing and peak-memory snapshot. + */ public static function fromArray(mixed $data, string $path): self { $payload = Payload::object($data, $path)->shape(['start', 'end', 'memory']); @@ -26,7 +36,9 @@ public static function fromArray(mixed $data, string $path): self } /** - * @return array + * Returns the snapshot for JSON serialization. + * + * @return array Serialized start time, end time, and peak memory. */ public function jsonSerialize(): array { diff --git a/src/Panel/User/UserSnapshot.php b/src/Panel/User/UserSnapshot.php index 1325be3..30c13a8 100644 --- a/src/Panel/User/UserSnapshot.php +++ b/src/Panel/User/UserSnapshot.php @@ -14,6 +14,8 @@ use ArrayPayloadSnapshot; /** + * Returns the captured identity and RBAC payload. + * * @return array Captured identity attributes, roles, and permissions. */ public function data(): array @@ -21,6 +23,11 @@ public function data(): array return $this->values(); } + /** + * Returns the key under which the user payload is stored. + * + * @return string Payload key. + */ protected static function payloadKey(): string { return 'data'; diff --git a/tests/Panel/Db/SqlHighlighterTest.php b/tests/Panel/Db/SqlHighlighterTest.php index 218e025..f84623a 100644 --- a/tests/Panel/Db/SqlHighlighterTest.php +++ b/tests/Panel/Db/SqlHighlighterTest.php @@ -5,11 +5,15 @@ namespace PHPForge\Debug\Tests\Panel\Db; use PHPForge\Debug\Panel\Db\SqlHighlighter; -use PHPUnit\Framework\Attributes\Group; +use PHPForge\Debug\Tests\Provider\SqlHighlighterProvider; +use PHPUnit\Framework\Attributes\{DataProviderExternal, Group}; use PHPUnit\Framework\TestCase; /** - * Unit tests for {@see SqlHighlighter} covering token classification, escaping, and pass-through behavior. + * Unit tests for {@see SqlHighlighter} covering statement detection, token classification, escaping, and + * pass-through behavior. + * + * {@see SqlHighlighterProvider} for statement detection cases. */ #[Group('panel')] #[Group('db')] @@ -159,4 +163,16 @@ public function testHighlightWrapsLineAndBlockComments(): void 'Both comment forms must be wrapped.', ); } + + #[DataProviderExternal(SqlHighlighterProvider::class, 'statements')] + public function testIsStatementAcceptsRawSql(string $value): void + { + self::assertTrue(SqlHighlighter::isStatement($value), 'Statement must be detected.'); + } + + #[DataProviderExternal(SqlHighlighterProvider::class, 'nonStatements')] + public function testIsStatementRejectsProseBorrowingSqlVerbs(string $value): void + { + self::assertFalse(SqlHighlighter::isStatement($value), 'Prose must stay plain text.'); + } } diff --git a/tests/Panel/Log/LogCellRendererTest.php b/tests/Panel/Log/LogCellRendererTest.php index 7010a2d..aa74742 100644 --- a/tests/Panel/Log/LogCellRendererTest.php +++ b/tests/Panel/Log/LogCellRendererTest.php @@ -216,6 +216,24 @@ public function testRenderMessageCellHighlightsSqlForDbCommandCategory(): void } + public function testRenderMessageCellHighlightsSqlWhenTheMessageIsAStatement(): void + { + $html = LogCellRenderer::renderMessageCell( + self::makeRow(message: 'SELECT * FROM "post"', category: 'application'), + self::traceLine(), + ); + + self::assertSame( + <<<'HTML' +
+ SELECT * FROM "post" +
+ HTML, + $html, + 'Statements logged outside the DB category must wear the mono wrapper.', + ); + } + public function testRenderMessageCellKeepsPlainEscapingForNonDbCategory(): void { $html = LogCellRenderer::renderMessageCell( diff --git a/tests/Panel/Profile/ProfileCellRendererTest.php b/tests/Panel/Profile/ProfileCellRendererTest.php index 47e2759..e2c6f33 100644 --- a/tests/Panel/Profile/ProfileCellRendererTest.php +++ b/tests/Panel/Profile/ProfileCellRendererTest.php @@ -178,6 +178,24 @@ public function testRenderInfoCellHighlightsSqlForYii3DbCommandBlocks(): void ); } + public function testRenderInfoCellHighlightsSqlWhenTheInfoIsAStatement(): void + { + self::assertSame( + <<<'HTML' +
+ SELECT * FROM "post" +
+ HTML, + ProfileCellRenderer::renderInfoCell( + self::makeRow( + category: 'Yiisoft\\Db\\Driver\\Pdo\\AbstractPdoCommand::queryInternal', + info: 'SELECT * FROM "post"', + ), + ), + 'Statements profiled outside the known categories must wear the mono wrapper.', + ); + } + public function testRenderInfoCellKeepsPlainInfoUnhighlighted(): void { $html = ProfileCellRenderer::renderInfoCell(self::makeRow(category: 'application', info: 'SELECT me')); diff --git a/tests/Provider/SqlHighlighterProvider.php b/tests/Provider/SqlHighlighterProvider.php new file mode 100644 index 0000000..831d6f6 --- /dev/null +++ b/tests/Provider/SqlHighlighterProvider.php @@ -0,0 +1,57 @@ + + */ + public static function nonStatements(): iterable + { + yield 'empty value' => ['']; + yield 'markup' => ['']; + yield 'connection notice' => ['Opening DB connection: sqlite:/tmp/db.sqlite']; + yield 'prose opening with BEGIN' => ['Begin processing the queue']; + yield 'prose opening with CREATE' => ['Create something new']; + yield 'prose opening with DROP' => ['Drop the cache directory']; + yield 'prose opening with SELECT' => ['Select me']; + yield 'prose opening with UPDATE' => ['Update available for the package']; + yield 'projection without source' => ['SELECT 1']; + } + + /** + * @return iterable + */ + public static function statements(): iterable + { + yield 'alter table' => ['ALTER TABLE "post" ADD "views" integer']; + yield 'begin terminated by a semicolon' => ['BEGIN;']; + yield 'commit' => ['COMMIT']; + yield 'common table expression' => ['WITH "recent" AS (SELECT 1) SELECT * FROM "recent"']; + yield 'create table' => ['CREATE TABLE "post" ("id" integer)']; + yield 'create unique index' => ['CREATE UNIQUE INDEX "idx_post_id" ON "post" ("id")']; + yield 'delete' => ['DELETE FROM "post" WHERE "id" = 1']; + yield 'drop view' => ['DROP VIEW "post_stats"']; + yield 'explain' => ['EXPLAIN QUERY PLAN SELECT * FROM "post"']; + yield 'insert' => ['INSERT INTO "post" ("id") VALUES (1)']; + yield 'lowercase select' => ['select * from "post"']; + yield 'pragma' => ['PRAGMA foreign_keys = ON']; + yield 'replace' => ['REPLACE INTO "post" ("id") VALUES (1)']; + yield 'rollback transaction' => ['ROLLBACK TRANSACTION']; + yield 'select spanning several lines' => ["SELECT \"id\"\nFROM \"post\""]; + yield 'select with leading whitespace' => [" \n SELECT * FROM \"post\""]; + yield 'show' => ['SHOW TABLES']; + yield 'truncate table' => ['TRUNCATE TABLE "post"']; + yield 'update' => ['UPDATE "post" SET "status" = 1']; + yield 'vacuum' => ['VACUUM "main"']; + } +}