- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
202 lines
6.4 KiB
PHP
202 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\View\Components;
|
|
|
|
use App\Framework\Core\ValueObjects\AccessibleLink;
|
|
use App\Framework\Core\ValueObjects\HtmlLink;
|
|
use App\Framework\Core\ValueObjects\LinkRel;
|
|
use App\Framework\Core\ValueObjects\LinkTarget;
|
|
use App\Framework\View\Attributes\ComponentName;
|
|
use App\Framework\View\Contracts\StaticComponent;
|
|
use App\Framework\View\Dom\ElementNode;
|
|
use App\Framework\View\Dom\Node;
|
|
use App\Framework\View\Dom\TextNode;
|
|
|
|
/**
|
|
* <x-a> Component
|
|
*
|
|
* Wrapper around HtmlLink and AccessibleLink Value Objects for template usage.
|
|
* Provides consistent, accessible link rendering throughout the application.
|
|
*
|
|
* Usage in templates:
|
|
* <x-a href="/dashboard">Dashboard</x-a>
|
|
* <x-a href="https://example.com" external="true">External Link</x-a>
|
|
* <x-a href="/current" aria-current="true">Current Page</x-a>
|
|
* <x-a href="/downloads/file.pdf" download="true">Download PDF</x-a>
|
|
* <x-a href="/page" class="custom-link" title="Tooltip">Link with custom class</x-a>
|
|
*/
|
|
#[ComponentName('a')]
|
|
final readonly class A implements StaticComponent
|
|
{
|
|
private AccessibleLink $link;
|
|
private string $text;
|
|
|
|
public function __construct(
|
|
string $content = '',
|
|
array $attributes = []
|
|
) {
|
|
// Content is the link text
|
|
$this->text = $content;
|
|
|
|
// Extract href from attributes (default to '#' if not provided)
|
|
$href = $attributes['href'] ?? '#';
|
|
|
|
// Extract other link attributes
|
|
$title = $attributes['title'] ?? null;
|
|
$class = $attributes['class'] ?? null;
|
|
$target = $attributes['target'] ?? null;
|
|
$rel = $attributes['rel'] ?? null;
|
|
$external = isset($attributes['external']);
|
|
$download = $attributes['download'] ?? null;
|
|
|
|
// Extract ARIA attributes
|
|
$ariaLabel = $attributes['aria-label'] ?? null;
|
|
$ariaCurrent = $attributes['aria-current'] ?? null;
|
|
$ariaDescribedBy = $attributes['aria-described-by'] ?? null;
|
|
|
|
// Build HtmlLink
|
|
$htmlLink = HtmlLink::create($href, $this->text);
|
|
|
|
if ($title !== null) {
|
|
$htmlLink = $htmlLink->withTitle($title);
|
|
}
|
|
|
|
if ($class !== null) {
|
|
$htmlLink = $htmlLink->withClass($class);
|
|
}
|
|
|
|
// Handle target attribute
|
|
if ($target !== null) {
|
|
$linkTarget = match ($target) {
|
|
'_blank' => LinkTarget::BLANK,
|
|
'_self' => LinkTarget::SELF,
|
|
'_parent' => LinkTarget::PARENT,
|
|
'_top' => LinkTarget::TOP,
|
|
default => null
|
|
};
|
|
if ($linkTarget !== null) {
|
|
$htmlLink = $htmlLink->withTarget($linkTarget);
|
|
}
|
|
}
|
|
|
|
// Handle rel attribute
|
|
if ($rel !== null) {
|
|
$relValues = array_map(
|
|
fn(string $r) => match (trim($r)) {
|
|
'noopener' => LinkRel::NOOPENER,
|
|
'noreferrer' => LinkRel::NOREFERRER,
|
|
'nofollow' => LinkRel::NOFOLLOW,
|
|
'external' => LinkRel::EXTERNAL,
|
|
default => null
|
|
},
|
|
explode(' ', $rel)
|
|
);
|
|
$relValues = array_filter($relValues);
|
|
if (!empty($relValues)) {
|
|
$htmlLink = $htmlLink->withRel(...$relValues);
|
|
}
|
|
}
|
|
|
|
// Handle external links (auto-adds security attributes)
|
|
if ($external) {
|
|
$htmlLink = HtmlLink::external($href, $this->text);
|
|
if ($class !== null) {
|
|
$htmlLink = $htmlLink->withClass($class);
|
|
}
|
|
if ($title !== null) {
|
|
$htmlLink = $htmlLink->withTitle($title);
|
|
}
|
|
}
|
|
|
|
// Handle downloads
|
|
if ($download !== null) {
|
|
$htmlLink = HtmlLink::download($href, $this->text, $download !== '' ? $download : null);
|
|
if ($class !== null) {
|
|
$htmlLink = $htmlLink->withClass($class);
|
|
}
|
|
if ($title !== null) {
|
|
$htmlLink = $htmlLink->withTitle($title);
|
|
}
|
|
}
|
|
|
|
// Wrap in AccessibleLink for ARIA support
|
|
$link = AccessibleLink::fromHtmlLink($htmlLink);
|
|
|
|
if ($ariaLabel !== null) {
|
|
$link = $link->withAriaLabel($ariaLabel);
|
|
}
|
|
|
|
if ($ariaCurrent !== null) {
|
|
$link = $link->withAriaCurrent(true, $ariaCurrent);
|
|
}
|
|
|
|
if ($ariaDescribedBy !== null) {
|
|
$link = new AccessibleLink(
|
|
baseLink: $link->baseLink,
|
|
ariaLabel: $link->ariaLabel,
|
|
ariaDescribedBy: $ariaDescribedBy,
|
|
ariaCurrent: $link->ariaCurrent,
|
|
ariaCurrentValue: $link->ariaCurrentValue,
|
|
);
|
|
}
|
|
|
|
// Store the AccessibleLink
|
|
$this->link = $link;
|
|
}
|
|
|
|
public function getRootNode(): Node
|
|
{
|
|
$a = new ElementNode('a');
|
|
|
|
// Add href
|
|
$a->setAttribute('href', (string) $this->link->baseLink->href);
|
|
|
|
// Add class if present
|
|
if ($this->link->baseLink->cssClass !== null) {
|
|
$a->setAttribute('class', $this->link->baseLink->cssClass);
|
|
}
|
|
|
|
// Add title if present
|
|
if ($this->link->baseLink->title !== null) {
|
|
$a->setAttribute('title', $this->link->baseLink->title);
|
|
}
|
|
|
|
// Add target if present
|
|
if ($this->link->baseLink->target !== null) {
|
|
$a->setAttribute('target', $this->link->baseLink->target->value);
|
|
}
|
|
|
|
// Add rel if present
|
|
if (!empty($this->link->baseLink->rel)) {
|
|
$relValues = array_map(fn($r) => $r->value, $this->link->baseLink->rel);
|
|
$a->setAttribute('rel', implode(' ', $relValues));
|
|
}
|
|
|
|
// Add download if present
|
|
if ($this->link->baseLink->download !== null) {
|
|
$a->setAttribute('download', $this->link->baseLink->download);
|
|
}
|
|
|
|
// Add ARIA attributes
|
|
if ($this->link->ariaLabel !== null) {
|
|
$a->setAttribute('aria-label', $this->link->ariaLabel);
|
|
}
|
|
|
|
if ($this->link->ariaCurrent) {
|
|
$a->setAttribute('aria-current', $this->link->ariaCurrentValue ?? 'true');
|
|
}
|
|
|
|
if ($this->link->ariaDescribedBy !== null) {
|
|
$a->setAttribute('aria-describedby', $this->link->ariaDescribedBy);
|
|
}
|
|
|
|
// Add text content
|
|
$textNode = new TextNode($this->text);
|
|
$a->appendChild($textNode);
|
|
|
|
return $a;
|
|
}
|
|
}
|