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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"ui-awesome/html-interop": "^0.4",
"ui-awesome/html-mixin": "^0.8.2",
"ui-awesome/html-svg": "^0.6",
"php-forge/debug": "^0.3"
"php-forge/debug": "^0.4"
Comment thread
terabytesoftw marked this conversation as resolved.
},
"require-dev": {
"infection/infection": "^0.35",
Expand Down
2 changes: 1 addition & 1 deletion resources/assets/dist/css/debug.min.css

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions resources/src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -3863,9 +3863,15 @@
}
}

/* Cards carry their own rhythm: no list wrapper supplies the gap. */
.yii-debug-entity + .yii-debug-entity {
margin-top: var(--yii-debug-space-3);
/*
* The cards of one set share a grid: the gap supplies their rhythm, and a
* card never narrows below the width its two body columns need, so wide
* panels place cards side by side and narrow ones stack them.
*/
.yii-debug-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 32rem), 1fr));
gap: var(--yii-debug-space-3);
}

.yii-debug-entity-head {
Expand Down
19 changes: 7 additions & 12 deletions src/Panel/Asset/AssetPanel.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@

use PHPForge\Debug\{ColumnStyle, Panel, PanelView, Tone};
use PHPForge\Debug\Helper\Fqcn;
use PHPForge\Debug\Presenter\{FileEntry, LinkInline};
use PHPForge\Debug\Presenter\{CardEntry, FileEntry, LinkInline};

use function array_map;
use function count;
use function sprintf;

/**
* Presents the registered asset bundles as a strip of headline statistics and one card per bundle.
* Presents the registered asset bundles as a strip of headline statistics and a grid holding one card per bundle.
*
* The optional Vite bridge snapshot is described first, because it governs how the bundles resolve their URLs.
*/
Expand Down Expand Up @@ -111,22 +111,17 @@ public function present(array $data): PanelView
);
}

foreach ($bundles as $bundle) {
$view = self::card($view, $bundle);
}

return $view;
return $view->cards(...array_map(self::card(...), $bundles));
}

/**
* Appends one bundle as a card: identity in the header, declared files and wiring in its columns.
* Describes one bundle as a card: identity in the header, declared files and wiring in its columns.
*
* @param PanelView $view View to extend.
* @param AssetBundleRow $bundle Captured bundle to describe.
*
* @return PanelView View completed with the bundle card.
* @return CardEntry Card describing the bundle.
*/
private static function card(PanelView $view, AssetBundleRow $bundle): PanelView
private static function card(AssetBundleRow $bundle): CardEntry
{
$cssCount = count($bundle->css);
$jsCount = count($bundle->js);
Expand Down Expand Up @@ -166,7 +161,7 @@ private static function card(PanelView $view, AssetBundleRow $bundle): PanelView

$namespace = Fqcn::namespacePart($bundle->name);

return $view->card(
return PanelView::card(
Fqcn::anchor($bundle->name),
AssetMessage::ID->value,
Fqcn::shortName($bundle->name),
Expand Down
10 changes: 7 additions & 3 deletions src/Panel/Dump/DumpCardRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ private static function sanitizeMessage(string $message): string
* Decodes HTML entities so the first payload character (`[`, `'`, `"`, digit, identifier) classifies the dumped
* value. A miss hides the badge without blocking the render.
*
* Besides the Yii2 `Class#id` form, `yiisoft/var-dumper` exports some objects as PHP expressions: a date as
* `new \DateTimeImmutable(...)` and a closure as its source (`function`, `fn`, or `static fn`), so the class after
* `new` names the object and a closure is labelled `Closure`.
*
* @return array{0: string, 1: string} `[typeKey, typeLabel]`, both `''` when the type cannot be determined.
*/
private static function sniffType(string $message): array
Expand All @@ -275,8 +279,8 @@ private static function sniffType(string $message): array
return ['string', 'string'];
}

if (preg_match('/^[A-Za-z_][A-Za-z0-9_\\\\]*/', $payload, $m) === 1) {
$name = $m[0];
if (preg_match('/^(?:new \\\\)?([A-Za-z_][A-Za-z0-9_\\\\]*)/', $payload, $m) === 1) {
$name = $m[1];

$lower = strtolower($name);

Expand All @@ -288,7 +292,7 @@ private static function sniffType(string $message): array
return ['null', 'null'];
}

return ['object', $name];
return ['object', in_array($lower, ['fn', 'function', 'static'], true) ? 'Closure' : $name];
}

if (preg_match('/^-?\d/', $payload) === 1) {
Expand Down
24 changes: 20 additions & 4 deletions src/Panel/PanelRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
use PHPForge\Debug\Presenter\{
BadgeInline,
Block,
CardBlock,
CardEntry,
CardsBlock,
DisclosureBlock,
EmptyStateBlock,
FactsBlock,
Expand Down Expand Up @@ -136,7 +137,7 @@ private function anchor(LinkInline $inline, string $class): string
private function block(Block $block): string
{
return match (true) {
$block instanceof CardBlock => $this->card($block),
$block instanceof CardsBlock => $this->cards($block),
$block instanceof DisclosureBlock => Disclosure::render(
$block->title,
Pre::tag()->content($block->content)->render(),
Expand Down Expand Up @@ -187,11 +188,11 @@ private function blocks(array $blocks): string
/**
* Renders one entity as a card: an identifying header and, when the entity has content, its titled columns.
*
* @param CardBlock $block Validated card.
* @param CardEntry $block Validated card.
*
* @return string Rendered card.
*/
private function card(CardBlock $block): string
private function card(CardEntry $block): string
{
$title = [
H2::tag()
Expand Down Expand Up @@ -258,6 +259,21 @@ private function card(CardBlock $block): string
return ($block->id === '' ? $card : $card->id($block->id))->render();
}

/**
* Renders a set of entity cards as one grid, so the frontend places them side by side when the panel is wide.
*
* @param CardsBlock $block Validated card set.
*
* @return string Rendered card grid.
*/
private function cards(CardsBlock $block): string
{
return Div::tag()
->class(Css::CARD_GRID)
->html(...array_map($this->card(...), $block->cards))
->render();
}

/**
* Renders the compact strip of label and value pairs.
*
Expand Down
4 changes: 4 additions & 0 deletions src/Theme/Css.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ final class Css
* Paragraph promoted to a semantic callout.
*/
public const string CALLOUT = 'yii-debug-callout';
/**
* Grid laying out a set of entity cards.
*/
public const string CARD_GRID = 'yii-debug-card-grid';
/**
* Grid cell rendered in the monospace face.
*/
Expand Down
68 changes: 39 additions & 29 deletions tests/Panel/Asset/AssetPanelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPForge\Debug\Panel\Asset\{AssetPanel, AssetSnapshot};
use PHPForge\Debug\Presenter\{
BadgeInline,
CardEntry,
EmptyStateBlock,
FactEntry,
FileEntry,
Expand All @@ -31,25 +32,23 @@ final class AssetPanelTest extends TestCase

public function testBundleWithoutFilesWiringOrDependenciesRendersABodylessCard(): void
{
$card = self::card(
self::blockAt(
self::present(
[
self::bundle(
[
'name' => 'app\\assets\\EmptyAsset',
'sourcePath' => '',
'basePath' => '',
'baseUrl' => '',
'css' => [],
'js' => [],
'depends' => [],
],
),
],
),
1,
$card = self::bundleCard(
self::present(
[
self::bundle(
[
'name' => 'app\\assets\\EmptyAsset',
'sourcePath' => '',
'basePath' => '',
'baseUrl' => '',
'css' => [],
'js' => [],
'depends' => [],
],
),
],
),
0,
);

self::assertSame(
Expand Down Expand Up @@ -123,11 +122,9 @@ public function testEmptyCaptureExplainsTheMissingBundles(): void

public function testGlobalNamespaceBundleDropsTheSubtitleAndAnchorsOnItsBareClassName(): void
{
$card = self::card(
self::blockAt(
self::present([self::bundle(['name' => 'GlobalAsset', 'js' => [], 'depends' => []])]),
1,
),
$card = self::bundleCard(
self::present([self::bundle(['name' => 'GlobalAsset', 'js' => [], 'depends' => []])]),
0,
);

self::assertSame(
Expand Down Expand Up @@ -214,7 +211,7 @@ public function testRegisteredBundlesProduceTheirCardsWithFilesAndWiringColumns(
'The toolbar must report the bundle count.',
);

$card = self::card(self::blockAt($view, 1));
$card = self::bundleCard($view, 0);

self::assertSame(
'app-assets-appasset-3c6a8113',
Expand Down Expand Up @@ -292,8 +289,8 @@ public function testRegisteredBundlesProduceTheirCardsWithFilesAndWiringColumns(
'The strip must count what it lists.',
);

$yii = self::card(self::blockAt($view, 2));
$jquery = self::card(self::blockAt($view, 3));
$yii = self::bundleCard($view, 1);
$jquery = self::bundleCard($view, 2);

self::assertSame(
['yii-web-yiiasset-7afeb318', 'yii-web-jqueryasset-2772d8b9'],
Expand Down Expand Up @@ -352,7 +349,7 @@ public function testSingleBundleAndDependencyUseTheSingularLabels(): void
'A lone bundle and a lone dependency must read in the singular.',
);

$card = self::card(self::blockAt($view, 1));
$card = self::bundleCard($view, 0);

self::assertEquals(
[
Expand Down Expand Up @@ -526,7 +523,7 @@ public function testWiringColumnAdaptsToTheCapturedPathsAndDependencies(): void
],
);

$dependsOnly = self::column(self::card(self::blockAt($view, 1)), 0);
$dependsOnly = self::column(self::bundleCard($view, 0), 0);

self::assertSame(
'Wiring',
Expand All @@ -544,7 +541,7 @@ public function testWiringColumnAdaptsToTheCapturedPathsAndDependencies(): void
'The strip must count what it lists.',
);

$pathsOnly = self::column(self::card(self::blockAt($view, 2)), 0);
$pathsOnly = self::column(self::bundleCard($view, 1), 0);

self::assertCount(
1,
Expand Down Expand Up @@ -582,6 +579,19 @@ private static function bundle(array $overrides = []): array
];
}

/**
* Reads one bundle card from the card set that follows the stat strip.
*
* @param PanelView $view Description built by the panel, without a Vite section.
* @param int $index Position of the bundle in registration order.
*
* @return CardEntry Card describing the requested bundle.
*/
private static function bundleCard(PanelView $view, int $index): CardEntry
{
return self::cardAt(self::cards(self::blockAt($view, 1)), $index);
}

/**
* Presents the given capture through the panel under test.
*
Expand Down
18 changes: 18 additions & 0 deletions tests/Panel/Dump/DumpCardRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,24 @@ public function testRenderMessageCellSniffsStringTypeFromQuoteCharacter(): void
);
}

public function testRenderMessageCellSniffsVarDumperObjectExpressions(): void
{
$cases = [
"<?php new \\DateTimeImmutable('2026-09-25T12:00:00.000+00:00')" => 'DateTimeImmutable',
'function () {/* source */}' => 'Closure',
'fn () => 1' => 'Closure',
'static fn () => 1' => 'Closure',
];

foreach ($cases as $message => $label) {
self::assertStringContainsString(
"<span class=\"yii-debug-dump-type\" data-type=\"object\">{$label}</span>",
DumpCardRenderer::renderMessageCell(self::makeRow(message: $message), self::traceLine(), 0),
"Payload `{$message}` must be labelled `{$label}`.",
);
}
}

public function testRenderMessageCellSubstitutesInvalidUtf8InTheExactCardHtml(): void
{
self::assertSame(
Expand Down
Loading
Loading