From 59b2e7ea961bc1a71d733c9f38a2ba3f104bde99 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Sep 2026 08:20:59 -0300 Subject: [PATCH 1/3] refactor: centralize exception messages and panel titles in typed enums while preserving diagnostics, labels, and configurable names. --- CHANGELOG.md | 1 + src/Capture/CapturePolicy.php | 3 +- src/Collector/CollectorCoordinator.php | 9 +- src/Exception/Message.php | 237 ++++++++++++++++++ src/Helper/SensitiveDataRedactor.php | 8 +- src/Helper/Tabs.php | 3 +- src/Panel/Asset/AssetSectionRenderer.php | 3 +- src/Panel/Db/DbExplainRenderer.php | 3 +- src/Panel/Db/NPlusOneDetector.php | 5 +- src/Panel/PanelTitle.php | 131 ++++++++++ src/Panel/Request/RequestHero.php | 28 +-- .../Request/Routing/CurrentRouteView.php | 36 +-- src/Panel/Request/Routing/RouteDefinition.php | 51 ++-- .../Request/Routing/RouteInventoryView.php | 24 +- src/Panel/Vite/ViteSectionRenderer.php | 3 +- src/Storage/HydrationException.php | 3 +- src/Storage/SnapshotStore.php | 64 +++-- src/View/Sidebar/SidebarView.php | 3 +- tests/Panel/Event/EventInspectionTest.php | 11 +- tests/Panel/Event/EventRowTest.php | 8 +- tests/Panel/Request/RequestHeroTest.php | 4 +- .../Request/RequestRoutingViewModelsTest.php | 8 +- tests/Panel/Request/RouteDefinitionTest.php | 4 +- tests/Toolbar/ToolbarDataTest.php | 6 +- 24 files changed, 527 insertions(+), 129 deletions(-) create mode 100644 src/Exception/Message.php create mode 100644 src/Panel/PanelTitle.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ee381a3..6e4bc32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,3 +42,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - feat(api): add framework-neutral `PanelComparison` for ordered panel IDs, failure precedence, capture states, and combined structural/state counts without changing adapter output or diagnostic values. - refactor: share captured URL-to-path display conversion through `Text::urlToPath()` while retaining original diagnostic URLs and adapter-owned presentation models. - feat(events): add the shared execution inspector, stable chronology, grouped filters, bounded optional context and traces, and explicit lifecycle correlation. +- refactor: centralize exception messages and panel titles in typed enums while preserving diagnostics, labels, and configurable names. diff --git a/src/Capture/CapturePolicy.php b/src/Capture/CapturePolicy.php index b905ab8..721d35b 100644 --- a/src/Capture/CapturePolicy.php +++ b/src/Capture/CapturePolicy.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Capture; use InvalidArgumentException; +use PHPForge\Debug\Exception\Message; use PHPForge\Debug\Helper\SensitiveDataRedactor; use SensitiveParameter; @@ -50,7 +51,7 @@ public function __construct( ) { if ($this->maxBodyBytes < 1) { throw new InvalidArgumentException( - 'The maximum body size must be greater than zero.', + Message::BODY_SIZE_INVALID->getMessage(), ); } diff --git a/src/Collector/CollectorCoordinator.php b/src/Collector/CollectorCoordinator.php index b2b3375..5d29239 100644 --- a/src/Collector/CollectorCoordinator.php +++ b/src/Collector/CollectorCoordinator.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Collector; use InvalidArgumentException; +use PHPForge\Debug\Exception\Message; use PHPForge\Debug\Storage\{DebugSnapshot, PanelFailure, RequestSummary}; use Throwable; @@ -37,11 +38,15 @@ public function __construct(iterable $collectors) $id = $collector->id(); if (trim($id) === '') { - throw new InvalidArgumentException('Debug collector ID must not be empty.'); + throw new InvalidArgumentException( + Message::COLLECTOR_ID_EMPTY->getMessage(), + ); } if (isset($this->collectors[$id])) { - throw new InvalidArgumentException("Duplicate debug collector ID: {$id}."); + throw new InvalidArgumentException( + Message::COLLECTOR_ID_DUPLICATE->getMessage($id), + ); } $this->collectors[$id] = $collector; diff --git a/src/Exception/Message.php b/src/Exception/Message.php new file mode 100644 index 0000000..c9fd1d3 --- /dev/null +++ b/src/Exception/Message.php @@ -0,0 +1,237 @@ +value, ...$argument); + } +} diff --git a/src/Helper/SensitiveDataRedactor.php b/src/Helper/SensitiveDataRedactor.php index 5216479..b9d7e6b 100644 --- a/src/Helper/SensitiveDataRedactor.php +++ b/src/Helper/SensitiveDataRedactor.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Helper; use InvalidArgumentException; +use PHPForge\Debug\Exception\Message; use SensitiveParameter; use function array_fill_keys; @@ -14,7 +15,6 @@ use function is_object; use function is_string; use function preg_match; -use function sprintf; use function str_starts_with; use function strtolower; @@ -204,7 +204,9 @@ private static function prefixes(array $prefixes): array { foreach ($prefixes as $prefix) { if ($prefix === '') { - throw new InvalidArgumentException('Sensitive key prefixes must not be empty.'); + throw new InvalidArgumentException( + Message::SENSITIVE_KEY_PREFIX_EMPTY->getMessage(), + ); } } @@ -243,7 +245,7 @@ private static function validatePatterns(array $patterns): void foreach ($patterns as $pattern) { if ($pattern === '' || @preg_match($pattern, '') === false) { throw new InvalidArgumentException( - sprintf('Sensitive key pattern "%s" is not a valid PCRE pattern.', $pattern), + Message::SENSITIVE_KEY_PATTERN_INVALID->getMessage($pattern), ); } } diff --git a/src/Helper/Tabs.php b/src/Helper/Tabs.php index 508f17f..55802cc 100644 --- a/src/Helper/Tabs.php +++ b/src/Helper/Tabs.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Helper; use InvalidArgumentException; +use PHPForge\Debug\Exception\Message; use UIAwesome\Html\Flow\Div; use UIAwesome\Html\List\{Li, Ul}; use UIAwesome\Html\Palpable\A; @@ -26,7 +27,7 @@ public static function render(string $id, string $ariaLabel, array $tabs, int $a { if ($tabs !== [] && !array_key_exists($activeIndex, $tabs)) { throw new InvalidArgumentException( - 'The active tab index must identify a supplied tab.', + Message::ACTIVE_TAB_INDEX_INVALID->getMessage(), ); } diff --git a/src/Panel/Asset/AssetSectionRenderer.php b/src/Panel/Asset/AssetSectionRenderer.php index e6736f3..140fd6e 100644 --- a/src/Panel/Asset/AssetSectionRenderer.php +++ b/src/Panel/Asset/AssetSectionRenderer.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Panel\Asset; use PHPForge\Debug\Helper\Icon; +use PHPForge\Debug\Panel\PanelTitle; use UIAwesome\Html\Flow\Div; use UIAwesome\Html\Heading\H1; use UIAwesome\Html\List\{Li, Ol}; @@ -49,7 +50,7 @@ public static function renderHeader(AssetSummary $summary): string return H1::tag() ->class('yii-debug-sr-only') - ->content('Asset Bundles') + ->content(PanelTitle::ASSETS) ->render() . Header::tag() ->class('yii-debug-asset-stats') diff --git a/src/Panel/Db/DbExplainRenderer.php b/src/Panel/Db/DbExplainRenderer.php index 5bf18a1..580b411 100644 --- a/src/Panel/Db/DbExplainRenderer.php +++ b/src/Panel/Db/DbExplainRenderer.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Panel\Db; use PHPForge\Debug\Helper\Dump; +use PHPForge\Debug\Panel\PanelTitle; use UIAwesome\Html\Flow\{Div, P, Pre}; use UIAwesome\Html\Heading\H1; use UIAwesome\Html\Phrasing\Em; @@ -50,7 +51,7 @@ private static function renderPlan(string $query, array $results, string|null $e $children = [ H1::tag() ->class('yii-debug-explain-title') - ->content('EXPLAIN'), + ->content(PanelTitle::EXPLAIN), ]; if ($query !== '') { diff --git a/src/Panel/Db/NPlusOneDetector.php b/src/Panel/Db/NPlusOneDetector.php index f29cbdd..a0f9f53 100644 --- a/src/Panel/Db/NPlusOneDetector.php +++ b/src/Panel/Db/NPlusOneDetector.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Panel\Db; use InvalidArgumentException; +use PHPForge\Debug\Exception\Message; use function min; use function strtoupper; @@ -23,7 +24,9 @@ final class NPlusOneDetector public static function detect(array $rows, int $threshold = 3): array { if ($threshold < 2) { - throw new InvalidArgumentException('The N+1 threshold must be at least two.'); + throw new InvalidArgumentException( + Message::N_PLUS_ONE_THRESHOLD_INVALID->getMessage(), + ); } /** diff --git a/src/Panel/PanelTitle.php b/src/Panel/PanelTitle.php new file mode 100644 index 0000000..1af6192 --- /dev/null +++ b/src/Panel/PanelTitle.php @@ -0,0 +1,131 @@ +flags = $flags; + $clone = clone $this; + $clone->flags = $flags; - return $copy; + return $clone; } public function withIp(string $ip): self { - $copy = clone $this; - $copy->ip = $ip; + $clone = clone $this; + $clone->ip = $ip; - return $copy; + return $clone; } public function withStatus(int $statusCode, string $statusVariant): self { - $copy = clone $this; - $copy->statusCode = $statusCode; - $copy->statusVariant = $statusVariant; + $clone = clone $this; + $clone->statusCode = $statusCode; + $clone->statusVariant = $statusVariant; - return $copy; + return $clone; } public function withTiming(string $time, string $durationMs): self { - $copy = clone $this; - $copy->time = $time; - $copy->durationMs = $durationMs; + $clone = clone $this; + $clone->time = $time; + $clone->durationMs = $durationMs; - return $copy; + return $clone; } } diff --git a/src/Panel/Request/Routing/CurrentRouteView.php b/src/Panel/Request/Routing/CurrentRouteView.php index 8a11be4..b3382a0 100644 --- a/src/Panel/Request/Routing/CurrentRouteView.php +++ b/src/Panel/Request/Routing/CurrentRouteView.php @@ -79,34 +79,34 @@ public function getTrace(): array public function withAction(string|null $action): self { - $copy = clone $this; - $copy->action = $action; + $clone = clone $this; + $clone->action = $action; - return $copy; + return $clone; } public function withDefinition(RouteDefinition|null $definition): self { - $copy = clone $this; - $copy->definition = $definition; + $clone = clone $this; + $clone->definition = $definition; - return $copy; + return $clone; } public function withError(string|null $error): self { - $copy = clone $this; - $copy->error = $error; + $clone = clone $this; + $clone->error = $error; - return $copy; + return $clone; } public function withMessage(string|null $message): self { - $copy = clone $this; - $copy->message = $message; + $clone = clone $this; + $clone->message = $message; - return $copy; + return $clone; } /** @@ -114,10 +114,10 @@ public function withMessage(string|null $message): self */ public function withParameters(array $parameters): self { - $copy = clone $this; - $copy->parameters = $parameters; + $clone = clone $this; + $clone->parameters = $parameters; - return $copy; + return $clone; } /** @@ -125,9 +125,9 @@ public function withParameters(array $parameters): self */ public function withTrace(array $trace): self { - $copy = clone $this; - $copy->trace = $trace; + $clone = clone $this; + $clone->trace = $trace; - return $copy; + return $clone; } } diff --git a/src/Panel/Request/Routing/RouteDefinition.php b/src/Panel/Request/Routing/RouteDefinition.php index cff3c4d..b7cf799 100644 --- a/src/Panel/Request/Routing/RouteDefinition.php +++ b/src/Panel/Request/Routing/RouteDefinition.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Panel\Request\Routing; use InvalidArgumentException; +use PHPForge\Debug\Exception\Message; use function array_diff; use function array_is_list; @@ -200,10 +201,10 @@ public function toArray(): array public function withAction(string|null $action): self { - $copy = clone $this; - $copy->action = $action; + $clone = clone $this; + $clone->action = $action; - return $copy; + return $clone; } /** @@ -211,10 +212,10 @@ public function withAction(string|null $action): self */ public function withHosts(array $hosts): self { - $copy = clone $this; - $copy->hosts = $hosts; + $clone = clone $this; + $clone->hosts = $hosts; - return $copy; + return $clone; } /** @@ -222,10 +223,10 @@ public function withHosts(array $hosts): self */ public function withMethods(array $methods): self { - $copy = clone $this; - $copy->methods = $methods; + $clone = clone $this; + $clone->methods = $methods; - return $copy; + return $clone; } /** @@ -233,42 +234,42 @@ public function withMethods(array $methods): self */ public function withMiddlewares(array|null $middlewares): self { - $copy = clone $this; - $copy->middlewares = $middlewares; + $clone = clone $this; + $clone->middlewares = $middlewares; - return $copy; + return $clone; } public function withMode(string|null $mode): self { - $copy = clone $this; - $copy->mode = $mode; + $clone = clone $this; + $clone->mode = $mode; - return $copy; + return $clone; } public function withSuffix(string|null $suffix): self { - $copy = clone $this; - $copy->suffix = $suffix; + $clone = clone $this; + $clone->suffix = $suffix; - return $copy; + return $clone; } public function withTarget(string|null $target): self { - $copy = clone $this; - $copy->target = $target; + $clone = clone $this; + $clone->target = $target; - return $copy; + return $clone; } public function withType(string|null $type): self { - $copy = clone $this; - $copy->type = $type; + $clone = clone $this; + $clone->type = $type; - return $copy; + return $clone; } private static function expectedFor(string $key): string @@ -285,7 +286,7 @@ private static function expectedFor(string $key): string private static function invalid(string $key, string $expected): InvalidArgumentException { return new InvalidArgumentException( - "Route definition key '{$key}' must be {$expected}.", + Message::ROUTE_DEFINITION_INVALID->getMessage($key, $expected), ); } diff --git a/src/Panel/Request/Routing/RouteInventoryView.php b/src/Panel/Request/Routing/RouteInventoryView.php index 21b9b8a..2804f49 100644 --- a/src/Panel/Request/Routing/RouteInventoryView.php +++ b/src/Panel/Request/Routing/RouteInventoryView.php @@ -69,33 +69,33 @@ public function isLive(): bool */ public function withBadges(array $badges): self { - $copy = clone $this; - $copy->badges = $badges; + $clone = clone $this; + $clone->badges = $badges; - return $copy; + return $clone; } public function withError(string|null $error): self { - $copy = clone $this; - $copy->error = $error; + $clone = clone $this; + $clone->error = $error; - return $copy; + return $clone; } public function withLive(bool $live): self { - $copy = clone $this; - $copy->live = $live; + $clone = clone $this; + $clone->live = $live; - return $copy; + return $clone; } public function withSource(string $source): self { - $copy = clone $this; - $copy->source = $source; + $clone = clone $this; + $clone->source = $source; - return $copy; + return $clone; } } diff --git a/src/Panel/Vite/ViteSectionRenderer.php b/src/Panel/Vite/ViteSectionRenderer.php index 6819617..132f608 100644 --- a/src/Panel/Vite/ViteSectionRenderer.php +++ b/src/Panel/Vite/ViteSectionRenderer.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Panel\Vite; use PHPForge\Debug\Helper\EmptyState; +use PHPForge\Debug\Panel\PanelTitle; use Stringable; use UIAwesome\Html\Flow\{Div, P}; use UIAwesome\Html\Heading\{H1, H2}; @@ -217,7 +218,7 @@ private static function renderHeader(ViteSummary $summary): string return H1::tag() ->class('yii-debug-sr-only') - ->content('Vite') + ->content(PanelTitle::VITE) ->render() . Header::tag() ->class('yii-debug-grid-summary') diff --git a/src/Storage/HydrationException.php b/src/Storage/HydrationException.php index afab278..f9b203d 100644 --- a/src/Storage/HydrationException.php +++ b/src/Storage/HydrationException.php @@ -4,6 +4,7 @@ namespace PHPForge\Debug\Storage; +use PHPForge\Debug\Exception\Message; use RuntimeException; /** @@ -22,7 +23,7 @@ final class HydrationException extends RuntimeException public static function at(string $path, string $expected): self { return new self( - "Invalid debug snapshot value at '{$path}': expected {$expected}.", + Message::SNAPSHOT_VALUE_INVALID->getMessage($path, $expected), ); } } diff --git a/src/Storage/SnapshotStore.php b/src/Storage/SnapshotStore.php index 312f8fa..0e93cce 100644 --- a/src/Storage/SnapshotStore.php +++ b/src/Storage/SnapshotStore.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\Storage; use JsonException; +use PHPForge\Debug\Exception\Message; use Throwable; use function array_key_exists; @@ -72,7 +73,7 @@ public function clear(): void foreach ($files === false ? [] : $files as $file) { if (is_file($file) && !@unlink($file)) { throw new StorageException( - "Unable to remove debug data file: {$file}", + Message::DATA_FILE_REMOVE_FAILED->getMessage($file), ); } } @@ -101,7 +102,9 @@ public function loadManifestResult(): ManifestReadResult { if (!is_dir($this->path)) { $error = is_file($this->path) - ? new StorageException("Debug data path is not a directory: {$this->path}") + ? new StorageException( + Message::DATA_PATH_NOT_DIRECTORY->getMessage($this->path), + ) : null; return new ManifestReadResult([], $error); @@ -126,13 +129,13 @@ public function loadManifestResult(): ManifestReadResult if ($raw === false) { throw new StorageException( - "Unable to read debug manifest: {$file}", + Message::MANIFEST_READ_FAILED->getMessage($file), ); } if ($raw === '') { throw new StorageException( - "Debug manifest is empty: {$file}", + Message::MANIFEST_EMPTY->getMessage($file), ); } @@ -143,7 +146,7 @@ public function loadManifestResult(): ManifestReadResult $error = $failure instanceof StorageException ? $failure : new StorageException( - 'Unable to read debug manifest.', + Message::MANIFEST_READ_ERROR->getMessage(), 0, $failure, ); @@ -176,13 +179,18 @@ public function readSnapshot(string $tag): DebugSnapshot|null public function readSnapshotResult(string $tag): SnapshotReadResult { if (!self::isValidTag($tag)) { - return new SnapshotReadResult(null, new StorageException("Invalid debug snapshot tag: {$tag}")); + return new SnapshotReadResult( + null, + new StorageException( + Message::SNAPSHOT_TAG_INVALID->getMessage($tag), + ), + ); } if (!is_dir($this->path)) { $error = is_file($this->path) ? new StorageException( - "Debug data path is not a directory: {$this->path}", + Message::DATA_PATH_NOT_DIRECTORY->getMessage($this->path), ) : null; @@ -208,13 +216,13 @@ public function readSnapshotResult(string $tag): SnapshotReadResult if ($raw === false) { throw new StorageException( - "Unable to read debug snapshot: {$file}", + Message::SNAPSHOT_READ_FAILED->getMessage($file), ); } if ($raw === '') { throw new StorageException( - "Debug snapshot is empty: {$file}", + Message::SNAPSHOT_EMPTY->getMessage($file), ); } @@ -222,7 +230,7 @@ public function readSnapshotResult(string $tag): SnapshotReadResult if ($snapshot->summary->tag !== $tag) { throw new StorageException( - "Debug snapshot tag does not match its filename: {$file}", + Message::SNAPSHOT_TAG_MISMATCH->getMessage($file), ); } @@ -231,7 +239,7 @@ public function readSnapshotResult(string $tag): SnapshotReadResult $error = $failure instanceof StorageException ? $failure : new StorageException( - "Unable to read debug snapshot: {$tag}", + Message::SNAPSHOT_READ_FAILED->getMessage($tag), 0, $failure, ); @@ -350,7 +358,7 @@ private function acquireLock(int $operation): mixed if ($lock === false) { throw new StorageException( - "Unable to open debug data lock file: {$lockFile}", + Message::LOCK_FILE_OPEN_FAILED->getMessage($lockFile), ); } @@ -358,7 +366,7 @@ private function acquireLock(int $operation): mixed fclose($lock); throw new StorageException( - "Unable to acquire debug data lock: {$lockFile}", + Message::LOCK_ACQUIRE_FAILED->getMessage($lockFile), ); } @@ -374,7 +382,7 @@ private static function assertValidHistorySize(int $historySize): void { if ($historySize < 0) { throw new StorageException( - "Invalid debug history size: {$historySize}", + Message::HISTORY_SIZE_INVALID->getMessage($historySize), ); } } @@ -391,7 +399,7 @@ private function atomicWrite(string $file, string $contents): void if ($temporary === false) { throw new StorageException( - "Unable to write temporary debug data file for: {$file}", + Message::TEMPORARY_FILE_WRITE_FAILED->getMessage($file), ); } @@ -399,7 +407,7 @@ private function atomicWrite(string $file, string $contents): void @unlink($temporary); throw new StorageException( - "Unable to write temporary debug data file for: {$file}", + Message::TEMPORARY_FILE_WRITE_FAILED->getMessage($file), ); } @@ -408,7 +416,7 @@ private function atomicWrite(string $file, string $contents): void @unlink($temporary); throw new StorageException( - "Unable to apply debug data file mode for: {$file}", + Message::DATA_FILE_MODE_FAILED->getMessage($file), ); } } @@ -417,7 +425,7 @@ private function atomicWrite(string $file, string $contents): void @unlink($temporary); throw new StorageException( - "Unable to replace debug data file: {$file}", + Message::DATA_FILE_REPLACE_FAILED->getMessage($file), ); } } @@ -504,13 +512,13 @@ private function initialize(): void if (!is_dir($this->path)) { throw new StorageException( - "Unable to create debug data directory: {$this->path}", + Message::DATA_DIRECTORY_CREATE_FAILED->getMessage($this->path), ); } if ($created && !@chmod($this->path, $this->dirMode)) { throw new StorageException( - "Unable to apply debug data directory mode: {$this->path}", + Message::DATA_DIRECTORY_MODE_FAILED->getMessage($this->path), ); } } @@ -551,7 +559,7 @@ private function readExistingFile(string $file): string|null if ($contents === false) { throw new StorageException( - "Unable to read debug data file: {$file}", + Message::DATA_FILE_READ_FAILED->getMessage($file), ); } @@ -575,7 +583,7 @@ private function readManifestFile(): array if ($raw === false) { throw new StorageException( - "Unable to read debug manifest: {$file}", + Message::MANIFEST_READ_FAILED->getMessage($file), ); } @@ -657,7 +665,7 @@ private function recoverTransaction(): void $transaction = self::decode($raw); } catch (JsonException $exception) { throw new StorageException( - "Invalid debug storage transaction journal: {$file}", + Message::TRANSACTION_JOURNAL_INVALID->getMessage($file), 0, $exception, ); @@ -674,7 +682,7 @@ private function recoverTransaction(): void || $transaction['snapshotBefore'] !== null && !is_string($transaction['snapshotBefore']) || $transaction['manifestBefore'] !== null && !is_string($transaction['manifestBefore']) ) { - throw new StorageException("Invalid debug storage transaction journal: {$file}"); + throw new StorageException(Message::TRANSACTION_JOURNAL_INVALID->getMessage($file)); } if ($transaction['state'] === 'committed') { @@ -684,7 +692,9 @@ private function recoverTransaction(): void } if ($transaction['state'] !== 'prepared') { - throw new StorageException("Invalid debug storage transaction journal: {$file}"); + throw new StorageException( + Message::TRANSACTION_JOURNAL_INVALID->getMessage($file), + ); } $snapshotFile = $this->snapshotFile($transaction['tag']); @@ -733,7 +743,7 @@ private function removeTransactionTarget(string $file): void { if (is_file($file) && !@unlink($file)) { throw new StorageException( - "Unable to roll back debug data file: {$file}", + Message::DATA_FILE_ROLLBACK_FAILED->getMessage($file), ); } } @@ -749,7 +759,7 @@ private function snapshotFile(string $tag): string { if (!self::isValidTag($tag)) { throw new StorageException( - "Invalid debug snapshot tag: {$tag}", + Message::SNAPSHOT_TAG_INVALID->getMessage($tag), ); } diff --git a/src/View/Sidebar/SidebarView.php b/src/View/Sidebar/SidebarView.php index b6b741c..897b7f8 100644 --- a/src/View/Sidebar/SidebarView.php +++ b/src/View/Sidebar/SidebarView.php @@ -5,6 +5,7 @@ namespace PHPForge\Debug\View\Sidebar; use InvalidArgumentException; +use PHPForge\Debug\Exception\Message; use function trim; @@ -34,7 +35,7 @@ public function __construct( foreach ($navGroups as $label => $_items) { if (trim($label) === '') { throw new InvalidArgumentException( - 'Sidebar navigation group labels must not be empty.', + Message::SIDEBAR_GROUP_LABEL_EMPTY->getMessage(), ); } } diff --git a/tests/Panel/Event/EventInspectionTest.php b/tests/Panel/Event/EventInspectionTest.php index f1df554..72d7c2c 100644 --- a/tests/Panel/Event/EventInspectionTest.php +++ b/tests/Panel/Event/EventInspectionTest.php @@ -314,11 +314,12 @@ public function testFluentMethodsReplaceOnlyTheirOwnGroup(Closure $update, array ->withLifecycle(1, 'enter', 2, 10.25); $payload = $original->jsonSerialize(); - $copy = $update($original); + + $clone = $update($original); self::assertNotSame( $original, - $copy, + $clone, 'Replacing an optional group must return a new instance.', ); self::assertSame( @@ -328,12 +329,12 @@ public function testFluentMethodsReplaceOnlyTheirOwnGroup(Closure $update, array ); self::assertSame( [...$payload, ...$changes], - $copy->jsonSerialize(), + $clone->jsonSerialize(), 'Only the selected group may change.', ); self::assertEquals( - $copy, - EventInspection::fromArray($copy->jsonSerialize(), '$.inspection'), + $clone, + EventInspection::fromArray($clone->jsonSerialize(), '$.inspection'), 'Replacement options must round-trip.', ); } diff --git a/tests/Panel/Event/EventRowTest.php b/tests/Panel/Event/EventRowTest.php index e12f4ca..0f3a7ea 100644 --- a/tests/Panel/Event/EventRowTest.php +++ b/tests/Panel/Event/EventRowTest.php @@ -107,11 +107,11 @@ public function testWithInspectionPreservesCapturedFieldsAndOriginalRow(): void $inspection = (new EventInspection()) ->withContext(['Action ID' => 'save'], 'captured'); - $copy = $row->withInspection($inspection); + $clone = $row->withInspection($inspection); self::assertNotSame( $row, - $copy, + $clone, 'Enrichment must return a new row.', ); self::assertNull( @@ -125,12 +125,12 @@ public function testWithInspectionPreservesCapturedFieldsAndOriginalRow(): void ); self::assertSame( $inspection, - $copy->inspection(), + $clone->inspection(), 'The copy must retain the supplied immutable diagnostics.', ); self::assertSame( [...$payload, 'inspection' => $inspection->jsonSerialize()], - $copy->jsonSerialize(), + $clone->jsonSerialize(), 'Enrichment must preserve every captured field and add only the inspection payload.', ); } diff --git a/tests/Panel/Request/RequestHeroTest.php b/tests/Panel/Request/RequestHeroTest.php index 960a783..490ecdb 100644 --- a/tests/Panel/Request/RequestHeroTest.php +++ b/tests/Panel/Request/RequestHeroTest.php @@ -74,10 +74,10 @@ public function testEveryOptionReturnsAnIndependentCopy(): void $hero->withIp('127.0.0.1'), $hero->withTiming('12:00:00', '3.5 ms'), $hero->withFlags(['AJAX']), - ] as $copy) { + ] as $clone) { self::assertNotSame( $hero, - $copy, + $clone, 'Each optional change must return a separate Request identity object.', ); } diff --git a/tests/Panel/Request/RequestRoutingViewModelsTest.php b/tests/Panel/Request/RequestRoutingViewModelsTest.php index f274f23..7734df5 100644 --- a/tests/Panel/Request/RequestRoutingViewModelsTest.php +++ b/tests/Panel/Request/RequestRoutingViewModelsTest.php @@ -161,10 +161,10 @@ public function testCurrentRouteOptionsReturnIndependentCopies(): void $current->withMessage('Matched.'), $current->withTrace([$trace]), $current->withError('Captured failure.'), - ] as $copy) { + ] as $clone) { self::assertNotSame( $current, - $copy, + $clone, 'Every current-route option must return a separate diagnostics object.', ); } @@ -272,10 +272,10 @@ public function testInventoryOptionsReturnIndependentCopies(): void $inventory->withSource('Captured configuration'), $inventory->withLive(false), $inventory->withError('Inventory failure.'), - ] as $copy) { + ] as $clone) { self::assertNotSame( $inventory, - $copy, + $clone, 'Every inventory option must return a separate inventory.', ); } diff --git a/tests/Panel/Request/RouteDefinitionTest.php b/tests/Panel/Request/RouteDefinitionTest.php index 524c429..b3c56ee 100644 --- a/tests/Panel/Request/RouteDefinitionTest.php +++ b/tests/Panel/Request/RouteDefinitionTest.php @@ -73,10 +73,10 @@ public function testEveryOptionReturnsAnIndependentCopy(): void $definition->withSuffix('.json'), $definition->withMode('BOTH'), $definition->withType('rule'), - ] as $copy) { + ] as $clone) { self::assertNotSame( $definition, - $copy, + $clone, 'Every route option must return a separate definition.', ); } diff --git a/tests/Toolbar/ToolbarDataTest.php b/tests/Toolbar/ToolbarDataTest.php index 1cc8dd9..e66ab55 100644 --- a/tests/Toolbar/ToolbarDataTest.php +++ b/tests/Toolbar/ToolbarDataTest.php @@ -289,11 +289,11 @@ public function testWithPanelsReturnsAnImmutableCopy(): void yiiVersion: '3', ); - $copy = $source->withPanels([$replacementPanel]); + $clone = $source->withPanels([$replacementPanel]); self::assertNotSame( $source, - $copy, + $clone, 'Panel enrichment must return a new toolbar payload.', ); self::assertSame( @@ -317,7 +317,7 @@ public function testWithPanelsReturnsAnImmutableCopy(): void phpVersion: '8.5.9', yiiVersion: '3', ), - $copy, + $clone, 'Panel enrichment must replace only the toolbar panels.', ); } From 99e4b2d128a996c879eb98c6a33f7312c12881ee Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Sep 2026 09:56:05 -0300 Subject: [PATCH 2/3] refactor: use shared `PanelIcon` enum values for built-in panel SVG keys. --- CHANGELOG.md | 1 + src/Panel/PanelIcon.php | 88 +++++++++++++++++++++++++++++++++++ tests/Panel/PanelIconTest.php | 56 ++++++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 src/Panel/PanelIcon.php create mode 100644 tests/Panel/PanelIconTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4bc32..054af4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,3 +43,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - refactor: share captured URL-to-path display conversion through `Text::urlToPath()` while retaining original diagnostic URLs and adapter-owned presentation models. - feat(events): add the shared execution inspector, stable chronology, grouped filters, bounded optional context and traces, and explicit lifecycle correlation. - refactor: centralize exception messages and panel titles in typed enums while preserving diagnostics, labels, and configurable names. +- refactor: use shared `PanelIcon` enum values for built-in panel SVG keys. diff --git a/src/Panel/PanelIcon.php b/src/Panel/PanelIcon.php new file mode 100644 index 0000000..94caa26 --- /dev/null +++ b/src/Panel/PanelIcon.php @@ -0,0 +1,88 @@ + 'asset', + 'CONFIGURATION' => 'config', + 'DATABASE' => 'db', + 'DUMP' => 'dump', + 'EVENTS' => 'events', + 'INERTIA' => 'inertia', + 'LOGS' => 'logs', + 'MAIL' => 'mail', + 'PROFILING' => 'profiling', + 'QUEUE' => 'queue', + 'REQUEST' => 'request', + 'ROUTER' => 'router', + 'TIMELINE' => 'timeline', + 'USER' => 'user', + 'VITE' => 'brand-javascript', + ]; + + self::assertCount( + count($keys), + PanelIcon::cases(), + 'Every built-in icon must have a stable key assertion.', + ); + + foreach (PanelIcon::cases() as $icon) { + self::assertSame( + $keys[$icon->name], + $icon->value, + 'Existing panel icon keys must remain unchanged.', + ); + self::assertStringContainsString( + 'value), + 'Every built-in panel icon must resolve to a bundled SVG.', + ); + } + } +} From 6ff79e3d15612039e22d23d765c4b447f68d2e3d Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Sep 2026 10:01:43 -0300 Subject: [PATCH 3/3] Fix ECS ci. --- tests/Panel/PanelIconTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Panel/PanelIconTest.php b/tests/Panel/PanelIconTest.php index db132a6..8388cbb 100644 --- a/tests/Panel/PanelIconTest.php +++ b/tests/Panel/PanelIconTest.php @@ -7,6 +7,7 @@ use PHPForge\Debug\Helper\Icon; use PHPForge\Debug\Panel\PanelIcon; use PHPUnit\Framework\TestCase; + use function count; /**