chore: lots of changes
This commit is contained in:
18
src/Framework/View/Compiler.php
Normal file
18
src/Framework/View/Compiler.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
final readonly class Compiler
|
||||
{
|
||||
public function compile(string $html): \DOMDocument
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
return $dom;
|
||||
}
|
||||
}
|
||||
47
src/Framework/View/ComponentRenderer.php
Normal file
47
src/Framework/View/ComponentRenderer.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
final class ComponentRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TemplateLoader $loader = new TemplateLoader(),
|
||||
private readonly Compiler $compiler = new Compiler(),
|
||||
private readonly TemplateProcessor $processor = new TemplateProcessor(),
|
||||
private string $cacheDir = __DIR__ . "/cache/components/"
|
||||
) {}
|
||||
|
||||
public function render(string $componentName, array $data): string
|
||||
{
|
||||
$path = $this->loader->getComponentPath($componentName);
|
||||
if (!file_exists($path)) {
|
||||
return "<!-- Komponente '$componentName' nicht gefunden -->";
|
||||
}
|
||||
|
||||
# Cache prüfen
|
||||
$hash = md5_file($path) . '_' . md5(serialize($data));
|
||||
$cacheFile = $this->cacheDir . "/{$componentName}_{$hash}.html";;
|
||||
|
||||
if(file_exists($cacheFile)) {
|
||||
return file_get_contents($cacheFile);
|
||||
}
|
||||
|
||||
|
||||
$template = file_get_contents($path);
|
||||
$compiled = $this->compiler->compile($template)->saveHTML();
|
||||
|
||||
$context = new RenderContext(
|
||||
template: $componentName,
|
||||
data: $data
|
||||
);
|
||||
|
||||
$output = $this->processor->render($context, $compiled);
|
||||
|
||||
if(!is_dir($this->cacheDir)) {
|
||||
mkdir($this->cacheDir, 0777, true);
|
||||
}
|
||||
|
||||
file_put_contents($cacheFile, $output);
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
48
src/Framework/View/DOM/DomParser.php
Normal file
48
src/Framework/View/DOM/DomParser.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\DOM;
|
||||
|
||||
class DomParser
|
||||
{
|
||||
public function domNodeToHTMLElement(\DOMNode $node, ?HTMLElement $parent = null): ?HTMLElement
|
||||
{
|
||||
switch ($node->nodeType) {
|
||||
case XML_ELEMENT_NODE:
|
||||
$attributes = [];
|
||||
if ($node instanceof \DOMElement && $node->hasAttributes()) {
|
||||
foreach ($node->attributes as $attr) {
|
||||
$attributes[$attr->nodeName] = $attr->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
$el = new HTMLElement(
|
||||
tagName: $node->nodeName,
|
||||
attributes: $attributes,
|
||||
children: [],
|
||||
textContent: null,
|
||||
nodeType: 'element',
|
||||
namespace: $node->namespaceURI,
|
||||
parent: $parent
|
||||
);
|
||||
|
||||
foreach ($node->childNodes as $child) {
|
||||
$childEl = $this->domNodeToHTMLElement($child, $el);
|
||||
if ($childEl) {
|
||||
$el->addChild($childEl);
|
||||
}
|
||||
}
|
||||
|
||||
return $el;
|
||||
|
||||
case XML_TEXT_NODE:
|
||||
return new HTMLElement(textContent: $node->nodeValue, nodeType: 'text');
|
||||
|
||||
case XML_COMMENT_NODE:
|
||||
return new HTMLElement(textContent: $node->nodeValue, nodeType: 'comment');
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
65
src/Framework/View/DOM/HTMLElement.php
Normal file
65
src/Framework/View/DOM/HTMLElement.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\DOM;
|
||||
|
||||
class HTMLElement
|
||||
{
|
||||
public function __construct(
|
||||
public string $tagName = '',
|
||||
public array $attributes = [],
|
||||
public array $children = [],
|
||||
public ?string $textContent = null,
|
||||
public string $nodeType = 'element', // 'element', 'text', 'comment'
|
||||
public ?string $namespace = null,
|
||||
public ?HTMLElement $parent = null
|
||||
) {}
|
||||
|
||||
public function attr(string $name, ?string $value = null): self|string|null
|
||||
{
|
||||
if ($value === null) {
|
||||
return $this->attributes[$name] ?? null;
|
||||
}
|
||||
|
||||
$this->attributes[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function text(?string $value = null): self|string|null
|
||||
{
|
||||
if ($value === null) {
|
||||
return $this->textContent;
|
||||
}
|
||||
|
||||
$this->textContent = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addChild(HTMLElement $child): self
|
||||
{
|
||||
$child->parent = $this;
|
||||
$this->children[] = $child;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->nodeType === 'text') {
|
||||
return htmlspecialchars($this->textContent ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
if ($this->nodeType === 'comment') {
|
||||
return "<!-- " . $this->textContent . " -->";
|
||||
}
|
||||
|
||||
$attrs = implode(' ', array_map(
|
||||
fn($k, $v) => htmlspecialchars($k) . '="' . htmlspecialchars($v) . '"',
|
||||
array_keys($this->attributes),
|
||||
$this->attributes
|
||||
));
|
||||
|
||||
$content = implode('', array_map(fn($c) => (string)$c, $this->children));
|
||||
$tag = htmlspecialchars($this->tagName);
|
||||
|
||||
return "<{$tag}" . ($attrs ? " $attrs" : "") . ">$content</{$tag}>";
|
||||
}
|
||||
}
|
||||
57
src/Framework/View/DOM/HtmlDocument.php
Normal file
57
src/Framework/View/DOM/HtmlDocument.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\DOM;
|
||||
|
||||
final readonly class HtmlDocument
|
||||
{
|
||||
private \DOMDocument $dom;
|
||||
|
||||
public function __construct(string $html = '')
|
||||
{
|
||||
$this->dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
if ($html !== '') {
|
||||
libxml_use_internal_errors(true);
|
||||
$success = $this->dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
if (!$success) {
|
||||
throw new \RuntimeException("HTML Parsing failed.");
|
||||
}
|
||||
libxml_clear_errors();
|
||||
}
|
||||
}
|
||||
|
||||
public function querySelector(string $tagName): ?HtmlElement
|
||||
{
|
||||
$xpath = new \DOMXPath($this->dom);
|
||||
$node = $xpath->query("//{$tagName}")->item(0);
|
||||
|
||||
return $node ? new DomParser()->domNodeToHTMLElement($node) : null;
|
||||
}
|
||||
|
||||
public function querySelectorAll(string $tagName): NodeList
|
||||
{
|
||||
$xpath = new \DOMXPath($this->dom);
|
||||
$nodes = $xpath->query("//{$tagName}");
|
||||
$parser = new DomParser();
|
||||
|
||||
$elements = [];
|
||||
foreach ($nodes as $node) {
|
||||
$el = $parser->domNodeToHTMLElement($node);
|
||||
if ($el) {
|
||||
$elements[] = $el;
|
||||
}
|
||||
}
|
||||
|
||||
return new NodeList(...$elements);
|
||||
}
|
||||
|
||||
public function toHtml(): string
|
||||
{
|
||||
return $this->dom->saveHTML() ?: '';
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toHtml();
|
||||
}
|
||||
|
||||
}
|
||||
55
src/Framework/View/DOM/HtmlDocumentFormatter.php
Normal file
55
src/Framework/View/DOM/HtmlDocumentFormatter.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\DOM;
|
||||
|
||||
class HtmlDocumentFormatter
|
||||
{
|
||||
private HtmlFormatter $formatter;
|
||||
private DomParser $parser;
|
||||
|
||||
public function __construct(?HtmlFormatter $formatter = null)
|
||||
{
|
||||
$this->formatter = $formatter ?? new HtmlFormatter();
|
||||
$this->parser = new DomParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wandelt ein HTML-String in ein strukturiertes HTMLElement-Baumobjekt und formatiert es
|
||||
*/
|
||||
public function formatHtmlString(string $html): string
|
||||
{
|
||||
$document = new \DOMDocument('1.0', 'UTF-8');
|
||||
libxml_use_internal_errors(true);
|
||||
$success = $document->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
if (!$success) {
|
||||
throw new \RuntimeException("HTML Parsing failed.");
|
||||
}
|
||||
libxml_clear_errors();
|
||||
|
||||
$root = $document->documentElement;
|
||||
if (!$root) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$element = $this->parser->domNodeToHTMLElement($root);
|
||||
return $this->formatter->format($element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert ein HtmlDocument direkt
|
||||
*/
|
||||
public function formatDocument(HtmlDocument $doc): string
|
||||
{
|
||||
$element = $doc->querySelector('html') ?? $doc->querySelector('body');
|
||||
return $this->formatter->format($element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert einen Teilbaum ab einem gegebenen HTMLElement
|
||||
*/
|
||||
public function formatElement(HTMLElement $element): string
|
||||
{
|
||||
return $this->formatter->format($element);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/Framework/View/DOM/HtmlFormatter.php
Normal file
53
src/Framework/View/DOM/HtmlFormatter.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\DOM;
|
||||
|
||||
class HtmlFormatter
|
||||
{
|
||||
private int $indentSize;
|
||||
private string $indentChar;
|
||||
|
||||
public function __construct(int $indentSize = 2, string $indentChar = ' ')
|
||||
{
|
||||
$this->indentSize = $indentSize;
|
||||
$this->indentChar = $indentChar;
|
||||
}
|
||||
|
||||
public function format(HTMLElement $element, int $level = 0): string
|
||||
{
|
||||
$indent = str_repeat($this->indentChar, $level * $this->indentSize);
|
||||
|
||||
if ($element->nodeType === 'text') {
|
||||
return $indent . htmlspecialchars($element->textContent ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "\n";
|
||||
}
|
||||
|
||||
if ($element->nodeType === 'comment') {
|
||||
return $indent . "<!-- " . $element->textContent . " -->\n";
|
||||
}
|
||||
|
||||
$tag = htmlspecialchars($element->tagName);
|
||||
|
||||
$attrs = '';
|
||||
foreach ($element->attributes as $key => $value) {
|
||||
$attrs .= ' ' . htmlspecialchars($key) . '="' . htmlspecialchars($value) . '"';
|
||||
}
|
||||
|
||||
if (empty($element->children) && empty($element->textContent)) {
|
||||
return $indent . "<{$tag}{$attrs} />\n";
|
||||
}
|
||||
|
||||
$output = $indent . "<{$tag}{$attrs}>\n";
|
||||
|
||||
foreach ($element->children as $child) {
|
||||
$output .= $this->format($child, $level + 1);
|
||||
}
|
||||
|
||||
if ($element->textContent !== null && $element->textContent !== '') {
|
||||
$output .= str_repeat($this->indentChar, ($level + 1) * $this->indentSize);
|
||||
$output .= htmlspecialchars($element->textContent, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "\n";
|
||||
}
|
||||
|
||||
$output .= $indent . "</{$tag}>\n";
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
73
src/Framework/View/DOM/NodeList.php
Normal file
73
src/Framework/View/DOM/NodeList.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\DOM;
|
||||
|
||||
class NodeList implements \IteratorAggregate, \Countable, \ArrayAccess
|
||||
{
|
||||
/** @var HTMLElement[] */
|
||||
private array $nodes = [];
|
||||
|
||||
public function __construct(HTMLElement ...$nodes)
|
||||
{
|
||||
$this->nodes = $nodes;
|
||||
}
|
||||
|
||||
public function getIterator(): \ArrayIterator
|
||||
{
|
||||
return new \ArrayIterator($this->nodes);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->nodes);
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->nodes[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->nodes[$offset] ?? null;
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
if (!$value instanceof HTMLElement) {
|
||||
throw new \InvalidArgumentException("Only HTMLElement instances allowed.");
|
||||
}
|
||||
|
||||
if ($offset === null) {
|
||||
$this->nodes[] = $value;
|
||||
} else {
|
||||
$this->nodes[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->nodes[$offset]);
|
||||
}
|
||||
|
||||
// Beispiel für Hilfsmethoden:
|
||||
public function first(): ?HTMLElement
|
||||
{
|
||||
return $this->nodes[0] ?? null;
|
||||
}
|
||||
|
||||
public function filter(callable $fn): self
|
||||
{
|
||||
return new self(...array_filter($this->nodes, $fn));
|
||||
}
|
||||
|
||||
public function map(callable $fn): array
|
||||
{
|
||||
return array_map($fn, $this->nodes);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->nodes;
|
||||
}
|
||||
}
|
||||
16
src/Framework/View/DomProcessor.php
Normal file
16
src/Framework/View/DomProcessor.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
interface DomProcessor
|
||||
{
|
||||
/**
|
||||
* Manipuliert das gegebene DOM.
|
||||
* @param \DOMDocument $dom
|
||||
* @param array $data
|
||||
* @param callable|null $componentRenderer Optional, kann z.B. für Komponenten übergeben werden
|
||||
*/
|
||||
##public function process(\DOMDocument $dom, array $data, ?callable $componentRenderer = null): void;
|
||||
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void;
|
||||
}
|
||||
31
src/Framework/View/DomTemplateParser.php
Normal file
31
src/Framework/View/DomTemplateParser.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
use DOMDocument;
|
||||
|
||||
final class DomTemplateParser
|
||||
{
|
||||
public function parse(string $html): DOMDocument
|
||||
{
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$dom->loadHTML(
|
||||
$html,
|
||||
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
|
||||
);
|
||||
|
||||
libxml_clear_errors();
|
||||
|
||||
return $dom;
|
||||
}
|
||||
|
||||
public function toHtml(DOMDocument $dom): string
|
||||
{
|
||||
return $dom->saveHTML();
|
||||
}
|
||||
}
|
||||
53
src/Framework/View/Engine.php
Normal file
53
src/Framework/View/Engine.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
use App\Framework\View\Processors\ComponentProcessor;
|
||||
use App\Framework\View\Processors\LayoutTagProcessor;
|
||||
use App\Framework\View\Processors\PlaceholderReplacer;
|
||||
|
||||
final readonly class Engine implements TemplateRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private TemplateLoader $loader = new TemplateLoader(),
|
||||
private Compiler $compiler = new Compiler(),
|
||||
private Renderer $renderer = new Renderer(),
|
||||
private TemplateProcessor $processor = new TemplateProcessor()
|
||||
) {
|
||||
$this->processor->registerDom(new ComponentProcessor());
|
||||
$this->processor->registerDom(new LayoutTagProcessor());
|
||||
$this->processor->registerDom(new PlaceholderReplacer());
|
||||
}
|
||||
|
||||
public function render(RenderContext $context): string
|
||||
{
|
||||
|
||||
$template = $context->template;
|
||||
$data = $context->data;
|
||||
|
||||
$cacheFile = __DIR__ . "/cache/{$template}.cache.html";
|
||||
|
||||
$templateFile = $this->loader->getTemplatePath($template); // Neue Methode in TemplateLoader
|
||||
|
||||
// Prüfen ob Cache existiert und nicht älter als das Template
|
||||
if (file_exists($cacheFile) && filemtime($cacheFile) >= filemtime($templateFile)) {
|
||||
$content = file_get_contents($cacheFile);
|
||||
#$dom = $this->compiler->compile($content);
|
||||
} else {
|
||||
// Template normal laden und kompilieren
|
||||
$content = $this->loader->load($template, $context->controllerClass);;
|
||||
$content = "<test>{$content}</test>";
|
||||
$dom = $this->compiler->compile($content);
|
||||
$html = $dom->saveHTML();
|
||||
// (Optional) VOR dynamischer Verarbeitung rohe Struktur cachen
|
||||
file_put_contents($cacheFile, $dom->saveHTML());
|
||||
$content = $html;
|
||||
}
|
||||
|
||||
|
||||
return $this->processor->render($context, $content);
|
||||
#return $this->renderer->render($dom, $data, $this);
|
||||
}
|
||||
}
|
||||
18
src/Framework/View/Processors/CommentStripProcessor.php
Normal file
18
src/Framework/View/Processors/CommentStripProcessor.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\DomProcessor;
|
||||
use App\Framework\View\RenderContext;
|
||||
|
||||
final readonly class CommentStripProcessor implements DomProcessor
|
||||
{
|
||||
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
foreach ($xpath->query('//comment()') as $commentNode) {
|
||||
$commentNode->parentNode?->removeChild($commentNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/Framework/View/Processors/ComponentProcessor.php
Normal file
37
src/Framework/View/Processors/ComponentProcessor.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\ComponentRenderer;
|
||||
use App\Framework\View\DomProcessor;
|
||||
use App\Framework\View\RenderContext;
|
||||
|
||||
final readonly class ComponentProcessor implements DomProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private ComponentRenderer $renderer = new ComponentRenderer()
|
||||
){}
|
||||
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
foreach ($xpath->query('//component') as $node) {
|
||||
$name = $node->getAttribute('name');
|
||||
$attributes = [];
|
||||
|
||||
foreach ($node->attributes as $attr) {
|
||||
if($attr->nodeName !== 'name') {
|
||||
$attributes[$attr->nodeName] = $attr->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
$componentHtml = $this->renderer->render($name, array_merge($context->data, $attributes));
|
||||
|
||||
$fragment = $dom->createDocumentFragment();
|
||||
$fragment->appendXML($componentHtml);
|
||||
|
||||
$node->parentNode->replaceChild($fragment, $node);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Framework/View/Processors/DateFormatProcessor.php
Normal file
29
src/Framework/View/Processors/DateFormatProcessor.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\RenderContext;
|
||||
use App\Framework\View\StringProcessor;
|
||||
|
||||
# Formatiert Datumsfelder etwa mit einem Tag `<date value="iso_date" format="d.m.Y" />`.
|
||||
|
||||
final readonly class DateFormatProcessor implements StringProcessor
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function process(string $html, RenderContext $context): string
|
||||
{
|
||||
return preg_replace_callback('/{{\s*date\((\w+),\s*["\']([^"\']+)["\']\)\s*}}/', function ($matches) use ($context) {
|
||||
$key = $matches[1];
|
||||
$format = $matches[2];
|
||||
|
||||
if (!isset($context->data[$key]) || !($context->data[$key] instanceof \DateTimeInterface)) {
|
||||
return $matches[0]; // Unverändert lassen
|
||||
}
|
||||
|
||||
return $context->data[$key]->format($format);
|
||||
}, $html);
|
||||
}
|
||||
}
|
||||
24
src/Framework/View/Processors/EscapeProcessor.php
Normal file
24
src/Framework/View/Processors/EscapeProcessor.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\RenderContext;
|
||||
|
||||
final readonly class EscapeProcessor
|
||||
{
|
||||
public function process(string $html, RenderContext $context): string
|
||||
{
|
||||
// Erst rohe Inhalte einsetzen (drei geschweifte Klammern)
|
||||
$html = preg_replace_callback('/{{{\s*(\w+)\s*}}}/', function ($matches) use ($context) {
|
||||
return $context->data[$matches[1]] ?? '';
|
||||
}, $html);
|
||||
|
||||
// Dann alle übrigen Variablen escapen
|
||||
$html = preg_replace_callback('/{{\s*(\w+)\s*}}/', function ($matches) use ($context) {
|
||||
$value = $context->data[$matches[1]] ?? '';
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_HTML5);
|
||||
}, $html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
59
src/Framework/View/Processors/ForProcessor.php
Normal file
59
src/Framework/View/Processors/ForProcessor.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\DomProcessor;
|
||||
|
||||
final class ForProcessor implements DomProcessor
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
foreach ($xpath->query('//for[@var][@in]') as $node) {
|
||||
$var = $node->getAttribute('var');
|
||||
$in = $node->getAttribute('in');
|
||||
|
||||
$output = '';
|
||||
|
||||
if (isset($context->data[$in]) && is_iterable($context->data[$in])) {
|
||||
foreach ($context->data[$in] as $item) {
|
||||
$clone = $node->cloneNode(true);
|
||||
foreach ($clone->childNodes as $child) {
|
||||
$this->replacePlaceholdersRecursive($child, [$var => $item] + $context->data);
|
||||
}
|
||||
|
||||
$fragment = $dom->createDocumentFragment();
|
||||
foreach ($clone->childNodes as $child) {
|
||||
$fragment->appendChild($child->cloneNode(true));
|
||||
}
|
||||
|
||||
$output .= $dom->saveHTML($fragment);
|
||||
}
|
||||
}
|
||||
|
||||
$replacement = $dom->createDocumentFragment();
|
||||
@$replacement->appendXML($output);
|
||||
$node->parentNode?->replaceChild($replacement, $node);
|
||||
}
|
||||
}
|
||||
|
||||
private function replacePlaceholdersRecursive(\DOMNode $node, array $data): void
|
||||
{
|
||||
if ($node->nodeType === XML_TEXT_NODE) {
|
||||
$node->nodeValue = preg_replace_callback('/{{\s*(\w+)\s*}}/', function ($matches) use ($data) {
|
||||
return htmlspecialchars((string)($data[$matches[1]] ?? $matches[0]), ENT_QUOTES | ENT_HTML5);
|
||||
}, $node->nodeValue);
|
||||
}
|
||||
|
||||
if ($node->hasChildNodes()) {
|
||||
foreach ($node->childNodes as $child) {
|
||||
$this->replacePlaceholdersRecursive($child, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Framework/View/Processors/IfProcessor.php
Normal file
39
src/Framework/View/Processors/IfProcessor.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\DomProcessor;
|
||||
|
||||
final readonly class IfProcessor implements DomProcessor
|
||||
{
|
||||
public function process(\DOMDocument $dom, \App\Framework\View\RenderContext $context): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
foreach ($xpath->query('//*[@if]') as $node) {
|
||||
$condition = $node->getAttribute('if');
|
||||
|
||||
$value = $context->data[$condition] ?? null;
|
||||
|
||||
// Entferne, wenn die Bedingung nicht erfüllt ist
|
||||
if (!$this->isTruthy($value)) {
|
||||
$node->parentNode?->removeChild($node);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Entferne Attribut bei Erfolg
|
||||
$node->removeAttribute('if');
|
||||
}
|
||||
}
|
||||
|
||||
private function isTruthy(mixed $value): bool
|
||||
{
|
||||
if (is_bool($value)) return $value;
|
||||
if (is_null($value)) return false;
|
||||
if (is_string($value)) return trim($value) !== '';
|
||||
if (is_numeric($value)) return $value != 0;
|
||||
if (is_array($value)) return count($value) > 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
41
src/Framework/View/Processors/IncludeProcessor.php
Normal file
41
src/Framework/View/Processors/IncludeProcessor.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\DomProcessor;
|
||||
use App\Framework\View\DomTemplateParser;
|
||||
use App\Framework\View\RenderContext;
|
||||
use App\Framework\View\TemplateLoader;
|
||||
|
||||
final readonly class IncludeProcessor implements DomProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private TemplateLoader $loader,
|
||||
private DomTemplateParser $parser = new DomTemplateParser()
|
||||
) {}
|
||||
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
foreach ($xpath->query('//include[@file]') as $includeNode) {
|
||||
$file = $includeNode->getAttribute('file');
|
||||
|
||||
try {
|
||||
$html = $this->loader->load($file);
|
||||
$includedDom = $this->parser->parse($html);
|
||||
|
||||
$fragment = $dom->createDocumentFragment();
|
||||
foreach ($includedDom->documentElement->childNodes as $child) {
|
||||
$fragment->appendChild($dom->importNode($child, true));
|
||||
}
|
||||
|
||||
$includeNode->parentNode?->replaceChild($fragment, $includeNode);
|
||||
} catch (\Throwable $e) {
|
||||
// Optional: Fehlerkommentar ins Template schreiben
|
||||
$error = $dom->createComment("Fehler beim Laden von '$file': " . $e->getMessage());
|
||||
$includeNode->parentNode?->replaceChild($error, $includeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/Framework/View/Processors/LayoutTagProcessor.php
Normal file
102
src/Framework/View/Processors/LayoutTagProcessor.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\DomTemplateParser;
|
||||
use App\Framework\View\RenderContext;
|
||||
use App\Framework\View\TemplateLoader;
|
||||
use App\Framework\View\DomProcessor;
|
||||
|
||||
final readonly class LayoutTagProcessor implements DomProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private TemplateLoader $loader = new TemplateLoader(),
|
||||
private DomTemplateParser $parser = new DomTemplateParser()
|
||||
) {}
|
||||
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$layoutTags = $xpath->query('//layout[@src]');
|
||||
|
||||
if ($layoutTags->length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$layoutTag = $layoutTags->item(0);
|
||||
$layoutFile = $layoutTag->getAttribute('src');
|
||||
$layoutHtml = $this->loader->load('/layouts/'.$layoutFile);
|
||||
$layoutDom = $this->parser->parse($layoutHtml);
|
||||
|
||||
|
||||
// Body-Slot finden
|
||||
$layoutXPath = new \DOMXPath($layoutDom);
|
||||
/*$slotNodes = $layoutXPath->query('//slot[@name="body"]');
|
||||
|
||||
if ($slotNodes->length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$slot = $slotNodes->item(0);*/
|
||||
|
||||
$slot = $layoutXPath->query('//main')->item(0);
|
||||
|
||||
if (! $slot) {
|
||||
return; // Kein <main> vorhanden → Layout kann nicht angewendet werden
|
||||
}
|
||||
|
||||
// Die Kindknoten des ursprünglichen <layout>-Elternelements einsammeln (ohne <layout>-Tag selbst)
|
||||
$parent = $layoutTag->parentNode;
|
||||
|
||||
// Alle Knoten nach <layout> einsammeln (direkt nach <layout>)
|
||||
$contentNodes = [];
|
||||
for ($node = $layoutTag->nextSibling; $node !== null; $node = $node->nextSibling) {
|
||||
$contentNodes[] = $node;
|
||||
}
|
||||
|
||||
// Vor dem Entfernen: Layout-Tag aus DOM löschen
|
||||
$parent->removeChild($layoutTag);
|
||||
|
||||
// Inhalt einfügen
|
||||
$fragment = $layoutDom->createDocumentFragment();
|
||||
foreach ($contentNodes as $contentNode) {
|
||||
// Hole alle noch im Original existierenden Nodes...
|
||||
$fragment->appendChild($layoutDom->importNode($contentNode, true));
|
||||
}
|
||||
|
||||
// <main> ersetzen
|
||||
$slot->parentNode->replaceChild($fragment, $slot);
|
||||
|
||||
// Ersetze gesamtes DOM
|
||||
$newDom = $this->parser->parse($layoutDom->saveHTML());
|
||||
$dom->replaceChild(
|
||||
$dom->importNode($newDom->documentElement, true),
|
||||
$dom->documentElement
|
||||
);
|
||||
|
||||
return;
|
||||
|
||||
|
||||
// Layout-Tag aus Original entfernen
|
||||
$layoutTag->parentNode?->removeChild($layoutTag);
|
||||
|
||||
// Inhalt des Haupttemplates extrahieren (ohne das Layout-Tag selbst)
|
||||
$fragment = $dom->createDocumentFragment();
|
||||
|
||||
|
||||
foreach ($dom->documentElement->childNodes as $child) {
|
||||
$fragment->appendChild($layoutDom->importNode($child, true));
|
||||
}
|
||||
|
||||
// Ersetze Slot im Layout durch den gerenderten Body
|
||||
$slot->parentNode?->replaceChild($fragment, $slot);
|
||||
|
||||
// Ersetze gesamtes DOM durch Layout-DOM
|
||||
$newDom = $this->parser->parse($layoutDom->saveHTML());
|
||||
|
||||
$dom->replaceChild(
|
||||
$dom->importNode($newDom->documentElement, true),
|
||||
$dom->documentElement
|
||||
);
|
||||
}
|
||||
}
|
||||
24
src/Framework/View/Processors/MetaManipulator.php
Normal file
24
src/Framework/View/Processors/MetaManipulator.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\DomProcessor;
|
||||
use App\Framework\View\RenderContext;
|
||||
|
||||
final readonly class MetaManipulator implements DomProcessor
|
||||
{
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
foreach ($xpath->query('//meta[@name][@content]') as $meta) {
|
||||
$name = $meta->getAttribute('name');
|
||||
$content = $meta->getAttribute('content');
|
||||
|
||||
// Wenn Variable bereits im Context gesetzt ist, nicht überschreiben
|
||||
if (!array_key_exists($name, $context->data)) {
|
||||
$context->data[$name] = $content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/Framework/View/Processors/PlaceholderReplacer.php
Normal file
42
src/Framework/View/Processors/PlaceholderReplacer.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\DomProcessor;
|
||||
use App\Framework\View\RenderContext;
|
||||
|
||||
final readonly class PlaceholderReplacer implements DomProcessor
|
||||
{
|
||||
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
foreach ($xpath->query('//text()') as $textNode) {
|
||||
$textNode->nodeValue = preg_replace_callback(
|
||||
'/{{\s*([\w.]+)\s*}}/',
|
||||
fn($m) => $this->resolveValue($context->data, $m[1]),
|
||||
$textNode->nodeValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveValue(array $data, string $expr): string
|
||||
{
|
||||
$keys = explode('.', $expr);
|
||||
$value = $data;
|
||||
foreach ($keys as $key) {
|
||||
if (!is_array($value) || !array_key_exists($key, $value)) {
|
||||
return "{{ $expr }}"; // Platzhalter bleibt erhalten
|
||||
}
|
||||
$value = $value[$key];
|
||||
}
|
||||
return is_scalar($value) ? (string)$value : '';
|
||||
}
|
||||
|
||||
public function supports(\DOMElement $element): bool
|
||||
{
|
||||
return $element->tagName === 'text';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
42
src/Framework/View/Processors/SlotProcessor.php
Normal file
42
src/Framework/View/Processors/SlotProcessor.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\DomProcessor;
|
||||
use App\Framework\View\RenderContext;
|
||||
|
||||
/*
|
||||
|
||||
<component name="card">
|
||||
<slot name="header">Default Header</slot>
|
||||
<slot>Main Content</slot>
|
||||
</component>
|
||||
|
||||
*/
|
||||
|
||||
|
||||
final readonly class SlotProcessor implements DomProcessor
|
||||
{
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
foreach ($xpath->query('//slot[@name]') as $slotNode) {
|
||||
$slotName = $slotNode->getAttribute('name');
|
||||
$html = $context->slots[$slotName] ?? null;
|
||||
|
||||
$replacement = $dom->createDocumentFragment();
|
||||
|
||||
if ($html !== null) {
|
||||
@$replacement->appendXML($html);
|
||||
} else {
|
||||
// Fallback-Inhalt erhalten (die inneren Nodes des slot-Tags)
|
||||
foreach ($slotNode->childNodes as $child) {
|
||||
$replacement->appendChild($child->cloneNode(true));
|
||||
}
|
||||
}
|
||||
|
||||
$slotNode->parentNode?->replaceChild($replacement, $slotNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/Framework/View/Processors/SwitchCaseProcessor.php
Normal file
61
src/Framework/View/Processors/SwitchCaseProcessor.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Processors;
|
||||
|
||||
use App\Framework\View\DomProcessor;
|
||||
use App\Framework\View\RenderContext;
|
||||
use DOMXPath;
|
||||
|
||||
# Ersetzt ein eigenes `<switch value="foo">` mit inneren `<case when="bar">`-Elementen.
|
||||
|
||||
final readonly class SwitchCaseProcessor implements DomProcessor
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function process(\DOMDocument $dom, RenderContext $context): void
|
||||
{
|
||||
$xpath = new DOMXPath($dom);
|
||||
|
||||
foreach ($xpath->query('//switch[@value]') as $switchNode) {
|
||||
$key = $switchNode->getAttribute('value');
|
||||
$value = $context->data[$key] ?? null;
|
||||
|
||||
$matchingCase = null;
|
||||
$defaultCase = null;
|
||||
|
||||
foreach ($switchNode->childNodes as $child) {
|
||||
if (!($child instanceof \DOMElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($child->tagName === 'case') {
|
||||
$caseValue = $child->getAttribute('value');
|
||||
if ((string)$value === $caseValue) {
|
||||
$matchingCase = $child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($child->tagName === 'default') {
|
||||
$defaultCase = $child;
|
||||
}
|
||||
}
|
||||
|
||||
$replacement = $dom->createDocumentFragment();
|
||||
|
||||
if ($matchingCase) {
|
||||
foreach ($matchingCase->childNodes as $child) {
|
||||
$replacement->appendChild($child->cloneNode(true));
|
||||
}
|
||||
} elseif ($defaultCase) {
|
||||
foreach ($defaultCase->childNodes as $child) {
|
||||
$replacement->appendChild($child->cloneNode(true));
|
||||
}
|
||||
}
|
||||
|
||||
$switchNode->parentNode?->replaceChild($replacement, $switchNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Framework/View/RenderContext.php
Normal file
14
src/Framework/View/RenderContext.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
final readonly class RenderContext
|
||||
{
|
||||
public function __construct(
|
||||
public string $template, // Dateiname oder Template-Key
|
||||
public array $data = [], // Variablen wie ['title' => '...']
|
||||
public ?string $layout = null, // Optionales Layout
|
||||
public array $slots = [], // Benannte Slots wie ['main' => '<p>...</p>']
|
||||
public ?string $controllerClass = null
|
||||
) {}
|
||||
}
|
||||
145
src/Framework/View/Renderer.php
Normal file
145
src/Framework/View/Renderer.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
use App\Framework\View\Processors\CommentStripProcessor;
|
||||
use App\Framework\View\Processors\ComponentProcessor;
|
||||
use App\Framework\View\Processors\LayoutTagProcessor;
|
||||
use App\Framework\View\Processors\MetaManipulator;
|
||||
use App\Framework\View\Processors\PlaceholderReplacer;
|
||||
|
||||
final readonly class Renderer
|
||||
{
|
||||
public function __construct(
|
||||
private TemplateManipulator $manipulator = new TemplateManipulator(
|
||||
new LayoutTagProcessor(),
|
||||
#new PlaceholderReplacer(),
|
||||
new MetaManipulator(),
|
||||
new ComponentProcessor(),
|
||||
new CommentStripProcessor()
|
||||
)
|
||||
){}
|
||||
public function render(\DOMDocument $dom, array $data, Engine $engine): string
|
||||
{
|
||||
$componentRenderer = function (string $name, array $attributes, array $data) use ($engine) {
|
||||
$filename = __DIR__ . "/templates/components/$name.html";
|
||||
if (!file_exists($filename)) {
|
||||
return "<!-- Komponente '$name' nicht gefunden -->";
|
||||
}
|
||||
$content = file_get_contents($filename);
|
||||
// Optional: Rekursive/statische Komponentenverarbeitung
|
||||
return $this->renderComponentPartial($content, array_merge($data, $attributes), $engine);
|
||||
};
|
||||
|
||||
$this->manipulator->manipulate($dom, $data, $componentRenderer);
|
||||
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
|
||||
// 2. Schleifen <for var="item" in="items"></for>
|
||||
foreach ($xpath->query('//*[local-name() = "for"]') as $forNode) {
|
||||
$var = $forNode->getAttribute('var');
|
||||
$in = $forNode->getAttribute('in');
|
||||
$html = '';
|
||||
if (isset($data[$in]) && is_iterable($data[$in])) {
|
||||
foreach ($data[$in] as $item) {
|
||||
$clone = $forNode->cloneNode(true);
|
||||
foreach ($clone->childNodes as $node) {
|
||||
$this->renderNode($node, [$var => $item] + $data);
|
||||
}
|
||||
$frag = $dom->createDocumentFragment();
|
||||
foreach ($clone->childNodes as $child) {
|
||||
$frag->appendChild($child->cloneNode(true));
|
||||
}
|
||||
$html .= $dom->saveHTML($frag);
|
||||
}
|
||||
}
|
||||
// Ersetze das <for>-Element
|
||||
$fragment = $dom->createDocumentFragment();
|
||||
$fragment->appendXML($html);
|
||||
$forNode->parentNode->replaceChild($fragment, $forNode);
|
||||
}
|
||||
|
||||
// Analog: <if>- und <include>-Elemente einbauen (optional, auf Anfrage)
|
||||
|
||||
return $dom->saveHTML();
|
||||
}
|
||||
|
||||
private function renderNode(\DOMNode $node, array $data): void
|
||||
{
|
||||
// Rekursiv auf allen Kindknoten Platzhalter ersetzen (wie oben)
|
||||
if ($node->nodeType === XML_TEXT_NODE) {
|
||||
$original = $node->nodeValue;
|
||||
$replaced = preg_replace_callback('/{{\s*(\w+(?:\.\w+)*)\s*}}/', function ($matches) use ($data) {
|
||||
$keys = explode('.', $matches[1]);
|
||||
$value = $data;
|
||||
foreach ($keys as $key) {
|
||||
$value = is_array($value) && array_key_exists($key, $value) ? $value[$key] : $matches[0];
|
||||
}
|
||||
|
||||
return is_scalar($value)
|
||||
? htmlspecialchars((string)$value, ENT_QUOTES | ENT_HTML5)
|
||||
: $matches[0];
|
||||
}, $original);
|
||||
if ($original !== $replaced) {
|
||||
$node->nodeValue = $replaced;
|
||||
}
|
||||
}
|
||||
if ($node->hasChildNodes()) {
|
||||
foreach ($node->childNodes as $child) {
|
||||
$this->renderNode($child, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert ein Komponententemplate inklusive rekursivem Komponenten-Parsing.
|
||||
*/
|
||||
private function renderComponentPartial(string $content, array $data, Engine $engine): string
|
||||
{
|
||||
$cacheDir = __DIR__ . "/cache/components";
|
||||
if (!is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0777, true);
|
||||
}
|
||||
|
||||
$hash = md5($content . serialize($data));
|
||||
$cacheFile = $cacheDir . "/component_$hash.html";
|
||||
|
||||
if (file_exists($cacheFile)) {
|
||||
return file_get_contents($cacheFile);
|
||||
}
|
||||
|
||||
|
||||
$dom = new \DOMDocument();
|
||||
@$dom->loadHTML('<!DOCTYPE html><html lang="de"><body>'.$content.'</body></html>');
|
||||
|
||||
// Komponenten innerhalb des Partials auflösen (rekursiv!)
|
||||
$this->manipulator->processComponents(
|
||||
$dom,
|
||||
$data,
|
||||
function ($name, $attributes, $data) use ($engine) {
|
||||
$filename = __DIR__ . "/templates/components/$name.html";
|
||||
if (!file_exists($filename)) {
|
||||
return "<!-- Komponente '$name' nicht gefunden -->";
|
||||
}
|
||||
$subContent = file_get_contents($filename);
|
||||
return $this->renderComponentPartial($subContent, array_merge($data, $attributes), $engine);
|
||||
}
|
||||
);
|
||||
// Platzhalter in diesem Partial ersetzen
|
||||
$this->manipulator->replacePlaceholders($dom, $data);
|
||||
|
||||
// Nur den Inhalt von <body> extrahieren
|
||||
$body = $dom->getElementsByTagName('body')->item(0);
|
||||
$innerHTML = '';
|
||||
foreach ($body->childNodes as $child) {
|
||||
$innerHTML .= $dom->saveHTML($child);
|
||||
}
|
||||
file_put_contents($cacheFile, $innerHTML);
|
||||
return $innerHTML;
|
||||
}
|
||||
|
||||
}
|
||||
8
src/Framework/View/StringProcessor.php
Normal file
8
src/Framework/View/StringProcessor.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
interface StringProcessor
|
||||
{
|
||||
public function process(string $html, RenderContext $context): string;
|
||||
}
|
||||
38
src/Framework/View/TemplateLoader.php
Normal file
38
src/Framework/View/TemplateLoader.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
final readonly class TemplateLoader
|
||||
{
|
||||
public function __construct(
|
||||
private string $templatePath = __DIR__ . '/templates'
|
||||
) {
|
||||
}
|
||||
|
||||
public function load(string $template, ?string $controllerClass = null): string
|
||||
{
|
||||
$file = $this->getTemplatePath($template, $controllerClass);
|
||||
if (! file_exists($file)) {
|
||||
throw new \RuntimeException("Template \"$template\" nicht gefunden ($file).");
|
||||
}
|
||||
|
||||
return file_get_contents($file);
|
||||
}
|
||||
|
||||
public function getTemplatePath(string $template, ?string $controllerClass = null): string
|
||||
{
|
||||
if($controllerClass) {
|
||||
$rc = new \ReflectionClass($controllerClass);
|
||||
$dir = dirname($rc->getFileName());
|
||||
return $dir . DIRECTORY_SEPARATOR . $template . '.html';
|
||||
}
|
||||
return $this->templatePath . '/' . $template . '.html';
|
||||
}
|
||||
|
||||
public function getComponentPath(string $name): string
|
||||
{
|
||||
return __DIR__ . "/templates/components/{$name}.html";
|
||||
}
|
||||
}
|
||||
73
src/Framework/View/TemplateManipulator.php
Normal file
73
src/Framework/View/TemplateManipulator.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
final readonly class TemplateManipulator
|
||||
{
|
||||
private array $processors;
|
||||
public function __construct(
|
||||
DomProcessor ...$processor
|
||||
){
|
||||
$this->processors = $processor;
|
||||
}
|
||||
|
||||
public function manipulate(\DOMDocument $dom, array $data, callable $componentRenderer):void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
foreach ($xpath->query('//*') as $element) {
|
||||
if (!$element instanceof \DOMElement) continue;
|
||||
foreach ($this->processors as $processor) {
|
||||
if ($processor->supports($element)) {
|
||||
$processor->process($dom, $data, $componentRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Du könntest hier noch Platzhalter, Komponenten etc. ergänzen
|
||||
public function replacePlaceholders(\DOMDocument $dom, array $data): void
|
||||
{
|
||||
// Einfaches Beispiel für {{ variable }}-Platzhalter in Textknoten
|
||||
$xpath = new \DOMXPath($dom);
|
||||
foreach ($xpath->query('//text()') as $textNode) {
|
||||
foreach ($data as $key => $value) {
|
||||
$placeholder = '{{ ' . $key . ' }}';
|
||||
if (str_contains($textNode->nodeValue, $placeholder)) {
|
||||
$textNode->nodeValue = str_replace($placeholder, $value, $textNode->nodeValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst und ersetzt <component>-Elemente im DOM.
|
||||
* @param \DOMDocument $dom
|
||||
* @param array $data
|
||||
* @param callable $componentRenderer Funktion: (Name, Attribute, Daten) => HTML
|
||||
*/
|
||||
public function processComponents(\DOMDocument $dom, array $data, callable $componentRenderer): void
|
||||
{
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
// Alle <component>-Elemente durchgehen (XPath ist hier namespace-unabhängig)
|
||||
foreach ($xpath->query('//component') as $componentNode) {
|
||||
/** @var \DOMElement $componentNode */
|
||||
$name = $componentNode->getAttribute('name');
|
||||
// Alle Attribute als Array sammeln
|
||||
$attributes = [];
|
||||
foreach ($componentNode->attributes as $attr) {
|
||||
$attributes[$attr->nodeName] = $attr->nodeValue;
|
||||
}
|
||||
|
||||
// Hole das gerenderte HTML für diese Komponente
|
||||
$componentHtml = $componentRenderer($name, $attributes, $data);
|
||||
|
||||
// Ersetze das Node durch neues HTML-Fragment
|
||||
$fragment = $dom->createDocumentFragment();
|
||||
$fragment->appendXML($componentHtml);
|
||||
$componentNode->parentNode->replaceChild($fragment, $componentNode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
40
src/Framework/View/TemplateProcessor.php
Normal file
40
src/Framework/View/TemplateProcessor.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
class TemplateProcessor
|
||||
{
|
||||
/** @var DomProcessor[] */
|
||||
private array $domProcessors = [];
|
||||
|
||||
/** @var StringProcessor[] */
|
||||
private array $stringProcessors = [];
|
||||
|
||||
public function registerDom(DomProcessor $processor): void
|
||||
{
|
||||
$this->domProcessors[] = $processor;
|
||||
}
|
||||
|
||||
public function registerString(StringProcessor $processor): void
|
||||
{
|
||||
$this->stringProcessors[] = $processor;
|
||||
}
|
||||
|
||||
public function render(RenderContext $context, string $html): string
|
||||
{
|
||||
$parser = new DomTemplateParser();
|
||||
$dom = $parser->parse($html);
|
||||
|
||||
foreach ($this->domProcessors as $processor) {
|
||||
$processor->process($dom, $context);
|
||||
}
|
||||
|
||||
$html = $parser->toHtml($dom);
|
||||
|
||||
foreach ($this->stringProcessors as $processor) {
|
||||
$html = $processor->process($html, $context);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
10
src/Framework/View/TemplateRenderer.php
Normal file
10
src/Framework/View/TemplateRenderer.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
interface TemplateRenderer
|
||||
{
|
||||
public function render(RenderContext $context): string;
|
||||
}
|
||||
5
src/Framework/View/cache/epk.cache.html
vendored
Normal file
5
src/Framework/View/cache/epk.cache.html
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
<test><layout src="main"></layout>
|
||||
|
||||
<h1>EPK</h1>
|
||||
<p>Das ist mein EPK</p>
|
||||
</test>
|
||||
4
src/Framework/View/cache/impressum.cache.html
vendored
Normal file
4
src/Framework/View/cache/impressum.cache.html
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<test><layout src="main"></layout>
|
||||
<h1>Impressum!</h1>
|
||||
<p>Das ist deine Seite</p>
|
||||
</test>
|
||||
4
src/Framework/View/cache/test.cache.html
vendored
Normal file
4
src/Framework/View/cache/test.cache.html
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<test><layout src="main"></layout>
|
||||
<h1>Willkommen!</h1>
|
||||
<p>Das ist deine Seite</p>
|
||||
</test>
|
||||
3
src/Framework/View/templates/components/alert.html
Normal file
3
src/Framework/View/templates/components/alert.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="alert alert-{{ type }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
6
src/Framework/View/templates/components/card.html
Normal file
6
src/Framework/View/templates/components/card.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<div class="card">
|
||||
<div class="card-title">{{ title }}</div>
|
||||
<div class="card-body">
|
||||
<component name="alert" type="warning" message="{{ message }}" />
|
||||
</div>
|
||||
</div>
|
||||
4
src/Framework/View/templates/epk.html
Normal file
4
src/Framework/View/templates/epk.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<layout src="main"></layout>
|
||||
|
||||
<h1>EPK</h1>
|
||||
<p>Das ist mein EPK</p>
|
||||
3
src/Framework/View/templates/impressum.html
Normal file
3
src/Framework/View/templates/impressum.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<layout src="main"/>
|
||||
<h1>Impressum!</h1>
|
||||
<p>Das ist deine Seite</p>
|
||||
73
src/Framework/View/templates/layouts/main.html
Normal file
73
src/Framework/View/templates/layouts/main.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
|
||||
<!-- Favicon (optional) -->
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||
|
||||
<!-- CSS Reset oder Normalize (optional) -->
|
||||
<!-- <link rel="stylesheet" href="https://unpkg.com/modern-css-reset/dist/reset.min.css"> -->
|
||||
|
||||
<!-- Eigene Styles -->
|
||||
<link rel="stylesheet" href="http://localhost:5173/assets/css/styles.css">
|
||||
|
||||
<!-- Open Graph / SEO Meta-Tags (optional) -->
|
||||
<meta name="description" content="Kurzbeschreibung der Seite">
|
||||
<meta property="og:title" content="Titel der Seite">
|
||||
<meta property="og:description" content="Kurzbeschreibung der Seite">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://example.com">
|
||||
<meta property="og:image" content="https://example.com/image.jpg">
|
||||
|
||||
<!-- Dark Mode Unterstützung (optional) -->
|
||||
<meta name="color-scheme" content="light dark">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<a href="/" aria-label="Zur Startseite">
|
||||
Logo
|
||||
</a>
|
||||
|
||||
<button aria-label="Navigation öffnen" aria-controls="Main Navigation" aria-expanded="false">☰</button>
|
||||
</div>
|
||||
<nav aria-label="Main Navigation">
|
||||
<!-- Navigation -->
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<h1>Willkommen!</h1>
|
||||
<p>Das ist der Hauptinhalt deiner Seite.</p>
|
||||
|
||||
<component name="alert" type="danger" message="Dies ist ein Hinweis" />
|
||||
|
||||
<component name="card" title="Titel" message="Nachricht" />
|
||||
|
||||
|
||||
<!-- @dev START -->
|
||||
<div>Dies ist nur für Entwickler sichtbar.</div>
|
||||
<!-- @dev END -->
|
||||
<div>Das ist öffentlich.</div>
|
||||
|
||||
</main>
|
||||
|
||||
|
||||
<footer role="contentinfo">
|
||||
<p>© 2025 Michael Schiemer</p>
|
||||
|
||||
<nav aria-label="Footer Navigation">
|
||||
<ul>
|
||||
<li><a href="/impressum">Impressum</a></li>
|
||||
<li><a href="/impressum">Datenschutz</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/assets/js/main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
3
src/Framework/View/templates/test.html
Normal file
3
src/Framework/View/templates/test.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<layout src="main"/>
|
||||
<h1>Willkommen!</h1>
|
||||
<p>Das ist deine Seite</p>
|
||||
Reference in New Issue
Block a user