- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
260 lines
7.5 KiB
PHP
260 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\View\Table;
|
|
|
|
final readonly class Table
|
|
{
|
|
/**
|
|
* @param TableColumn[] $columns
|
|
* @param TableRow[] $rows
|
|
*/
|
|
public function __construct(
|
|
public array $columns,
|
|
public array $rows,
|
|
public ?string $cssClass = null,
|
|
public ?TableOptions $options = null,
|
|
public ?string $id = null
|
|
) {
|
|
}
|
|
|
|
public function render(): string
|
|
{
|
|
$options = $this->options ?? new TableOptions();
|
|
|
|
$classString = $this->buildClassString($options);
|
|
$attributesString = $this->buildAttributesString($options);
|
|
$idString = $this->id ? " id=\"{$this->id}\"" : '';
|
|
|
|
$thead = $this->renderThead();
|
|
$tbody = $this->renderTbody($options);
|
|
|
|
return "<table{$classString}{$idString}{$attributesString}>{$thead}{$tbody}</table>";
|
|
}
|
|
|
|
private function renderThead(): string
|
|
{
|
|
$headerCells = array_map(fn (TableColumn $column) => $column->renderHeader(), $this->columns);
|
|
|
|
return "<thead><tr>" . implode('', $headerCells) . "</tr></thead>";
|
|
}
|
|
|
|
private function renderTbody(TableOptions $options): string
|
|
{
|
|
if (empty($this->rows)) {
|
|
$colspan = count($this->columns);
|
|
$emptyMessage = $options->emptyMessage ?? 'No data available';
|
|
|
|
return "<tbody><tr><td colspan=\"{$colspan}\" class=\"text-center empty-message\">{$emptyMessage}</td></tr></tbody>";
|
|
}
|
|
|
|
$rowsHtml = array_map(fn (TableRow $row) => $row->render(), $this->rows);
|
|
|
|
return "<tbody>" . implode('', $rowsHtml) . "</tbody>";
|
|
}
|
|
|
|
private function buildClassString(TableOptions $options): string
|
|
{
|
|
$classes = [];
|
|
|
|
if ($this->cssClass) {
|
|
$classes[] = $this->cssClass;
|
|
}
|
|
|
|
if ($options->striped) {
|
|
$classes[] = 'table-striped';
|
|
}
|
|
|
|
if ($options->bordered) {
|
|
$classes[] = 'table-bordered';
|
|
}
|
|
|
|
if ($options->hover) {
|
|
$classes[] = 'table-hover';
|
|
}
|
|
|
|
if ($options->responsive) {
|
|
$classes[] = 'table-responsive';
|
|
}
|
|
|
|
return $classes ? ' class="' . implode(' ', $classes) . '"' : '';
|
|
}
|
|
|
|
private function buildAttributesString(TableOptions $options): string
|
|
{
|
|
if (! $options->tableAttributes) {
|
|
return '';
|
|
}
|
|
|
|
$parts = [];
|
|
foreach ($options->tableAttributes as $key => $value) {
|
|
$parts[] = $key . '="' . htmlspecialchars((string) $value, ENT_QUOTES) . '"';
|
|
}
|
|
|
|
return $parts ? ' ' . implode(' ', $parts) : '';
|
|
}
|
|
|
|
/**
|
|
* Create table from array data with column definitions
|
|
*/
|
|
public static function fromArray(array $data, array $columnDefs, ?TableOptions $options = null): self
|
|
{
|
|
$columns = [];
|
|
$rows = [];
|
|
|
|
// Build columns
|
|
foreach ($columnDefs as $key => $def) {
|
|
if (is_string($def)) {
|
|
// Simple string header
|
|
$columns[] = TableColumn::text($key, $def);
|
|
} elseif ($def instanceof TableColumn) {
|
|
// Already a TableColumn
|
|
$columns[] = $def;
|
|
} elseif (is_array($def)) {
|
|
// Array definition
|
|
$columns[] = new TableColumn(
|
|
key: $key,
|
|
header: $def['header'] ?? $key,
|
|
cssClass: $def['class'] ?? null,
|
|
formatter: $def['formatter'] ?? null,
|
|
sortable: $def['sortable'] ?? false,
|
|
width: $def['width'] ?? null,
|
|
defaultType: $def['type'] ?? null
|
|
);
|
|
}
|
|
}
|
|
|
|
// Build rows
|
|
foreach ($data as $rowData) {
|
|
$rows[] = TableRow::fromData($rowData, $columns);
|
|
}
|
|
|
|
return new self($columns, $rows, null, $options);
|
|
}
|
|
|
|
/**
|
|
* Create simple table from 2D array
|
|
*/
|
|
public static function fromSimpleArray(array $headers, array $data, ?TableOptions $options = null): self
|
|
{
|
|
$columns = array_map(
|
|
fn ($header, $index) => TableColumn::text((string) $index, $header),
|
|
$headers,
|
|
array_keys($headers)
|
|
);
|
|
|
|
$rows = array_map(
|
|
fn ($rowData) => TableRow::fromValues($rowData),
|
|
$data
|
|
);
|
|
|
|
return new self($columns, $rows, null, $options);
|
|
}
|
|
|
|
/**
|
|
* Create environment variables table
|
|
*/
|
|
public static function forEnvironmentVars(array $env): self
|
|
{
|
|
$columns = [
|
|
TableColumn::text('key', 'Variable', 'env-key'),
|
|
TableColumn::text('value', 'Wert', 'env-value'),
|
|
];
|
|
|
|
$rows = [];
|
|
foreach ($env as $key => $value) {
|
|
// Mask sensitive values - ensure key is string
|
|
$displayValue = self::maskSensitiveValue((string) $key, $value);
|
|
|
|
$rows[] = TableRow::fromData([
|
|
'key' => (string) $key,
|
|
'value' => $displayValue,
|
|
], $columns, 'env-row');
|
|
}
|
|
|
|
return new self(
|
|
columns: $columns,
|
|
rows: $rows,
|
|
cssClass: 'admin-table',
|
|
options: TableOptions::admin(),
|
|
id: 'envTable'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create migrations table
|
|
*/
|
|
public static function forMigrations(array $migrations): self
|
|
{
|
|
$columns = [
|
|
TableColumn::withFormatter(
|
|
'status',
|
|
'Status',
|
|
Formatters\StatusFormatter::withBadges(),
|
|
'status-col'
|
|
),
|
|
TableColumn::text('version', 'Version', 'version-col'),
|
|
TableColumn::text('description', 'Description', 'description-col'),
|
|
TableColumn::withFormatter(
|
|
'applied',
|
|
'Applied',
|
|
Formatters\BooleanFormatter::germanWithBadges(),
|
|
'applied-col'
|
|
),
|
|
];
|
|
|
|
$rows = [];
|
|
foreach ($migrations as $migration) {
|
|
$statusData = [
|
|
'status_icon' => $migration['status_icon'] ?? '',
|
|
'status_text' => $migration['status_text'] ?? '',
|
|
'status_class' => $migration['status_class'] ?? 'secondary',
|
|
];
|
|
|
|
$rows[] = TableRow::fromData([
|
|
'status' => $statusData,
|
|
'version' => $migration['version'] ?? '',
|
|
'description' => $migration['description'] ?? '',
|
|
'applied' => $migration['applied'] ?? false,
|
|
], $columns, 'migration-row');
|
|
}
|
|
|
|
return new self(
|
|
columns: $columns,
|
|
rows: $rows,
|
|
cssClass: 'admin-table',
|
|
options: TableOptions::admin(),
|
|
id: 'migrationsTable'
|
|
);
|
|
}
|
|
|
|
private static function maskSensitiveValue(string $key, mixed $value): string
|
|
{
|
|
$sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
|
|
|
|
foreach ($sensitiveKeys as $sensitiveKey) {
|
|
if (stripos($key, $sensitiveKey) !== false) {
|
|
return '********';
|
|
}
|
|
}
|
|
|
|
// Handle arrays and objects properly
|
|
if (is_array($value)) {
|
|
$encoded = json_encode($value, JSON_PRETTY_PRINT);
|
|
|
|
return $encoded ?: '[Array]';
|
|
}
|
|
|
|
if (is_object($value)) {
|
|
if (method_exists($value, '__toString')) {
|
|
return (string) $value;
|
|
}
|
|
|
|
return get_class($value);
|
|
}
|
|
|
|
return (string) $value;
|
|
}
|
|
}
|