Skip to content

Commit fb6b305

Browse files
committed
feat: add shared framework-neutral normalization and presentation helpers for debug adapters.
1 parent e68a35e commit fb6b305

23 files changed

Lines changed: 1670 additions & 6 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
- feat: add framework-agnostic debug snapshot contracts, strict JSON hydration, and filesystem persistence.
1111
- fix: bundle toolbar control icons and retry snapshot data while Yii3 finishes its post-response persistence.
12+
- feat: add shared framework-neutral normalization and presentation helpers for debug adapters.

‎README.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Debug Core
22

3-
Framework-agnostic contracts, snapshots, storage primitives, and complete frontend for PHP debugger adapters.
3+
Framework-agnostic contracts, snapshots, storage primitives, normalization and presentation helpers, and complete
4+
frontend for PHP debugger adapters.
45

56
This package is the shared engine used by framework-specific integrations. Applications should install an adapter
67
instead of requiring this package directly.
@@ -15,10 +16,11 @@ composer require php-forge/debug-core
1516

1617
## Architecture
1718

18-
The core package owns portable debug data, persistence, the frontend source and compiled files, shared fonts and icons,
19-
the toolbar data contract, and framework-neutral PHP templates composed with the agnostic `ui-awesome/html` helpers.
20-
It does not register assets, render responses, inject toolbar markup, or depend on Yii2, Yii3, an application
21-
container, a view implementation, or a framework request lifecycle.
19+
The core package owns portable debug data, persistence, normalization and presentation primitives under
20+
`PHPForge\Debug\Helper`, the frontend source and compiled files, shared fonts and icons, the toolbar data contract, and
21+
framework-neutral PHP templates composed with the agnostic UI Awesome HTML helpers. It does not register assets,
22+
render responses, inject toolbar markup, or depend on Yii2, Yii3, an application container, a view implementation, or
23+
a framework request lifecycle.
2224

2325
Adapters collect framework data, convert it into immutable snapshots, expose toolbar data endpoints, define and
2426
publish assets through their framework, and render the shared templates with their framework view component. They also

‎composer.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"require": {
2222
"php": ">=8.3",
2323
"ext-mbstring": "*",
24-
"ui-awesome/html": "^0.6"
24+
"ui-awesome/html": "^0.6",
25+
"ui-awesome/html-svg": "^0.6"
2526
},
2627
"require-dev": {
2728
"infection/infection": "^0.34",

‎src/Helper/Avatar.php‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPForge\Debug\Helper;
6+
7+
use function abs;
8+
use function crc32;
9+
use function strtolower;
10+
11+
/**
12+
* Derives stable, deterministic avatar colours from arbitrary identifying strings.
13+
*
14+
* Two debug-panel renderers (mail and queue) display a colored circle next to each item; both used to compute the hue
15+
* with the same `abs(crc32(strtolower(...))) % 360` formula. This helper centralises that derivation so the colour
16+
* stays consistent across renderers.
17+
*/
18+
final class Avatar
19+
{
20+
/**
21+
* Fallback hue used when `$seed` is empty.
22+
*/
23+
private const int DEFAULT_HUE = 210;
24+
25+
/**
26+
* Returns a stable hue (`0..359`) for the given seed, or {@see self::DEFAULT_HUE} when the seed is empty.
27+
*
28+
* Usage example:
29+
* ```php
30+
* $hue = \PHPForge\Debug\Helper\Avatar::hueFor('Alice');
31+
* ```
32+
*
33+
* @param string $seed Identifying value used to derive the hue.
34+
*
35+
* @return int Hue in the `0..359` range.
36+
*/
37+
public static function hueFor(string $seed): int
38+
{
39+
if ($seed === '') {
40+
return self::DEFAULT_HUE;
41+
}
42+
43+
return abs(crc32(strtolower($seed))) % 360;
44+
}
45+
}

‎src/Helper/CellMore.php‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPForge\Debug\Helper;
6+
7+
use UIAwesome\Html\Flow\Div;
8+
use UIAwesome\Html\Form\Button;
9+
use UIAwesome\Html\Form\Values\ButtonType;
10+
11+
use function strlen;
12+
13+
/**
14+
* Wraps long grid-cell content in a collapsible clamp with an expand/collapse pill toggle.
15+
*
16+
* The body collapses to a few lines through a CSS max-height clamp — the markup is never truncated server side, so
17+
* any cell payload (plain text, highlighted SQL, trace lists) stays intact. The `debug.min.js` `cell-more` delegation
18+
* flips the `is-open` state and swaps the toggle label.
19+
*/
20+
final class CellMore
21+
{
22+
/**
23+
* Row count beyond which a table is worth collapsing.
24+
*
25+
* Table height tracks the number of rows, not the payload size: a handful of verbose rows reads fine, while many
26+
* terse ones still push the page past the viewport.
27+
*/
28+
public const int ROW_THRESHOLD = 12;
29+
/**
30+
* Source length (bytes) beyond which a cell is worth collapsing.
31+
*/
32+
public const int THRESHOLD = 600;
33+
34+
/**
35+
* Wraps the content only when its source payload is long enough to be worth collapsing.
36+
*
37+
* The decision reads the raw source rather than the rendered markup, so highlighting or trace lists never tip a
38+
* short value over the threshold.
39+
*
40+
* Usage example:
41+
* ```php
42+
* \PHPForge\Debug\Helper\CellMore::clamp($highlightedSql, $row->query);
43+
* ```
44+
*
45+
* @param string $content Rendered cell HTML.
46+
* @param string $source Raw payload the content was rendered from.
47+
*
48+
* @return string Original content or collapsible markup when the source exceeds the threshold.
49+
*/
50+
public static function clamp(string $content, string $source): string
51+
{
52+
return strlen($source) > self::THRESHOLD ? self::wrap($content) : $content;
53+
}
54+
55+
/**
56+
* Wraps the rendered cell content in the collapsible clamp container.
57+
*
58+
* Usage example:
59+
* ```php
60+
* \PHPForge\Debug\Helper\CellMore::wrap($renderedCellHtml);
61+
* ```
62+
*
63+
* @param string $content Rendered cell HTML to clamp; emitted verbatim inside the body container.
64+
*
65+
* @return string Collapsible cell markup.
66+
*/
67+
public static function wrap(string $content): string
68+
{
69+
return Div::tag()
70+
->class('yii-debug-cell-more')
71+
->html(
72+
Div::tag()
73+
->class('yii-debug-cell-more-body')
74+
->html($content),
75+
Button::tag()
76+
->addAriaAttribute('expanded', 'false')
77+
->addAttribute('data-yii-debug-toggle', 'cell-more')
78+
->class('yii-debug-cell-more-toggle')
79+
->content('[+] Show more')
80+
->type(ButtonType::BUTTON),
81+
)
82+
->render();
83+
}
84+
}

‎src/Helper/Coerce.php‎

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPForge\Debug\Helper;
6+
7+
use Stringable;
8+
9+
use function is_array;
10+
use function is_numeric;
11+
use function is_scalar;
12+
use function is_string;
13+
14+
/**
15+
* Narrows arbitrary mixed payloads into the typed scalars debug-panel renderers expect.
16+
*
17+
* Framework callbacks, logger entries, request parameters, and renderer rows remain mixed even though persisted
18+
* snapshots use strict JSON DTOs. This helper is limited to those external runtime boundaries; snapshot hydration is
19+
* handled by {@see \PHPForge\Debug\Storage\Payload} without scalar coercion.
20+
*/
21+
final class Coerce
22+
{
23+
/**
24+
* Returns a numeric value as a float or the supplied default.
25+
*
26+
* Usage example:
27+
* ```php
28+
* $duration = \PHPForge\Debug\Helper\Coerce::float($payload['duration'] ?? null, 0.0);
29+
* ```
30+
*
31+
* @param mixed $value Value to narrow.
32+
* @param float $default Value returned for non-numeric input.
33+
*
34+
* @return float Numeric value or the configured default.
35+
*/
36+
public static function float(mixed $value, float $default = 0.0): float
37+
{
38+
return self::floatOrNull($value) ?? $default;
39+
}
40+
41+
/**
42+
* Returns the value as a float when it is numeric, `null` otherwise.
43+
*
44+
* Usage example:
45+
* ```php
46+
* $duration = \PHPForge\Debug\Helper\Coerce::floatOrNull($payload['duration'] ?? null);
47+
* ```
48+
*
49+
* @param mixed $value Value to narrow.
50+
*
51+
* @return float|null Numeric value or `null`.
52+
*/
53+
public static function floatOrNull(mixed $value): float|null
54+
{
55+
return is_numeric($value) ? (float) $value : null;
56+
}
57+
58+
/**
59+
* Returns a numeric value as an int or the supplied default.
60+
*
61+
* Usage example:
62+
* ```php
63+
* $count = \PHPForge\Debug\Helper\Coerce::int($payload['count'] ?? null);
64+
* ```
65+
*
66+
* @param mixed $value Value to narrow.
67+
* @param int $default Value returned for non-numeric input.
68+
*
69+
* @return int Numeric value or the configured default.
70+
*/
71+
public static function int(mixed $value, int $default = 0): int
72+
{
73+
return self::intOrNull($value) ?? $default;
74+
}
75+
76+
/**
77+
* Returns the value as an int when it is an integer or numeric, `null` otherwise.
78+
*
79+
* Usage example:
80+
* ```php
81+
* $statusCode = \PHPForge\Debug\Helper\Coerce::intOrNull($payload['statusCode'] ?? null);
82+
* ```
83+
*
84+
* @param mixed $value Value to narrow.
85+
*
86+
* @return int|null Numeric value or `null`.
87+
*/
88+
public static function intOrNull(mixed $value): int|null
89+
{
90+
return is_numeric($value) ? (int) $value : null;
91+
}
92+
93+
/**
94+
* Returns a string value or the supplied default.
95+
*
96+
* Usage example:
97+
* ```php
98+
* $category = \PHPForge\Debug\Helper\Coerce::string($payload['category'] ?? null, 'application');
99+
* ```
100+
*
101+
* @param mixed $value Value to narrow.
102+
* @param string $default Value returned for non-string input.
103+
*
104+
* @return string String value or the configured default.
105+
*/
106+
public static function string(mixed $value, string $default = ''): string
107+
{
108+
return is_string($value) ? $value : $default;
109+
}
110+
111+
/**
112+
* Returns only the entries of `$data` whose key is a string, preserving order.
113+
*
114+
* Narrows a mixed/`array<array-key, mixed>` snapshot down to the `array<string, mixed>` shape downstream view-model
115+
* normalizers expect.
116+
*
117+
* Usage example:
118+
* ```php
119+
* $data = \PHPForge\Debug\Helper\Coerce::stringKeyedArray(['name' => 'debug', 0 => 'ignored']);
120+
* ```
121+
*
122+
* @param array<array-key, mixed> $data Source array with arbitrary keys.
123+
*
124+
* @return array<string, mixed> Entries whose key was already a string, in original order.
125+
*/
126+
public static function stringKeyedArray(array $data): array
127+
{
128+
$normalized = [];
129+
130+
foreach ($data as $key => $value) {
131+
if (is_string($key)) {
132+
$normalized[$key] = $value;
133+
}
134+
}
135+
136+
return $normalized;
137+
}
138+
139+
/**
140+
* Returns only the string entries of a raw list, preserving order.
141+
*
142+
* Usage example:
143+
* ```php
144+
* $categories = \PHPForge\Debug\Helper\Coerce::stringList(['application', 42, 'database']);
145+
* ```
146+
*
147+
* @param mixed $values Raw list, typically a user-configured category list.
148+
*
149+
* @return list<string> String entries in original order, possibly empty.
150+
*/
151+
public static function stringList(mixed $values): array
152+
{
153+
if (!is_array($values)) {
154+
return [];
155+
}
156+
157+
$strings = [];
158+
159+
foreach ($values as $value) {
160+
if (is_string($value)) {
161+
$strings[] = $value;
162+
}
163+
}
164+
165+
return $strings;
166+
}
167+
168+
/**
169+
* Returns the value as a string when it is scalar or {@see Stringable}, `null` otherwise.
170+
*
171+
* Usage example:
172+
* ```php
173+
* $label = \PHPForge\Debug\Helper\Coerce::stringOrNull($payload['label'] ?? null);
174+
* ```
175+
*
176+
* @param mixed $value Value to narrow.
177+
*
178+
* @return string|null String representation or `null`.
179+
*/
180+
public static function stringOrNull(mixed $value): string|null
181+
{
182+
if (is_scalar($value) || $value instanceof Stringable) {
183+
return (string) $value;
184+
}
185+
186+
return null;
187+
}
188+
189+
/**
190+
* Narrows a raw trace value (as captured by Yii's logger) into the `list<array<string, mixed>>` shape every panel
191+
* renderer consumes.
192+
*
193+
* Each frame keeps only its string-keyed entries; non-array frames are dropped.
194+
*
195+
* Usage example:
196+
* ```php
197+
* $frames = \PHPForge\Debug\Helper\Coerce::traceFrames($payload['trace'] ?? null);
198+
* ```
199+
*
200+
* @param mixed $value Raw trace payload.
201+
*
202+
* @return list<array<string, mixed>> Trace frames normalized to string-keyed maps.
203+
*/
204+
public static function traceFrames(mixed $value): array
205+
{
206+
if (!is_array($value)) {
207+
return [];
208+
}
209+
210+
$frames = [];
211+
212+
foreach ($value as $frame) {
213+
if (!is_array($frame)) {
214+
continue;
215+
}
216+
217+
$frames[] = self::stringKeyedArray($frame);
218+
}
219+
220+
return $frames;
221+
}
222+
}

0 commit comments

Comments
 (0)