fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled

- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
2025-11-09 14:46:15 +01:00
parent 85c369e846
commit 36ef2a1e2c
1366 changed files with 104925 additions and 28719 deletions

View File

@@ -0,0 +1,387 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components;
use App\Framework\Display\Components\Console\Badge as ConsoleBadge;
use App\Framework\Display\Components\Console\Card as ConsoleCard;
use App\Framework\Display\Components\Console\CodeBlock as ConsoleCodeBlock;
use App\Framework\Display\Components\Console\Divider as ConsoleDivider;
use App\Framework\Display\Components\Console\ListItem as ConsoleListItem;
use App\Framework\Display\Components\Console\Stats as ConsoleStats;
use App\Framework\Display\Components\Console\Table as ConsoleTable;
use App\Framework\Display\Components\Console\TextBox as ConsoleTextBox;
use App\Framework\Display\Components\Console\TreeHelper as ConsoleTreeHelper;
use App\Framework\Display\Components\Html\HtmlBadge;
use App\Framework\Display\Components\Html\HtmlCard;
use App\Framework\Display\Components\Html\HtmlCodeBlock;
use App\Framework\Display\Components\Html\HtmlDivider;
use App\Framework\Display\Components\Html\HtmlListItem;
use App\Framework\Display\Components\Html\HtmlStats;
use App\Framework\Display\Components\Html\HtmlTable;
use App\Framework\Display\Components\Html\HtmlTextBox;
use App\Framework\Display\Components\Html\HtmlTreeHelper;
use App\Framework\Display\Themes\ConsoleTheme;
use App\Framework\Display\Themes\DefaultThemes;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Factory for creating display components based on output format
*/
final readonly class ComponentFactory
{
/**
* Create a TextBox component for the given output format
*/
public static function createTextBox(
OutputFormat $format,
DisplayOptions $options,
string $content,
?string $title = null
): DisplayComponentInterface {
return match ($format) {
OutputFormat::CONSOLE => self::createConsoleTextBox($options, $content, $title),
OutputFormat::HTML => self::createHtmlTextBox($options, $content, $title),
};
}
/**
* Create a TreeHelper component for the given output format
*/
public static function createTreeHelper(
OutputFormat $format,
DisplayOptions $options,
string $title = ''
): DisplayComponentInterface {
return match ($format) {
OutputFormat::CONSOLE => self::createConsoleTreeHelper($options, $title),
OutputFormat::HTML => self::createHtmlTreeHelper($options, $title),
};
}
/**
* Create a Table component for the given output format
*/
public static function createTable(
OutputFormat $format,
DisplayOptions $options
): DisplayComponentInterface {
return match ($format) {
OutputFormat::CONSOLE => self::createConsoleTable($options),
OutputFormat::HTML => self::createHtmlTable($options),
};
}
private static function createConsoleTextBox(
DisplayOptions $options,
string $content,
?string $title
): ConsoleTextBox {
$theme = self::getConsoleTheme($options);
$borderStyle = \App\Framework\Console\ConsoleStyle::create(color: $theme->separatorColor);
$contentStyle = \App\Framework\Console\ConsoleStyle::create(color: $theme->valueColor);
return new ConsoleTextBox(
content: $content,
borderStyle: $borderStyle,
contentStyle: $contentStyle,
title: $title
);
}
private static function createConsoleTreeHelper(
DisplayOptions $options,
string $title
): ConsoleTreeHelper {
$theme = self::getConsoleTheme($options);
$tree = new ConsoleTreeHelper($title);
$tree->setNodeStyle(\App\Framework\Console\ConsoleStyle::create(color: $theme->keyColor));
$tree->setLeafStyle(\App\Framework\Console\ConsoleStyle::create(color: $theme->valueColor));
$tree->setLineStyle(\App\Framework\Console\ConsoleStyle::create(color: $theme->separatorColor));
return $tree;
}
private static function createConsoleTable(
DisplayOptions $options
): ConsoleTable {
$theme = self::getConsoleTheme($options);
$headerStyle = \App\Framework\Console\ConsoleStyle::create(color: $theme->classNameColor);
$rowStyle = \App\Framework\Console\ConsoleStyle::create(color: $theme->valueColor);
$borderStyle = \App\Framework\Console\ConsoleStyle::create(color: $theme->separatorColor);
return new ConsoleTable(
headerStyle: $headerStyle,
rowStyle: $rowStyle,
borderStyle: $borderStyle
);
}
private static function createHtmlTextBox(
DisplayOptions $options,
string $content,
?string $title
): HtmlTextBox {
$theme = self::getHtmlTheme($options);
return new HtmlTextBox(
theme: $theme,
options: $options,
content: $content,
title: $title
);
}
private static function createHtmlTreeHelper(
DisplayOptions $options,
string $title
): HtmlTreeHelper {
$theme = self::getHtmlTheme($options);
$tree = new HtmlTreeHelper(
theme: $theme,
options: $options,
title: $title
);
return $tree;
}
private static function createHtmlTable(
DisplayOptions $options
): HtmlTable {
$theme = self::getHtmlTheme($options);
return new HtmlTable(
theme: $theme,
options: $options
);
}
/**
* Create a List component for the given output format
*/
public static function createList(
OutputFormat $format,
DisplayOptions $options,
array $items,
bool $ordered = false
): DisplayComponentInterface {
return match ($format) {
OutputFormat::CONSOLE => self::createConsoleList($options, $items, $ordered),
OutputFormat::HTML => self::createHtmlList($options, $items, $ordered),
};
}
/**
* Create a Badge component for the given output format
*/
public static function createBadge(
OutputFormat $format,
DisplayOptions $options,
string $text,
string $style = 'info'
): DisplayComponentInterface {
return match ($format) {
OutputFormat::CONSOLE => self::createConsoleBadge($options, $text, $style),
OutputFormat::HTML => self::createHtmlBadge($options, $text, $style),
};
}
/**
* Create a Divider component for the given output format
*/
public static function createDivider(
OutputFormat $format,
DisplayOptions $options,
?string $label = null
): DisplayComponentInterface {
return match ($format) {
OutputFormat::CONSOLE => self::createConsoleDivider($options, $label),
OutputFormat::HTML => self::createHtmlDivider($options, $label),
};
}
/**
* Create a Card component for the given output format
*/
public static function createCard(
OutputFormat $format,
DisplayOptions $options,
?string $title = null,
string $body = '',
?string $footer = null
): DisplayComponentInterface {
return match ($format) {
OutputFormat::CONSOLE => self::createConsoleCard($options, $title, $body, $footer),
OutputFormat::HTML => self::createHtmlCard($options, $title, $body, $footer),
};
}
/**
* Create a CodeBlock component for the given output format
*/
public static function createCodeBlock(
OutputFormat $format,
DisplayOptions $options,
string $code,
?string $language = null,
bool $lineNumbers = false,
array $highlightLines = []
): DisplayComponentInterface {
return match ($format) {
OutputFormat::CONSOLE => self::createConsoleCodeBlock($options, $code, $language, $lineNumbers, $highlightLines),
OutputFormat::HTML => self::createHtmlCodeBlock($options, $code, $language, $lineNumbers, $highlightLines),
};
}
/**
* Create a Stats component for the given output format
*/
public static function createStats(
OutputFormat $format,
DisplayOptions $options,
string|int|float $value,
?string $label = null
): DisplayComponentInterface {
return match ($format) {
OutputFormat::CONSOLE => self::createConsoleStats($options, $value, $label),
OutputFormat::HTML => self::createHtmlStats($options, $value, $label),
};
}
private static function createConsoleList(
DisplayOptions $options,
array $items,
bool $ordered
): ConsoleListItem {
$theme = self::getConsoleTheme($options);
return ConsoleListItem::fromOptions($items, $options, $theme, $ordered);
}
private static function createHtmlList(
DisplayOptions $options,
array $items,
bool $ordered
): HtmlListItem {
$theme = self::getHtmlTheme($options);
return HtmlListItem::fromOptions($items, $options, $theme, $ordered);
}
private static function createConsoleBadge(
DisplayOptions $options,
string $text,
string $style
): ConsoleBadge {
$theme = self::getConsoleTheme($options);
$badgeStyle = \App\Framework\Display\Components\Console\BadgeStyle::from($style);
return ConsoleBadge::fromOptions($text, $options, $theme, $badgeStyle);
}
private static function createHtmlBadge(
DisplayOptions $options,
string $text,
string $style
): HtmlBadge {
$theme = self::getHtmlTheme($options);
$badgeStyle = \App\Framework\Display\Components\Html\BadgeStyle::from($style);
return HtmlBadge::fromOptions($text, $options, $theme, $badgeStyle);
}
private static function createConsoleDivider(
DisplayOptions $options,
?string $label
): ConsoleDivider {
$theme = self::getConsoleTheme($options);
return ConsoleDivider::fromOptions($options, $theme, $label);
}
private static function createHtmlDivider(
DisplayOptions $options,
?string $label
): HtmlDivider {
$theme = self::getHtmlTheme($options);
return HtmlDivider::fromOptions($options, $theme, $label);
}
private static function createConsoleCard(
DisplayOptions $options,
?string $title,
string $body,
?string $footer
): ConsoleCard {
$theme = self::getConsoleTheme($options);
return ConsoleCard::fromOptions($title, $body, $footer, $options, $theme);
}
private static function createHtmlCard(
DisplayOptions $options,
?string $title,
string $body,
?string $footer
): HtmlCard {
$theme = self::getHtmlTheme($options);
return HtmlCard::fromOptions($title, $body, $footer, $options, $theme);
}
private static function createConsoleCodeBlock(
DisplayOptions $options,
string $code,
?string $language,
bool $lineNumbers,
array $highlightLines
): ConsoleCodeBlock {
$theme = self::getConsoleTheme($options);
return ConsoleCodeBlock::fromOptions($code, $options, $theme, $language, $lineNumbers, $highlightLines);
}
private static function createHtmlCodeBlock(
DisplayOptions $options,
string $code,
?string $language,
bool $lineNumbers,
array $highlightLines
): HtmlCodeBlock {
$theme = self::getHtmlTheme($options);
return HtmlCodeBlock::fromOptions($code, $options, $theme, $language, $lineNumbers, $highlightLines);
}
private static function createConsoleStats(
DisplayOptions $options,
string|int|float $value,
?string $label
): ConsoleStats {
$theme = self::getConsoleTheme($options);
return ConsoleStats::fromOptions($value, $label, $options, $theme);
}
private static function createHtmlStats(
DisplayOptions $options,
string|int|float $value,
?string $label
): HtmlStats {
$theme = self::getHtmlTheme($options);
return HtmlStats::fromOptions($value, $label, $options, $theme);
}
private static function getConsoleTheme(DisplayOptions $options): ConsoleTheme
{
if ($options->customTheme instanceof ConsoleTheme) {
return $options->customTheme;
}
return DefaultThemes::getConsoleTheme($options->theme);
}
private static function getHtmlTheme(DisplayOptions $options): HtmlTheme
{
if ($options->customTheme instanceof HtmlTheme) {
return $options->customTheme;
}
return DefaultThemes::getHtmlTheme($options->theme);
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Console;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\ConsoleTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Badge/Label component for displaying small tags or labels in console
*/
final readonly class Badge implements DisplayComponentInterface
{
public function __construct(
private string $text,
private BadgeStyle $style = BadgeStyle::INFO,
private ?ConsoleStyle $customStyle = null,
) {
}
public static function fromOptions(string $text, DisplayOptions $options, ConsoleTheme $theme, BadgeStyle $style = BadgeStyle::INFO): self
{
$colors = match ($style) {
BadgeStyle::INFO => $theme->infoColor,
BadgeStyle::SUCCESS => ConsoleColor::GREEN,
BadgeStyle::WARNING => $theme->warningColor,
BadgeStyle::ERROR => $theme->errorColor,
BadgeStyle::DEFAULT => $theme->typeColor,
};
$customStyle = ConsoleStyle::create(color: $colors, format: ConsoleFormat::BOLD);
return new self(
text: $text,
style: $style,
customStyle: $customStyle
);
}
public function render(): string
{
$style = $this->customStyle ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
return '[' . $style->apply($this->text) . ']';
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::CONSOLE;
}
}
/**
* Badge style enum
*/
enum BadgeStyle: string
{
case INFO = 'info';
case SUCCESS = 'success';
case WARNING = 'warning';
case ERROR = 'error';
case DEFAULT = 'default';
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Console;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\ConsoleTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Card component for displaying information in card-like containers in console
*/
final readonly class Card implements DisplayComponentInterface
{
public function __construct(
private ?string $title = null,
private string $body = '',
private ?string $footer = null,
private int $width = 80,
private ?ConsoleStyle $borderStyle = null,
private ?ConsoleStyle $titleStyle = null,
private ?ConsoleStyle $bodyStyle = null,
private ?ConsoleStyle $footerStyle = null,
) {
}
public static function fromOptions(
?string $title,
string $body,
?string $footer,
DisplayOptions $options,
ConsoleTheme $theme
): self {
$borderStyle = ConsoleStyle::create(color: $theme->separatorColor);
$titleStyle = ConsoleStyle::create(color: $theme->classNameColor, format: ConsoleFormat::BOLD);
$bodyStyle = ConsoleStyle::create(color: $theme->valueColor);
$footerStyle = ConsoleStyle::create(color: $theme->metadataColor);
$width = $options->maxLineLength > 0 ? $options->maxLineLength : 80;
return new self(
title: $title,
body: $body,
footer: $footer,
width: $width,
borderStyle: $borderStyle,
titleStyle: $titleStyle,
bodyStyle: $bodyStyle,
footerStyle: $footerStyle
);
}
public function render(): string
{
$borderStyle = $this->borderStyle ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
$titleStyle = $this->titleStyle ?? ConsoleStyle::create(color: ConsoleColor::WHITE, format: ConsoleFormat::BOLD);
$bodyStyle = $this->bodyStyle ?? ConsoleStyle::create(color: ConsoleColor::WHITE);
$footerStyle = $this->footerStyle ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
$output = '';
$contentWidth = $this->width - 4; // Padding links/rechts
// Top border
$output .= $borderStyle->apply('┌' . str_repeat('─', $this->width - 2) . '┐') . "\n";
// Title
if ($this->title !== null) {
$title = mb_substr($this->title, 0, $contentWidth);
$titlePadding = str_repeat(' ', max(0, $contentWidth - mb_strlen($title)));
$output .= $borderStyle->apply('│') . ' ' . $titleStyle->apply($title . $titlePadding) . ' ' . $borderStyle->apply('│') . "\n";
$output .= $borderStyle->apply('├' . str_repeat('─', $this->width - 2) . '┤') . "\n";
}
// Body
if ($this->body !== '') {
$lines = explode("\n", $this->body);
foreach ($lines as $line) {
$wrappedLines = $this->wrapLine($line, $contentWidth);
foreach ($wrappedLines as $wrappedLine) {
$linePadding = str_repeat(' ', max(0, $contentWidth - mb_strlen($wrappedLine)));
$output .= $borderStyle->apply('│') . ' ' . $bodyStyle->apply($wrappedLine . $linePadding) . ' ' . $borderStyle->apply('│') . "\n";
}
}
}
// Footer
if ($this->footer !== null) {
if ($this->body !== '') {
$output .= $borderStyle->apply('├' . str_repeat('─', $this->width - 2) . '┤') . "\n";
}
$footer = mb_substr($this->footer, 0, $contentWidth);
$footerPadding = str_repeat(' ', max(0, $contentWidth - mb_strlen($footer)));
$output .= $borderStyle->apply('│') . ' ' . $footerStyle->apply($footer . $footerPadding) . ' ' . $borderStyle->apply('│') . "\n";
}
// Bottom border
$output .= $borderStyle->apply('└' . str_repeat('─', $this->width - 2) . '┘') . "\n";
return $output;
}
private function wrapLine(string $line, int $width): array
{
if (mb_strlen($line) <= $width) {
return [$line];
}
$lines = [];
$words = explode(' ', $line);
$currentLine = '';
foreach ($words as $word) {
$testLine = empty($currentLine) ? $word : $currentLine . ' ' . $word;
if (mb_strlen($testLine) <= $width) {
$currentLine = $testLine;
} else {
if (!empty($currentLine)) {
$lines[] = $currentLine;
$currentLine = $word;
} else {
$lines[] = mb_substr($word, 0, $width);
$currentLine = mb_substr($word, $width);
}
}
}
if (!empty($currentLine)) {
$lines[] = $currentLine;
}
return $lines ?: [''];
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::CONSOLE;
}
}

View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Console;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\ConsoleTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
use App\Framework\SyntaxHighlighter\SyntaxHighlighter;
/**
* CodeBlock component for displaying code snippets with syntax highlighting in console
*/
final readonly class CodeBlock implements DisplayComponentInterface
{
public function __construct(
private string $code,
private ?string $language = null,
private bool $lineNumbers = false,
private array $highlightLines = [],
private ?SyntaxHighlighter $highlighter = null,
) {
}
public static function fromOptions(
string $code,
DisplayOptions $options,
ConsoleTheme $theme,
?string $language = null,
bool $lineNumbers = false,
array $highlightLines = []
): self {
$highlighter = new SyntaxHighlighter();
return new self(
code: $code,
language: $language ?? self::detectLanguage($code),
lineNumbers: $lineNumbers,
highlightLines: $highlightLines,
highlighter: $highlighter
);
}
private static function detectLanguage(string $code): ?string
{
// Simple detection based on code patterns
$trimmed = trim($code);
// JSON
if (($trimmed[0] === '{' || $trimmed[0] === '[') && json_decode($code) !== null) {
return 'json';
}
// XML
if (preg_match('/^<\?xml\s/i', $trimmed) || preg_match('/^<[a-z]+/i', $trimmed)) {
return 'xml';
}
// YAML (basic detection)
if (preg_match('/^[a-z_][a-z0-9_]*:\s/mi', $trimmed)) {
return 'yaml';
}
// PHP (default for SyntaxHighlighter)
return 'php';
}
public function render(): string
{
$language = $this->language ?? 'php';
// Use SyntaxHighlighter for PHP
if ($language === 'php') {
$options = [
'lineNumbers' => $this->lineNumbers,
'highlightLines' => $this->highlightLines,
'colorize' => true,
];
return $this->highlighter?->highlight($this->code, 'console', $options) ?? $this->code;
}
// For other languages, use simple highlighting
return $this->highlightSimple($this->code, $language);
}
private function highlightSimple(string $code, string $language): string
{
// Simple regex-based highlighting for non-PHP languages
$lines = explode("\n", $code);
$output = '';
foreach ($lines as $lineNum => $line) {
$lineNumDisplay = $lineNum + 1;
$isHighlighted = in_array($lineNumDisplay, $this->highlightLines, true);
if ($this->lineNumbers) {
$output .= sprintf("\033[90m%4d |\033[0m ", $lineNumDisplay);
}
if ($isHighlighted) {
$output .= "\033[43m"; // Yellow background
}
$highlightedLine = match ($language) {
'json' => $this->highlightJson($line),
'xml' => $this->highlightXml($line),
'yaml' => $this->highlightYaml($line),
default => $line,
};
$output .= $highlightedLine;
if ($isHighlighted) {
$output .= "\033[0m"; // Reset
}
$output .= "\n";
}
return $output;
}
private function highlightJson(string $line): string
{
// Simple JSON highlighting
$line = preg_replace('/("(?:[^"\\\\]|\\\\.)*")\s*:/', "\033[33m$1\033[0m:", $line); // Keys
$line = preg_replace('/:\s*("(?:[^"\\\\]|\\\\.)*")/', ": \033[32m$1\033[0m", $line); // String values
$line = preg_replace('/:\s*(\d+)/', ": \033[34m$1\033[0m", $line); // Numbers
$line = preg_replace('/:\s*(true|false|null)/', ": \033[35m$1\033[0m", $line); // Booleans/null
return $line;
}
private function highlightXml(string $line): string
{
// Simple XML highlighting
$line = preg_replace('/<(\/?)([a-z][a-z0-9]*)/i', "<\033[33m$1$2\033[0m", $line); // Tags
$line = preg_replace('/([a-z]+)="([^"]*)"/i', "\033[36m$1\033[0m=\"\033[32m$2\033[0m\"", $line); // Attributes
return $line;
}
private function highlightYaml(string $line): string
{
// Simple YAML highlighting
$line = preg_replace('/^([a-z_][a-z0-9_]*):\s*/mi', "\033[33m$1\033[0m: ", $line); // Keys
$line = preg_replace('/:\s*("(?:[^"\\\\]|\\\\.)*")/', ": \033[32m$1\033[0m", $line); // String values
$line = preg_replace('/:\s*(\d+)/', ": \033[34m$1\033[0m", $line); // Numbers
return $line;
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::CONSOLE;
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Console;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\ConsoleTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Divider/Separator component for visual separation in console
*/
final readonly class Divider implements DisplayComponentInterface
{
public function __construct(
private ?string $label = null,
private int $width = 80,
private string $character = '─',
private ?ConsoleStyle $style = null,
) {
}
public static function fromOptions(DisplayOptions $options, ConsoleTheme $theme, ?string $label = null): self
{
$style = ConsoleStyle::create(color: $theme->separatorColor);
$width = $options->maxLineLength > 0 ? $options->maxLineLength : 80;
return new self(
label: $label,
width: $width,
character: '─',
style: $style
);
}
public function render(): string
{
$style = $this->style ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
if ($this->label !== null) {
$labelLength = mb_strlen($this->label);
$availableSpace = $this->width - $labelLength - 4; // Abzug für ' ' und ' '
$leftWidth = max(0, (int)floor($availableSpace / 2));
$rightWidth = max(0, $availableSpace - $leftWidth);
return $style->apply(str_repeat($this->character, $leftWidth) . ' ' . $this->label . ' ' . str_repeat($this->character, $rightWidth)) . "\n";
}
return $style->apply(str_repeat($this->character, $this->width)) . "\n";
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::CONSOLE;
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Console;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\ConsoleTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* List component for displaying ordered/unordered lists in console
*/
final readonly class ListItem implements DisplayComponentInterface
{
/**
* @param array<string|int, mixed> $items List items
* @param bool $ordered Whether to use ordered (numbered) list
* @param string $bullet Character to use for unordered lists
* @param ConsoleStyle|null $itemStyle Style for list items
* @param ConsoleStyle|null $markerStyle Style for list markers (numbers/bullets)
*/
public function __construct(
private array $items,
private bool $ordered = false,
private string $bullet = '•',
private ?ConsoleStyle $itemStyle = null,
private ?ConsoleStyle $markerStyle = null,
) {
}
public static function fromOptions(array $items, DisplayOptions $options, ConsoleTheme $theme, bool $ordered = false): self
{
$itemStyle = ConsoleStyle::create(color: $theme->valueColor);
$markerStyle = ConsoleStyle::create(color: $theme->keyColor);
return new self(
items: $items,
ordered: $ordered,
bullet: '•',
itemStyle: $itemStyle,
markerStyle: $markerStyle
);
}
public function render(): string
{
if (empty($this->items)) {
return '';
}
$output = '';
$itemStyle = $this->itemStyle ?? ConsoleStyle::create(color: ConsoleColor::WHITE);
$markerStyle = $this->markerStyle ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
foreach ($this->items as $index => $item) {
$marker = $this->ordered
? (string)($index + 1) . '.'
: $this->bullet;
$itemText = is_scalar($item) ? (string)$item : get_debug_type($item);
$output .= $markerStyle->apply($marker . ' ') . $itemStyle->apply($itemText) . "\n";
}
return $output;
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::CONSOLE;
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Console;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\ConsoleTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Stats/Metric component for displaying numerical statistics in console
*/
final readonly class Stats implements DisplayComponentInterface
{
public function __construct(
private string|int|float $value,
private ?string $label = null,
private ?ConsoleStyle $valueStyle = null,
private ?ConsoleStyle $labelStyle = null,
) {
}
public static function fromOptions(
string|int|float $value,
?string $label,
DisplayOptions $options,
ConsoleTheme $theme
): self {
$valueStyle = ConsoleStyle::create(color: $theme->numberColor, format: ConsoleFormat::BOLD);
$labelStyle = ConsoleStyle::create(color: $theme->metadataColor);
return new self(
value: $value,
label: $label,
valueStyle: $valueStyle,
labelStyle: $labelStyle
);
}
public function render(): string
{
$valueStyle = $this->valueStyle ?? ConsoleStyle::create(color: ConsoleColor::BRIGHT_BLUE, format: ConsoleFormat::BOLD);
$labelStyle = $this->labelStyle ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
$valueFormatted = is_float($this->value) ? number_format($this->value, 2) : (string)$this->value;
$output = $valueStyle->apply($valueFormatted);
if ($this->label !== null) {
$output .= ' ' . $labelStyle->apply($this->label);
}
return $output;
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::CONSOLE;
}
}

View File

@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Console;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\ValueObjects\ArrayStructure;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Rendert eine Tabelle in der Konsole.
*/
final class Table implements DisplayComponentInterface
{
private array $headers = [];
private array $rows = [];
private array $columnWidths = [];
private int $padding = 1;
public function __construct(
private ?ConsoleStyle $headerStyle = null,
private ?ConsoleStyle $rowStyle = null,
private ?ConsoleStyle $borderStyle = null,
private readonly bool $showBorders = true
) {
$this->headerStyle ??= ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE, format: ConsoleFormat::BOLD);
$this->rowStyle ??= ConsoleStyle::create();
$this->borderStyle ??= ConsoleStyle::create(color: ConsoleColor::GRAY);
}
/**
* Setzt die Spaltenüberschriften der Tabelle.
*/
public function setHeaders(array $headers): self
{
$this->headers = $headers;
$this->calculateColumnWidths();
return $this;
}
/**
* Fügt eine Zeile zur Tabelle hinzu.
*/
public function addRow(array $row): self
{
$this->rows[] = $row;
$this->calculateColumnWidths();
return $this;
}
/**
* Setzt alle Zeilen der Tabelle.
*/
public function setRows(array $rows): self
{
$this->rows = $rows;
$this->calculateColumnWidths();
return $this;
}
/**
* Setzt den Innenabstand der Zellen.
*/
public function setPadding(int $padding): self
{
$this->padding = max(0, $padding);
return $this;
}
/**
* Rendert die Tabelle und gibt den Text zurück.
*/
public function render(): string
{
if (empty($this->headers) && empty($this->rows)) {
return '';
}
$output = '';
if ($this->showBorders) {
$output .= $this->renderBorder('top') . "\n";
}
// Header
if (! empty($this->headers)) {
$output .= $this->renderRow($this->headers, $this->headerStyle) . "\n";
if ($this->showBorders) {
$output .= $this->renderBorder('middle') . "\n";
}
}
// Zeilen
foreach ($this->rows as $index => $row) {
$output .= $this->renderRow($row, $this->rowStyle) . "\n";
}
if ($this->showBorders) {
$output .= $this->renderBorder('bottom') . "\n";
}
return $output;
}
/**
* Rendert eine Zeile der Tabelle.
*/
private function renderRow(array $cells, ConsoleStyle $style): string
{
$output = '';
if ($this->showBorders) {
$output .= $this->borderStyle->apply('│');
}
foreach ($cells as $index => $cell) {
$width = $this->columnWidths[$index] ?? 10;
$cellText = (string)$cell;
// Linksbündige Ausrichtung mit exakter Breite
$paddedCell = str_pad($cellText, $width, ' ', STR_PAD_RIGHT);
// Padding hinzufügen
$padding = str_repeat(' ', $this->padding);
$finalCell = $padding . $paddedCell . $padding;
$output .= $style->apply($finalCell);
if ($this->showBorders) {
$output .= $this->borderStyle->apply('│');
}
}
return $output;
}
/**
* Rendert eine Trennlinie der Tabelle.
*/
private function renderBorder(string $type): string
{
$left = match($type) {
'top' => '┌',
'middle' => '├',
'row' => '├',
'bottom' => '└',
default => '│'
};
$right = match($type) {
'top' => '┐',
'middle' => '┤',
'row' => '┤',
'bottom' => '┘',
default => '│'
};
$horizontal = '─';
$junction = match($type) {
'top' => '┬',
'middle' => '┼',
'row' => '┼',
'bottom' => '┴',
default => '│'
};
$border = $left;
foreach ($this->columnWidths as $index => $width) {
$cellWidth = $width + ($this->padding * 2);
$border .= str_repeat($horizontal, $cellWidth);
if ($index < count($this->columnWidths) - 1) {
$border .= $junction;
}
}
$border .= $right;
return $this->borderStyle->apply($border);
}
/**
* Berechnet die Breite jeder Spalte basierend auf dem Inhalt.
*/
private function calculateColumnWidths(): void
{
$this->columnWidths = [];
// Header-Breiten
foreach ($this->headers as $index => $header) {
$this->columnWidths[$index] = mb_strlen((string)$header);
}
// Zeilen-Breiten
foreach ($this->rows as $row) {
foreach ($row as $index => $cell) {
$length = mb_strlen((string)$cell);
$this->columnWidths[$index] = max($this->columnWidths[$index] ?? 0, $length);
}
}
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::CONSOLE;
}
/**
* Build table from Display ArrayStructure
*/
public function fromArrayStructure(ArrayStructure $structure): self
{
$headers = $structure->isAssociative ? ['Key', 'Value'] : ['Index', 'Value'];
$this->setHeaders($headers);
$rows = [];
foreach ($structure->items as $key => $value) {
$valueStr = is_scalar($value) ? (string) $value : get_debug_type($value);
$rows[] = [(string) $key, $valueStr];
}
$this->setRows($rows);
return $this;
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Console;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Rendert eine Textbox in der Konsole.
*/
final readonly class TextBox implements DisplayComponentInterface
{
public function __construct(
private string $content,
private int $width = 80,
private int $padding = 1,
private ?ConsoleStyle $borderStyle = null,
private ?ConsoleStyle $contentStyle = null,
private ?string $title = null
) {
}
public function render(): string
{
$borderStyle = $this->borderStyle ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
$contentStyle = $this->contentStyle ?? ConsoleStyle::create();
$contentWidth = $this->width - ($this->padding * 2) - 2;
$lines = $this->wrapText($this->content, $contentWidth);
$output = '';
// Obere Linie
if ($this->title) {
$titleLength = mb_strlen($this->title);
$availableSpace = $this->width - $titleLength - 6; // Abzug für '[ ]' und Ränder
$leftPadding = max(0, (int)floor($availableSpace / 2));
$rightPadding = max(0, $availableSpace - $leftPadding);
$output .= $borderStyle->apply('┌' . str_repeat('─', $leftPadding) . '[ ' . $this->title . ' ]' . str_repeat('─', $rightPadding) . '┐') . "\n";
} else {
$output .= $borderStyle->apply('┌' . str_repeat('─', $this->width - 2) . '┐') . "\n";
}
// Padding oben
for ($i = 0; $i < $this->padding; $i++) {
$output .= $borderStyle->apply('│') . str_repeat(' ', $this->width - 2) . $borderStyle->apply('│') . "\n";
}
// Inhalt
foreach ($lines as $line) {
$lineLength = mb_strlen($line);
$spaces = $contentWidth - $lineLength;
$paddedLine = $line . str_repeat(' ', $spaces);
$output .= $borderStyle->apply('│') .
str_repeat(' ', $this->padding) .
$contentStyle->apply($paddedLine) .
str_repeat(' ', $this->padding) .
$borderStyle->apply('│') . "\n";
}
// Padding unten
for ($i = 0; $i < $this->padding; $i++) {
$output .= $borderStyle->apply('│') . str_repeat(' ', $this->width - 2) . $borderStyle->apply('│') . "\n";
}
// Untere Linie
$output .= $borderStyle->apply('└' . str_repeat('─', $this->width - 2) . '┘') . "\n";
return $output;
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::CONSOLE;
}
private function wrapText(string $text, int $width): array
{
$lines = explode("\n", $text);
$wrapped = [];
foreach ($lines as $line) {
if (mb_strlen($line) <= $width) {
$wrapped[] = $line;
} else {
$wrapped = array_merge($wrapped, $this->splitTextIntoLines($line, $width));
}
}
// Leere Zeile hinzufügen, falls keine Inhalte vorhanden
if (empty($wrapped)) {
$wrapped[] = '';
}
return $wrapped;
}
private function splitTextIntoLines(string $text, int $width): array
{
$lines = [];
$words = explode(' ', $text);
$currentLine = '';
foreach ($words as $word) {
$testLine = empty($currentLine) ? $word : $currentLine . ' ' . $word;
if (mb_strlen($testLine) <= $width) {
$currentLine = $testLine;
} else {
if (! empty($currentLine)) {
$lines[] = $currentLine;
$currentLine = $word;
} else {
// Wort ist länger als die Zeile - hart umbrechen
$lines[] = mb_substr($word, 0, $width);
$currentLine = mb_substr($word, $width);
}
}
}
if (! empty($currentLine)) {
$lines[] = $currentLine;
}
return $lines ?: [''];
}
}

View File

@@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Console;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\ValueObjects\ArrayStructure;
use App\Framework\Display\ValueObjects\ObjectStructure;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* TreeHelper zum Anzeigen hierarchischer Baumstrukturen in der Konsole.
* Ähnlich dem Symfony TreeHelper, aber angepasst an unser Styling-System.
*/
final class TreeHelper implements DisplayComponentInterface
{
private string $prefix = '';
private bool $isLastElement = true;
private ?ConsoleStyle $nodeStyle = null;
private ?ConsoleStyle $leafStyle = null;
private ?ConsoleStyle $lineStyle = null;
/**
* @var array<array{title: string, node: ?self, isLeaf: bool}>
*/
private array $nodes = [];
public function __construct(
private string $title = '',
#private readonly ConsoleOutput $output = new ConsoleOutput(),
) {
$this->nodeStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD);
$this->leafStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
$this->lineStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
}
/**
* Setzt den Stil für Knotentitel (Verzeichnisse/Kategorien).
*/
public function setNodeStyle(?ConsoleStyle $style): self
{
$this->nodeStyle = $style;
return $this;
}
/**
* Setzt den Stil für Blätter/Endpunkte.
*/
public function setLeafStyle(?ConsoleStyle $style): self
{
$this->leafStyle = $style;
return $this;
}
/**
* Setzt den Stil für Baumlinien.
*/
public function setLineStyle(?ConsoleStyle $style): self
{
$this->lineStyle = $style;
return $this;
}
/**
* Setzt den Haupttitel des Baums.
*/
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
/**
* Fügt einen Unterknoten (z.B. Unterverzeichnis) hinzu.
*/
public function addNode(string $title): self
{
$node = new self($title);
$node->nodeStyle = $this->nodeStyle;
$node->leafStyle = $this->leafStyle;
$node->lineStyle = $this->lineStyle;
$this->nodes[] = [
'title' => $title,
'node' => $node,
'isLeaf' => false,
];
return $node;
}
/**
* Fügt einen Endpunkt (z.B. Datei) hinzu.
*/
public function addLeaf(string $title): self
{
$this->nodes[] = [
'title' => $title,
'node' => null,
'isLeaf' => true,
];
return $this;
}
/**
* Zeigt die vollständige Baumstruktur an.
*/
public function display(ConsoleOutput $output): void
{
if (! empty($this->title)) {
$output->writeLine($this->title, $this->nodeStyle);
}
$this->displayTree($output);
}
/**
* Rendert die Baumstruktur und gibt den Text zurück.
*/
public function render(): string
{
$output = '';
if (! empty($this->title)) {
$output .= $this->nodeStyle->apply($this->title) . "\n";
}
$output .= $this->renderTree();
return $output;
}
/**
* Setzt den Präfix für die aktuelle Ebene.
* (Interne Methode für rekursives Rendern)
*/
private function setPrefix(string $prefix, bool $isLastElement): self
{
$this->prefix = $prefix;
$this->isLastElement = $isLastElement;
return $this;
}
/**
* Zeigt die Baumstruktur mit dem aktuellen Präfix an.
* (Interne Methode für rekursives Rendern)
*/
private function displayTree(ConsoleOutput $output): void
{
$count = count($this->nodes);
foreach ($this->nodes as $index => $item) {
$isLast = ($index === $count - 1);
$nodePrefix = $this->prefix . ($this->isLastElement ? ' ' : '│ ');
// Baumlinien und Verbindungen
$connector = $isLast ? '└── ' : '├── ';
$linePrefix = $this->prefix . $connector;
// Titel anzeigen
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
$title = $linePrefix . $item['title'];
$output->writeLine(
$this->lineStyle->apply($linePrefix) .
$style->apply($item['title'])
);
// Unterelemente rekursiv anzeigen
if (! $item['isLeaf'] && $item['node'] !== null) {
$item['node']
->setPrefix($nodePrefix, $isLast)
->displayTree($output);
}
}
}
/**
* Rendert die Baumstruktur mit dem aktuellen Präfix und gibt den Text zurück.
* (Interne Methode für rekursives Rendern)
*/
private function renderTree(): string
{
$output = '';
$count = count($this->nodes);
foreach ($this->nodes as $index => $item) {
$isLast = ($index === $count - 1);
$nodePrefix = $this->prefix . ($this->isLastElement ? ' ' : '│ ');
// Baumlinien und Verbindungen
$connector = $isLast ? '└── ' : '├── ';
$linePrefix = $this->prefix . $connector;
// Titel formatieren
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
$title = $item['title'];
$output .= $this->lineStyle->apply($linePrefix) .
$style->apply($title) . "\n";
// Unterelemente rekursiv rendern
if (! $item['isLeaf'] && $item['node'] !== null) {
$childOutput = $item['node']
->setPrefix($nodePrefix, $isLast)
->renderTree();
$output .= $childOutput;
}
}
return $output;
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::CONSOLE;
}
/**
* Build tree from Display ArrayStructure
*/
public function fromArrayStructure(ArrayStructure $structure, string $separator = ': '): self
{
foreach ($structure->items as $key => $value) {
if (isset($structure->nestedArrays[$key])) {
$nested = $structure->nestedArrays[$key];
if ($nested->isCircularReference) {
$this->addLeaf((string) $key . $separator . '[circular reference]');
} else {
$node = $this->addNode((string) $key);
$node->fromArrayStructure($nested, $separator);
}
} else {
$valueStr = is_scalar($value) ? (string) $value : get_debug_type($value);
$this->addLeaf((string) $key . $separator . $valueStr);
}
}
return $this;
}
/**
* Build tree from Display ObjectStructure
*/
public function fromObjectStructure(ObjectStructure $structure, string $separator = ': '): self
{
foreach ($structure->properties as $property) {
if (isset($structure->nestedObjects[$property->name])) {
$nested = $structure->nestedObjects[$property->name];
if ($nested->isCircularReference) {
$this->addLeaf($property->name . $separator . '[circular reference]');
} else {
$node = $this->addNode($property->name . $separator . $nested->className);
$node->fromObjectStructure($nested, $separator);
}
} else {
$valueStr = $property->isValueObject && is_object($property->value)
? (string) $property->value
: (is_scalar($property->value) ? (string) $property->value : get_debug_type($property->value));
$this->addLeaf($property->name . $separator . $valueStr);
}
}
return $this;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Interface for Display components that can render data structures
*/
interface DisplayComponentInterface
{
/**
* Render the component and return the output string
*/
public function render(): string;
/**
* Get the output format this component produces
*/
public function getOutputFormat(): OutputFormat;
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Html;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Badge/Label component for displaying small tags or labels in HTML
*/
final readonly class HtmlBadge implements DisplayComponentInterface
{
public function __construct(
private string $text,
private BadgeStyle $style = BadgeStyle::INFO,
private ?string $customClass = null,
) {
}
public static function fromOptions(string $text, DisplayOptions $options, HtmlTheme $theme, BadgeStyle $style = BadgeStyle::INFO): self
{
$baseClass = match ($style) {
BadgeStyle::INFO => $theme->infoClass,
BadgeStyle::SUCCESS => 'display-success',
BadgeStyle::WARNING => $theme->warningClass,
BadgeStyle::ERROR => $theme->errorClass,
BadgeStyle::DEFAULT => $theme->typeClass,
};
$customClass = $theme->themeClass . ' ' . $baseClass;
return new self(
text: $text,
style: $style,
customClass: $customClass
);
}
public function render(): string
{
$class = $this->customClass ?? 'display-badge';
$text = htmlspecialchars($this->text, ENT_QUOTES, 'UTF-8');
return '<span class="' . htmlspecialchars($class, ENT_QUOTES, 'UTF-8') . '">' . $text . '</span>';
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::HTML;
}
}
/**
* Badge style enum
*/
enum BadgeStyle: string
{
case INFO = 'info';
case SUCCESS = 'success';
case WARNING = 'warning';
case ERROR = 'error';
case DEFAULT = 'default';
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Html;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Card component for displaying information in card-like containers in HTML
*/
final readonly class HtmlCard implements DisplayComponentInterface
{
public function __construct(
private ?string $title = null,
private string $body = '',
private ?string $footer = null,
private ?string $cardClass = null,
private ?string $headerClass = null,
private ?string $bodyClass = null,
private ?string $footerClass = null,
) {
}
public static function fromOptions(
?string $title,
string $body,
?string $footer,
DisplayOptions $options,
HtmlTheme $theme
): self {
$cardClass = $theme->themeClass . ' display-card';
$headerClass = $theme->classNameClass;
$bodyClass = $theme->valueClass;
$footerClass = $theme->metadataClass;
return new self(
title: $title,
body: $body,
footer: $footer,
cardClass: $cardClass,
headerClass: $headerClass,
bodyClass: $bodyClass,
footerClass: $footerClass
);
}
public function render(): string
{
$cardClass = $this->cardClass ?? 'display-card';
$output = '<div class="' . htmlspecialchars($cardClass, ENT_QUOTES, 'UTF-8') . '">';
// Header
if ($this->title !== null) {
$headerClass = $this->headerClass ?? 'display-card-header';
$title = htmlspecialchars($this->title, ENT_QUOTES, 'UTF-8');
$output .= '<div class="' . htmlspecialchars($headerClass, ENT_QUOTES, 'UTF-8') . '">' . $title . '</div>';
}
// Body
if ($this->body !== '') {
$bodyClass = $this->bodyClass ?? 'display-card-body';
$body = nl2br(htmlspecialchars($this->body, ENT_QUOTES, 'UTF-8'));
$output .= '<div class="' . htmlspecialchars($bodyClass, ENT_QUOTES, 'UTF-8') . '">' . $body . '</div>';
}
// Footer
if ($this->footer !== null) {
$footerClass = $this->footerClass ?? 'display-card-footer';
$footer = htmlspecialchars($this->footer, ENT_QUOTES, 'UTF-8');
$output .= '<div class="' . htmlspecialchars($footerClass, ENT_QUOTES, 'UTF-8') . '">' . $footer . '</div>';
}
$output .= '</div>';
return $output;
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::HTML;
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Html;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
use App\Framework\SyntaxHighlighter\SyntaxHighlighter;
/**
* CodeBlock component for displaying code snippets with syntax highlighting in HTML
*/
final readonly class HtmlCodeBlock implements DisplayComponentInterface
{
public function __construct(
private string $code,
private ?string $language = null,
private bool $lineNumbers = false,
private array $highlightLines = [],
private ?string $codeClass = null,
private ?SyntaxHighlighter $highlighter = null,
) {
}
public static function fromOptions(
string $code,
DisplayOptions $options,
HtmlTheme $theme,
?string $language = null,
bool $lineNumbers = false,
array $highlightLines = []
): self {
$highlighter = new SyntaxHighlighter();
$codeClass = $theme->themeClass . ' display-code-block';
return new self(
code: $code,
language: $language ?? self::detectLanguage($code),
lineNumbers: $lineNumbers,
highlightLines: $highlightLines,
codeClass: $codeClass,
highlighter: $highlighter
);
}
private static function detectLanguage(string $code): ?string
{
// Simple detection based on code patterns
$trimmed = trim($code);
// JSON
if (($trimmed[0] === '{' || $trimmed[0] === '[') && json_decode($code) !== null) {
return 'json';
}
// XML
if (preg_match('/^<\?xml\s/i', $trimmed) || preg_match('/^<[a-z]+/i', $trimmed)) {
return 'xml';
}
// YAML (basic detection)
if (preg_match('/^[a-z_][a-z0-9_]*:\s/mi', $trimmed)) {
return 'yaml';
}
// PHP (default for SyntaxHighlighter)
return 'php';
}
public function render(): string
{
$language = $this->language ?? 'php';
$codeClass = $this->codeClass ?? 'display-code-block';
$languageAttr = $language !== null ? ' data-language="' . htmlspecialchars($language, ENT_QUOTES, 'UTF-8') . '"' : '';
// Use SyntaxHighlighter for PHP
if ($language === 'php') {
$options = [
'lineNumbers' => $this->lineNumbers,
'highlightLines' => $this->highlightLines,
'includeCss' => false, // CSS should be included separately
];
$highlightedCode = $this->highlighter?->highlight($this->code, 'html', $options) ?? htmlspecialchars($this->code, ENT_QUOTES, 'UTF-8');
return '<pre class="' . htmlspecialchars($codeClass, ENT_QUOTES, 'UTF-8') . '"' . $languageAttr . '><code>' . $highlightedCode . '</code></pre>';
}
// For other languages, use simple highlighting
$highlightedCode = $this->highlightSimple($this->code, $language);
return '<pre class="' . htmlspecialchars($codeClass, ENT_QUOTES, 'UTF-8') . '"' . $languageAttr . '><code>' . $highlightedCode . '</code></pre>';
}
private function highlightSimple(string $code, string $language): string
{
$lines = explode("\n", htmlspecialchars($code, ENT_QUOTES, 'UTF-8'));
$output = '';
foreach ($lines as $lineNum => $line) {
$lineNumDisplay = $lineNum + 1;
$isHighlighted = in_array($lineNumDisplay, $this->highlightLines, true);
$highlightClass = $isHighlighted ? ' class="highlight-line"' : '';
if ($this->lineNumbers) {
$output .= '<span class="line-number">' . $lineNumDisplay . '</span>';
}
$highlightedLine = match ($language) {
'json' => $this->highlightJson($line),
'xml' => $this->highlightXml($line),
'yaml' => $this->highlightYaml($line),
default => $line,
};
$output .= '<span' . $highlightClass . '>' . $highlightedLine . '</span>';
if ($lineNum < count($lines) - 1) {
$output .= "\n";
}
}
return $output;
}
private function highlightJson(string $line): string
{
// Simple JSON highlighting with HTML
$line = preg_replace('/("(?:[^"\\\\]|\\\\.)*")\s*:/', '<span class="json-key">$1</span>:', $line);
$line = preg_replace('/:\s*("(?:[^"\\\\]|\\\\.)*")/', ': <span class="json-string">$1</span>', $line);
$line = preg_replace('/:\s*(\d+)/', ': <span class="json-number">$1</span>', $line);
$line = preg_replace('/:\s*(true|false|null)/', ': <span class="json-boolean">$1</span>', $line);
return $line;
}
private function highlightXml(string $line): string
{
// Simple XML highlighting with HTML
$line = preg_replace('/<(\/?)([a-z][a-z0-9]*)/i', '<span class="xml-tag">&lt;$1$2</span>', $line);
$line = preg_replace('/([a-z]+)="([^"]*)"/i', '<span class="xml-attr">$1</span>="<span class="xml-value">$2</span>"', $line);
return $line;
}
private function highlightYaml(string $line): string
{
// Simple YAML highlighting with HTML
$line = preg_replace('/^([a-z_][a-z0-9_]*):\s*/mi', '<span class="yaml-key">$1</span>: ', $line);
$line = preg_replace('/:\s*("(?:[^"\\\\]|\\\\.)*")/', ': <span class="yaml-string">$1</span>', $line);
$line = preg_replace('/:\s*(\d+)/', ': <span class="yaml-number">$1</span>', $line);
return $line;
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::HTML;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Html;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Divider/Separator component for visual separation in HTML
*/
final readonly class HtmlDivider implements DisplayComponentInterface
{
public function __construct(
private ?string $label = null,
private ?string $class = null,
) {
}
public static function fromOptions(DisplayOptions $options, HtmlTheme $theme, ?string $label = null): self
{
$class = $theme->themeClass . ' ' . $theme->separatorClass;
return new self(
label: $label,
class: $class
);
}
public function render(): string
{
$class = $this->class ?? 'display-divider';
if ($this->label !== null) {
$label = htmlspecialchars($this->label, ENT_QUOTES, 'UTF-8');
return '<hr class="' . htmlspecialchars($class, ENT_QUOTES, 'UTF-8') . '" data-label="' . $label . '">';
}
return '<hr class="' . htmlspecialchars($class, ENT_QUOTES, 'UTF-8') . '">';
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::HTML;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Html;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* List component for displaying ordered/unordered lists in HTML
*/
final readonly class HtmlListItem implements DisplayComponentInterface
{
/**
* @param array<string|int, mixed> $items List items
* @param bool $ordered Whether to use ordered (numbered) list
* @param string|null $listClass CSS class for the list element
* @param string|null $itemClass CSS class for list items
*/
public function __construct(
private array $items,
private bool $ordered = false,
private ?string $listClass = null,
private ?string $itemClass = null,
) {
}
public static function fromOptions(array $items, DisplayOptions $options, HtmlTheme $theme, bool $ordered = false): self
{
$listClass = $theme->themeClass . ' ' . ($options->theme ?? '');
$itemClass = $theme->valueClass;
return new self(
items: $items,
ordered: $ordered,
listClass: $listClass,
itemClass: $itemClass
);
}
public function render(): string
{
if (empty($this->items)) {
return '';
}
$tag = $this->ordered ? 'ol' : 'ul';
$listClassAttr = $this->listClass !== null ? ' class="' . htmlspecialchars($this->listClass, ENT_QUOTES, 'UTF-8') . '"' : '';
$itemClassAttr = $this->itemClass !== null ? ' class="' . htmlspecialchars($this->itemClass, ENT_QUOTES, 'UTF-8') . '"' : '';
$output = '<' . $tag . $listClassAttr . '>';
foreach ($this->items as $item) {
$itemText = is_scalar($item) ? htmlspecialchars((string)$item, ENT_QUOTES, 'UTF-8') : htmlspecialchars(get_debug_type($item), ENT_QUOTES, 'UTF-8');
$output .= '<li' . $itemClassAttr . '>' . $itemText . '</li>';
}
$output .= '</' . $tag . '>';
return $output;
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::HTML;
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Html;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Stats/Metric component for displaying numerical statistics in HTML
*/
final readonly class HtmlStats implements DisplayComponentInterface
{
public function __construct(
private string|int|float $value,
private ?string $label = null,
private ?string $statsClass = null,
private ?string $valueClass = null,
private ?string $labelClass = null,
) {
}
public static function fromOptions(
string|int|float $value,
?string $label,
DisplayOptions $options,
HtmlTheme $theme
): self {
$statsClass = $theme->themeClass . ' display-stats';
$valueClass = $theme->numberClass . ' display-stats-value';
$labelClass = $theme->metadataClass . ' display-stats-label';
return new self(
value: $value,
label: $label,
statsClass: $statsClass,
valueClass: $valueClass,
labelClass: $labelClass
);
}
public function render(): string
{
$statsClass = $this->statsClass ?? 'display-stats';
$valueClass = $this->valueClass ?? 'display-stats-value';
$labelClass = $this->labelClass ?? 'display-stats-label';
$valueFormatted = is_float($this->value) ? number_format($this->value, 2) : htmlspecialchars((string)$this->value, ENT_QUOTES, 'UTF-8');
$output = '<div class="' . htmlspecialchars($statsClass, ENT_QUOTES, 'UTF-8') . '">';
$output .= '<span class="' . htmlspecialchars($valueClass, ENT_QUOTES, 'UTF-8') . '">' . $valueFormatted . '</span>';
if ($this->label !== null) {
$label = htmlspecialchars($this->label, ENT_QUOTES, 'UTF-8');
$output .= '<span class="' . htmlspecialchars($labelClass, ENT_QUOTES, 'UTF-8') . '">' . $label . '</span>';
}
$output .= '</div>';
return $output;
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::HTML;
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Html;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\ArrayStructure;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* HTML equivalent of Console Table component
*/
final class HtmlTable implements DisplayComponentInterface
{
private array $headers = [];
private array $rows = [];
public function __construct(
private HtmlTheme $theme,
private DisplayOptions $options,
private int $padding = 1,
private bool $showBorders = true
) {
}
/**
* Set table headers
*/
public function setHeaders(array $headers): self
{
$this->headers = $headers;
return $this;
}
/**
* Add a row to the table
*/
public function addRow(array $row): self
{
$this->rows[] = $row;
return $this;
}
/**
* Set all rows at once
*/
public function setRows(array $rows): self
{
$this->rows = $rows;
return $this;
}
/**
* Set padding
*/
public function setPadding(int $padding): self
{
$this->padding = max(0, $padding);
return $this;
}
public function render(): string
{
if (empty($this->headers) && empty($this->rows)) {
return '';
}
$output = [];
$borderClass = $this->showBorders ? ' display-table-bordered' : '';
$output[] = '<table class="display-table ' . $this->theme->themeClass . $borderClass . '">';
if (! empty($this->headers)) {
$output[] = '<thead>';
$output[] = '<tr>';
foreach ($this->headers as $header) {
$output[] = '<th class="' . $this->theme->classNameClass . '" style="padding: ' . $this->padding . 'em;">' . htmlspecialchars((string) $header) . '</th>';
}
$output[] = '</tr>';
$output[] = '</thead>';
}
$output[] = '<tbody>';
foreach ($this->rows as $row) {
$output[] = '<tr>';
foreach ($row as $cell) {
$output[] = '<td class="' . $this->theme->valueClass . '" style="padding: ' . $this->padding . 'em;">' . htmlspecialchars((string) $cell) . '</td>';
}
$output[] = '</tr>';
}
$output[] = '</tbody>';
$output[] = '</table>';
return implode("\n", $output);
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::HTML;
}
/**
* Build table from Display ArrayStructure
*/
public function fromArrayStructure(ArrayStructure $structure): self
{
$headers = $structure->isAssociative ? ['Key', 'Value'] : ['Index', 'Value'];
$this->setHeaders($headers);
foreach ($structure->items as $key => $value) {
$valueStr = is_scalar($value) ? (string) $value : get_debug_type($value);
$this->addRow([(string) $key, $valueStr]);
}
return $this;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Html;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* HTML equivalent of Console TextBox component
*/
final readonly class HtmlTextBox implements DisplayComponentInterface
{
public function __construct(
private HtmlTheme $theme,
private DisplayOptions $options,
private string $content,
private ?string $title = null,
private int $width = 80,
private int $padding = 1
) {
}
public function render(): string
{
$output = [];
$output[] = '<div class="' . $this->theme->themeClass . ' display-textbox" style="max-width: ' . $this->width . 'ch; padding: ' . $this->padding . 'em;">';
if ($this->title !== null) {
$output[] = '<div class="display-textbox-title ' . $this->theme->classNameClass . '">' . htmlspecialchars($this->title) . '</div>';
}
$output[] = '<div class="display-textbox-content ' . $this->theme->valueClass . '">';
$output[] = nl2br(htmlspecialchars($this->content));
$output[] = '</div>';
$output[] = '</div>';
return implode("\n", $output);
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::HTML;
}
}

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Components\Html;
use App\Framework\Display\Components\DisplayComponentInterface;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjects\ArrayStructure;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\ObjectStructure;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* HTML equivalent of Console TreeHelper component
*/
final class HtmlTreeHelper implements DisplayComponentInterface
{
private string $title;
/** @var array<array{title: string, node: ?self, isLeaf: bool}> */
private array $nodes = [];
public function __construct(
private HtmlTheme $theme,
private DisplayOptions $options,
string $title = ''
) {
$this->title = $title;
}
/**
* Add a child node (e.g., subdirectory)
*/
public function addNode(string $title): self
{
$node = new self($this->theme, $this->options, $title);
$this->nodes[] = [
'title' => $title,
'node' => $node,
'isLeaf' => false,
];
return $node;
}
/**
* Add a leaf node (e.g., file)
*/
public function addLeaf(string $title): self
{
$this->nodes[] = [
'title' => $title,
'node' => null,
'isLeaf' => true,
];
return $this;
}
public function render(): string
{
$output = [];
if ($this->title !== '') {
$output[] = '<div class="' . $this->theme->classNameClass . '">' . htmlspecialchars($this->title) . '</div>';
}
$output[] = '<ul class="display-tree ' . $this->theme->themeClass . '">';
$output[] = $this->renderTreeInternal('');
$output[] = '</ul>';
return implode("\n", $output);
}
private function renderTreeInternal(string $prefix): string
{
$output = [];
$count = count($this->nodes);
foreach ($this->nodes as $index => $item) {
$isLast = ($index === $count - 1);
$itemClass = $item['isLeaf'] ? $this->theme->valueClass : $this->theme->keyClass;
$output[] = '<li class="display-tree-item' . ($isLast ? ' display-tree-item-last' : '') . '">';
$output[] = '<span class="' . $itemClass . '">' . htmlspecialchars($item['title']) . '</span>';
if (! $item['isLeaf'] && $item['node'] !== null) {
$output[] = '<ul class="display-tree-nested">';
$output[] = $item['node']->renderTreeInternal($prefix . ($isLast ? ' ' : '│ '));
$output[] = '</ul>';
}
$output[] = '</li>';
}
return implode("\n", $output);
}
public function getOutputFormat(): OutputFormat
{
return OutputFormat::HTML;
}
/**
* Build tree from Display ArrayStructure
*/
public function fromArrayStructure(ArrayStructure $structure, string $separator = ': '): self
{
foreach ($structure->items as $key => $value) {
if (isset($structure->nestedArrays[$key])) {
$nested = $structure->nestedArrays[$key];
if ($nested->isCircularReference) {
$this->addLeaf(htmlspecialchars((string) $key . $separator . '[circular reference]'));
} else {
$node = $this->addNode(htmlspecialchars((string) $key));
$node->fromArrayStructure($nested, $separator);
}
} else {
$valueStr = is_scalar($value) ? (string) $value : get_debug_type($value);
$this->addLeaf(htmlspecialchars((string) $key . $separator . $valueStr));
}
}
return $this;
}
/**
* Build tree from Display ObjectStructure
*/
public function fromObjectStructure(ObjectStructure $structure, string $separator = ': '): self
{
foreach ($structure->properties as $property) {
if (isset($structure->nestedObjects[$property->name])) {
$nested = $structure->nestedObjects[$property->name];
if ($nested->isCircularReference) {
$this->addLeaf(htmlspecialchars($property->name . $separator . '[circular reference]'));
} else {
$node = $this->addNode(htmlspecialchars($property->name . $separator . $nested->className));
$node->fromObjectStructure($nested, $separator);
}
} else {
$valueStr = $property->isValueObject && is_object($property->value)
? (string) $property->value
: (is_scalar($property->value) ? (string) $property->value : get_debug_type($property->value));
$this->addLeaf(htmlspecialchars($property->name . $separator . $valueStr));
}
}
return $this;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Display\Inspectors\ArrayInspector;
use App\Framework\Display\Renderers\ConsoleRenderer;
use App\Framework\Display\Renderers\HtmlRenderer;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* High-level formatter for arrays combining inspector and renderers
*/
final readonly class ArrayFormatter
{
public function __construct(
private ArrayInspector $inspector = new ArrayInspector(),
private ConsoleRenderer $consoleRenderer = new ConsoleRenderer(),
private HtmlRenderer $htmlRenderer = new HtmlRenderer(),
) {
}
public function format(array $data, DisplayOptions $options, OutputFormat $format): string
{
$structure = $this->inspector->inspect($data, $options);
return match ($format) {
OutputFormat::CONSOLE => $this->consoleRenderer->renderArray($structure, $options),
OutputFormat::HTML => $this->htmlRenderer->renderArray($structure, $options),
};
}
public function formatForConsole(array $data, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($data, $options, OutputFormat::CONSOLE);
}
public function formatForHtml(array $data, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($data, $options, OutputFormat::HTML);
}
}

View File

@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Display\Formatters\CsvFormatter;
use App\Framework\Display\Formatters\JsonFormatter;
use App\Framework\Display\Formatters\XmlFormatter;
use App\Framework\Display\Formatters\YamlFormatter;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* Auto-detecting formatter that automatically selects the appropriate formatter
* based on the input data type.
*
* This is the main entry point for the Display module. It automatically detects
* the type of input data and routes it to the appropriate formatter:
* - Arrays → ArrayFormatter
* - Objects → ObjectFormatter
* - JSON strings → JsonFormatter
* - YAML strings → YamlFormatter
* - XML strings → XmlFormatter
* - CSV strings → CsvFormatter
* - Class names → ClassFormatter
* - Directory paths → FilesystemFormatter
* - Scalars → Scalar formatting
*
* @example
* $formatter = new AutoFormatter();
* echo $formatter->formatForConsole($data);
*/
final readonly class AutoFormatter
{
public function __construct(
private ArrayFormatter $arrayFormatter = new ArrayFormatter(),
private ObjectFormatter $objectFormatter = new ObjectFormatter(),
private ClassFormatter $classFormatter = new ClassFormatter(),
private FilesystemFormatter $filesystemFormatter = new FilesystemFormatter(),
private JsonFormatter $jsonFormatter = new JsonFormatter(),
private YamlFormatter $yamlFormatter = new YamlFormatter(),
private XmlFormatter $xmlFormatter = new XmlFormatter(),
private CsvFormatter $csvFormatter = new CsvFormatter(),
private CodeFormatter $codeFormatter = new CodeFormatter(),
) {
}
/**
* Format data with specified options and output format.
*
* @param mixed $value The data to format (array, object, string, etc.)
* @param DisplayOptions $options Display configuration options
* @param OutputFormat $format Output format (CONSOLE or HTML)
* @return string Formatted output string
*/
public function format(mixed $value, DisplayOptions $options, OutputFormat $format): string
{
return match (true) {
is_array($value) => $this->formatArray($value, $options, $format),
is_object($value) => $this->formatObject($value, $options, $format),
is_string($value) && $this->isJsonString($value) => $this->formatJsonString($value, $options, $format),
is_string($value) && $this->isYamlString($value) => $this->formatYamlString($value, $options, $format),
is_string($value) && $this->isXmlString($value) => $this->formatXmlString($value, $options, $format),
is_string($value) && $this->isCsvString($value) => $this->formatCsvString($value, $options, $format),
is_string($value) && CodeFormatter::isPhpCode($value) => $this->formatCode($value, $options, $format),
is_string($value) && class_exists($value) => $this->formatClass($value, $options, $format),
is_string($value) && (is_dir($value) || ($value instanceof FilePath && is_dir($value->toString()))) => $this->formatDirectory($value, $options, $format),
$value instanceof FilePath => $this->formatDirectory($value, $options, $format),
default => $this->formatScalar($value, $options, $format),
};
}
/**
* Format data for console output with default options.
*
* @param mixed $value The data to format
* @param DisplayOptions|null $options Optional display options (defaults to DisplayOptions::default())
* @return string Formatted console output with ANSI colors
*/
public function formatForConsole(mixed $value, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($value, $options, OutputFormat::CONSOLE);
}
/**
* Format data for HTML output with default options.
*
* @param mixed $value The data to format
* @param DisplayOptions|null $options Optional display options (defaults to DisplayOptions::default())
* @return string Formatted HTML output with semantic HTML/CSS classes
*/
public function formatForHtml(mixed $value, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($value, $options, OutputFormat::HTML);
}
private function formatArray(array $value, DisplayOptions $options, OutputFormat $format): string
{
// Check if array contains objects - could use ObjectFormatter for items
$hasObjects = false;
foreach ($value as $item) {
if (is_object($item)) {
$hasObjects = true;
break;
}
}
return $this->arrayFormatter->format($value, $options, $format);
}
private function formatObject(object $value, DisplayOptions $options, OutputFormat $format): string
{
return $this->objectFormatter->format($value, $options, $format);
}
private function formatJsonString(string $value, DisplayOptions $options, OutputFormat $format): string
{
return $this->jsonFormatter->format($value, $options, $format);
}
private function formatYamlString(string $value, DisplayOptions $options, OutputFormat $format): string
{
return $this->yamlFormatter->format($value, $options, $format);
}
private function formatXmlString(string $value, DisplayOptions $options, OutputFormat $format): string
{
return $this->xmlFormatter->format($value, $options, $format);
}
private function formatCsvString(string $value, DisplayOptions $options, OutputFormat $format): string
{
return $this->csvFormatter->format($value, $options, $format);
}
private function formatCode(string $value, DisplayOptions $options, OutputFormat $format): string
{
return $this->codeFormatter->format($value, $options, $format);
}
private function formatClass(string $className, DisplayOptions $options, OutputFormat $format): string
{
return $this->classFormatter->format($className, $options, $format);
}
private function formatDirectory(string|FilePath $path, DisplayOptions $options, OutputFormat $format): string
{
return $this->filesystemFormatter->format($path, $options, $format);
}
private function formatScalar(mixed $value, DisplayOptions $options, OutputFormat $format): string
{
$formatted = match (true) {
is_string($value) => $format === OutputFormat::HTML
? '<span class="display-string">' . htmlspecialchars($value) . '</span>'
: "\033[32m\"{$value}\"\033[0m",
is_int($value) => $format === OutputFormat::HTML
? '<span class="display-number">' . $value . '</span>'
: "\033[94m{$value}\033[0m",
is_float($value) => $format === OutputFormat::HTML
? '<span class="display-number">' . $value . '</span>'
: "\033[94m{$value}\033[0m",
is_bool($value) => $format === OutputFormat::HTML
? '<span class="display-boolean">' . ($value ? 'true' : 'false') . '</span>'
: "\033[95m" . ($value ? 'true' : 'false') . "\033[0m",
is_null($value) => $format === OutputFormat::HTML
? '<span class="display-null">null</span>'
: "\033[90mnull\033[0m",
default => $format === OutputFormat::HTML
? '<span class="display-type">' . htmlspecialchars(get_debug_type($value)) . '</span>'
: get_debug_type($value),
};
return $formatted;
}
private function isJsonString(string $value): bool
{
if (empty($value)) {
return false;
}
$trimmed = trim($value);
if (! ($trimmed[0] === '{' || $trimmed[0] === '[')) {
return false;
}
return json_validate($value);
}
private function isYamlString(string $value): bool
{
if (empty($value)) {
return false;
}
$trimmed = trim($value);
// Basic YAML detection: starts with key: or has --- or ...
// More sophisticated detection would require a YAML parser
return preg_match('/^(\s*[a-zA-Z_][a-zA-Z0-9_]*\s*:|\s*---|\s*\.\.\.)/', $trimmed) === 1;
}
private function isXmlString(string $value): bool
{
if (empty($value)) {
return false;
}
$trimmed = trim($value);
// Basic XML detection: starts with < or <?xml
return str_starts_with($trimmed, '<') || str_starts_with($trimmed, '<?xml');
}
private function isCsvString(string $value): bool
{
if (empty($value)) {
return false;
}
$lines = explode("\n", $value);
if (count($lines) < 2) {
return false;
}
// Check if the first line contains common CSV separators
$firstLine = $lines[0];
$separators = [',', ';', "\t"];
foreach ($separators as $separator) {
$parts = explode($separator, $firstLine);
if (count($parts) >= 2) {
// Check if second line has same number of parts
if (isset($lines[1])) {
$secondParts = explode($separator, $lines[1]);
if (count($secondParts) >= 2 && abs(count($parts) - count($secondParts)) <= 1) {
return true;
}
}
}
}
return false;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Display\Inspectors\ClassInspector;
use App\Framework\Display\Renderers\ConsoleRenderer;
use App\Framework\Display\Renderers\HtmlRenderer;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* High-level formatter for classes combining inspector and renderers
*/
final readonly class ClassFormatter
{
public function __construct(
private ClassInspector $inspector = new ClassInspector(),
private ConsoleRenderer $consoleRenderer = new ConsoleRenderer(),
private HtmlRenderer $htmlRenderer = new HtmlRenderer(),
) {
}
public function format(string|object $class, DisplayOptions $options, OutputFormat $format): string
{
$structure = $this->inspector->inspect($class, $options);
return match ($format) {
OutputFormat::CONSOLE => $this->consoleRenderer->renderClass($structure, $options),
OutputFormat::HTML => $this->htmlRenderer->renderClass($structure, $options),
};
}
public function formatForConsole(string|object $class, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($class, $options, OutputFormat::CONSOLE);
}
public function formatForHtml(string|object $class, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($class, $options, OutputFormat::HTML);
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
use App\Framework\SyntaxHighlighter\SyntaxHighlighter;
/**
* Formatter for PHP code strings with syntax highlighting and tooltips
*/
final readonly class CodeFormatter
{
public function __construct(
private SyntaxHighlighter $highlighter = new SyntaxHighlighter()
) {
}
/**
* Format PHP code with syntax highlighting
*/
public function format(string $code, DisplayOptions $options, OutputFormat $format): string
{
if ($format === OutputFormat::CONSOLE) {
// For console, use console formatter
$formatter = $this->highlighter->getFormatter('console');
return $formatter->format(
$this->highlighter->tokenize($code),
['includeCss' => false]
);
}
// For HTML, use HTML formatter with tooltips enabled
$formatter = $this->highlighter->getFormatter('html');
$formatOptions = [
'includeCss' => true,
'lineNumbers' => $options->showLineNumbers ?? true,
'enableTooltips' => true, // Enable tooltips with metadata
];
return $formatter->format(
$this->highlighter->tokenize($code),
$formatOptions
);
}
/**
* Check if a string looks like PHP code
*/
public static function isPhpCode(string $value): bool
{
$trimmed = trim($value);
// Check for PHP tags
if (str_starts_with($trimmed, '<?php') || str_starts_with($trimmed, '<?=')) {
return true;
}
// Check for common PHP keywords and patterns
$phpPatterns = [
'/\b(public|private|protected|static|function|class|interface|trait|enum)\b/',
'/\$[a-zA-Z_][a-zA-Z0-9_]*/',
'/->[a-zA-Z_][a-zA-Z0-9_]*/',
'/::[a-zA-Z_][a-zA-Z0-9_]*/',
'/\b(new|return|if|else|foreach|for|while|switch|case|break|continue)\b/',
];
$matches = 0;
foreach ($phpPatterns as $pattern) {
if (preg_match($pattern, $trimmed)) {
$matches++;
}
}
// If we have at least 2 PHP patterns, it's likely PHP code
return $matches >= 2;
}
}

View File

@@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Formatter for CSV strings with table formatting
*/
final readonly class CsvFormatter
{
public function format(string $csv, DisplayOptions $options, OutputFormat $format): string
{
// Parse CSV
$data = $this->parseCsv($csv);
if ($data === null) {
// Invalid CSV - return as plain string with error indication
return $format === OutputFormat::HTML
? '<span class="display-error">Invalid CSV format</span>'
: ConsoleStyle::create(color: ConsoleColor::BRIGHT_RED)->apply('Invalid CSV format');
}
return match ($format) {
OutputFormat::CONSOLE => $this->formatConsole($data, $options),
OutputFormat::HTML => $this->formatHtml($data, $options),
};
}
public function formatForConsole(string $csv, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($csv, $options, OutputFormat::CONSOLE);
}
public function formatForHtml(string $csv, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($csv, $options, OutputFormat::HTML);
}
/**
* Parse CSV string into array of rows
*
* @return array<array<string>>|null
*/
private function parseCsv(string $csv): ?array
{
if (empty(trim($csv))) {
return null;
}
$lines = explode("\n", $csv);
$rows = [];
$separator = $this->detectSeparator($lines[0] ?? '');
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
$row = str_getcsv($line, $separator);
if (! empty($row)) {
$rows[] = $row;
}
}
return empty($rows) ? null : $rows;
}
/**
* Detect CSV separator
*/
private function detectSeparator(string $firstLine): string
{
$separators = [',', ';', "\t"];
$separatorCounts = [];
foreach ($separators as $sep) {
$separatorCounts[$sep] = substr_count($firstLine, $sep);
}
// Return separator with highest count
$maxCount = max($separatorCounts);
if ($maxCount > 0) {
foreach ($separatorCounts as $sep => $count) {
if ($count === $maxCount) {
return $sep;
}
}
}
// Default to comma
return ',';
}
private function formatConsole(array $data, DisplayOptions $options): string
{
if (empty($data)) {
return $this->style('(empty CSV)', ConsoleColor::GRAY);
}
// Calculate column widths
$columnWidths = $this->calculateColumnWidths($data);
$output = [];
$borderStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
$headerStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_CYAN, format: \App\Framework\Console\ConsoleFormat::BOLD);
$cellStyle = ConsoleStyle::create(color: ConsoleColor::GREEN);
// Top border
$topBorder = '┌' . implode('┬', array_map(fn ($w) => str_repeat('─', $w + 2), $columnWidths)) . '┐';
$output[] = $borderStyle->apply($topBorder);
// First row as header (if multiple rows)
$isHeader = count($data) > 1;
$rowIndex = 0;
foreach ($data as $row) {
$cells = [];
foreach ($row as $colIndex => $cell) {
$width = $columnWidths[$colIndex] ?? 10;
$padded = str_pad((string) $cell, $width, ' ', STR_PAD_RIGHT);
$cells[] = $padded;
}
$rowText = '│ ' . implode(' │ ', $cells) . ' │';
if ($isHeader && $rowIndex === 0) {
$output[] = $headerStyle->apply($rowText);
} else {
$output[] = $cellStyle->apply($rowText);
}
// Separator between header and body
if ($isHeader && $rowIndex === 0) {
$separator = '├' . implode('┼', array_map(fn ($w) => str_repeat('─', $w + 2), $columnWidths)) . '┤';
$output[] = $borderStyle->apply($separator);
}
$rowIndex++;
}
// Bottom border
$bottomBorder = '└' . implode('┴', array_map(fn ($w) => str_repeat('─', $w + 2), $columnWidths)) . '┘';
$output[] = $borderStyle->apply($bottomBorder);
return implode("\n", $output);
}
private function formatHtml(array $data, DisplayOptions $options): string
{
if (empty($data)) {
return '<div class="display-empty">(empty CSV)</div>';
}
$output = [];
$output[] = '<table class="display-csv-table">';
// First row as header (if multiple rows)
$isHeader = count($data) > 1;
$rowIndex = 0;
foreach ($data as $row) {
$tag = ($isHeader && $rowIndex === 0) ? 'thead' : ($rowIndex === ($isHeader ? 1 : 0) ? '<tbody>' : '');
if ($tag === '<tbody>') {
$output[] = '<tbody>';
} elseif ($tag === 'thead') {
$output[] = '<thead>';
}
$output[] = '<tr>';
foreach ($row as $cell) {
$tag = ($isHeader && $rowIndex === 0) ? 'th' : 'td';
$output[] = '<' . $tag . ' class="display-csv-cell">' . htmlspecialchars((string) $cell) . '</' . $tag . '>';
}
$output[] = '</tr>';
if ($isHeader && $rowIndex === 0) {
$output[] = '</thead>';
}
$rowIndex++;
}
if ($isHeader || count($data) > 0) {
$output[] = '</tbody>';
}
$output[] = '</table>';
return implode("\n", $output);
}
/**
* Calculate column widths for console table
*
* @param array<array<string>> $data
* @return array<int, int>
*/
private function calculateColumnWidths(array $data): array
{
$widths = [];
foreach ($data as $row) {
foreach ($row as $colIndex => $cell) {
$cellLength = mb_strlen((string) $cell);
$widths[$colIndex] = max($widths[$colIndex] ?? 0, $cellLength);
}
}
// Limit max width for readability
foreach ($widths as $colIndex => $width) {
$widths[$colIndex] = min($width, 50);
}
return $widths;
}
private function style(string $text, ConsoleColor $color): string
{
return ConsoleStyle::create(color: $color)->apply($text);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Display\Inspectors\FilesystemInspector;
use App\Framework\Display\Renderers\ConsoleRenderer;
use App\Framework\Display\Renderers\HtmlRenderer;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* High-level formatter for filesystem directories combining inspector and renderers
*/
final readonly class FilesystemFormatter
{
public function __construct(
private FilesystemInspector $inspector = new FilesystemInspector(),
private ConsoleRenderer $consoleRenderer = new ConsoleRenderer(),
private HtmlRenderer $htmlRenderer = new HtmlRenderer(),
) {
}
public function format(string|FilePath $path, DisplayOptions $options, OutputFormat $format): string
{
$structure = $this->inspector->inspect($path, $options);
return match ($format) {
OutputFormat::CONSOLE => $this->consoleRenderer->renderDirectory($structure, $options),
OutputFormat::HTML => $this->htmlRenderer->renderDirectory($structure, $options),
};
}
public function formatForConsole(string|FilePath $path, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($path, $options, OutputFormat::CONSOLE);
}
public function formatForHtml(string|FilePath $path, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($path, $options, OutputFormat::HTML);
}
}

View File

@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Formatter for JSON strings with syntax highlighting
*/
final readonly class JsonFormatter
{
public function format(string $json, DisplayOptions $options, OutputFormat $format): string
{
// Validate and decode JSON
$decoded = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
// Invalid JSON - return as plain string with error indication
return $format === OutputFormat::HTML
? '<span class="display-error">Invalid JSON: ' . htmlspecialchars(json_last_error_msg()) . '</span>'
: ConsoleStyle::create(color: ConsoleColor::BRIGHT_RED)->apply('Invalid JSON: ' . json_last_error_msg());
}
// Pretty print JSON
$prettyJson = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
return match ($format) {
OutputFormat::CONSOLE => $this->formatConsole($prettyJson, $options),
OutputFormat::HTML => $this->formatHtml($prettyJson, $options),
};
}
public function formatForConsole(string $json, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($json, $options, OutputFormat::CONSOLE);
}
public function formatForHtml(string $json, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($json, $options, OutputFormat::HTML);
}
private function formatConsole(string $prettyJson, DisplayOptions $options): string
{
$lines = explode("\n", $prettyJson);
$formattedLines = [];
foreach ($lines as $line) {
$formattedLines[] = $this->colorizeJsonLine($line);
}
return implode("\n", $formattedLines);
}
private function formatHtml(string $prettyJson, DisplayOptions $options): string
{
$lines = explode("\n", $prettyJson);
$formattedLines = [];
foreach ($lines as $line) {
$formattedLines[] = $this->htmlColorizeJsonLine($line);
}
return '<pre class="display-json"><code>' . implode("\n", $formattedLines) . '</code></pre>';
}
private function colorizeJsonLine(string $line): string
{
// JSON key (before colon)
$line = preg_replace_callback(
'/"([^"]+)":\s*/',
function ($matches) {
$keyStyle = ConsoleStyle::create(color: ConsoleColor::YELLOW);
return $keyStyle->apply('"' . $matches[1] . '"') . ': ';
},
$line
);
// JSON string value
$line = preg_replace_callback(
'/:\s*"([^"]*)"/',
function ($matches) {
$stringStyle = ConsoleStyle::create(color: ConsoleColor::GREEN);
return ': ' . $stringStyle->apply('"' . $matches[1] . '"');
},
$line
);
// JSON number
$line = preg_replace_callback(
'/:\s*(-?\d+\.?\d*)/',
function ($matches) {
$numberStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_BLUE);
return ': ' . $numberStyle->apply($matches[1]);
},
$line
);
// JSON boolean
$line = preg_replace_callback(
'/:\s*(true|false)/',
function ($matches) {
$boolStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_MAGENTA);
return ': ' . $boolStyle->apply($matches[1]);
},
$line
);
// JSON null
$line = preg_replace_callback(
'/:\s*(null)/',
function ($matches) {
$nullStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
return ': ' . $nullStyle->apply('null');
},
$line
);
// JSON brackets and braces
$line = str_replace(['{', '}'], [
ConsoleStyle::create(color: ConsoleColor::WHITE)->apply('{'),
ConsoleStyle::create(color: ConsoleColor::WHITE)->apply('}'),
], $line);
$line = str_replace(['[', ']'], [
ConsoleStyle::create(color: ConsoleColor::WHITE)->apply('['),
ConsoleStyle::create(color: ConsoleColor::WHITE)->apply(']'),
], $line);
return $line;
}
private function htmlColorizeJsonLine(string $line): string
{
$line = htmlspecialchars($line, ENT_QUOTES, 'UTF-8');
// JSON key (before colon)
$line = preg_replace_callback(
'/"([^"]+)":\s*/',
function ($matches) {
return '<span class="display-json-key">"' . htmlspecialchars($matches[1]) . '"</span>: ';
},
$line
);
// JSON string value
$line = preg_replace_callback(
'/:\s*"([^"]*)"/',
function ($matches) {
return ': <span class="display-json-string">"' . htmlspecialchars($matches[1]) . '"</span>';
},
$line
);
// JSON number
$line = preg_replace_callback(
'/:\s*(-?\d+\.?\d*)/',
function ($matches) {
return ': <span class="display-json-number">' . htmlspecialchars($matches[1]) . '</span>';
},
$line
);
// JSON boolean
$line = preg_replace_callback(
'/:\s*(true|false)/',
function ($matches) {
return ': <span class="display-json-boolean">' . htmlspecialchars($matches[1]) . '</span>';
},
$line
);
// JSON null
$line = preg_replace_callback(
'/:\s*(null)/',
function ($matches) {
return ': <span class="display-json-null">null</span>';
},
$line
);
// JSON brackets and braces
$line = str_replace(['{', '}'], [
'<span class="display-json-brace">{</span>',
'<span class="display-json-brace">}</span>',
], $line);
$line = str_replace(['[', ']'], [
'<span class="display-json-bracket">[</span>',
'<span class="display-json-bracket">]</span>',
], $line);
return $line;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Display\Inspectors\ObjectInspector;
use App\Framework\Display\Renderers\ConsoleRenderer;
use App\Framework\Display\Renderers\HtmlRenderer;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* High-level formatter for objects combining inspector and renderers
*/
final readonly class ObjectFormatter
{
public function __construct(
private ObjectInspector $inspector = new ObjectInspector(),
private ConsoleRenderer $consoleRenderer = new ConsoleRenderer(),
private HtmlRenderer $htmlRenderer = new HtmlRenderer(),
) {
}
public static function create(): self
{
return new self(
inspector: new ObjectInspector(),
consoleRenderer: ConsoleRenderer::create(),
htmlRenderer: HtmlRenderer::create(),
);
}
public function format(object $data, DisplayOptions $options, OutputFormat $format): string
{
$structure = $this->inspector->inspect($data, $options);
return match ($format) {
OutputFormat::CONSOLE => $this->consoleRenderer->renderObject($structure, $options),
OutputFormat::HTML => $this->htmlRenderer->renderObject($structure, $options),
};
}
public function formatForConsole(object $data, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($data, $options, OutputFormat::CONSOLE);
}
public function formatForHtml(object $data, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($data, $options, OutputFormat::HTML);
}
}

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
use DOMDocument;
/**
* Formatter for XML strings with syntax highlighting
*/
final readonly class XmlFormatter
{
public function format(string $xml, DisplayOptions $options, OutputFormat $format): string
{
// Validate and parse XML
$dom = new DOMDocument();
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$previousErrors = libxml_use_internal_errors(true);
$loaded = $dom->loadXML($xml);
$errors = libxml_get_errors();
libxml_use_internal_errors($previousErrors);
if (! $loaded || ! empty($errors)) {
// Invalid XML - return as plain string with error indication
$errorMsg = ! empty($errors) ? $errors[0]->message : 'Invalid XML';
return $format === OutputFormat::HTML
? '<span class="display-error">Invalid XML: ' . htmlspecialchars($errorMsg) . '</span>'
: ConsoleStyle::create(color: ConsoleColor::BRIGHT_RED)->apply('Invalid XML: ' . $errorMsg);
}
// Pretty print XML
$prettyXml = $dom->saveXML();
return match ($format) {
OutputFormat::CONSOLE => $this->formatConsole($prettyXml, $options),
OutputFormat::HTML => $this->formatHtml($prettyXml, $options),
};
}
public function formatForConsole(string $xml, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($xml, $options, OutputFormat::CONSOLE);
}
public function formatForHtml(string $xml, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($xml, $options, OutputFormat::HTML);
}
private function formatConsole(string $prettyXml, DisplayOptions $options): string
{
$lines = explode("\n", trim($prettyXml));
$formattedLines = [];
foreach ($lines as $line) {
$formattedLines[] = $this->colorizeXmlLine($line);
}
return implode("\n", $formattedLines);
}
private function formatHtml(string $prettyXml, DisplayOptions $options): string
{
$lines = explode("\n", trim($prettyXml));
$formattedLines = [];
foreach ($lines as $line) {
$formattedLines[] = $this->htmlColorizeXmlLine($line);
}
return '<pre class="display-xml"><code>' . implode("\n", $formattedLines) . '</code></pre>';
}
private function colorizeXmlLine(string $line): string
{
// XML opening/closing tags
$line = preg_replace_callback(
'/<(\/?)([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)>/',
function ($matches) {
$slash = $matches[1];
$tagName = $matches[2];
$attributes = $matches[3];
$tagStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_CYAN);
$tagNameStyle = ConsoleStyle::create(color: ConsoleColor::CYAN);
// Colorize attributes
$attributes = preg_replace_callback(
'/([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*"([^"]*)"/',
function ($attrMatches) {
$attrNameStyle = ConsoleStyle::create(color: ConsoleColor::YELLOW);
$attrValueStyle = ConsoleStyle::create(color: ConsoleColor::GREEN);
return $attrNameStyle->apply($attrMatches[1]) . '="' . $attrValueStyle->apply($attrMatches[2]) . '"';
},
$attributes
);
return '<' . $slash . $tagNameStyle->apply($tagName) . $tagStyle->apply($attributes) . '>';
},
$line
);
// XML comments
$line = preg_replace_callback(
'/<!--(.*?)-->/s',
function ($matches) {
$commentStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
return $commentStyle->apply('<!--' . $matches[1] . '-->');
},
$line
);
// CDATA sections
$line = preg_replace_callback(
'/<!\[CDATA\[(.*?)\]\]>/s',
function ($matches) {
$cdataStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_MAGENTA);
return $cdataStyle->apply('<![CDATA[' . $matches[1] . ']]>');
},
$line
);
// Processing instructions
$line = preg_replace_callback(
'/<\?([^?]+)\?>/',
function ($matches) {
$piStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW);
return $piStyle->apply('<?' . $matches[1] . '?>');
},
$line
);
return $line;
}
private function htmlColorizeXmlLine(string $line): string
{
$line = htmlspecialchars($line, ENT_QUOTES, 'UTF-8');
// XML opening/closing tags
$line = preg_replace_callback(
'/&lt;(\/?)([a-zA-Z][a-zA-Z0-9_-]*)([^&]*)&gt;/',
function ($matches) {
$slash = $matches[1];
$tagName = $matches[2];
$attributes = $matches[3];
// Colorize attributes
$attributes = preg_replace_callback(
'/([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*"([^"]*)"/',
function ($attrMatches) {
return '<span class="display-xml-attr-name">' . htmlspecialchars($attrMatches[1]) . '</span>="' .
'<span class="display-xml-attr-value">' . htmlspecialchars($attrMatches[2]) . '</span>"';
},
$attributes
);
return '&lt;' . $slash . '<span class="display-xml-tag">' . htmlspecialchars($tagName) . '</span>' .
htmlspecialchars($attributes) . '&gt;';
},
$line
);
// XML comments
$line = preg_replace_callback(
'/&lt;!--(.*?)--&gt;/s',
function ($matches) {
return '&lt;!--<span class="display-xml-comment">' . htmlspecialchars($matches[1]) . '</span>--&gt;';
},
$line
);
// CDATA sections
$line = preg_replace_callback(
'/&lt;!\[CDATA\[(.*?)\]\]&gt;/s',
function ($matches) {
return '<span class="display-xml-cdata">&lt;![CDATA[' . htmlspecialchars($matches[1]) . ']]&gt;</span>';
},
$line
);
// Processing instructions
$line = preg_replace_callback(
'/&lt;\?([^?]+)\?&gt;/',
function ($matches) {
return '<span class="display-xml-pi">&lt;?' . htmlspecialchars($matches[1]) . '?&gt;</span>';
},
$line
);
return $line;
}
}

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Formatters;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
/**
* Formatter for YAML strings with syntax highlighting
* Note: This is a basic implementation. For full YAML parsing, consider using Symfony YAML component.
*/
final readonly class YamlFormatter
{
public function format(string $yaml, DisplayOptions $options, OutputFormat $format): string
{
return match ($format) {
OutputFormat::CONSOLE => $this->formatConsole($yaml, $options),
OutputFormat::HTML => $this->formatHtml($yaml, $options),
};
}
public function formatForConsole(string $yaml, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($yaml, $options, OutputFormat::CONSOLE);
}
public function formatForHtml(string $yaml, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
return $this->format($yaml, $options, OutputFormat::HTML);
}
private function formatConsole(string $yaml, DisplayOptions $options): string
{
$lines = explode("\n", $yaml);
$formattedLines = [];
foreach ($lines as $line) {
$formattedLines[] = $this->colorizeYamlLine($line);
}
return implode("\n", $formattedLines);
}
private function formatHtml(string $yaml, DisplayOptions $options): string
{
$lines = explode("\n", $yaml);
$formattedLines = [];
foreach ($lines as $line) {
$formattedLines[] = $this->htmlColorizeYamlLine($line);
}
return '<pre class="display-yaml"><code>' . implode("\n", $formattedLines) . '</code></pre>';
}
private function colorizeYamlLine(string $line): string
{
// YAML key (before colon)
$line = preg_replace_callback(
'/^(\s*)([^#:]+?)(\s*):\s*/',
function ($matches) {
$indentStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
$keyStyle = ConsoleStyle::create(color: ConsoleColor::YELLOW);
return $indentStyle->apply($matches[1]) . $keyStyle->apply($matches[2]) . $matches[3] . ': ';
},
$line
);
// YAML string value (quoted or unquoted)
$line = preg_replace_callback(
'/:\s*(["\'])(.*?)\1/',
function ($matches) {
$stringStyle = ConsoleStyle::create(color: ConsoleColor::GREEN);
return ': ' . $matches[1] . $stringStyle->apply($matches[2]) . $matches[1];
},
$line
);
// YAML number
$line = preg_replace_callback(
'/:\s*(-?\d+\.?\d*)/',
function ($matches) {
$numberStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_BLUE);
return ': ' . $numberStyle->apply($matches[1]);
},
$line
);
// YAML boolean
$line = preg_replace_callback(
'/:\s*(true|false|yes|no|on|off)/i',
function ($matches) {
$boolStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_MAGENTA);
return ': ' . $boolStyle->apply($matches[1]);
},
$line
);
// YAML null
$line = preg_replace_callback(
'/:\s*(null|~|\bNULL\b)/i',
function ($matches) {
$nullStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
return ': ' . $nullStyle->apply($matches[1]);
},
$line
);
// YAML comments
$line = preg_replace_callback(
'/(\s*)(#.*)$/',
function ($matches) {
$commentStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
return $matches[1] . $commentStyle->apply($matches[2]);
},
$line
);
return $line;
}
private function htmlColorizeYamlLine(string $line): string
{
$line = htmlspecialchars($line, ENT_QUOTES, 'UTF-8');
// YAML key (before colon)
$line = preg_replace_callback(
'/^(\s*)([^#:]+?)(\s*):\s*/',
function ($matches) {
return '<span class="display-yaml-indent">' . htmlspecialchars($matches[1]) . '</span>' .
'<span class="display-yaml-key">' . htmlspecialchars($matches[2]) . '</span>' .
htmlspecialchars($matches[3]) . ': ';
},
$line
);
// YAML string value
$line = preg_replace_callback(
'/:\s*(["\'])(.*?)\1/',
function ($matches) {
return ': ' . $matches[1] . '<span class="display-yaml-string">' . htmlspecialchars($matches[2]) . '</span>' . $matches[1];
},
$line
);
// YAML number
$line = preg_replace_callback(
'/:\s*(-?\d+\.?\d*)/',
function ($matches) {
return ': <span class="display-yaml-number">' . htmlspecialchars($matches[1]) . '</span>';
},
$line
);
// YAML boolean
$line = preg_replace_callback(
'/:\s*(true|false|yes|no|on|off)/i',
function ($matches) {
return ': <span class="display-yaml-boolean">' . htmlspecialchars($matches[1]) . '</span>';
},
$line
);
// YAML null
$line = preg_replace_callback(
'/:\s*(null|~|\bNULL\b)/i',
function ($matches) {
return ': <span class="display-yaml-null">' . htmlspecialchars($matches[1]) . '</span>';
},
$line
);
// YAML comments
$line = preg_replace_callback(
'/(\s*)(#.*)$/',
function ($matches) {
return htmlspecialchars($matches[1]) . '<span class="display-yaml-comment">' . htmlspecialchars($matches[2]) . '</span>';
},
$line
);
return $line;
}
}

View File

@@ -0,0 +1,227 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Inspectors;
use App\Framework\Display\ValueObjects\ArrayStructure;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\DisplayRules;
/**
* Analyzes arrays and creates normalized ArrayStructure
*/
final readonly class ArrayInspector
{
public function inspect(array $data, DisplayOptions $options): ArrayStructure
{
return $this->inspectInternal($data, $options, true, []);
}
/**
* Internal inspection method with root/nested context and visited tracking
*
* @param array<int, bool> $visited Array hashes of already visited arrays (for circular refs)
*/
private function inspectInternal(array $data, DisplayOptions $options, bool $isRoot, array $visited): ArrayStructure
{
$rules = $options->getRules();
$arrayHash = $this->getArrayHash($data);
// Check for circular reference
if (isset($visited[$arrayHash])) {
return new ArrayStructure(
items: [],
isAssociative: false,
depth: 0,
count: count($data),
keys: [],
valueTypes: [],
nestedArrays: [],
statistics: [],
isCircularReference: true,
isNested: ! $isRoot,
isSummary: false,
summaryInfo: [],
truncatedCount: null,
);
}
// Mark as visited
$visited[$arrayHash] = true;
$count = count($data);
// Check if array should be summarized
$threshold = $isRoot ? $rules->maxArraySizeForSummary : $rules->maxArraySizeForSummaryNested;
if ($count > $threshold) {
return $this->createSummaryStructure($data, $isRoot);
}
// Check for truncation
$maxItems = $rules->maxItems;
$truncatedCount = null;
if ($count > $maxItems) {
$data = array_slice($data, 0, $maxItems, true);
$truncatedCount = $count - $maxItems;
}
return $this->createFullStructure($data, $options, $isRoot, $visited, $truncatedCount);
}
private function createSummaryStructure(array $data, bool $isRoot): ArrayStructure
{
$count = count($data);
$typeDistribution = $this->analyzeTypeDistribution($data);
$keys = array_keys($data);
$isAssociative = ! $this->isIndexed($keys);
return new ArrayStructure(
items: [],
isAssociative: $isAssociative,
depth: 0,
count: $count,
keys: $keys,
valueTypes: [],
nestedArrays: [],
statistics: [],
isCircularReference: false,
isNested: ! $isRoot,
isSummary: true,
summaryInfo: [
'size' => $count,
'type_distribution' => $typeDistribution,
'associative' => $isAssociative,
],
truncatedCount: null,
);
}
private function createFullStructure(array $data, DisplayOptions $options, bool $isRoot, array $visited, ?int $truncatedCount): ArrayStructure
{
if (empty($data)) {
return new ArrayStructure(
items: [],
isAssociative: false,
depth: 0,
count: 0,
keys: [],
valueTypes: [],
nestedArrays: [],
statistics: [],
isCircularReference: false,
isNested: ! $isRoot,
isSummary: false,
summaryInfo: [],
truncatedCount: null,
);
}
$keys = array_keys($data);
$isAssociative = ! $this->isIndexed($keys);
$count = count($data);
$valueTypes = [];
$nestedArrays = [];
$items = [];
$currentDepth = $isRoot ? 0 : 1;
foreach ($data as $key => $value) {
$valueType = get_debug_type($value);
$valueTypes[$key] = $valueType;
if (is_array($value) && $currentDepth < $options->maxDepth) {
$nestedArrays[$key] = $this->inspectInternal($value, $options, false, $visited);
$items[$key] = '[array:' . count($value) . ']';
} else {
$items[$key] = $value;
}
}
$depth = $currentDepth;
if (! empty($nestedArrays)) {
$maxNestedDepth = max(array_map(
fn (ArrayStructure $nested) => $nested->depth,
$nestedArrays
));
$depth = max($depth, $maxNestedDepth);
}
$statistics = [
'total_items' => $count,
'associative' => $isAssociative ? 1 : 0,
'indexed' => $isAssociative ? 0 : 1,
'nested_count' => count($nestedArrays),
'unique_types' => count(array_unique($valueTypes)),
];
return new ArrayStructure(
items: $items,
isAssociative: $isAssociative,
depth: $depth,
count: $count,
keys: $keys,
valueTypes: $valueTypes,
nestedArrays: $nestedArrays,
statistics: $statistics,
isCircularReference: false,
isNested: ! $isRoot,
isSummary: false,
summaryInfo: [],
truncatedCount: $truncatedCount,
);
}
private function analyzeTypeDistribution(array $data): array
{
$typeCounts = [];
$total = count($data);
foreach ($data as $value) {
$type = get_debug_type($value);
$typeCounts[$type] = ($typeCounts[$type] ?? 0) + 1;
}
$distribution = [];
foreach ($typeCounts as $type => $count) {
$distribution[$type] = [
'count' => $count,
'percentage' => round(($count / $total) * 100, 1),
];
}
return $distribution;
}
private function isIndexed(array $keys): bool
{
if (empty($keys)) {
return true;
}
$expected = 0;
foreach ($keys as $key) {
if (! is_int($key) || $key !== $expected) {
return false;
}
$expected++;
}
return true;
}
/**
* Generate a hash for an array to detect circular references
* Uses serialization to create a stable hash
*/
private function getArrayHash(array $array): int
{
// Use a combination of count and first few keys for hash
// This is not perfect but works for most cases
$keys = array_keys($array);
$hashString = count($array) . '-' . implode('-', array_slice($keys, 0, 5));
return crc32($hashString);
}
}

View File

@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Inspectors;
use App\Framework\Display\ValueObjects\ClassStructure;
use App\Framework\Display\ValueObjects\ConstantInfo;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\MethodInfo;
use App\Framework\Display\ValueObjects\PropertyInfo;
use ReflectionClass;
use ReflectionClassConstant;
use ReflectionMethod;
use ReflectionProperty;
/**
* Analyzes classes via Reflection and creates normalized ClassStructure
*/
final readonly class ClassInspector
{
public function inspect(string|object $class, DisplayOptions $options): ClassStructure
{
$className = is_string($class) ? $class : get_class($class);
$reflection = new ReflectionClass($className);
$namespace = $reflection->getNamespaceName();
$parentClass = $reflection->getParentClass()?->getName();
$interfaces = $reflection->getInterfaceNames();
$attributes = $this->extractAttributes($reflection);
$methods = $this->inspectMethods($reflection, $options);
$properties = $this->inspectProperties($reflection, $options);
$constants = $this->inspectConstants($reflection, $options);
$metadata = [
'file' => $reflection->getFileName() ?: '',
'start_line' => $reflection->getStartLine() ?: 0,
'end_line' => $reflection->getEndLine() ?: 0,
'is_instantiable' => $reflection->isInstantiable(),
'is_cloneable' => $reflection->isCloneable(),
];
return new ClassStructure(
className: $reflection->getShortName(),
namespace: $namespace,
parentClass: $parentClass,
methods: $methods,
properties: $properties,
constants: $constants,
interfaces: $interfaces,
attributes: $attributes,
isFinal: $reflection->isFinal(),
isAbstract: $reflection->isAbstract(),
isReadonly: $reflection->isReadOnly(),
isInterface: $reflection->isInterface(),
isTrait: $reflection->isTrait(),
isEnum: $reflection->isEnum(),
metadata: $metadata,
);
}
/**
* @return array<MethodInfo>
*/
private function inspectMethods(ReflectionClass $reflection, DisplayOptions $options): array
{
$methods = [];
$allMethods = $reflection->getMethods();
foreach ($allMethods as $method) {
$visibility = $method->getModifiers();
$isPrivate = ($visibility & ReflectionMethod::IS_PRIVATE) !== 0;
$isProtected = ($visibility & ReflectionMethod::IS_PROTECTED) !== 0;
// Skip private/protected if not showing them
if (($isPrivate || $isProtected) && ! $options->showPrivateProperties) {
continue;
}
$returnType = $method->getReturnType();
$returnTypeString = $returnType ? $returnType->__toString() : 'mixed';
$parameters = [];
foreach ($method->getParameters() as $param) {
$paramType = $param->getType();
$paramString = $paramType ? $paramType->__toString() : 'mixed';
$paramString .= ' $' . $param->getName();
if ($param->isDefaultValueAvailable()) {
$defaultValue = $param->getDefaultValue();
$paramString .= ' = ' . $this->formatDefaultValue($defaultValue);
}
$parameters[] = $paramString;
}
$attributes = $this->extractMethodAttributes($method);
$methods[$method->getName()] = new MethodInfo(
name: $method->getName(),
visibility: $visibility,
returnType: $returnTypeString,
parameters: $parameters,
isStatic: $method->isStatic(),
isAbstract: $method->isAbstract(),
isFinal: $method->isFinal(),
attributes: $attributes,
);
}
return $methods;
}
/**
* @return array<PropertyInfo>
*/
private function inspectProperties(ReflectionClass $reflection, DisplayOptions $options): array
{
$properties = [];
$allProperties = $reflection->getProperties();
foreach ($allProperties as $property) {
$visibility = $property->getModifiers();
$isPrivate = ($visibility & ReflectionProperty::IS_PRIVATE) !== 0;
$isProtected = ($visibility & ReflectionProperty::IS_PROTECTED) !== 0;
// Skip private/protected if not showing them
if (($isPrivate || $isProtected) && ! $options->showPrivateProperties) {
continue;
}
$type = $property->getType();
$typeString = $type ? $type->__toString() : 'mixed';
$propertyInfo = new PropertyInfo(
name: $property->getName(),
type: $typeString,
value: null, // Properties don't have values in class context
visibility: $visibility,
isReadonly: $property->isReadOnly(),
isStatic: $property->isStatic(),
isNullable: $type?->allowsNull() ?? false,
declaringClass: $property->getDeclaringClass()->getName(),
);
$properties[$property->getName()] = $propertyInfo;
}
return $properties;
}
/**
* @return array<ConstantInfo>
*/
private function inspectConstants(ReflectionClass $reflection, DisplayOptions $options): array
{
$constants = [];
$allConstants = $reflection->getReflectionConstants();
foreach ($allConstants as $constant) {
$visibility = $constant->getModifiers();
$isPrivate = ($visibility & ReflectionClassConstant::IS_PRIVATE) !== 0;
$isProtected = ($visibility & ReflectionClassConstant::IS_PROTECTED) !== 0;
// Skip private/protected if not showing them
if (($isPrivate || $isProtected) && ! $options->showPrivateProperties) {
continue;
}
$value = $constant->getValue();
$constants[$constant->getName()] = new ConstantInfo(
name: $constant->getName(),
value: $value,
type: get_debug_type($value),
visibility: $visibility,
isFinal: $constant->isFinal(),
);
}
return $constants;
}
/**
* @return array<string>
*/
private function extractAttributes(ReflectionClass $reflection): array
{
$attributes = [];
foreach ($reflection->getAttributes() as $attribute) {
$attributes[] = $attribute->getName();
}
return $attributes;
}
/**
* @return array<string>
*/
private function extractMethodAttributes(ReflectionMethod $method): array
{
$attributes = [];
foreach ($method->getAttributes() as $attribute) {
$attributes[] = $attribute->getName();
}
return $attributes;
}
private function formatDefaultValue(mixed $value): string
{
return match (true) {
is_string($value) => "'{$value}'",
is_null($value) => 'null',
is_bool($value) => $value ? 'true' : 'false',
is_array($value) => '[]',
default => (string) $value,
};
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Inspectors;
use App\Framework\Display\ValueObjects\DirectoryStructure;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\FileEntry;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* Analyzes filesystem directory structures and creates normalized DirectoryStructure
*/
final readonly class FilesystemInspector
{
public function inspect(string|FilePath $path, DisplayOptions $options): DirectoryStructure
{
$pathString = $path instanceof FilePath ? $path->toString() : $path;
if (! is_dir($pathString)) {
throw new \InvalidArgumentException("Path is not a directory: {$pathString}");
}
return $this->scanDirectory($pathString, $options, 0);
}
private function scanDirectory(string $path, DisplayOptions $options, int $currentDepth): DirectoryStructure
{
$files = [];
$directories = [];
$totalFiles = 0;
$totalDirectories = 0;
if ($currentDepth >= $options->maxDepth) {
return new DirectoryStructure(
path: $path,
files: [],
directories: [],
depth: $currentDepth,
totalFiles: 0,
totalDirectories: 0,
statistics: ['max_depth_reached' => true],
);
}
$items = scandir($path);
if ($items === false) {
throw new \RuntimeException("Cannot scan directory: {$path}");
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$itemPath = $path . DIRECTORY_SEPARATOR . $item;
if (is_dir($itemPath)) {
$totalDirectories++;
$subDirectory = $this->scanDirectory($itemPath, $options, $currentDepth + 1);
$directories[$item] = $subDirectory;
$totalFiles += $subDirectory->totalFiles;
$totalDirectories += $subDirectory->totalDirectories;
} elseif (is_file($itemPath)) {
$totalFiles++;
$files[] = $this->createFileEntry($itemPath, $options);
}
}
$statistics = [
'files_count' => count($files),
'directories_count' => count($directories),
'total_files' => $totalFiles,
'total_directories' => $totalDirectories,
];
return new DirectoryStructure(
path: $path,
files: $files,
directories: $directories,
depth: $currentDepth,
totalFiles: $totalFiles,
totalDirectories: $totalDirectories,
statistics: $statistics,
);
}
private function createFileEntry(string $filePath, DisplayOptions $options): FileEntry
{
$fileInfo = new \SplFileInfo($filePath);
$path = FilePath::create($filePath);
return new FileEntry(
path: $path,
name: $fileInfo->getFilename(),
size: $options->showFileSizes ? ($fileInfo->getSize() ?: 0) : 0,
lastModified: $fileInfo->getMTime() ?: 0,
extension: $fileInfo->getExtension(),
isReadable: $fileInfo->isReadable(),
isWritable: $fileInfo->isWritable(),
);
}
}

View File

@@ -0,0 +1,417 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Inspectors;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\ObjectStructure;
use App\Framework\Display\ValueObjects\PropertyInfo;
use ReflectionClass;
use ReflectionProperty;
/**
* Analyzes objects and creates normalized ObjectStructure
*/
final readonly class ObjectInspector
{
public function inspect(object $object, DisplayOptions $options): ObjectStructure
{
return $this->inspectInternal($object, $options, true, []);
}
/**
* Internal inspection method with visited objects tracking
*
* @param array<int, bool> $visited Object IDs of already visited objects
*/
private function inspectInternal(object $object, DisplayOptions $options, bool $isRoot, array $visited): ObjectStructure
{
$objectId = spl_object_id($object);
// Check for circular reference
if (isset($visited[$objectId])) {
$reflection = new ReflectionClass($object);
return new ObjectStructure(
className: $reflection->getName(),
properties: [],
nestedObjects: [],
depth: 0,
metadata: [
'class' => $reflection->getName(),
'circular_reference' => true,
],
isCircularReference: true,
isNested: ! $isRoot,
isCollection: false,
collectionCount: null,
collectionItemType: null,
isSummary: false,
summaryInfo: [],
);
}
// Mark as visited
$visited[$objectId] = true;
$reflection = new ReflectionClass($object);
$className = $reflection->getName();
$rules = $options->getRules();
// Check blacklist
if ($rules->isBlacklisted($className)) {
return new ObjectStructure(
className: $className,
properties: [],
nestedObjects: [],
depth: 0,
metadata: [
'class' => $className,
'blacklisted' => true,
],
isCircularReference: false,
isNested: ! $isRoot,
isCollection: false,
collectionCount: null,
collectionItemType: null,
isSummary: true,
summaryInfo: [
'reason' => 'blacklisted',
'class' => $className,
],
);
}
// Check if object is a collection
$isCollection = false;
$collectionCount = null;
$collectionItemType = null;
if ($rules->collectionDetection) {
$collectionInfo = $this->detectCollection($object);
if ($collectionInfo !== null) {
$isCollection = true;
$collectionCount = $collectionInfo['count'];
$collectionItemType = $collectionInfo['itemType'];
}
}
// If collection, return compact structure
if ($isCollection) {
return new ObjectStructure(
className: $className,
properties: [],
nestedObjects: [],
depth: 0,
metadata: [
'class' => $className,
'collection' => true,
'count' => $collectionCount,
],
isCircularReference: false,
isNested: ! $isRoot,
isCollection: true,
collectionCount: $collectionCount,
collectionItemType: $collectionItemType,
isSummary: false,
summaryInfo: [],
);
}
// Get all properties first to check thresholds
$allProperties = $reflection->getProperties();
$propertyCount = count($allProperties);
// Check if object should be summarized
$threshold = $isRoot ? $rules->maxObjectPropertiesForSummary : $rules->maxObjectPropertiesForSummaryNested;
if ($propertyCount > $threshold) {
return $this->createSummaryStructure($object, $reflection, $className, $isRoot);
}
$properties = [];
$nestedObjects = [];
$metadata = [
'class' => $className,
'namespace' => $reflection->getNamespaceName(),
'is_final' => $reflection->isFinal(),
'is_abstract' => $reflection->isAbstract(),
'is_readonly' => $reflection->isReadOnly(),
];
foreach ($allProperties as $property) {
$visibility = $property->getModifiers();
$isPrivate = ($visibility & ReflectionProperty::IS_PRIVATE) !== 0;
$isProtected = ($visibility & ReflectionProperty::IS_PROTECTED) !== 0;
// Skip private/protected if not showing them
if ($isPrivate || $isProtected) {
if (! $options->showPrivateProperties) {
continue;
}
}
$property->setAccessible(true);
$value = $property->getValue($object);
$isValueObject = is_object($value) && $this->isValueObject($value);
$propertyInfo = new PropertyInfo(
name: $property->getName(),
type: $this->getPropertyType($property, $value),
value: $value,
visibility: $visibility,
isReadonly: $property->isReadOnly(),
isStatic: $property->isStatic(),
isNullable: $property->getType()?->allowsNull() ?? false,
declaringClass: $property->getDeclaringClass()->getName(),
isValueObject: $isValueObject,
);
$properties[$property->getName()] = $propertyInfo;
// Check for nested objects (but skip Value Objects if compact mode is enabled)
if (is_object($value) && ! $value instanceof \Closure) {
$valueClass = get_class($value);
if ($valueClass !== 'stdClass' || count((array) $value) > 0) {
// Skip nested inspection for Value Objects if compact mode is enabled
if (! $isValueObject || ! $options->compactValueObjects) {
$nestedDepth = $this->calculateDepthInternal($value, $options, $visited);
if ($nestedDepth < $options->maxDepth) {
$nestedObjects[$property->getName()] = $this->inspectInternal($value, $options, false, $visited);
}
}
}
}
}
$depth = 0;
if (! empty($nestedObjects)) {
$maxNestedDepth = max(array_map(
fn (ObjectStructure $nested) => $nested->depth,
$nestedObjects
));
$depth = $maxNestedDepth + 1;
}
return new ObjectStructure(
className: $className,
properties: $properties,
nestedObjects: $nestedObjects,
depth: $depth,
metadata: $metadata,
isCircularReference: false,
isNested: ! $isRoot,
isCollection: false,
collectionCount: null,
collectionItemType: null,
isSummary: false,
summaryInfo: [],
);
}
/**
* Detect if object is a collection (implements IteratorAggregate + Countable)
*/
private function detectCollection(object $object): ?array
{
if (! ($object instanceof \IteratorAggregate) || ! ($object instanceof \Countable)) {
return null;
}
$count = $object->count();
$itemType = $this->detectCollectionItemType($object);
return [
'count' => $count,
'itemType' => $itemType,
];
}
/**
* Try to detect the item type of a collection
*/
private function detectCollectionItemType(object $collection): ?string
{
try {
$iterator = $collection->getIterator();
if ($iterator instanceof \ArrayIterator && $iterator->count() > 0) {
$iterator->rewind();
if ($iterator->valid()) {
$firstItem = $iterator->current();
if (is_object($firstItem)) {
return get_class($firstItem);
}
}
}
} catch (\Throwable $e) {
// Ignore errors
}
// Try reflection on common collection methods
$reflection = new ReflectionClass($collection);
if ($reflection->hasMethod('toArray')) {
try {
$toArrayMethod = $reflection->getMethod('toArray');
if ($toArrayMethod->isPublic() && $toArrayMethod->getNumberOfParameters() === 0) {
$array = $toArrayMethod->invoke($collection);
if (is_array($array) && ! empty($array)) {
$firstItem = reset($array);
if (is_object($firstItem)) {
return get_class($firstItem);
}
}
}
} catch (\Throwable $e) {
// Ignore errors
}
}
return null;
}
/**
* Create a summary structure for large objects
*/
private function createSummaryStructure(object $object, ReflectionClass $reflection, string $className, bool $isRoot): ObjectStructure
{
$properties = $reflection->getProperties();
$propertyCounts = [];
$arrayPropertySizes = [];
foreach ($properties as $property) {
$property->setAccessible(true);
try {
$value = $property->getValue($object);
$type = get_debug_type($value);
$propertyCounts[$type] = ($propertyCounts[$type] ?? 0) + 1;
if (is_array($value)) {
$arrayPropertySizes[$property->getName()] = count($value);
}
} catch (\Throwable $e) {
// Ignore errors accessing properties
}
}
return new ObjectStructure(
className: $className,
properties: [],
nestedObjects: [],
depth: 0,
metadata: [
'class' => $className,
'summary' => true,
],
isCircularReference: false,
isNested: ! $isRoot,
isCollection: false,
collectionCount: null,
collectionItemType: null,
isSummary: true,
summaryInfo: [
'property_count' => count($properties),
'property_counts_by_type' => $propertyCounts,
'array_property_sizes' => $arrayPropertySizes,
],
);
}
private function getPropertyType(ReflectionProperty $property, mixed $value): string
{
$type = $property->getType();
if ($type !== null) {
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
return $type->__toString();
}
return $type->getName();
}
return get_debug_type($value);
}
private function calculateDepth(object $object, DisplayOptions $options): int
{
return $this->calculateDepthInternal($object, $options, []);
}
/**
* Internal depth calculation with visited objects tracking
*
* @param array<int, bool> $visited Object IDs of already visited objects
*/
private function calculateDepthInternal(object $object, DisplayOptions $options, array $visited): int
{
$objectId = spl_object_id($object);
// Circular reference - return 0 to avoid infinite recursion
if (isset($visited[$objectId])) {
return 0;
}
$visited[$objectId] = true;
$reflection = new ReflectionClass($object);
$depth = 0;
foreach ($reflection->getProperties() as $property) {
$property->setAccessible(true);
$value = $property->getValue($object);
if (is_object($value) && ! $value instanceof \Closure) {
$depth = max($depth, $this->calculateDepthInternal($value, $options, $visited) + 1);
}
}
return $depth;
}
/**
* Detect if an object is a Value Object
*/
private function isValueObject(object $object): bool
{
$className = get_class($object);
$reflection = new ReflectionClass($object);
// Check namespace for ValueObjects
$namespace = $reflection->getNamespaceName();
if (str_contains($namespace, 'ValueObjects')) {
return true;
}
// Check if implements Stringable
if ($object instanceof \Stringable) {
return true;
}
// Check if has toString() method
if ($reflection->hasMethod('toString')) {
$toStringMethod = $reflection->getMethod('toString');
if ($toStringMethod->isPublic() && $toStringMethod->getNumberOfParameters() === 0) {
return true;
}
}
// Check if final readonly (typical Value Object pattern)
if ($reflection->isFinal() && $reflection->isReadOnly()) {
// Additional heuristic: few public methods (typical for Value Objects)
$publicMethods = array_filter(
$reflection->getMethods(),
fn ($m) => $m->isPublic() && ! $m->isConstructor() && ! $m->isStatic()
);
if (count($publicMethods) <= 5) {
// Check for common Value Object methods
$valueObjectMethods = ['equals', 'toString', 'toArray', 'toJson', 'toDecimal', 'format'];
foreach ($publicMethods as $method) {
if (in_array($method->getName(), $valueObjectMethods, true)) {
return true;
}
}
}
}
return false;
}
}

View File

@@ -0,0 +1,583 @@
# Display Module
A comprehensive data formatting and display module for the Framework, supporting both Console and HTML output formats. The module provides intelligent formatting for arrays, objects, classes, filesystem structures, and various data formats (JSON, YAML, XML, CSV).
## Table of Contents
- [Introduction](#introduction)
- [Quick Start](#quick-start)
- [Architecture Overview](#architecture-overview)
- [Formatters](#formatters)
- [Configuration](#configuration)
- [Value Object Formatting](#value-object-formatting)
- [Advanced Usage](#advanced-usage)
- [Performance Tips](#performance-tips)
- [Examples](#examples)
## Introduction
The Display module provides a unified interface for formatting and displaying various data types in both console and HTML environments. It automatically detects data types and applies appropriate formatting, making it easy to display complex data structures in a readable format.
### Key Features
- **Multi-Format Support**: Console (ANSI colors) and HTML (semantic HTML/CSS)
- **Auto-Detection**: Automatically selects the appropriate formatter based on data type
- **Smart Filtering**: Configurable rules for handling large structures and blacklisted classes
- **Collection Detection**: Automatically detects and compactly displays collection objects
- **Circular Reference Detection**: Prevents infinite loops when displaying object graphs
- **Value Object Support**: Special formatting for Value Objects with custom formatters
- **Multiple Data Formats**: Support for JSON, YAML, XML, CSV, arrays, objects, classes, and filesystem structures
## Quick Start
### Basic Usage
```php
use App\Framework\Display\Formatters\AutoFormatter;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
$formatter = new AutoFormatter();
// Format for console
$output = $formatter->formatForConsole($data);
// Format for HTML
$output = $formatter->formatForHtml($data);
// With custom options
$options = DisplayOptions::default()
->withMaxDepth(5)
->withCompactValueObjects(true);
$output = $formatter->format($data, $options, OutputFormat::CONSOLE);
```
### Formatting Different Data Types
```php
$formatter = new AutoFormatter();
// Arrays
$array = ['name' => 'John', 'age' => 30, 'city' => 'Berlin'];
echo $formatter->formatForConsole($array);
// Objects
$object = new User(name: 'John', age: 30);
echo $formatter->formatForConsole($object);
// JSON strings
$json = '{"name":"John","age":30}';
echo $formatter->formatForConsole($json);
// YAML strings
$yaml = "name: John\nage: 30";
echo $formatter->formatForConsole($yaml);
// XML strings
$xml = '<user><name>John</name><age>30</age></user>';
echo $formatter->formatForConsole($xml);
// CSV strings
$csv = "name,age\nJohn,30\nJane,25";
echo $formatter->formatForConsole($csv);
// Classes
echo $formatter->formatForConsole(User::class);
// Filesystem directories
echo $formatter->formatForConsole('/path/to/directory');
```
## Architecture Overview
The Display module follows a three-layer architecture:
### 1. Inspectors
Inspectors analyze raw data and create normalized Value Objects:
- **ArrayInspector**: Analyzes arrays and creates `ArrayStructure`
- **ObjectInspector**: Analyzes objects and creates `ObjectStructure`
- **ClassInspector**: Analyzes classes and creates `ClassStructure`
- **FilesystemInspector**: Analyzes filesystem structures and creates `DirectoryStructure`
### 2. Renderers
Renderers take normalized Value Objects and render them into the target output format:
- **ConsoleRenderer**: Renders structures for console output with ANSI colors
- **HtmlRenderer**: Renders structures for HTML output with semantic HTML/CSS
### 3. Formatters
Formatters provide high-level APIs combining inspectors and renderers:
- **AutoFormatter**: Automatically detects data type and selects appropriate formatter
- **ArrayFormatter**: Formats arrays
- **ObjectFormatter**: Formats objects
- **ClassFormatter**: Formats classes
- **FilesystemFormatter**: Formats filesystem structures
- **JsonFormatter**: Formats JSON strings
- **YamlFormatter**: Formats YAML strings
- **XmlFormatter**: Formats XML strings
- **CsvFormatter**: Formats CSV strings
## Formatters
### AutoFormatter
The `AutoFormatter` automatically detects the input data type and selects the appropriate formatter:
```php
$formatter = new AutoFormatter();
// Automatically detects and formats:
// - Arrays → ArrayFormatter
// - Objects → ObjectFormatter
// - JSON strings → JsonFormatter
// - YAML strings → YamlFormatter
// - XML strings → XmlFormatter
// - CSV strings → CsvFormatter
// - Class names → ClassFormatter
// - Directory paths → FilesystemFormatter
// - Scalars → Scalar formatting
$output = $formatter->formatForConsole($data);
```
### ArrayFormatter
Formats arrays with tree, table, or flat display styles:
```php
use App\Framework\Display\Formatters\ArrayFormatter;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
$formatter = new ArrayFormatter();
// Tree style (default)
$options = DisplayOptions::default();
$output = $formatter->format($array, $options, OutputFormat::CONSOLE);
// Table style
$options = DisplayOptions::default()->withFormatStyle('table');
$output = $formatter->format($array, $options, OutputFormat::CONSOLE);
// Flat style
$options = DisplayOptions::default()->withFormatStyle('flat');
$output = $formatter->format($array, $options, OutputFormat::CONSOLE);
```
### ObjectFormatter
Formats objects with property inspection:
```php
use App\Framework\Display\Formatters\ObjectFormatter;
$formatter = new ObjectFormatter();
// Show private/protected properties
$options = DisplayOptions::default()->withShowPrivateProperties(true);
$output = $formatter->formatForConsole($object, $options);
```
### JsonFormatter
Formats JSON strings with syntax highlighting:
```php
use App\Framework\Display\Formatters\JsonFormatter;
$formatter = new JsonFormatter();
$json = '{"name":"John","age":30,"active":true}';
// Console output with ANSI colors
$output = $formatter->formatForConsole($json);
// HTML output with CSS classes
$output = $formatter->formatForHtml($json);
```
### YamlFormatter
Formats YAML strings with syntax highlighting:
```php
use App\Framework\Display\Formatters\YamlFormatter;
$formatter = new YamlFormatter();
$yaml = "name: John\nage: 30\nactive: true";
$output = $formatter->formatForConsole($yaml);
```
### XmlFormatter
Formats XML strings with syntax highlighting:
```php
use App\Framework\Display\Formatters\XmlFormatter;
$formatter = new XmlFormatter();
$xml = '<user><name>John</name><age>30</age></user>';
$output = $formatter->formatForConsole($xml);
```
### CsvFormatter
Formats CSV strings as tables:
```php
use App\Framework\Display\Formatters\CsvFormatter;
$formatter = new CsvFormatter();
$csv = "name,age\nJohn,30\nJane,25";
// Console output as table
$output = $formatter->formatForConsole($csv);
// HTML output as HTML table
$output = $formatter->formatForHtml($csv);
```
## Configuration
### DisplayOptions
`DisplayOptions` controls how data is displayed:
```php
use App\Framework\Display\ValueObjects\DisplayOptions;
// Default options
$options = DisplayOptions::default();
// Compact mode
$options = DisplayOptions::compact(); // maxDepth: 3, no metadata
// Verbose mode
$options = DisplayOptions::verbose(); // maxDepth: 20, show all properties
// Custom options
$options = DisplayOptions::default()
->withMaxDepth(5) // Maximum nesting depth
->withFormatStyle('tree') // 'tree', 'table', or 'flat'
->withShowPrivateProperties(true) // Show private/protected properties
->withCompactValueObjects(true) // Compact Value Object display
->withShowValueObjectType(true) // Show Value Object type comments
->withIndentSize(2) // Indentation size
->withPrettyPrint(true); // Pretty print output
```
### DisplayRules
`DisplayRules` controls filtering and performance optimizations:
```php
use App\Framework\Display\ValueObjects\DisplayRules;
use App\Framework\Display\ValueObjects\DisplayOptions;
// Default rules
$rules = DisplayRules::default();
// Custom rules
$rules = DisplayRules::default()
->withBlacklistedClasses([
'App\\Framework\\DI\\DefaultContainer',
'App\\Framework\\Discovery\\DiscoveryRegistry',
])
->withMaxArraySizeForSummary(1000) // Root arrays > 1000 show summary
->withMaxArraySizeForSummaryNested(100) // Nested arrays > 100 show summary
->withMaxObjectPropertiesForSummary(50) // Root objects > 50 properties show summary
->withMaxObjectPropertiesForSummaryNested(20) // Nested objects > 20 properties show summary
->withMaxItems(100) // Max items before truncation
->withCollectionDetection(true) // Enable collection detection
->withCompactNestedObjects(true); // Compact display for nested objects
$options = DisplayOptions::default()->withRules($rules);
```
## Value Object Formatting
The Display module provides special formatting for Value Objects. Value Objects are automatically detected and can be displayed compactly or with custom formatters.
### Automatic Value Object Detection
Value Objects are detected based on:
- Namespace containing "ValueObjects"
- Implements `Stringable` interface
- Has `toString()` method
- `final readonly` class pattern with limited public methods
### Compact Value Object Display
By default, Value Objects are displayed compactly:
```php
$options = DisplayOptions::default()
->withCompactValueObjects(true) // Enable compact display
->withShowValueObjectType(true); // Show type comment
// Value Objects will be displayed as:
// ClassName(value) // ClassName
```
### Custom Value Object Formatters
Create custom formatters for specific Value Objects:
```php
use App\Framework\Display\ValueObjectFormatters\ValueObjectFormatterInterface;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\OutputFormat;
final readonly class CustomValueObjectFormatter implements ValueObjectFormatterInterface
{
public function supports(object $valueObject): bool
{
return $valueObject instanceof CustomValueObject;
}
public function format(object $valueObject, DisplayOptions $options, OutputFormat $outputFormat): string
{
if (! $valueObject instanceof CustomValueObject) {
return (string) $valueObject;
}
return match ($outputFormat) {
OutputFormat::CONSOLE => $this->formatConsole($valueObject, $options),
OutputFormat::HTML => $this->formatHtml($valueObject, $options),
};
}
private function formatConsole(CustomValueObject $vo, DisplayOptions $options): string
{
// Custom console formatting
return ConsoleStyle::create(color: ConsoleColor::GREEN)
->apply($vo->toString());
}
private function formatHtml(CustomValueObject $vo, DisplayOptions $options): string
{
// Custom HTML formatting
return '<span class="custom-vo">' . htmlspecialchars($vo->toString()) . '</span>';
}
}
// Register the formatter
$registry = new ValueObjectFormatterRegistry();
$registry->register(new CustomValueObjectFormatter());
// Use with renderers
$renderer = new ConsoleRenderer($registry);
```
## Advanced Usage
### Circular Reference Detection
The module automatically detects and handles circular references:
```php
class A {
public B $b;
}
class B {
public A $a;
}
$a = new A();
$b = new B();
$a->b = $b;
$b->a = $a;
// Circular references are displayed as [circular reference]
$formatter = new AutoFormatter();
echo $formatter->formatForConsole($a);
```
### Collection Detection
Objects implementing `IteratorAggregate` and `Countable` are automatically detected as collections:
```php
class UserCollection implements IteratorAggregate, Countable
{
private array $users = [];
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->users);
}
public function count(): int
{
return count($this->users);
}
}
// Collections are displayed as:
// UserCollection (75 User objects)
```
### Summary Views for Large Structures
Large arrays and objects are automatically summarized:
```php
// Large array (> 1000 items for root, > 100 for nested)
// Displays as: [array: 10000 items, types: string(60%), int(30%), object(10%)]
// Large object (> 50 properties for root, > 20 for nested)
// Displays as: ClassName [summary: 150 properties, types: string(40), array(30), object(80)]
```
### Blacklist Support
Blacklisted classes are never fully displayed:
```php
$rules = DisplayRules::default()
->withBlacklistedClasses([
'App\\Framework\\DI\\DefaultContainer',
'App\\Framework\\Discovery\\DiscoveryRegistry',
]);
$options = DisplayOptions::default()->withRules($rules);
// Blacklisted objects are displayed as:
// ClassName [blacklisted]
```
## Performance Tips
1. **Use Summary Views**: Configure `DisplayRules` to show summaries for large structures
2. **Limit Depth**: Use `withMaxDepth()` to limit nesting depth
3. **Compact Value Objects**: Enable `compactValueObjects` for better performance
4. **Blacklist Large Objects**: Add large objects to blacklist to prevent inspection
5. **Use Collection Detection**: Collections are displayed compactly without full inspection
## Examples
### Console Command Integration
```php
use App\Framework\Display\Formatters\AutoFormatter;
use App\Framework\Console\ConsoleCommand;
#[ConsoleCommand('display:show')]
final class DisplayShowCommand
{
public function __construct(
private AutoFormatter $formatter
) {}
public function handle(array $data): void
{
$output = $this->formatter->formatForConsole($data);
echo $output . "\n";
}
}
```
### Web View Integration
```php
use App\Framework\Display\Formatters\AutoFormatter;
use App\Framework\Display\ValueObjects\OutputFormat;
class DebugController
{
public function __construct(
private AutoFormatter $formatter
) {}
public function showData(array $data): string
{
$html = $this->formatter->format($data, DisplayOptions::default(), OutputFormat::HTML);
return "<div class='debug-output'>{$html}</div>";
}
}
```
### Custom Configuration Example
```php
use App\Framework\Display\Formatters\AutoFormatter;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\DisplayRules;
// Create custom configuration
$rules = DisplayRules::default()
->withMaxArraySizeForSummary(500)
->withMaxItems(50);
$options = DisplayOptions::default()
->withMaxDepth(3)
->withFormatStyle('tree')
->withRules($rules);
$formatter = new AutoFormatter();
$output = $formatter->format($data, $options, OutputFormat::CONSOLE);
```
### Formatting Different Data Types
```php
$formatter = new AutoFormatter();
// Array
$array = ['key' => 'value', 'nested' => ['a' => 1, 'b' => 2]];
echo $formatter->formatForConsole($array);
// Object
$user = new User(name: 'John', age: 30);
echo $formatter->formatForConsole($user);
// JSON
$json = '{"name":"John","age":30}';
echo $formatter->formatForConsole($json);
// Class structure
echo $formatter->formatForConsole(User::class);
// Directory structure
echo $formatter->formatForConsole('/path/to/directory');
```
## API Reference
### Main Classes
- **AutoFormatter**: Main entry point for automatic formatting
- **ArrayFormatter**: Formats arrays
- **ObjectFormatter**: Formats objects
- **ClassFormatter**: Formats classes
- **FilesystemFormatter**: Formats filesystem structures
- **JsonFormatter**: Formats JSON strings
- **YamlFormatter**: Formats YAML strings
- **XmlFormatter**: Formats XML strings
- **CsvFormatter**: Formats CSV strings
### Value Objects
- **DisplayOptions**: Configuration options for display
- **DisplayRules**: Rules for filtering and performance
- **OutputFormat**: Output format enum (CONSOLE, HTML)
- **ArrayStructure**: Normalized array representation
- **ObjectStructure**: Normalized object representation
- **ClassStructure**: Normalized class representation
- **DirectoryStructure**: Normalized directory representation
### Interfaces
- **ValueObjectFormatterInterface**: Interface for custom Value Object formatters
For detailed API documentation, see the PHPDoc comments in the source files.

View File

@@ -0,0 +1,611 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Renderers;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\Components\ComponentFactory;
use App\Framework\Display\Themes\ConsoleTheme;
use App\Framework\Display\Themes\DefaultThemes;
use App\Framework\Display\ValueObjectRegistry\DefaultValueObjectFormatterRegistry;
use App\Framework\Display\ValueObjectRegistry\ValueObjectFormatterRegistry;
use App\Framework\Display\ValueObjects\ArrayStructure;
use App\Framework\Display\ValueObjects\ClassStructure;
use App\Framework\Display\ValueObjects\DirectoryStructure;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\FileEntry;
use App\Framework\Display\ValueObjects\MethodInfo;
use App\Framework\Display\ValueObjects\ObjectStructure;
use App\Framework\Display\ValueObjects\OutputFormat;
use App\Framework\Display\ValueObjects\PropertyInfo;
/**
* Renders data structures for console output with ANSI colors
*/
final readonly class ConsoleRenderer
{
public function __construct(
private ValueObjectFormatterRegistry $valueObjectRegistry = new ValueObjectFormatterRegistry(),
) {
}
public static function create(): self
{
return new self(DefaultValueObjectFormatterRegistry::create());
}
public function renderArray(ArrayStructure $structure, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
// Handle summary display
if ($structure->isSummary) {
$summaryStyle = $this->getStyle($theme->summaryColor, $options);
$summary = $this->formatArraySummary($structure);
$bracketColor = $options->colorDepth === ColorDepth::NONE ? ConsoleColor::GRAY : $theme->arrayBracketColor;
return $this->style('[array:', $bracketColor) . ' ' . $summary . ' ' . $summaryStyle->apply(']');
}
if ($structure->isEmpty()) {
$bracketColor = $options->colorDepth === ColorDepth::NONE ? ConsoleColor::GRAY : $theme->arrayBracketColor;
return $this->style('[]', $bracketColor);
}
// Show truncation message if applicable
if ($structure->truncatedCount !== null && $structure->truncatedCount > 0) {
$truncationStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
$truncationMsg = "\n" . $truncationStyle->apply('... (' . $structure->truncatedCount . ' more items)');
} else {
$truncationMsg = '';
}
$result = match ($options->formatStyle) {
'tree' => $this->renderArrayTree($structure, $options),
'table' => $this->renderArrayTable($structure, $options),
default => $this->renderArrayFlat($structure, $options),
};
return $result . $truncationMsg;
}
public function renderObject(ObjectStructure $structure, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
// Handle collection display
if ($structure->isCollection) {
$classNameStyle = $this->getStyle($theme->collectionColor, $options, ConsoleFormat::BOLD);
$collectionInfo = $structure->className;
if ($structure->collectionItemType !== null) {
$collectionInfo .= ' (' . $structure->collectionCount . ' ' . $structure->collectionItemType . ' objects)';
} else {
$collectionInfo .= ' (' . $structure->collectionCount . ' items)';
}
return $classNameStyle->apply($collectionInfo);
}
// Handle summary display
if ($structure->isSummary) {
$classNameStyle = $this->getStyle($theme->classNameColor, $options, ConsoleFormat::BOLD);
$summaryStyle = $this->getStyle($theme->summaryColor, $options);
if (isset($structure->summaryInfo['reason']) && $structure->summaryInfo['reason'] === 'blacklisted') {
return $classNameStyle->apply($structure->className) . ' ' . $summaryStyle->apply('[blacklisted]');
}
$summary = $this->formatSummary($structure);
return $classNameStyle->apply($structure->className) . ' ' . $summaryStyle->apply($summary);
}
$theme = $this->getTheme($options);
$output = [];
$classNameStyle = $this->getStyle($theme->classNameColor, $options, ConsoleFormat::BOLD);
$bracketOpen = $options->bracketStyle->getOpen();
$bracketClose = $options->bracketStyle->getClose();
$bracketColor = $options->colorDepth === ColorDepth::NONE ? ConsoleColor::GRAY : $theme->objectBracketColor;
$output[] = $classNameStyle->apply($structure->className) .
($bracketOpen !== '' ? ' ' . $this->style($bracketOpen, $bracketColor) : '');
$indent = $options->getIndentString();
$separator = $options->getSeparator();
$propertyStyle = $this->getStyle($theme->keyColor, $options);
foreach ($structure->properties as $propertyInfo) {
$line = $indent . $propertyStyle->apply($propertyInfo->name) . $separator;
if (isset($structure->nestedObjects[$propertyInfo->name])) {
$nested = $structure->nestedObjects[$propertyInfo->name];
if ($nested->isCircularReference) {
$circularStyle = $this->getStyle($theme->circularReferenceColor, $options);
$line .= $circularStyle->apply('[circular reference]');
} else {
$nestedOutput = $this->renderObject($nested, $options);
$nestedLines = explode("\n", $nestedOutput);
$nestedIndent = $options->getIndentString();
$line .= "\n" . $nestedIndent . $nestedLines[0];
for ($i = 1; $i < count($nestedLines); $i++) {
$line .= "\n" . $nestedIndent . $nestedLines[$i];
}
}
} elseif ($propertyInfo->isValueObject && $options->compactValueObjects && is_object($propertyInfo->value)) {
// Compact Value Object display
$formatted = $this->formatValueObject($propertyInfo->value, $options);
$line .= $formatted;
if ($options->showValueObjectType) {
$typeStyle = $this->getStyle($theme->typeColor, $options);
$line .= ' ' . $typeStyle->apply('// ' . $propertyInfo->type);
}
} else {
$value = $this->formatValue($propertyInfo->value, $options);
$line .= $value;
}
// Show type if enabled
if ($options->showTypes && ! $propertyInfo->isValueObject) {
$typeStyle = $this->getStyle($theme->typeColor, $options);
$line .= ' ' . $typeStyle->apply('<' . $propertyInfo->type . '>');
}
$visibility = $propertyInfo->getVisibilityString();
if ($visibility !== 'public') {
$metadataStyle = $this->getStyle($theme->metadataColor, $options);
$line .= ' ' . $metadataStyle->apply("({$visibility})");
}
// Apply line wrapping if enabled
if ($options->lineWrap && mb_strlen($line) > $options->maxLineLength) {
$line = $this->wrapLine($line, $options->maxLineLength, $indent);
}
$output[] = $line;
}
if ($bracketClose !== '') {
$output[] = $this->style($bracketClose, $bracketColor);
}
return implode("\n", $output);
}
public function renderClass(ClassStructure $structure, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
$output = [];
$classNameStyle = $this->getStyle($theme->classNameColor, $options, ConsoleFormat::BOLD);
$fullName = $structure->getFullName();
$output[] = $classNameStyle->apply($fullName);
// Class modifiers
$modifiers = [];
if ($structure->isFinal) {
$modifiers[] = $this->style('final', ConsoleColor::BRIGHT_YELLOW);
}
if ($structure->isAbstract) {
$modifiers[] = $this->style('abstract', ConsoleColor::BRIGHT_YELLOW);
}
if ($structure->isReadonly) {
$modifiers[] = $this->style('readonly', ConsoleColor::BRIGHT_YELLOW);
}
if ($structure->isInterface) {
$modifiers[] = $this->style('interface', ConsoleColor::BRIGHT_MAGENTA);
}
if ($structure->isTrait) {
$modifiers[] = $this->style('trait', ConsoleColor::BRIGHT_MAGENTA);
}
if ($structure->isEnum) {
$modifiers[] = $this->style('enum', ConsoleColor::BRIGHT_MAGENTA);
}
if (! empty($modifiers)) {
$output[] = implode(' ', $modifiers);
}
// Parent class
if ($structure->parentClass !== null) {
$output[] = $this->style('extends', ConsoleColor::GRAY) . ' ' . $this->style($structure->parentClass, ConsoleColor::CYAN);
}
// Interfaces
if (! empty($structure->interfaces)) {
$interfacesList = implode(', ', array_map(fn (string $iface) => $this->style($iface, ConsoleColor::CYAN), $structure->interfaces));
$output[] = $this->style('implements', ConsoleColor::GRAY) . ' ' . $interfacesList;
}
$bracketOpen = $options->bracketStyle->getOpen();
$bracketClose = $options->bracketStyle->getClose();
if ($bracketOpen !== '') {
$bracketColor = $options->colorDepth === ColorDepth::NONE ? ConsoleColor::GRAY : $theme->objectBracketColor;
$output[] = $this->style($bracketOpen, $bracketColor);
}
$output[] = '';
$indent = $options->getIndentString();
// Properties
if (! empty($structure->properties)) {
$commentStyle = $this->getStyle($theme->metadataColor, $options);
$output[] = $commentStyle->apply('// Properties (' . count($structure->properties) . ')');
foreach ($structure->properties as $property) {
$output[] = $this->renderProperty($property, $indent, $options);
}
$output[] = '';
}
// Methods
if (! empty($structure->methods)) {
$commentStyle = $this->getStyle($theme->metadataColor, $options);
$output[] = $commentStyle->apply('// Methods (' . count($structure->methods) . ')');
foreach ($structure->methods as $method) {
$output[] = $this->renderMethod($method, $indent, $options);
}
$output[] = '';
}
// Constants
if (! empty($structure->constants)) {
$commentStyle = $this->getStyle($theme->metadataColor, $options);
$output[] = $commentStyle->apply('// Constants (' . count($structure->constants) . ')');
foreach ($structure->constants as $constant) {
$output[] = $this->renderConstant($constant, $indent, $options);
}
}
if ($bracketClose !== '') {
$bracketColor = $options->colorDepth === ColorDepth::NONE ? ConsoleColor::GRAY : $theme->objectBracketColor;
$output[] = $this->style($bracketClose, $bracketColor);
}
return implode("\n", $output);
}
public function renderDirectory(DirectoryStructure $structure, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
$output = [];
$pathStyle = $this->getStyle($theme->classNameColor, $options, ConsoleFormat::BOLD);
$output[] = $pathStyle->apply($structure->getPathString());
if ($structure->isEmpty()) {
$infoStyle = $this->getStyle($theme->infoColor, $options);
$output[] = $infoStyle->apply('(empty)');
return implode("\n", $output);
}
return $this->renderDirectoryTree($structure, $options, '');
}
private function renderDirectoryTree(DirectoryStructure $structure, DisplayOptions $options, string $prefix): string
{
$output = [];
$items = [];
// Add directories
foreach ($structure->directories as $name => $subDirectory) {
$items[] = ['type' => 'directory', 'name' => $name, 'structure' => $subDirectory];
}
// Add files
foreach ($structure->files as $file) {
$items[] = ['type' => 'file', 'name' => $file->name, 'file' => $file];
}
$count = count($items);
foreach ($items as $index => $item) {
$isLast = ($index === $count - 1);
$connector = $isLast ? '└── ' : '├── ';
$childPrefix = $prefix . ($isLast ? ' ' : '│ ');
$theme = $this->getTheme($options);
if ($item['type'] === 'directory') {
$name = $item['name'];
$subDir = $item['structure'];
$dirStyle = $this->getStyle($theme->keyColor, $options);
$output[] = $prefix . $connector . $dirStyle->apply($name . '/');
if (! $subDir->isEmpty()) {
$output[] = $this->renderDirectoryTree($subDir, $options, $childPrefix);
}
} else {
$file = $item['file'];
$fileStyle = $this->getStyle($theme->valueColor, $options);
$line = $prefix . $connector . $fileStyle->apply($file->name);
if ($options->showFileSizes) {
$sizeStyle = $this->getStyle($theme->metadataColor, $options);
$line .= ' ' . $sizeStyle->apply('(' . $file->getFormattedSize() . ')');
}
$output[] = $line;
}
}
return implode("\n", $output);
}
private function renderProperty(PropertyInfo $property, string $indent, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
$theme = $this->getTheme($options);
$visibility = $property->getVisibilityString();
$visibilityStyle = $this->getStyle($theme->metadataColor, $options);
$nameStyle = $this->getStyle($theme->keyColor, $options);
$typeStyle = $this->getStyle($theme->typeColor, $options);
$modifiers = [];
if ($property->isStatic) {
$modifiers[] = $this->style('static', ConsoleColor::BRIGHT_YELLOW);
}
if ($property->isReadonly) {
$modifiers[] = $this->style('readonly', ConsoleColor::BRIGHT_YELLOW);
}
$modifiersStr = ! empty($modifiers) ? implode(' ', $modifiers) . ' ' : '';
$nullable = $property->isNullable ? '?' : '';
return $indent . $modifiersStr . $visibilityStyle->apply($visibility) . ' ' . $nullable . $typeStyle->apply($property->type) . ' $' . $nameStyle->apply($property->name) . ';';
}
private function renderMethod(MethodInfo $method, string $indent, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
$theme = $this->getTheme($options);
$visibility = $method->getVisibilityString();
$visibilityStyle = $this->getStyle($theme->metadataColor, $options);
$nameStyle = $this->getStyle($theme->keyColor, $options);
$typeStyle = $this->getStyle($theme->typeColor, $options);
$modifiers = [];
if ($method->isStatic) {
$modifiers[] = $this->style('static', ConsoleColor::BRIGHT_YELLOW);
}
if ($method->isAbstract) {
$modifiers[] = $this->style('abstract', ConsoleColor::BRIGHT_YELLOW);
}
if ($method->isFinal) {
$modifiers[] = $this->style('final', ConsoleColor::BRIGHT_YELLOW);
}
$modifiersStr = ! empty($modifiers) ? implode(' ', $modifiers) . ' ' : '';
return $indent . $modifiersStr . $visibilityStyle->apply($visibility) . ' function ' . $nameStyle->apply($method->name) . '(...): ' . $typeStyle->apply($method->returnType);
}
private function renderConstant(\App\Framework\Display\ValueObjects\ConstantInfo $constant, string $indent, ?DisplayOptions $options = null): string
{
$options ??= DisplayOptions::default();
$theme = $this->getTheme($options);
$visibility = $constant->getVisibilityString();
$visibilityStyle = $this->getStyle($theme->metadataColor, $options);
$nameStyle = $this->getStyle($theme->keyColor, $options);
$valueStyle = $this->getStyle($theme->stringColor, $options);
$modifiers = $constant->isFinal ? $this->style('final ', ConsoleColor::BRIGHT_YELLOW) : '';
$value = $this->formatValue($constant->value, $options);
return $indent . $modifiers . $visibilityStyle->apply($visibility) . ' const ' . $nameStyle->apply($constant->name) . ' = ' . $valueStyle->apply($value) . ';';
}
private function renderArrayTree(ArrayStructure $structure, DisplayOptions $options, int $depth = 0, string $prefix = ''): string
{
$tree = ComponentFactory::createTreeHelper(OutputFormat::CONSOLE, $options);
assert($tree instanceof \App\Framework\Display\Components\Console\TreeHelper);
$tree->fromArrayStructure($structure, $options->getSeparator());
return $tree->render();
}
private function renderArrayTable(ArrayStructure $structure, DisplayOptions $options): string
{
$table = ComponentFactory::createTable(OutputFormat::CONSOLE, $options);
assert($table instanceof \App\Framework\Display\Components\Console\Table);
// Format values in table cells before building table
$rows = [];
foreach ($structure->items as $key => $value) {
$formattedValue = $this->formatValue($value, $options);
$rows[] = [(string) $key, $formattedValue];
}
// Build table with formatted values
$headers = $structure->isAssociative ? ['Key', 'Value'] : ['Index', 'Value'];
$table->setHeaders($headers);
$table->setRows($rows);
return $table->render();
}
private function renderArrayFlat(ArrayStructure $structure, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
$items = $structure->items;
$formattedItems = [];
$separator = $options->getSeparator();
foreach ($items as $key => $value) {
$keyStyle = $this->getStyle($theme->keyColor, $options);
$formattedKey = $keyStyle->apply((string) $key);
$formattedValue = $this->formatValue($value, $options);
$formattedItems[] = $formattedKey . $separator . $formattedValue;
}
$bracketOpen = $options->bracketStyle->getOpen();
$bracketClose = $options->bracketStyle->getClose();
$bracketColor = $options->colorDepth === ColorDepth::NONE ? ConsoleColor::GRAY : $theme->arrayBracketColor;
$bracketOpenStr = $bracketOpen !== '' ? $this->style($bracketOpen, $bracketColor) . ' ' : '';
$bracketCloseStr = $bracketClose !== '' ? ' ' . $this->style($bracketClose, $bracketColor) : '';
return $bracketOpenStr . implode(', ', $formattedItems) . $bracketCloseStr;
}
private function formatValue(mixed $value, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
// If colorDepth is 'none', use gray for everything
if ($options->colorDepth === ColorDepth::NONE) {
return match (true) {
is_string($value) => $this->style('"' . $value . '"', ConsoleColor::GRAY),
is_int($value) => $this->style((string) $value, ConsoleColor::GRAY),
is_float($value) => $this->style((string) $value, ConsoleColor::GRAY),
is_bool($value) => $this->style($value ? 'true' : 'false', ConsoleColor::GRAY),
is_null($value) => $this->style('null', ConsoleColor::GRAY),
is_array($value) => $this->style('[array:' . count($value) . ($options->showSizes ? ' items' : '') . ']', ConsoleColor::GRAY),
is_object($value) => $this->style('[object:' . get_class($value) . ']', ConsoleColor::GRAY),
is_resource($value) => $this->style('[resource]', ConsoleColor::GRAY),
default => $this->style(get_debug_type($value), ConsoleColor::GRAY),
};
}
// Basic color depth uses limited colors
if ($options->colorDepth === ColorDepth::BASIC) {
return match (true) {
is_string($value) => $this->style('"' . $value . '"', ConsoleColor::WHITE),
is_int($value) || is_float($value) => $this->style((string) $value, ConsoleColor::WHITE),
is_bool($value) => $this->style($value ? 'true' : 'false', ConsoleColor::WHITE),
is_null($value) => $this->style('null', ConsoleColor::GRAY),
is_array($value) => $this->style('[array:' . count($value) . ($options->showSizes ? ' items' : '') . ']', ConsoleColor::CYAN),
is_object($value) => $this->style('[object:' . get_class($value) . ']', ConsoleColor::CYAN),
is_resource($value) => $this->style('[resource]', ConsoleColor::GRAY),
default => $this->style(get_debug_type($value), ConsoleColor::GRAY),
};
}
// Full color depth uses theme colors
return match (true) {
is_string($value) => $this->style('"' . $value . '"', $theme->stringColor),
is_int($value) => $this->style((string) $value, $theme->numberColor),
is_float($value) => $this->style((string) $value, $theme->numberColor),
is_bool($value) => $this->style($value ? 'true' : 'false', $theme->booleanColor),
is_null($value) => $this->style('null', $theme->nullColor),
is_array($value) => $this->style('[array:' . count($value) . ($options->showSizes ? ' items' : '') . ']', $theme->arrayBracketColor),
is_object($value) => $this->style('[object:' . get_class($value) . ']', $theme->objectBracketColor),
is_resource($value) => $this->style('[resource]', $theme->typeColor),
default => $this->style(get_debug_type($value), $theme->typeColor),
};
}
private function style(string $text, ConsoleColor $color): string
{
return ConsoleStyle::create(color: $color)->apply($text);
}
/**
* Get theme for display options
*/
private function getTheme(DisplayOptions $options): ConsoleTheme
{
// If custom theme is provided and it's a ConsoleTheme, use it
if ($options->customTheme instanceof ConsoleTheme) {
return $options->customTheme;
}
// Otherwise get theme by name
return DefaultThemes::getConsoleTheme($options->theme);
}
/**
* Get style based on theme color and color depth
*/
private function getStyle(ConsoleColor $color, DisplayOptions $options, ?ConsoleFormat $format = null): ConsoleStyle
{
// If colorDepth is 'none', use gray
if ($options->colorDepth === ColorDepth::NONE) {
return ConsoleStyle::create(color: ConsoleColor::GRAY, format: $format);
}
return ConsoleStyle::create(color: $color, format: $format);
}
/**
* Wrap long line to max length
*/
private function wrapLine(string $line, int $maxLength, string $indent): string
{
if (mb_strlen($line) <= $maxLength) {
return $line;
}
$lines = [];
$currentLine = '';
$parts = explode(' ', $line);
foreach ($parts as $part) {
$testLine = $currentLine === '' ? $part : $currentLine . ' ' . $part;
if (mb_strlen($testLine) <= $maxLength) {
$currentLine = $testLine;
} else {
if ($currentLine !== '') {
$lines[] = $currentLine;
}
$currentLine = $indent . $part;
}
}
if ($currentLine !== '') {
$lines[] = $currentLine;
}
return implode("\n", $lines);
}
private function formatValueObject(object $valueObject, DisplayOptions $options): string
{
// Use default registry if empty
$registry = empty($this->valueObjectRegistry->getAllFormatters())
? DefaultValueObjectFormatterRegistry::create()
: $this->valueObjectRegistry;
return $registry->format($valueObject, $options, forHtml: false);
}
private function formatSummary(ObjectStructure $structure): string
{
$info = $structure->summaryInfo;
$parts = [];
if (isset($info['property_count'])) {
$parts[] = $info['property_count'] . ' properties';
}
if (isset($info['property_counts_by_type'])) {
$typeCounts = [];
foreach ($info['property_counts_by_type'] as $type => $count) {
$typeCounts[] = $type . '(' . $count . ')';
}
if (! empty($typeCounts)) {
$parts[] = 'types: ' . implode(', ', $typeCounts);
}
}
return '[summary: ' . implode(', ', $parts) . ']';
}
private function formatArraySummary(ArrayStructure $structure): string
{
$info = $structure->summaryInfo;
$parts = [];
if (isset($info['size'])) {
$parts[] = $info['size'] . ' items';
}
if (isset($info['type_distribution'])) {
$typeParts = [];
foreach ($info['type_distribution'] as $type => $data) {
$typeParts[] = $type . '(' . $data['percentage'] . '%)';
}
if (! empty($typeParts)) {
$parts[] = 'types: ' . implode(', ', array_slice($typeParts, 0, 5));
}
}
return implode(', ', $parts);
}
}

View File

@@ -0,0 +1,520 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Renderers;
use App\Framework\Display\Components\ComponentFactory;
use App\Framework\Display\Themes\DefaultThemes;
use App\Framework\Display\Themes\HtmlTheme;
use App\Framework\Display\ValueObjectRegistry\DefaultValueObjectFormatterRegistry;
use App\Framework\Display\ValueObjectRegistry\ValueObjectFormatterRegistry;
use App\Framework\Display\ValueObjects\ArrayStructure;
use App\Framework\Display\ValueObjects\OutputFormat;
use App\Framework\Display\ValueObjects\ClassStructure;
use App\Framework\Display\ValueObjects\ConstantInfo;
use App\Framework\Display\ValueObjects\DirectoryStructure;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Display\ValueObjects\FileEntry;
use App\Framework\Display\ValueObjects\MethodInfo;
use App\Framework\Display\ValueObjects\ObjectStructure;
use App\Framework\Display\ValueObjects\PropertyInfo;
/**
* Renders data structures for HTML output
*/
final readonly class HtmlRenderer
{
public function __construct(
private ValueObjectFormatterRegistry $valueObjectRegistry = new ValueObjectFormatterRegistry(),
) {
}
public static function create(): self
{
return new self(DefaultValueObjectFormatterRegistry::create());
}
public function renderArray(ArrayStructure $structure, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
// Handle summary display
if ($structure->isSummary) {
$summary = $this->formatArraySummaryHtml($structure);
$bracketOpen = $options->bracketStyle->getOpen();
$bracketClose = $options->bracketStyle->getClose();
$bracketOpenStr = $bracketOpen !== '' ? '<span class="' . $theme->arrayBracketClass . '">' . htmlspecialchars($bracketOpen) . 'array:</span>' : '';
$bracketCloseStr = $bracketClose !== '' ? '<span class="' . $theme->arrayBracketClass . '">' . htmlspecialchars($bracketClose) . '</span>' : '';
return '<div class="display-array-summary">' . $bracketOpenStr . ' ' . $summary . ' ' . $bracketCloseStr . '</div>';
}
if ($structure->isEmpty()) {
$bracketOpen = $options->bracketStyle->getOpen();
$bracketClose = $options->bracketStyle->getClose();
$emptyBrackets = $bracketOpen !== '' && $bracketClose !== '' ? htmlspecialchars($bracketOpen . $bracketClose) : '[]';
return '<div class="display-empty"><span class="' . $theme->arrayBracketClass . '">' . $emptyBrackets . '</span></div>';
}
$truncationMsg = '';
if ($structure->truncatedCount !== null && $structure->truncatedCount > 0) {
$truncationMsg = '<div class="display-truncation">... (' . $structure->truncatedCount . ' more items)</div>';
}
$result = match ($options->formatStyle) {
'tree' => $this->renderArrayTree($structure, $options),
'table' => $this->renderArrayTable($structure, $options),
default => $this->renderArrayFlat($structure, $options),
};
return $result . $truncationMsg;
}
public function renderObject(ObjectStructure $structure, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
// Handle collection display
if ($structure->isCollection) {
$collectionInfo = '<span class="' . $theme->collectionClass . '">' . htmlspecialchars($structure->className) . '</span>';
if ($structure->collectionItemType !== null) {
$collectionInfo .= ' <span class="' . $theme->summaryClass . '">(' . $structure->collectionCount . ' ' . htmlspecialchars($structure->collectionItemType) . ' objects)' . ($options->showSizes ? ' - size: ' . $structure->collectionCount : '') . '</span>';
} else {
$collectionInfo .= ' <span class="' . $theme->summaryClass . '">(' . $structure->collectionCount . ' items)' . ($options->showSizes ? ' - size: ' . $structure->collectionCount : '') . '</span>';
}
return '<div class="' . $theme->collectionClass . '">' . $collectionInfo . '</div>';
}
// Handle summary display
if ($structure->isSummary) {
$className = '<span class="' . $theme->classNameClass . '">' . htmlspecialchars($structure->className) . '</span>';
if (isset($structure->summaryInfo['reason']) && $structure->summaryInfo['reason'] === 'blacklisted') {
return '<div class="display-object-summary">' . $className . ' <span class="' . $theme->errorClass . '">[blacklisted]</span></div>';
}
$summary = $this->formatSummaryHtml($structure);
return '<div class="display-object-summary">' . $className . ' <span class="' . $theme->summaryClass . '">' . $summary . '</span></div>';
}
$bracketOpen = $options->bracketStyle->getOpen();
$bracketClose = $options->bracketStyle->getClose();
$output = [];
$output[] = '<div class="display-object ' . $theme->themeClass . '">';
$output[] = '<div class="display-object-header">';
$output[] = '<span class="' . $theme->classNameClass . '">' . htmlspecialchars($structure->className) . '</span>';
if ($bracketOpen !== '') {
$output[] = ' <span class="' . $theme->objectBracketClass . '">' . htmlspecialchars($bracketOpen) . '</span>';
}
$output[] = '</div>';
$output[] = '<div class="display-object-properties">';
foreach ($structure->properties as $propertyInfo) {
$output[] = $this->renderProperty($propertyInfo, $structure, $options);
}
$output[] = '</div>';
if ($bracketClose !== '') {
$output[] = '<div class="display-object-footer"><span class="' . $theme->objectBracketClass . '">' . htmlspecialchars($bracketClose) . '</span></div>';
}
$output[] = '</div>';
return implode("\n", $output);
}
private function renderArrayTree(ArrayStructure $structure, DisplayOptions $options, int $depth = 0): string
{
$tree = ComponentFactory::createTreeHelper(OutputFormat::HTML, $options);
assert($tree instanceof \App\Framework\Display\Components\Html\HtmlTreeHelper);
$tree->fromArrayStructure($structure, htmlspecialchars($options->getSeparator()));
return $tree->render();
}
private function renderArrayTable(ArrayStructure $structure, DisplayOptions $options): string
{
$table = ComponentFactory::createTable(OutputFormat::HTML, $options);
assert($table instanceof \App\Framework\Display\Components\Html\HtmlTable);
// Format values in table cells before building table
$rows = [];
foreach ($structure->items as $key => $value) {
$formattedValue = $this->formatValue($value, $options);
$rows[] = [(string) $key, $formattedValue];
}
// Build table with formatted values
$headers = $structure->isAssociative ? ['Key', 'Value'] : ['Index', 'Value'];
$table->setHeaders($headers);
$table->setRows($rows);
return $table->render();
}
private function renderArrayFlat(ArrayStructure $structure, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
$separator = htmlspecialchars($options->getSeparator());
$bracketOpen = $options->bracketStyle->getOpen();
$bracketClose = $options->bracketStyle->getClose();
$output = [];
$output[] = '<div class="display-array ' . $theme->themeClass . '">';
if ($options->showTypes || $options->showSizes) {
$output[] = '<div class="display-array-header">';
if ($options->showTypes) {
$output[] = '<span class="' . $theme->typeClass . '">' . ($structure->isAssociative ? 'associative' : 'indexed') . ' array</span>';
}
if ($options->showSizes) {
$output[] = '<span class="' . $theme->metadataClass . '">(' . $structure->count . ' items)</span>';
}
$output[] = '</div>';
}
$output[] = '<div class="display-array-items">';
if ($bracketOpen !== '') {
$output[] = '<span class="' . $theme->arrayBracketClass . '">' . htmlspecialchars($bracketOpen) . '</span>';
}
$items = [];
foreach ($structure->items as $key => $value) {
$items[] = '<span class="' . $theme->keyClass . '">' . htmlspecialchars((string) $key) . '</span>' .
'<span class="' . $theme->separatorClass . '">' . $separator . '</span>' .
$this->formatValue($value, $options);
}
$output[] = implode(', ', $items);
if ($bracketClose !== '') {
$output[] = '<span class="' . $theme->arrayBracketClass . '">' . htmlspecialchars($bracketClose) . '</span>';
}
$output[] = '</div>';
$output[] = '</div>';
return implode("\n", $output);
}
private function renderProperty(PropertyInfo $property, ObjectStructure $structure, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
$separator = htmlspecialchars($options->getSeparator());
$indent = $options->getIndentString();
$output = [];
$output[] = '<div class="display-property" style="margin-left: ' . strlen($indent) . 'em;">';
$output[] = '<span class="' . $theme->keyClass . '">' . htmlspecialchars($property->name) . '</span>';
if ($options->showTypes) {
$output[] = '<span class="' . $theme->typeClass . '">&lt;' . htmlspecialchars($property->type) . '&gt;</span>';
}
$output[] = '<span class="' . $theme->separatorClass . '">' . $separator . '</span>';
if (isset($structure->nestedObjects[$property->name])) {
$nested = $structure->nestedObjects[$property->name];
if ($nested->isCircularReference) {
$output[] = '<span class="' . $theme->circularReferenceClass . '">[circular reference]</span>';
} else {
$nestedHtml = $this->renderObject($nested, $options);
$output[] = $nestedHtml;
}
} elseif ($property->isValueObject && $options->compactValueObjects && is_object($property->value)) {
// Compact Value Object display
$formatted = $this->formatValueObject($property->value, $options);
$output[] = '<span class="' . $theme->valueClass . '">' . $formatted . '</span>';
if ($options->showValueObjectType) {
$output[] = '<span class="' . $theme->typeClass . '"> // ' . htmlspecialchars($property->type) . '</span>';
}
} else {
$formattedValue = $this->formatValue($property->value, $options);
$output[] = '<span class="' . $theme->valueClass . '">' . $formattedValue . '</span>';
}
if ($property->getVisibilityString() !== 'public') {
$output[] = '<span class="' . $theme->metadataClass . '">(' . htmlspecialchars($property->getVisibilityString()) . ')</span>';
}
$output[] = '</div>';
return implode("\n", $output);
}
public function renderClass(ClassStructure $structure, DisplayOptions $options): string
{
$output = [];
$output[] = '<div class="display-class">';
$output[] = '<div class="display-class-header">';
$output[] = '<span class="display-class-name">' . htmlspecialchars($structure->getFullName()) . '</span>';
// Modifiers
$modifiers = [];
if ($structure->isFinal) {
$modifiers[] = '<span class="display-modifier">final</span>';
}
if ($structure->isAbstract) {
$modifiers[] = '<span class="display-modifier">abstract</span>';
}
if ($structure->isReadonly) {
$modifiers[] = '<span class="display-modifier">readonly</span>';
}
if ($structure->isInterface) {
$modifiers[] = '<span class="display-modifier display-interface">interface</span>';
}
if ($structure->isTrait) {
$modifiers[] = '<span class="display-modifier display-trait">trait</span>';
}
if ($structure->isEnum) {
$modifiers[] = '<span class="display-modifier display-enum">enum</span>';
}
if (! empty($modifiers)) {
$output[] = implode(' ', $modifiers);
}
// Parent class
if ($structure->parentClass !== null) {
$output[] = '<span class="display-keyword">extends</span> <span class="display-type-name">' . htmlspecialchars($structure->parentClass) . '</span>';
}
// Interfaces
if (! empty($structure->interfaces)) {
$interfacesList = implode(', ', array_map(fn (string $iface) => '<span class="display-type-name">' . htmlspecialchars($iface) . '</span>', $structure->interfaces));
$output[] = '<span class="display-keyword">implements</span> ' . $interfacesList;
}
$output[] = '</div>'; // header
$output[] = '<div class="display-class-body">';
// Properties
if (! empty($structure->properties)) {
$output[] = '<div class="display-section">';
$output[] = '<h3 class="display-section-title">Properties (' . count($structure->properties) . ')</h3>';
$output[] = '<div class="display-properties">';
foreach ($structure->properties as $property) {
$output[] = $this->renderPropertyInfo($property);
}
$output[] = '</div>';
$output[] = '</div>';
}
// Methods
if (! empty($structure->methods)) {
$output[] = '<div class="display-section">';
$output[] = '<h3 class="display-section-title">Methods (' . count($structure->methods) . ')</h3>';
$output[] = '<div class="display-methods">';
foreach ($structure->methods as $method) {
$output[] = $this->renderMethodInfo($method);
}
$output[] = '</div>';
$output[] = '</div>';
}
// Constants
if (! empty($structure->constants)) {
$output[] = '<div class="display-section">';
$output[] = '<h3 class="display-section-title">Constants (' . count($structure->constants) . ')</h3>';
$output[] = '<div class="display-constants">';
foreach ($structure->constants as $constant) {
$output[] = $this->renderConstantInfo($constant);
}
$output[] = '</div>';
$output[] = '</div>';
}
$output[] = '</div>'; // body
$output[] = '</div>'; // class
return implode("\n", $output);
}
public function renderDirectory(DirectoryStructure $structure, DisplayOptions $options): string
{
$output = [];
$output[] = '<div class="display-directory">';
$output[] = '<div class="display-directory-header">';
$output[] = '<span class="display-directory-path">' . htmlspecialchars($structure->getPathString()) . '</span>';
$output[] = '</div>';
if ($structure->isEmpty()) {
$output[] = '<div class="display-empty">(empty)</div>';
$output[] = '</div>';
return implode("\n", $output);
}
$output[] = $this->renderDirectoryTree($structure, $options);
$output[] = '</div>';
return implode("\n", $output);
}
private function renderDirectoryTree(DirectoryStructure $structure, DisplayOptions $options): string
{
$output = [];
$output[] = '<ul class="display-directory-tree">';
// Directories
foreach ($structure->directories as $name => $subDirectory) {
$output[] = '<li class="display-directory-item">';
$output[] = '<span class="display-directory-name">' . htmlspecialchars($name) . '/</span>';
if (! $subDirectory->isEmpty()) {
$output[] = $this->renderDirectoryTree($subDirectory, $options);
}
$output[] = '</li>';
}
// Files
foreach ($structure->files as $file) {
$output[] = '<li class="display-file-item">';
$output[] = '<span class="display-file-name">' . htmlspecialchars($file->name) . '</span>';
if ($options->showFileSizes) {
$output[] = '<span class="display-file-size">(' . htmlspecialchars($file->getFormattedSize()) . ')</span>';
}
$output[] = '</li>';
}
$output[] = '</ul>';
return implode("\n", $output);
}
private function renderPropertyInfo(PropertyInfo $property): string
{
$modifiers = [];
if ($property->isStatic) {
$modifiers[] = '<span class="display-modifier">static</span>';
}
if ($property->isReadonly) {
$modifiers[] = '<span class="display-modifier">readonly</span>';
}
$modifiersStr = ! empty($modifiers) ? implode(' ', $modifiers) . ' ' : '';
$nullable = $property->isNullable ? '?' : '';
return '<div class="display-property-info">' .
$modifiersStr .
'<span class="display-visibility">' . htmlspecialchars($property->getVisibilityString()) . '</span> ' .
$nullable . '<span class="display-type">' . htmlspecialchars($property->type) . '</span> ' .
'<span class="display-property-name">$' . htmlspecialchars($property->name) . '</span>' .
'</div>';
}
private function renderMethodInfo(MethodInfo $method): string
{
$modifiers = [];
if ($method->isStatic) {
$modifiers[] = '<span class="display-modifier">static</span>';
}
if ($method->isAbstract) {
$modifiers[] = '<span class="display-modifier">abstract</span>';
}
if ($method->isFinal) {
$modifiers[] = '<span class="display-modifier">final</span>';
}
$modifiersStr = ! empty($modifiers) ? implode(' ', $modifiers) . ' ' : '';
$params = implode(', ', array_map(fn (string $param) => '<span class="display-parameter">' . htmlspecialchars($param) . '</span>', $method->parameters));
return '<div class="display-method-info">' .
$modifiersStr .
'<span class="display-visibility">' . htmlspecialchars($method->getVisibilityString()) . '</span> ' .
'<span class="display-keyword">function</span> ' .
'<span class="display-method-name">' . htmlspecialchars($method->name) . '</span>' .
'(<span class="display-parameters">' . $params . '</span>): ' .
'<span class="display-type">' . htmlspecialchars($method->returnType) . '</span>' .
'</div>';
}
private function renderConstantInfo(ConstantInfo $constant): string
{
$modifiers = $constant->isFinal ? '<span class="display-modifier">final </span>' : '';
$value = $this->formatValue($constant->value, DisplayOptions::default());
return '<div class="display-constant-info">' .
$modifiers .
'<span class="display-visibility">' . htmlspecialchars($constant->getVisibilityString()) . '</span> ' .
'<span class="display-keyword">const</span> ' .
'<span class="display-constant-name">' . htmlspecialchars($constant->name) . '</span> = ' .
$value .
'</div>';
}
private function formatValue(mixed $value, DisplayOptions $options): string
{
$theme = $this->getTheme($options);
return match (true) {
is_string($value) => '<span class="' . $theme->stringClass . '">"' . htmlspecialchars($value) . '"</span>',
is_int($value) => '<span class="' . $theme->numberClass . '">' . $value . '</span>',
is_float($value) => '<span class="' . $theme->numberClass . '">' . $value . '</span>',
is_bool($value) => '<span class="' . $theme->booleanClass . '">' . ($value ? 'true' : 'false') . '</span>',
is_null($value) => '<span class="' . $theme->nullClass . '">null</span>',
is_array($value) => '<span class="' . $theme->typeClass . '">[array:' . count($value) . ($options->showSizes ? ' items' : '') . ']</span>',
is_object($value) => '<span class="' . $theme->typeClass . '">[object:' . htmlspecialchars(get_class($value)) . ']</span>',
is_resource($value) => '<span class="' . $theme->typeClass . '">[resource]</span>',
default => '<span class="' . $theme->typeClass . '">' . htmlspecialchars(get_debug_type($value)) . '</span>',
};
}
private function formatValueObject(object $valueObject, DisplayOptions $options): string
{
// Use default registry if empty
$registry = empty($this->valueObjectRegistry->getAllFormatters())
? DefaultValueObjectFormatterRegistry::create()
: $this->valueObjectRegistry;
return $registry->format($valueObject, $options, forHtml: true);
}
private function formatSummaryHtml(ObjectStructure $structure): string
{
$info = $structure->summaryInfo;
$parts = [];
if (isset($info['property_count'])) {
$parts[] = $info['property_count'] . ' properties';
}
if (isset($info['property_counts_by_type'])) {
$typeCounts = [];
foreach ($info['property_counts_by_type'] as $type => $count) {
$typeCounts[] = htmlspecialchars($type) . '(' . $count . ')';
}
if (! empty($typeCounts)) {
$parts[] = 'types: ' . implode(', ', $typeCounts);
}
}
return '[summary: ' . implode(', ', $parts) . ']';
}
private function formatArraySummaryHtml(ArrayStructure $structure): string
{
$info = $structure->summaryInfo;
$parts = [];
if (isset($info['size'])) {
$parts[] = $info['size'] . ' items';
}
if (isset($info['type_distribution'])) {
$typeParts = [];
foreach ($info['type_distribution'] as $type => $data) {
$typeParts[] = htmlspecialchars($type) . '(' . $data['percentage'] . '%)';
}
if (! empty($typeParts)) {
$parts[] = 'types: ' . implode(', ', array_slice($typeParts, 0, 5));
}
}
return implode(', ', $parts);
}
/**
* Get theme for display options
*/
private function getTheme(DisplayOptions $options): HtmlTheme
{
// If custom theme is provided and it's an HtmlTheme, use it
if ($options->customTheme instanceof HtmlTheme) {
return $options->customTheme;
}
// Otherwise get theme by name
return DefaultThemes::getHtmlTheme($options->theme);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Themes;
use App\Framework\Console\ConsoleColor;
/**
* Theme for console output with color mappings for different element types
*/
final readonly class ConsoleTheme
{
public function __construct(
public ConsoleColor $keyColor = ConsoleColor::YELLOW,
public ConsoleColor $valueColor = ConsoleColor::WHITE,
public ConsoleColor $stringColor = ConsoleColor::GREEN,
public ConsoleColor $numberColor = ConsoleColor::BRIGHT_BLUE,
public ConsoleColor $booleanColor = ConsoleColor::BRIGHT_MAGENTA,
public ConsoleColor $nullColor = ConsoleColor::GRAY,
public ConsoleColor $typeColor = ConsoleColor::GRAY,
public ConsoleColor $classNameColor = ConsoleColor::BRIGHT_CYAN,
public ConsoleColor $errorColor = ConsoleColor::BRIGHT_RED,
public ConsoleColor $warningColor = ConsoleColor::BRIGHT_YELLOW,
public ConsoleColor $infoColor = ConsoleColor::CYAN,
public ConsoleColor $circularReferenceColor = ConsoleColor::BRIGHT_RED,
public ConsoleColor $collectionColor = ConsoleColor::BRIGHT_CYAN,
public ConsoleColor $summaryColor = ConsoleColor::GRAY,
public ConsoleColor $arrayBracketColor = ConsoleColor::WHITE,
public ConsoleColor $objectBracketColor = ConsoleColor::WHITE,
public ConsoleColor $separatorColor = ConsoleColor::GRAY,
public ConsoleColor $metadataColor = ConsoleColor::GRAY,
) {
}
/**
* Create a new theme with some colors overridden
*/
public function withColors(
?ConsoleColor $keyColor = null,
?ConsoleColor $valueColor = null,
?ConsoleColor $stringColor = null,
?ConsoleColor $numberColor = null,
?ConsoleColor $booleanColor = null,
?ConsoleColor $nullColor = null,
?ConsoleColor $typeColor = null,
?ConsoleColor $classNameColor = null,
?ConsoleColor $errorColor = null,
?ConsoleColor $warningColor = null,
?ConsoleColor $infoColor = null,
?ConsoleColor $circularReferenceColor = null,
?ConsoleColor $collectionColor = null,
?ConsoleColor $summaryColor = null,
?ConsoleColor $arrayBracketColor = null,
?ConsoleColor $objectBracketColor = null,
?ConsoleColor $separatorColor = null,
?ConsoleColor $metadataColor = null,
): self {
return new self(
keyColor: $keyColor ?? $this->keyColor,
valueColor: $valueColor ?? $this->valueColor,
stringColor: $stringColor ?? $this->stringColor,
numberColor: $numberColor ?? $this->numberColor,
booleanColor: $booleanColor ?? $this->booleanColor,
nullColor: $nullColor ?? $this->nullColor,
typeColor: $typeColor ?? $this->typeColor,
classNameColor: $classNameColor ?? $this->classNameColor,
errorColor: $errorColor ?? $this->errorColor,
warningColor: $warningColor ?? $this->warningColor,
infoColor: $infoColor ?? $this->infoColor,
circularReferenceColor: $circularReferenceColor ?? $this->circularReferenceColor,
collectionColor: $collectionColor ?? $this->collectionColor,
summaryColor: $summaryColor ?? $this->summaryColor,
arrayBracketColor: $arrayBracketColor ?? $this->arrayBracketColor,
objectBracketColor: $objectBracketColor ?? $this->objectBracketColor,
separatorColor: $separatorColor ?? $this->separatorColor,
metadataColor: $metadataColor ?? $this->metadataColor,
);
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Themes;
use App\Framework\Console\ConsoleColor;
/**
* Factory for creating default console themes
*/
final readonly class DefaultConsoleThemes
{
public static function default(): ConsoleTheme
{
return new ConsoleTheme();
}
public static function dark(): ConsoleTheme
{
return new ConsoleTheme(
keyColor: ConsoleColor::BRIGHT_YELLOW,
valueColor: ConsoleColor::BRIGHT_WHITE,
stringColor: ConsoleColor::BRIGHT_GREEN,
numberColor: ConsoleColor::BRIGHT_BLUE,
booleanColor: ConsoleColor::BRIGHT_MAGENTA,
nullColor: ConsoleColor::GRAY,
typeColor: ConsoleColor::GRAY,
classNameColor: ConsoleColor::BRIGHT_CYAN,
errorColor: ConsoleColor::BRIGHT_RED,
warningColor: ConsoleColor::BRIGHT_YELLOW,
infoColor: ConsoleColor::CYAN,
circularReferenceColor: ConsoleColor::BRIGHT_RED,
collectionColor: ConsoleColor::BRIGHT_CYAN,
summaryColor: ConsoleColor::GRAY,
arrayBracketColor: ConsoleColor::WHITE,
objectBracketColor: ConsoleColor::WHITE,
separatorColor: ConsoleColor::GRAY,
metadataColor: ConsoleColor::GRAY,
);
}
public static function light(): ConsoleTheme
{
return new ConsoleTheme(
keyColor: ConsoleColor::YELLOW,
valueColor: ConsoleColor::BLACK,
stringColor: ConsoleColor::GREEN,
numberColor: ConsoleColor::BLUE,
booleanColor: ConsoleColor::MAGENTA,
nullColor: ConsoleColor::GRAY,
typeColor: ConsoleColor::GRAY,
classNameColor: ConsoleColor::CYAN,
errorColor: ConsoleColor::RED,
warningColor: ConsoleColor::YELLOW,
infoColor: ConsoleColor::CYAN,
circularReferenceColor: ConsoleColor::RED,
collectionColor: ConsoleColor::CYAN,
summaryColor: ConsoleColor::GRAY,
arrayBracketColor: ConsoleColor::BLACK,
objectBracketColor: ConsoleColor::BLACK,
separatorColor: ConsoleColor::GRAY,
metadataColor: ConsoleColor::GRAY,
);
}
public static function colorful(): ConsoleTheme
{
return new ConsoleTheme(
keyColor: ConsoleColor::BRIGHT_YELLOW,
valueColor: ConsoleColor::BRIGHT_WHITE,
stringColor: ConsoleColor::BRIGHT_GREEN,
numberColor: ConsoleColor::BRIGHT_BLUE,
booleanColor: ConsoleColor::BRIGHT_MAGENTA,
nullColor: ConsoleColor::GRAY,
typeColor: ConsoleColor::CYAN,
classNameColor: ConsoleColor::BRIGHT_CYAN,
errorColor: ConsoleColor::BRIGHT_RED,
warningColor: ConsoleColor::BRIGHT_YELLOW,
infoColor: ConsoleColor::BRIGHT_CYAN,
circularReferenceColor: ConsoleColor::BRIGHT_RED,
collectionColor: ConsoleColor::BRIGHT_MAGENTA,
summaryColor: ConsoleColor::CYAN,
arrayBracketColor: ConsoleColor::BRIGHT_WHITE,
objectBracketColor: ConsoleColor::BRIGHT_WHITE,
separatorColor: ConsoleColor::BRIGHT_CYAN,
metadataColor: ConsoleColor::CYAN,
);
}
public static function minimal(): ConsoleTheme
{
return new ConsoleTheme(
keyColor: ConsoleColor::GRAY,
valueColor: ConsoleColor::WHITE,
stringColor: ConsoleColor::GRAY,
numberColor: ConsoleColor::GRAY,
booleanColor: ConsoleColor::GRAY,
nullColor: ConsoleColor::GRAY,
typeColor: ConsoleColor::GRAY,
classNameColor: ConsoleColor::WHITE,
errorColor: ConsoleColor::WHITE,
warningColor: ConsoleColor::WHITE,
infoColor: ConsoleColor::GRAY,
circularReferenceColor: ConsoleColor::WHITE,
collectionColor: ConsoleColor::WHITE,
summaryColor: ConsoleColor::GRAY,
arrayBracketColor: ConsoleColor::WHITE,
objectBracketColor: ConsoleColor::WHITE,
separatorColor: ConsoleColor::GRAY,
metadataColor: ConsoleColor::GRAY,
);
}
public static function colorblind(): ConsoleTheme
{
// Color-blind friendly palette using shapes and brightness differences
return new ConsoleTheme(
keyColor: ConsoleColor::BRIGHT_WHITE,
valueColor: ConsoleColor::WHITE,
stringColor: ConsoleColor::BRIGHT_GREEN,
numberColor: ConsoleColor::BRIGHT_BLUE,
booleanColor: ConsoleColor::BRIGHT_MAGENTA,
nullColor: ConsoleColor::GRAY,
typeColor: ConsoleColor::CYAN,
classNameColor: ConsoleColor::BRIGHT_CYAN,
errorColor: ConsoleColor::BRIGHT_RED,
warningColor: ConsoleColor::BRIGHT_YELLOW,
infoColor: ConsoleColor::CYAN,
circularReferenceColor: ConsoleColor::BRIGHT_RED,
collectionColor: ConsoleColor::BRIGHT_CYAN,
summaryColor: ConsoleColor::CYAN,
arrayBracketColor: ConsoleColor::BRIGHT_WHITE,
objectBracketColor: ConsoleColor::BRIGHT_WHITE,
separatorColor: ConsoleColor::WHITE,
metadataColor: ConsoleColor::CYAN,
);
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Themes;
/**
* Factory for creating default HTML themes
*/
final readonly class DefaultHtmlThemes
{
public static function default(): HtmlTheme
{
return new HtmlTheme(
themeClass: 'display-theme-default',
);
}
public static function dark(): HtmlTheme
{
return new HtmlTheme(
keyClass: 'display-key-dark',
valueClass: 'display-value-dark',
stringClass: 'display-string-dark',
numberClass: 'display-number-dark',
booleanClass: 'display-boolean-dark',
nullClass: 'display-null-dark',
typeClass: 'display-type-dark',
classNameClass: 'display-class-name-dark',
errorClass: 'display-error-dark',
warningClass: 'display-warning-dark',
infoClass: 'display-info-dark',
circularReferenceClass: 'display-circular-reference-dark',
collectionClass: 'display-collection-dark',
summaryClass: 'display-summary-dark',
arrayBracketClass: 'display-array-bracket-dark',
objectBracketClass: 'display-object-bracket-dark',
separatorClass: 'display-separator-dark',
metadataClass: 'display-metadata-dark',
themeClass: 'display-theme-dark',
);
}
public static function light(): HtmlTheme
{
return new HtmlTheme(
keyClass: 'display-key-light',
valueClass: 'display-value-light',
stringClass: 'display-string-light',
numberClass: 'display-number-light',
booleanClass: 'display-boolean-light',
nullClass: 'display-null-light',
typeClass: 'display-type-light',
classNameClass: 'display-class-name-light',
errorClass: 'display-error-light',
warningClass: 'display-warning-light',
infoClass: 'display-info-light',
circularReferenceClass: 'display-circular-reference-light',
collectionClass: 'display-collection-light',
summaryClass: 'display-summary-light',
arrayBracketClass: 'display-array-bracket-light',
objectBracketClass: 'display-object-bracket-light',
separatorClass: 'display-separator-light',
metadataClass: 'display-metadata-light',
themeClass: 'display-theme-light',
);
}
public static function colorful(): HtmlTheme
{
return new HtmlTheme(
keyClass: 'display-key-colorful',
valueClass: 'display-value-colorful',
stringClass: 'display-string-colorful',
numberClass: 'display-number-colorful',
booleanClass: 'display-boolean-colorful',
nullClass: 'display-null-colorful',
typeClass: 'display-type-colorful',
classNameClass: 'display-class-name-colorful',
errorClass: 'display-error-colorful',
warningClass: 'display-warning-colorful',
infoClass: 'display-info-colorful',
circularReferenceClass: 'display-circular-reference-colorful',
collectionClass: 'display-collection-colorful',
summaryClass: 'display-summary-colorful',
arrayBracketClass: 'display-array-bracket-colorful',
objectBracketClass: 'display-object-bracket-colorful',
separatorClass: 'display-separator-colorful',
metadataClass: 'display-metadata-colorful',
themeClass: 'display-theme-colorful',
);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Themes;
/**
* Factory for creating default theme registry with all built-in themes
*/
final readonly class DefaultThemes
{
public static function createRegistry(): ThemeRegistry
{
$registry = new ThemeRegistry();
// Register console themes
$registry->registerConsoleTheme('default', DefaultConsoleThemes::default());
$registry->registerConsoleTheme('dark', DefaultConsoleThemes::dark());
$registry->registerConsoleTheme('light', DefaultConsoleThemes::light());
$registry->registerConsoleTheme('colorful', DefaultConsoleThemes::colorful());
$registry->registerConsoleTheme('minimal', DefaultConsoleThemes::minimal());
$registry->registerConsoleTheme('colorblind', DefaultConsoleThemes::colorblind());
// Register HTML themes
$registry->registerHtmlTheme('default', DefaultHtmlThemes::default());
$registry->registerHtmlTheme('dark', DefaultHtmlThemes::dark());
$registry->registerHtmlTheme('light', DefaultHtmlThemes::light());
$registry->registerHtmlTheme('colorful', DefaultHtmlThemes::colorful());
return $registry;
}
public static function getConsoleTheme(string $name): ConsoleTheme
{
return match ($name) {
'default' => DefaultConsoleThemes::default(),
'dark' => DefaultConsoleThemes::dark(),
'light' => DefaultConsoleThemes::light(),
'colorful' => DefaultConsoleThemes::colorful(),
'minimal' => DefaultConsoleThemes::minimal(),
'colorblind' => DefaultConsoleThemes::colorblind(),
default => DefaultConsoleThemes::default(),
};
}
public static function getHtmlTheme(string $name): HtmlTheme
{
return match ($name) {
'default' => DefaultHtmlThemes::default(),
'dark' => DefaultHtmlThemes::dark(),
'light' => DefaultHtmlThemes::light(),
'colorful' => DefaultHtmlThemes::colorful(),
default => DefaultHtmlThemes::default(),
};
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Themes;
/**
* Theme for HTML output with CSS class mappings for different element types
*/
final readonly class HtmlTheme
{
public function __construct(
public string $keyClass = 'display-key',
public string $valueClass = 'display-value',
public string $stringClass = 'display-string',
public string $numberClass = 'display-number',
public string $booleanClass = 'display-boolean',
public string $nullClass = 'display-null',
public string $typeClass = 'display-type',
public string $classNameClass = 'display-class-name',
public string $errorClass = 'display-error',
public string $warningClass = 'display-warning',
public string $infoClass = 'display-info',
public string $circularReferenceClass = 'display-circular-reference',
public string $collectionClass = 'display-collection',
public string $summaryClass = 'display-summary',
public string $arrayBracketClass = 'display-array-bracket',
public string $objectBracketClass = 'display-object-bracket',
public string $separatorClass = 'display-separator',
public string $metadataClass = 'display-metadata',
public string $themeClass = 'display-theme-default',
) {
}
/**
* Create a new theme with some classes overridden
*/
public function withClasses(
?string $keyClass = null,
?string $valueClass = null,
?string $stringClass = null,
?string $numberClass = null,
?string $booleanClass = null,
?string $nullClass = null,
?string $typeClass = null,
?string $classNameClass = null,
?string $errorClass = null,
?string $warningClass = null,
?string $infoClass = null,
?string $circularReferenceClass = null,
?string $collectionClass = null,
?string $summaryClass = null,
?string $arrayBracketClass = null,
?string $objectBracketClass = null,
?string $separatorClass = null,
?string $metadataClass = null,
?string $themeClass = null,
): self {
return new self(
keyClass: $keyClass ?? $this->keyClass,
valueClass: $valueClass ?? $this->valueClass,
stringClass: $stringClass ?? $this->stringClass,
numberClass: $numberClass ?? $this->numberClass,
booleanClass: $booleanClass ?? $this->booleanClass,
nullClass: $nullClass ?? $this->nullClass,
typeClass: $typeClass ?? $this->typeClass,
classNameClass: $classNameClass ?? $this->classNameClass,
errorClass: $errorClass ?? $this->errorClass,
warningClass: $warningClass ?? $this->warningClass,
infoClass: $infoClass ?? $this->infoClass,
circularReferenceClass: $circularReferenceClass ?? $this->circularReferenceClass,
collectionClass: $collectionClass ?? $this->collectionClass,
summaryClass: $summaryClass ?? $this->summaryClass,
arrayBracketClass: $arrayBracketClass ?? $this->arrayBracketClass,
objectBracketClass: $objectBracketClass ?? $this->objectBracketClass,
separatorClass: $separatorClass ?? $this->separatorClass,
metadataClass: $metadataClass ?? $this->metadataClass,
themeClass: $themeClass ?? $this->themeClass,
);
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\Themes;
/**
* Registry for managing themes by name
*/
final class ThemeRegistry
{
/** @var array<string, ConsoleTheme> */
private array $consoleThemes = [];
/** @var array<string, HtmlTheme> */
private array $htmlThemes = [];
public function registerConsoleTheme(string $name, ConsoleTheme $theme): void
{
$this->consoleThemes[$name] = $theme;
}
public function registerHtmlTheme(string $name, HtmlTheme $theme): void
{
$this->htmlThemes[$name] = $theme;
}
public function getConsoleTheme(string $name): ?ConsoleTheme
{
return $this->consoleThemes[$name] ?? null;
}
public function getHtmlTheme(string $name): ?HtmlTheme
{
return $this->htmlThemes[$name] ?? null;
}
/**
* Get all registered console theme names
*
* @return array<string>
*/
public function getConsoleThemeNames(): array
{
return array_keys($this->consoleThemes);
}
/**
* Get all registered HTML theme names
*
* @return array<string>
*/
public function getHtmlThemeNames(): array
{
return array_keys($this->htmlThemes);
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjectFormatters;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Display\ValueObjects\DisplayOptions;
/**
* Formatter for Byte Value Objects with human-readable formatting
*/
final readonly class ByteFormatter implements ValueObjectFormatterInterface
{
public function supports(object $valueObject): bool
{
return $valueObject instanceof Byte;
}
public function formatConsole(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof Byte) {
return (string) $valueObject;
}
$humanReadable = $valueObject->toHumanReadable(2);
$bytes = $valueObject->toBytes();
// Color-code by size range
$color = match (true) {
$bytes < 1024 => ConsoleColor::WHITE, // Bytes
$bytes < 1024 * 1024 => ConsoleColor::GREEN, // KB
$bytes < 1024 * 1024 * 1024 => ConsoleColor::YELLOW, // MB
$bytes < 1024 * 1024 * 1024 * 1024 => ConsoleColor::BRIGHT_YELLOW, // GB
default => ConsoleColor::BRIGHT_RED, // TB+
};
return ConsoleStyle::create(color: $color)->apply($humanReadable);
}
public function formatHtml(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof Byte) {
return htmlspecialchars((string) $valueObject);
}
$humanReadable = $valueObject->toHumanReadable(2);
$bytes = $valueObject->toBytes();
// CSS class based on size range
$cssClass = match (true) {
$bytes < 1024 => 'display-byte-size',
$bytes < 1024 * 1024 => 'display-kb-size',
$bytes < 1024 * 1024 * 1024 => 'display-mb-size',
$bytes < 1024 * 1024 * 1024 * 1024 => 'display-gb-size',
default => 'display-tb-size',
};
return '<span class="' . $cssClass . '">' . htmlspecialchars($humanReadable) . '</span>';
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjectFormatters;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Display\ValueObjects\DisplayOptions;
/**
* Formatter for ClassName Value Objects with namespace highlighting
*/
final readonly class ClassNameFormatter implements ValueObjectFormatterInterface
{
public function supports(object $valueObject): bool
{
return $valueObject instanceof ClassName;
}
public function formatConsole(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof ClassName) {
return (string) $valueObject;
}
$namespace = $valueObject->getNamespace();
$shortName = $valueObject->getShortName();
if ($namespace === '') {
// Global namespace - just show class name
return ConsoleStyle::create(color: ConsoleColor::BRIGHT_CYAN)->apply($shortName);
}
// Namespace in gray, class name highlighted
$namespaceStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
$classNameStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_CYAN);
return $namespaceStyle->apply($namespace . '\\') . $classNameStyle->apply($shortName);
}
public function formatHtml(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof ClassName) {
return htmlspecialchars((string) $valueObject);
}
$namespace = $valueObject->getNamespace();
$shortName = $valueObject->getShortName();
if ($namespace === '') {
return '<span class="display-class-name">' . htmlspecialchars($shortName) . '</span>';
}
return '<span class="display-namespace">' . htmlspecialchars($namespace . '\\') . '</span>' .
'<span class="display-class-name">' . htmlspecialchars($shortName) . '</span>';
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjectFormatters;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\ValueObjects\DisplayOptions;
use Stringable;
/**
* Formatter for email-like strings (Value Objects that represent emails)
*/
final readonly class EmailFormatter implements ValueObjectFormatterInterface
{
public function supports(object $valueObject): bool
{
// Check if it's a Stringable that looks like an email
if (! $valueObject instanceof Stringable) {
return false;
}
$stringValue = (string) $valueObject;
// Simple email validation
return filter_var($stringValue, FILTER_VALIDATE_EMAIL) !== false;
}
public function formatConsole(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof Stringable) {
return (string) $valueObject;
}
$email = (string) $valueObject;
$parts = explode('@', $email, 2);
if (count($parts) !== 2) {
return (string) $valueObject;
}
[$localPart, $domain] = $parts;
$localStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_CYAN);
$atStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
$domainStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
return $localStyle->apply($localPart) . $atStyle->apply('@') . $domainStyle->apply($domain);
}
public function formatHtml(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof Stringable) {
return htmlspecialchars((string) $valueObject);
}
$email = (string) $valueObject;
$parts = explode('@', $email, 2);
if (count($parts) !== 2) {
return htmlspecialchars((string) $valueObject);
}
[$localPart, $domain] = $parts;
return '<span class="display-email-local">' . htmlspecialchars($localPart) . '</span>' .
'<span class="display-email-at">@</span>' .
'<span class="display-email-domain">' . htmlspecialchars($domain) . '</span>';
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjectFormatters;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Display\ValueObjects\DisplayOptions;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* Formatter for FilePath Value Objects with path highlighting
*/
final readonly class FilePathFormatter implements ValueObjectFormatterInterface
{
public function supports(object $valueObject): bool
{
return $valueObject instanceof FilePath;
}
public function formatConsole(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof FilePath) {
return (string) $valueObject;
}
$path = $valueObject->toString();
$directory = $valueObject->getDirectory();
$filename = $valueObject->getFilename();
$extension = $valueObject->getExtension();
if ($directory === '') {
// Just filename
if ($extension !== '') {
$basename = substr($filename, 0, -strlen($extension) - 1);
$fileStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
$extStyle = ConsoleStyle::create(color: ConsoleColor::CYAN);
return $fileStyle->apply($basename) . $extStyle->apply('.' . $extension);
}
return ConsoleStyle::create(color: ConsoleColor::WHITE)->apply($filename);
}
// Directory in gray, filename highlighted, extension in cyan
$dirStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
$fileStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
$extStyle = ConsoleStyle::create(color: ConsoleColor::CYAN);
$output = $dirStyle->apply($directory . DIRECTORY_SEPARATOR);
if ($extension !== '') {
$basename = substr($filename, 0, -strlen($extension) - 1);
$output .= $fileStyle->apply($basename) . $extStyle->apply('.' . $extension);
} else {
$output .= $fileStyle->apply($filename);
}
return $output;
}
public function formatHtml(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof FilePath) {
return htmlspecialchars((string) $valueObject);
}
$path = $valueObject->toString();
$directory = $valueObject->getDirectory();
$filename = $valueObject->getFilename();
$extension = $valueObject->getExtension();
if ($directory === '') {
if ($extension !== '') {
$basename = substr($filename, 0, -strlen($extension) - 1);
return '<span class="display-filename">' . htmlspecialchars($basename) . '</span>' .
'<span class="display-extension">.' . htmlspecialchars($extension) . '</span>';
}
return '<span class="display-filename">' . htmlspecialchars($filename) . '</span>';
}
$output = '<span class="display-directory-path">' . htmlspecialchars($directory . DIRECTORY_SEPARATOR) . '</span>';
if ($extension !== '') {
$basename = substr($filename, 0, -strlen($extension) - 1);
$output .= '<span class="display-filename">' . htmlspecialchars($basename) . '</span>' .
'<span class="display-extension">.' . htmlspecialchars($extension) . '</span>';
} else {
$output .= '<span class="display-filename">' . htmlspecialchars($filename) . '</span>';
}
return $output;
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjectFormatters;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Display\ValueObjects\DisplayOptions;
/**
* Formatter for Timestamp Value Objects with formatted date/time
*/
final readonly class TimestampFormatter implements ValueObjectFormatterInterface
{
public function supports(object $valueObject): bool
{
return $valueObject instanceof Timestamp;
}
public function formatConsole(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof Timestamp) {
return (string) $valueObject;
}
$formatted = $valueObject->format('Y-m-d H:i:s');
$now = time();
$diff = abs($valueObject->toTimestamp() - $now);
// Color-code by recency
$color = match (true) {
$diff < 60 => ConsoleColor::GREEN, // Less than 1 minute
$diff < 3600 => ConsoleColor::YELLOW, // Less than 1 hour
$diff < 86400 => ConsoleColor::BRIGHT_YELLOW, // Less than 1 day
$diff < 604800 => ConsoleColor::WHITE, // Less than 1 week
default => ConsoleColor::GRAY, // Older
};
$relative = $this->getRelativeTime($valueObject->toTimestamp(), $now);
return ConsoleStyle::create(color: $color)->apply($formatted) .
' ' . ConsoleStyle::create(color: ConsoleColor::GRAY)->apply('(' . $relative . ')');
}
public function formatHtml(object $valueObject, DisplayOptions $options): string
{
if (! $valueObject instanceof Timestamp) {
return htmlspecialchars((string) $valueObject);
}
$formatted = $valueObject->format('Y-m-d H:i:s');
$now = time();
$diff = abs($valueObject->toTimestamp() - $now);
// CSS class based on recency
$cssClass = match (true) {
$diff < 60 => 'display-timestamp-recent',
$diff < 3600 => 'display-timestamp-hour',
$diff < 86400 => 'display-timestamp-day',
$diff < 604800 => 'display-timestamp-week',
default => 'display-timestamp-old',
};
$relative = $this->getRelativeTime($valueObject->toTimestamp(), $now);
return '<span class="' . $cssClass . '">' . htmlspecialchars($formatted) . '</span>' .
' <span class="display-timestamp-relative">(' . htmlspecialchars($relative) . ')</span>';
}
private function getRelativeTime(int $timestamp, int $now): string
{
$diff = $now - $timestamp;
if ($diff < 0) {
$diff = abs($diff);
$future = true;
} else {
$future = false;
}
return match (true) {
$diff < 60 => $future ? 'in ' . $diff . 's' : $diff . 's ago',
$diff < 3600 => $future ? 'in ' . (int) ($diff / 60) . 'm' : (int) ($diff / 60) . 'm ago',
$diff < 86400 => $future ? 'in ' . (int) ($diff / 3600) . 'h' : (int) ($diff / 3600) . 'h ago',
$diff < 604800 => $future ? 'in ' . (int) ($diff / 86400) . 'd' : (int) ($diff / 86400) . 'd ago',
$diff < 2592000 => $future ? 'in ' . (int) ($diff / 604800) . 'w' : (int) ($diff / 604800) . 'w ago',
default => $future ? 'in the future' : 'long ago',
};
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjectFormatters;
use App\Framework\Display\ValueObjects\DisplayOptions;
/**
* Interface for custom Value Object formatters
*/
interface ValueObjectFormatterInterface
{
/**
* Check if this formatter supports the given Value Object
*/
public function supports(object $valueObject): bool;
/**
* Format Value Object for console output
*/
public function formatConsole(object $valueObject, DisplayOptions $options): string;
/**
* Format Value Object for HTML output
*/
public function formatHtml(object $valueObject, DisplayOptions $options): string;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjectRegistry;
use App\Framework\Display\ValueObjectFormatters\ByteFormatter;
use App\Framework\Display\ValueObjectFormatters\ClassNameFormatter;
use App\Framework\Display\ValueObjectFormatters\EmailFormatter;
use App\Framework\Display\ValueObjectFormatters\FilePathFormatter;
use App\Framework\Display\ValueObjectFormatters\TimestampFormatter;
/**
* Factory for creating default Value Object formatter registry with all built-in formatters
*/
final readonly class DefaultValueObjectFormatterRegistry
{
public static function create(): ValueObjectFormatterRegistry
{
$registry = new ValueObjectFormatterRegistry();
$registry->registerAll([
new ClassNameFormatter(),
new FilePathFormatter(),
new ByteFormatter(),
new TimestampFormatter(),
new EmailFormatter(),
]);
return $registry;
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjectRegistry;
use App\Framework\Display\ValueObjectFormatters\ValueObjectFormatterInterface;
use App\Framework\Display\ValueObjects\DisplayOptions;
/**
* Registry for Value Object formatters with priority-based matching
*/
final class ValueObjectFormatterRegistry
{
/**
* @var array<ValueObjectFormatterInterface>
*/
private array $formatters = [];
/**
* Register a Value Object formatter
* Formatters are checked in registration order (first match wins)
*/
public function register(ValueObjectFormatterInterface $formatter): void
{
$this->formatters[] = $formatter;
}
/**
* Register multiple formatters at once
*
* @param array<ValueObjectFormatterInterface> $formatters
*/
public function registerAll(array $formatters): void
{
foreach ($formatters as $formatter) {
$this->register($formatter);
}
}
/**
* Find a formatter that supports the given Value Object
*/
public function findFormatter(object $valueObject): ?ValueObjectFormatterInterface
{
foreach ($this->formatters as $formatter) {
if ($formatter->supports($valueObject)) {
return $formatter;
}
}
return null;
}
/**
* Format Value Object using registered formatters or fallback to string representation
*/
public function format(object $valueObject, DisplayOptions $options, bool $forHtml = false): string
{
$formatter = $this->findFormatter($valueObject);
if ($formatter !== null) {
return $forHtml
? $formatter->formatHtml($valueObject, $options)
: $formatter->formatConsole($valueObject, $options);
}
// Fallback to string representation
if ($valueObject instanceof \Stringable) {
return (string) $valueObject;
}
if (method_exists($valueObject, 'toString')) {
return $valueObject->toString();
}
// Last resort: class name
return get_class($valueObject);
}
/**
* Get all registered formatters
*
* @return array<ValueObjectFormatterInterface>
*/
public function getAllFormatters(): array
{
return $this->formatters;
}
/**
* Clear all registered formatters
*/
public function clear(): void
{
$this->formatters = [];
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Normalized array representation for display
*/
final readonly class ArrayStructure
{
/**
* @param array<string|int, mixed> $items
* @param array<string|int, ArrayStructure> $nestedArrays
*/
public function __construct(
public array $items,
public bool $isAssociative,
public int $depth,
public int $count,
public array $keys,
public array $valueTypes,
public array $nestedArrays = [],
public array $statistics = [],
public bool $isCircularReference = false,
public bool $isNested = false,
public bool $isSummary = false,
public array $summaryInfo = [],
public ?int $truncatedCount = null,
) {
}
public static function fromArray(array $data, int $maxDepth = 10, int $currentDepth = 0): self
{
if (empty($data)) {
return new self(
items: [],
isAssociative: false,
depth: 0,
count: 0,
keys: [],
valueTypes: [],
nestedArrays: [],
statistics: [],
isCircularReference: false,
isNested: $currentDepth > 0,
isSummary: false,
summaryInfo: [],
truncatedCount: null,
);
}
$keys = array_keys($data);
$isAssociative = ! self::isIndexed($keys);
$count = count($data);
$valueTypes = [];
$nestedArrays = [];
$items = [];
foreach ($data as $key => $value) {
$valueType = get_debug_type($value);
$valueTypes[$key] = $valueType;
if (is_array($value) && $currentDepth < $maxDepth) {
$nestedArrays[$key] = self::fromArray($value, $maxDepth, $currentDepth + 1);
$items[$key] = '[array:' . count($value) . ']';
} else {
$items[$key] = $value;
}
}
$depth = $currentDepth;
if (! empty($nestedArrays)) {
$maxNestedDepth = max(array_map(
fn (ArrayStructure $nested) => $nested->depth,
$nestedArrays
));
$depth = max($depth, $maxNestedDepth);
}
$statistics = [
'total_items' => $count,
'associative' => $isAssociative ? 1 : 0,
'indexed' => $isAssociative ? 0 : 1,
'nested_count' => count($nestedArrays),
'unique_types' => count(array_unique($valueTypes)),
];
return new self(
items: $items,
isAssociative: $isAssociative,
depth: $depth,
count: $count,
keys: $keys,
valueTypes: $valueTypes,
nestedArrays: $nestedArrays,
statistics: $statistics,
isCircularReference: false,
isNested: $currentDepth > 0,
isSummary: false,
summaryInfo: [],
truncatedCount: null,
);
}
private static function isIndexed(array $keys): bool
{
if (empty($keys)) {
return true;
}
$expected = 0;
foreach ($keys as $key) {
if (! is_int($key) || $key !== $expected) {
return false;
}
$expected++;
}
return true;
}
public function hasNestedArrays(): bool
{
return ! empty($this->nestedArrays);
}
public function isEmpty(): bool
{
return $this->count === 0;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Enum for bracket styles in display output
*/
enum BracketStyle: string
{
case CURLY = 'curly';
case SQUARE = 'square';
case NONE = 'none';
/**
* Get opening bracket character
*/
public function getOpen(): string
{
return match ($this) {
self::SQUARE => '[',
self::NONE => '',
default => '{',
};
}
/**
* Get closing bracket character
*/
public function getClose(): string
{
return match ($this) {
self::SQUARE => ']',
self::NONE => '',
default => '}',
};
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Normalized class representation for display
*/
final readonly class ClassStructure
{
/**
* @param array<MethodInfo> $methods
* @param array<PropertyInfo> $properties
* @param array<ConstantInfo> $constants
* @param array<string> $interfaces
* @param array<string> $attributes
*/
public function __construct(
public string $className,
public string $namespace,
public ?string $parentClass,
public array $methods,
public array $properties,
public array $constants,
public array $interfaces,
public array $attributes,
public bool $isFinal,
public bool $isAbstract,
public bool $isReadonly,
public bool $isInterface,
public bool $isTrait,
public bool $isEnum,
public array $metadata = [],
) {
}
public function getMethodCount(): int
{
return count($this->methods);
}
public function getPropertyCount(): int
{
return count($this->properties);
}
public function getConstantCount(): int
{
return count($this->constants);
}
public function getFullName(): string
{
return $this->namespace !== '' ? $this->namespace . '\\' . $this->className : $this->className;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Enum for color depth in display output
*/
enum ColorDepth: string
{
case NONE = 'none';
case BASIC = 'basic';
case FULL = 'full';
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Information about a class constant
*/
final readonly class ConstantInfo
{
public function __construct(
public string $name,
public mixed $value,
public string $type,
public int $visibility,
public bool $isFinal,
) {
}
public function isPublic(): bool
{
return ($this->visibility & \ReflectionClassConstant::IS_PUBLIC) !== 0;
}
public function isProtected(): bool
{
return ($this->visibility & \ReflectionClassConstant::IS_PROTECTED) !== 0;
}
public function isPrivate(): bool
{
return ($this->visibility & \ReflectionClassConstant::IS_PRIVATE) !== 0;
}
public function getVisibilityString(): string
{
if ($this->isPublic()) {
return 'public';
}
if ($this->isProtected()) {
return 'protected';
}
if ($this->isPrivate()) {
return 'private';
}
return 'unknown';
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* Normalized filesystem directory representation for display
*/
final readonly class DirectoryStructure
{
/**
* @param array<FileEntry> $files
* @param array<string, DirectoryStructure> $directories
*/
public function __construct(
public FilePath|string $path,
public array $files,
public array $directories,
public int $depth,
public int $totalFiles,
public int $totalDirectories,
public array $statistics = [],
) {
}
public function getPathString(): string
{
return $this->path instanceof FilePath ? $this->path->toString() : $this->path;
}
public function isEmpty(): bool
{
return empty($this->files) && empty($this->directories);
}
public function hasNestedDirectories(): bool
{
return ! empty($this->directories);
}
}

View File

@@ -0,0 +1,357 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
use App\Framework\Display\Themes\ConsoleTheme;
use App\Framework\Display\Themes\HtmlTheme;
use InvalidArgumentException;
/**
* Configuration options for displaying data structures.
*
* This Value Object holds all configuration options that control how data
* is analyzed and rendered. Use the factory methods or fluent interface
* to create configured instances.
*
* @see DisplayRules For filtering and performance rules
*/
final readonly class DisplayOptions
{
public function __construct(
public int $maxDepth = 10,
public string $colorScheme = 'default',
public string $theme = 'default',
public string|ConsoleTheme|HtmlTheme|null $customTheme = null,
public string $formatStyle = 'tree',
public bool $showPrivateProperties = false,
public bool $showFileSizes = true,
public bool $showFilePermissions = false,
public bool $showMetadata = true,
public bool $prettyPrint = true,
public int $indentSize = 2,
public IndentStyle $indentStyle = IndentStyle::SPACE,
public ?string $customIndent = null,
public bool $compactValueObjects = true,
public bool $showValueObjectType = true,
public SeparatorStyle $separatorStyle = SeparatorStyle::COLON,
public ?string $customSeparator = null,
public bool $lineWrap = false,
public int $maxLineLength = 80,
public BracketStyle $bracketStyle = BracketStyle::CURLY,
public bool $showTypes = false,
public bool $showSizes = false,
public ColorDepth $colorDepth = ColorDepth::FULL,
public ?DisplayRules $rules = null,
) {
if ($this->maxDepth < 0) {
throw new InvalidArgumentException('maxDepth must be >= 0');
}
if ($this->indentSize < 1) {
throw new InvalidArgumentException('indentSize must be >= 1');
}
if ($this->separatorStyle === SeparatorStyle::CUSTOM && $this->customSeparator === null) {
throw new InvalidArgumentException('customSeparator must be set when separatorStyle is CUSTOM');
}
if ($this->indentStyle === IndentStyle::CUSTOM && $this->customIndent === null) {
throw new InvalidArgumentException('customIndent must be set when indentStyle is CUSTOM');
}
if ($this->maxLineLength < 1) {
throw new InvalidArgumentException('maxLineLength must be >= 1');
}
}
/**
* Create default display options.
*
* Defaults:
* - maxDepth: 10
* - formatStyle: 'tree'
* - compactValueObjects: true
* - Includes default DisplayRules
*
* @return self Default options instance
*/
public static function default(): self
{
return new self(
rules: DisplayRules::default(),
);
}
/**
* Create compact display options.
*
* Optimized for minimal output:
* - maxDepth: 3
* - formatStyle: 'flat'
* - showMetadata: false
* - prettyPrint: false
* - showTypes: false
* - showSizes: false
*
* @return self Compact options instance
*/
public static function compact(): self
{
return new self(
maxDepth: 3,
formatStyle: 'flat',
showMetadata: false,
prettyPrint: false,
showTypes: false,
showSizes: false,
);
}
/**
* Create verbose display options.
*
* Shows maximum detail:
* - maxDepth: 20
* - showPrivateProperties: true
* - showFileSizes: true
* - showFilePermissions: true
* - showMetadata: true
* - showTypes: true
* - showSizes: true
*
* @return self Verbose options instance
*/
public static function verbose(): self
{
return new self(
maxDepth: 20,
showPrivateProperties: true,
showFileSizes: true,
showFilePermissions: true,
showMetadata: true,
showTypes: true,
showSizes: true,
);
}
/**
* Create debug display options.
*
* Optimized for debugging:
* - maxDepth: 20
* - showPrivateProperties: true
* - showTypes: true
* - showSizes: true
* - colorDepth: 'full'
* - theme: 'colorful'
* - showMetadata: true
*
* @return self Debug options instance
*/
public static function debug(): self
{
return new self(
maxDepth: 20,
showPrivateProperties: true,
showTypes: true,
showSizes: true,
colorDepth: ColorDepth::FULL,
theme: 'colorful',
showMetadata: true,
);
}
/**
* Create production display options.
*
* Optimized for production (minimal, no colors):
* - maxDepth: 3
* - showTypes: false
* - showSizes: false
* - colorDepth: 'none'
* - formatStyle: 'flat'
* - prettyPrint: false
*
* @return self Production options instance
*/
public static function production(): self
{
return new self(
maxDepth: 3,
showTypes: false,
showSizes: false,
colorDepth: ColorDepth::NONE,
formatStyle: 'flat',
prettyPrint: false,
);
}
/**
* Create pretty display options.
*
* Optimized for readability:
* - formatStyle: 'tree'
* - prettyPrint: true
* - indentSize: 4
* - lineWrap: true
* - maxLineLength: 80
*
* @return self Pretty options instance
*/
public static function pretty(): self
{
return new self(
formatStyle: 'tree',
prettyPrint: true,
indentSize: 4,
lineWrap: true,
maxLineLength: 80,
);
}
/**
* Create JSON-like formatting options.
*
* JSON-style formatting:
* - formatStyle: 'tree'
* - bracketStyle: 'square'
* - separatorStyle: 'colon'
* - prettyPrint: true
*
* @return self JSON-like options instance
*/
public static function json(): self
{
return new self(
formatStyle: 'tree',
bracketStyle: BracketStyle::SQUARE,
separatorStyle: SeparatorStyle::COLON,
prettyPrint: true,
);
}
/**
* Get display rules (creates default if not set).
*
* @return DisplayRules Active display rules
*/
public function getRules(): DisplayRules
{
return $this->rules ?? DisplayRules::default();
}
public function withMaxDepth(int $maxDepth): self
{
return new self(
maxDepth: $maxDepth,
colorScheme: $this->colorScheme,
theme: $this->theme,
customTheme: $this->customTheme,
formatStyle: $this->formatStyle,
showPrivateProperties: $this->showPrivateProperties,
showFileSizes: $this->showFileSizes,
showFilePermissions: $this->showFilePermissions,
showMetadata: $this->showMetadata,
prettyPrint: $this->prettyPrint,
indentSize: $this->indentSize,
indentStyle: $this->indentStyle,
customIndent: $this->customIndent,
compactValueObjects: $this->compactValueObjects,
showValueObjectType: $this->showValueObjectType,
separatorStyle: $this->separatorStyle,
customSeparator: $this->customSeparator,
lineWrap: $this->lineWrap,
maxLineLength: $this->maxLineLength,
bracketStyle: $this->bracketStyle,
showTypes: $this->showTypes,
showSizes: $this->showSizes,
colorDepth: $this->colorDepth,
rules: $this->rules,
);
}
public function withCompactValueObjects(bool $compactValueObjects): self
{
return new self(
maxDepth: $this->maxDepth,
colorScheme: $this->colorScheme,
theme: $this->theme,
customTheme: $this->customTheme,
formatStyle: $this->formatStyle,
showPrivateProperties: $this->showPrivateProperties,
showFileSizes: $this->showFileSizes,
showFilePermissions: $this->showFilePermissions,
showMetadata: $this->showMetadata,
prettyPrint: $this->prettyPrint,
indentSize: $this->indentSize,
indentStyle: $this->indentStyle,
customIndent: $this->customIndent,
compactValueObjects: $compactValueObjects,
showValueObjectType: $this->showValueObjectType,
separatorStyle: $this->separatorStyle,
customSeparator: $this->customSeparator,
lineWrap: $this->lineWrap,
maxLineLength: $this->maxLineLength,
bracketStyle: $this->bracketStyle,
showTypes: $this->showTypes,
showSizes: $this->showSizes,
colorDepth: $this->colorDepth,
rules: $this->rules,
);
}
/**
* Create a new instance with different display rules.
*
* @param DisplayRules $rules Display rules to use
* @return self New instance with updated rules
*/
public function withRules(DisplayRules $rules): self
{
return new self(
maxDepth: $this->maxDepth,
colorScheme: $this->colorScheme,
theme: $this->theme,
customTheme: $this->customTheme,
formatStyle: $this->formatStyle,
showPrivateProperties: $this->showPrivateProperties,
showFileSizes: $this->showFileSizes,
showFilePermissions: $this->showFilePermissions,
showMetadata: $this->showMetadata,
prettyPrint: $this->prettyPrint,
indentSize: $this->indentSize,
indentStyle: $this->indentStyle,
customIndent: $this->customIndent,
compactValueObjects: $this->compactValueObjects,
showValueObjectType: $this->showValueObjectType,
separatorStyle: $this->separatorStyle,
customSeparator: $this->customSeparator,
lineWrap: $this->lineWrap,
maxLineLength: $this->maxLineLength,
bracketStyle: $this->bracketStyle,
showTypes: $this->showTypes,
showSizes: $this->showSizes,
colorDepth: $this->colorDepth,
rules: $rules,
);
}
/**
* Get the separator string based on separatorStyle
*/
public function getSeparator(): string
{
return $this->separatorStyle->getSeparator($this->customSeparator);
}
/**
* Get the indentation string based on indentStyle
*/
public function getIndentString(): string
{
return $this->indentStyle->getIndentString($this->indentSize, $this->customIndent);
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Rules for filtering and displaying data structures.
*
* Controls performance optimizations and filtering behavior:
* - Blacklist: Classes that should never be displayed
* - Summary thresholds: When to show summaries instead of full content
* - Collection detection: Enable/disable collection detection
* - Truncation: Maximum items before truncation
*
* @see DisplayOptions For general display configuration
*/
final readonly class DisplayRules
{
/**
* @param array<string> $blacklistedClasses Classes that should never be displayed
* @param int $maxArraySizeForSummary Threshold for root arrays to show summary instead of full content
* @param int $maxArraySizeForSummaryNested Threshold for nested arrays to show summary
* @param int $maxObjectPropertiesForSummary Threshold for root objects to show summary
* @param int $maxObjectPropertiesForSummaryNested Threshold for nested objects to show summary
* @param int $maxItems Max items to show before truncation
* @param bool $collectionDetection Enable collection detection
* @param bool $compactNestedObjects More compact display for nested objects
*/
public function __construct(
public array $blacklistedClasses = [],
public int $maxArraySizeForSummary = 1000,
public int $maxArraySizeForSummaryNested = 100,
public int $maxObjectPropertiesForSummary = 50,
public int $maxObjectPropertiesForSummaryNested = 20,
public int $maxItems = 100,
public bool $collectionDetection = true,
public bool $compactNestedObjects = true,
) {
}
/**
* Create default display rules.
*
* Defaults:
* - Blacklists: DefaultContainer, DiscoveryRegistry
* - Array summary threshold: 1000 (root), 100 (nested)
* - Object summary threshold: 50 (root), 20 (nested)
* - Max items: 100
* - Collection detection: enabled
* - Compact nested objects: enabled
*
* @return self Default rules instance
*/
public static function default(): self
{
return new self(
blacklistedClasses: [
'App\\Framework\\DI\\DefaultContainer',
'App\\Framework\\Discovery\\DiscoveryRegistry',
],
maxArraySizeForSummary: 1000,
maxArraySizeForSummaryNested: 100,
maxObjectPropertiesForSummary: 50,
maxObjectPropertiesForSummaryNested: 20,
maxItems: 100,
collectionDetection: true,
compactNestedObjects: true,
);
}
public function withBlacklistedClasses(array $blacklistedClasses): self
{
return new self(
blacklistedClasses: $blacklistedClasses,
maxArraySizeForSummary: $this->maxArraySizeForSummary,
maxArraySizeForSummaryNested: $this->maxArraySizeForSummaryNested,
maxObjectPropertiesForSummary: $this->maxObjectPropertiesForSummary,
maxObjectPropertiesForSummaryNested: $this->maxObjectPropertiesForSummaryNested,
maxItems: $this->maxItems,
collectionDetection: $this->collectionDetection,
compactNestedObjects: $this->compactNestedObjects,
);
}
public function withMaxArraySizeForSummary(int $maxArraySizeForSummary): self
{
return new self(
blacklistedClasses: $this->blacklistedClasses,
maxArraySizeForSummary: $maxArraySizeForSummary,
maxArraySizeForSummaryNested: $this->maxArraySizeForSummaryNested,
maxObjectPropertiesForSummary: $this->maxObjectPropertiesForSummary,
maxObjectPropertiesForSummaryNested: $this->maxObjectPropertiesForSummaryNested,
maxItems: $this->maxItems,
collectionDetection: $this->collectionDetection,
compactNestedObjects: $this->compactNestedObjects,
);
}
/**
* Check if a class is blacklisted.
*
* Blacklisted classes are never fully displayed and instead show
* a summary or "[blacklisted]" marker.
*
* @param string $className Fully qualified class name
* @return bool True if class is blacklisted
*/
public function isBlacklisted(string $className): bool
{
return in_array($className, $this->blacklistedClasses, true);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* File entry in a directory structure
*/
final readonly class FileEntry
{
public function __construct(
public FilePath|string $path,
public string $name,
public int $size,
public int $lastModified,
public string $extension,
public bool $isReadable,
public bool $isWritable,
) {
}
public function getPathString(): string
{
return $this->path instanceof FilePath ? $this->path->toString() : $this->path;
}
public function getFormattedSize(): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$size = $this->size;
$unitIndex = 0;
while ($size >= 1024 && $unitIndex < count($units) - 1) {
$size /= 1024;
$unitIndex++;
}
return round($size, 2) . ' ' . $units[$unitIndex];
}
public function getFormattedTime(): string
{
return date('Y-m-d H:i:s', $this->lastModified);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Enum for indentation styles in display output
*/
enum IndentStyle: string
{
case SPACE = 'space';
case TAB = 'tab';
case CUSTOM = 'custom';
/**
* Get the indentation string for this style
*/
public function getIndentString(int $indentSize, ?string $customIndent = null): string
{
if ($this === self::CUSTOM && $customIndent !== null) {
return $customIndent;
}
return match ($this) {
self::TAB => "\t",
default => str_repeat(' ', $indentSize),
};
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Information about a class method
*/
final readonly class MethodInfo
{
/**
* @param array<string> $parameters
* @param array<string> $attributes
*/
public function __construct(
public string $name,
public int $visibility,
public string $returnType,
public array $parameters,
public bool $isStatic,
public bool $isAbstract,
public bool $isFinal,
public array $attributes = [],
) {
}
public function isPublic(): bool
{
return ($this->visibility & \ReflectionMethod::IS_PUBLIC) !== 0;
}
public function isProtected(): bool
{
return ($this->visibility & \ReflectionMethod::IS_PROTECTED) !== 0;
}
public function isPrivate(): bool
{
return ($this->visibility & \ReflectionMethod::IS_PRIVATE) !== 0;
}
public function getVisibilityString(): string
{
if ($this->isPublic()) {
return 'public';
}
if ($this->isProtected()) {
return 'protected';
}
if ($this->isPrivate()) {
return 'private';
}
return 'unknown';
}
public function getSignature(): string
{
$params = implode(', ', $this->parameters);
$static = $this->isStatic ? 'static ' : '';
$visibility = $this->getVisibilityString();
$return = $this->returnType !== '' ? ': ' . $this->returnType : '';
return $static . $visibility . ' function ' . $this->name . '(' . $params . ')' . $return;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Normalized object representation for display
*/
final readonly class ObjectStructure
{
/**
* @param array<string, PropertyInfo> $properties
* @param array<string, ObjectStructure> $nestedObjects
*/
public function __construct(
public string $className,
public array $properties,
public array $nestedObjects = [],
public int $depth = 0,
public array $metadata = [],
public bool $isCircularReference = false,
public bool $isNested = false,
public bool $isCollection = false,
public ?int $collectionCount = null,
public ?string $collectionItemType = null,
public bool $isSummary = false,
public array $summaryInfo = [],
) {
}
public function hasNestedObjects(): bool
{
return ! empty($this->nestedObjects);
}
public function getPropertyCount(): int
{
return count($this->properties);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Output format enum for displaying data structures
*/
enum OutputFormat: string
{
case CONSOLE = 'console';
case HTML = 'html';
public function isConsole(): bool
{
return $this === self::CONSOLE;
}
public function isHtml(): bool
{
return $this === self::HTML;
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Information about an object property
*/
final readonly class PropertyInfo
{
public function __construct(
public string $name,
public string $type,
public mixed $value,
public int $visibility,
public bool $isReadonly,
public bool $isStatic,
public bool $isNullable,
public ?string $declaringClass = null,
public bool $isValueObject = false,
) {
}
public function isPublic(): bool
{
return ($this->visibility & \ReflectionProperty::IS_PUBLIC) !== 0;
}
public function isProtected(): bool
{
return ($this->visibility & \ReflectionProperty::IS_PROTECTED) !== 0;
}
public function isPrivate(): bool
{
return ($this->visibility & \ReflectionProperty::IS_PRIVATE) !== 0;
}
public function getVisibilityString(): string
{
if ($this->isPublic()) {
return 'public';
}
if ($this->isProtected()) {
return 'protected';
}
if ($this->isPrivate()) {
return 'private';
}
return 'unknown';
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Context for rendering operations
*/
final readonly class RenderContext
{
public function __construct(
public OutputFormat $outputFormat,
public DisplayOptions $options,
public array $stylingPreferences = [],
public array $environment = [],
) {
}
public static function console(DisplayOptions $options): self
{
return new self(
outputFormat: OutputFormat::CONSOLE,
options: $options,
);
}
public static function html(DisplayOptions $options): self
{
return new self(
outputFormat: OutputFormat::HTML,
options: $options,
);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Framework\Display\ValueObjects;
/**
* Enum for separator styles in display output
*/
enum SeparatorStyle: string
{
case COLON = 'colon';
case ARROW = 'arrow';
case EQUALS = 'equals';
case CUSTOM = 'custom';
/**
* Get the separator string for this style
*/
public function getSeparator(?string $customSeparator = null): string
{
if ($this === self::CUSTOM && $customSeparator !== null) {
return $customSeparator;
}
return match ($this) {
self::ARROW => ' => ',
self::EQUALS => ' = ',
default => ': ',
};
}
}