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
10 changes: 10 additions & 0 deletions src/Panel/Asset/AssetBundleRow.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -92,6 +100,8 @@ public static function fromBundle(string $name, array $bundle): self
}

/**
* Returns the typed row for JSON serialization.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
Expand Down
20 changes: 19 additions & 1 deletion src/Panel/Asset/AssetSnapshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,32 @@
/**
* @param list<AssetBundleRow> $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<AssetBundleRow> Registered bundles in registration order.
*/
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)
Expand All @@ -48,6 +64,8 @@ public static function fromArray(mixed $data, string $path): self
}

/**
* Returns the panel snapshot for JSON serialization.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
Expand Down
10 changes: 10 additions & 0 deletions src/Panel/Asset/ViteChunk.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -57,6 +65,8 @@ public static function fromArray(mixed $data, string $path): self
}

/**
* Returns the typed row for JSON serialization.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
Expand Down
22 changes: 22 additions & 0 deletions src/Panel/Asset/ViteManifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,33 @@
* @param list<ViteChunk> $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)
Expand Down Expand Up @@ -53,6 +73,8 @@ public static function fromArray(mixed $data, string $path): self
}

/**
* Returns the typed manifest for JSON serialization.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
Expand Down
12 changes: 12 additions & 0 deletions src/Panel/Db/DbSnapshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,23 @@ public static function capture(array $rows): self
}

/**
* Returns the captured query rows.
*
* @return list<QueryRow> Executed statements in capture order.
*/
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(
Expand All @@ -56,6 +66,8 @@ public static function fromArray(mixed $data, string $path): self
}

/**
* Returns the panel snapshot for JSON serialization.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
Expand Down
6 changes: 6 additions & 0 deletions src/Panel/Db/NPlusOneFinding.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> $sequences Query sequences belonging to the group.
* @param string $representativeQuery Statement shown as the sample of the group.
*/
public function __construct(
public string $fingerprint,
Expand All @@ -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}";
Expand Down
8 changes: 8 additions & 0 deletions src/Panel/Db/QueryRow.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 33 additions & 1 deletion src/Panel/Db/SqlHighlighter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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 `<span>` class; an empty class emits the escaped token unwrapped.
*/
Expand Down Expand Up @@ -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;
}
}
10 changes: 10 additions & 0 deletions src/Panel/Dump/DumpRow.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -75,6 +83,8 @@ public static function fromLoggerTuple(array $message): self
}

/**
* Returns the typed row for JSON serialization.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
Expand Down
12 changes: 12 additions & 0 deletions src/Panel/Dump/DumpSnapshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,23 @@ public static function capture(array $messages): self
}

/**
* Returns the captured dump rows.
*
* @return list<DumpRow> Captured rows in capture order.
*/
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(
Expand All @@ -54,6 +64,8 @@ public static function fromArray(mixed $data, string $path): self
}

/**
* Returns the panel snapshot for JSON serialization.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
Expand Down
15 changes: 13 additions & 2 deletions src/Panel/Event/EventCapture.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
final class EventCapture
{
/**
* Bounds and sanitizes the adapter-selected context fields.
*
* @param array<string, string> $fields Adapter-selected fields only.
*
* @return array<string, string>
* @return array<string, string> At most sixteen sanitized fields, with sensitive values replaced by `[redacted]`.
*/
public static function context(array $fields): array
{
Expand All @@ -34,7 +36,7 @@
$result = [];

foreach (array_slice($fields, 0, 16, true) as $key => $value) {
$result[mb_strcut($key, 0, 128)] = self::text(

Check warning on line 39 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ $result = []; foreach (array_slice($fields, 0, 16, true) as $key => $value) { - $result[mb_strcut($key, 0, 128)] = self::text( + $result[mb_strcut($key, 0, 127)] = self::text( $policy->isSensitiveKey($key) ? '[redacted]' : $policy->redactText($value), ); }

Check warning on line 39 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ $result = []; foreach (array_slice($fields, 0, 16, true) as $key => $value) { - $result[mb_strcut($key, 0, 128)] = self::text( + $result[mb_strcut($key, 0, 129)] = self::text( $policy->isSensitiveKey($key) ? '[redacted]' : $policy->redactText($value), ); }

Check warning on line 39 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "MBString": @@ @@ $result = []; foreach (array_slice($fields, 0, 16, true) as $key => $value) { - $result[mb_strcut($key, 0, 128)] = self::text( + $result[substr($key, 0, 128)] = self::text( $policy->isSensitiveKey($key) ? '[redacted]' : $policy->redactText($value), ); }

Check warning on line 39 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "MBString": @@ @@ $result = []; foreach (array_slice($fields, 0, 16, true) as $key => $value) { - $result[mb_strcut($key, 0, 128)] = self::text( + $result[substr($key, 0, 128)] = self::text( $policy->isSensitiveKey($key) ? '[redacted]' : $policy->redactText($value), ); }

Check warning on line 39 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ $result = []; foreach (array_slice($fields, 0, 16, true) as $key => $value) { - $result[mb_strcut($key, 0, 128)] = self::text( + $result[mb_strcut($key, 0, 129)] = self::text( $policy->isSensitiveKey($key) ? '[redacted]' : $policy->redactText($value), ); }

Check warning on line 39 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ $result = []; foreach (array_slice($fields, 0, 16, true) as $key => $value) { - $result[mb_strcut($key, 0, 128)] = self::text( + $result[mb_strcut($key, 0, 127)] = self::text( $policy->isSensitiveKey($key) ? '[redacted]' : $policy->redactText($value), ); }
$policy->isSensitiveKey($key) ? '[redacted]' : $policy->redactText($value),
);
}
Expand All @@ -43,22 +45,24 @@
}

/**
* Bounds and sanitizes the source frames observed when an event fired.
*
* @param list<array<string, mixed>> $frames Backtrace acquired with `DEBUG_BACKTRACE_IGNORE_ARGS`.
* @param int $limit Maximum frames; hard-limited to sixteen.
* @param list<string> $skipFiles Adapter instrumentation files to omit.
*
* @return list<string>
* @return list<string> Sanitized `file:line` frames in call order.
*/
public static function trace(array $frames, int $limit, array $skipFiles = []): array
{
$result = [];
$skipFiles[] = __FILE__;

$skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php';

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = dirname(__DIR__, 2); foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = '/Instrumentation/InstrumentationGuard.php'; foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "Concat": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = '/Instrumentation/InstrumentationGuard.php' . dirname(__DIR__, 2); foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = dirname(__DIR__, 3) . '/Instrumentation/InstrumentationGuard.php'; foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = dirname(__DIR__, 1) . '/Instrumentation/InstrumentationGuard.php'; foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = dirname(__DIR__, 2); foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "Concat": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = '/Instrumentation/InstrumentationGuard.php' . dirname(__DIR__, 2); foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = dirname(__DIR__, 3) . '/Instrumentation/InstrumentationGuard.php'; foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = '/Instrumentation/InstrumentationGuard.php'; foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

Check warning on line 61 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ $result = []; $skipFiles[] = __FILE__; - $skipFiles[] = dirname(__DIR__, 2) . '/Instrumentation/InstrumentationGuard.php'; + $skipFiles[] = dirname(__DIR__, 1) . '/Instrumentation/InstrumentationGuard.php'; foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) {

foreach ($frames as $frame) {
if (count($result) >= min(16, $limit)) {
break;

Check warning on line 65 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "Break_": @@ @@ foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) { - break; + continue; } $file = $frame['file'] ?? null;

Check warning on line 65 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "Break_": @@ @@ foreach ($frames as $frame) { if (count($result) >= min(16, $limit)) { - break; + continue; } $file = $frame['file'] ?? null;
}

$file = $frame['file'] ?? null;
Expand All @@ -74,6 +78,13 @@
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.
Expand All @@ -81,7 +92,7 @@

$value = $prefix === false ? $value : $prefix;

$value = str_replace("\0", '', $value);

Check warning on line 95 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "UnwrapStrReplace": @@ @@ $value = $prefix === false ? $value : $prefix; - $value = str_replace("\0", '', $value); + $value = $value; return strlen($value) > 2048 ? mb_strcut($value, 0, 2036) . ' [truncated]' : $value; }

Check warning on line 95 in src/Panel/Event/EventCapture.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "UnwrapStrReplace": @@ @@ $value = $prefix === false ? $value : $prefix; - $value = str_replace("\0", '', $value); + $value = $value; return strlen($value) > 2048 ? mb_strcut($value, 0, 2036) . ' [truncated]' : $value; }

return strlen($value) > 2048 ? mb_strcut($value, 0, 2036) . ' [truncated]' : $value;
}
Expand Down
Loading
Loading