chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Framework\View\Caching\Analysis;
enum CacheStrategy: string
{
case FULL_PAGE = 'full_page';
case COMPONENT = 'component';
case FRAGMENT = 'fragment';
case USER_AWARE = 'user_aware';
case NO_CACHE = 'no_cache';
public function getTtl(): int
{
return match($this) {
self::FULL_PAGE => 3600, // 1 hour
self::COMPONENT => 1800, // 30 minutes
self::FRAGMENT => 900, // 15 minutes
self::USER_AWARE => 300, // 5 minutes
self::NO_CACHE => 0, // No caching
};
}
public function shouldCache(): bool
{
return $this !== self::NO_CACHE;
}
public function getDescription(): string
{
return match($this) {
self::FULL_PAGE => 'Full page caching for static content',
self::COMPONENT => 'Component-level caching for reusable parts',
self::FRAGMENT => 'Fragment-based caching for mixed content',
self::USER_AWARE => 'User-specific caching with short TTL',
self::NO_CACHE => 'No caching for fully dynamic content',
};
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Framework\View\Caching\Analysis;
class CacheabilityAnalyzer
{
public function analyze(string $content): CacheabilityScore
{
$score = new CacheabilityScore();
// Faktoren die Cacheability beeinflussen
$score->hasUserSpecificContent = $this->hasUserContent($content);
$score->hasCsrfTokens = $this->hasCsrfTokens($content);
$score->hasTimestamps = $this->hasTimestamps($content);
$score->hasRandomElements = $this->hasRandomElements($content);
$score->staticContentRatio = $this->calculateStaticRatio($content);
return $score;
}
private function hasUserContent(string $content)
{
}
private function hasCsrfTokens(string $content)
{
}
private function hasTimestamps(string $content)
{
}
private function hasRandomElements(string $content)
{
}
private function calculateStaticRatio(string $content)
{
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Framework\View\Caching\Analysis;
class CacheabilityScore
{
public bool $hasUserSpecificContent = false;
public bool $hasCsrfTokens = false;
public bool $hasTimestamps = false;
public bool $hasRandomElements = false;
public float $staticContentRatio = 0.0;
public function getScore(): float
{
$score = $this->staticContentRatio;
if ($this->hasUserSpecificContent) $score -= 0.3;
if ($this->hasCsrfTokens) $score -= 0.2;
if ($this->hasTimestamps) $score -= 0.1;
if ($this->hasRandomElements) $score -= 0.2;
return max(0, min(1, $score));
}
public function isCacheable(): bool
{
return $this->getScore() > 0.5;
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\Framework\View\Caching\Analysis;
use App\Framework\View\Loading\TemplateLoader;
final readonly class SmartTemplateAnalyzer implements TemplateAnalyzer
{
public function __construct(
private TemplateLoader $loader,
) {}
public function analyze(string $template): TemplateAnalysis
{
try {
$content = $this->loader->load($template);
} catch (\Throwable $e) {
return $this->createFallbackAnalysis($template);
}
$dependencies = $this->getDependencies($template);
$cacheability = $this->getCacheability($template);
$fragments = $this->findFragments($content);
$strategy = $this->determineOptimalStrategy($template, $cacheability, $dependencies);
$ttl = $this->calculateOptimalTtl($strategy, $cacheability);
return new TemplateAnalysis($template, $strategy, $ttl, $dependencies, $cacheability, $fragments);
}
public function getDependencies(string $template): array
{
try {
$content = $this->loader->load($template);
$dependencies = [];
// Finde @include directives
if (preg_match_all('/@include\s*\([\'"](.+?)[\'"]\)/', $content, $matches)) {
$dependencies['includes'] = $matches[1];
}
// Finde <component> tags
if (preg_match_all('/<component[^>]*name=[\'"]([^\'"]+)[\'"]/', $content, $matches)) {
$dependencies['components'] = $matches[1];
}
return $dependencies;
} catch (\Exception $e) {
return [];
}
}
public function getCacheability(string $template): CacheabilityScore
{
try {
$content = $this->loader->load($template);
$score = new CacheabilityScore();
// Analysiere Content
$score->hasUserSpecificContent = $this->hasUserContent($content);
$score->hasCsrfTokens = $this->hasCsrfTokens($content);
$score->hasTimestamps = $this->hasTimestamps($content);
$score->hasRandomElements = $this->hasRandomElements($content);
$score->staticContentRatio = $this->calculateStaticRatio($content);
return $score;
} catch (\Exception $e) {
return new CacheabilityScore();
}
}
private function determineOptimalStrategy(string $template, CacheabilityScore $cacheability, array $dependencies): CacheStrategy
{
// Nicht cacheable = NoCache
if (!$cacheability->isCacheable()) {
return CacheStrategy::NO_CACHE;
}
// Components
if (str_contains($template, 'component') || str_contains($template, 'partial')) {
return CacheStrategy::COMPONENT;
}
// Layouts und statische Seiten
if (str_contains($template, 'layout') || $cacheability->staticContentRatio > 0.8) {
return CacheStrategy::FULL_PAGE;
}
// Default: Fragment
return CacheStrategy::FRAGMENT;
}
private function calculateOptimalTtl(CacheStrategy $strategy, CacheabilityScore $cacheability): int
{
$baseTtl = match($strategy) {
CacheStrategy::FULL_PAGE => 1800,
CacheStrategy::COMPONENT => 900,
CacheStrategy::FRAGMENT => 300,
default => 0
};
// TTL basierend auf Cacheability Score anpassen
return (int) ($baseTtl * $cacheability->getScore());
}
private function findFragments(string $content)
{
// Vereinfachte Fragment-Erkennung
$fragments = [];
// Finde große statische HTML-Blöcke
if (preg_match_all('/<(?:nav|header|footer|aside)[^>]*>(.*?)<\/(?:nav|header|footer|aside)>/s', $content, $matches)) {
foreach ($matches[0] as $i => $match) {
if (strlen($match) > 200 && !str_contains($match, '{{')) {
$fragments["static_block_{$i}"] = [
'type' => 'static',
'size' => strlen($match),
'cacheable' => true
];
}
}
}
return $fragments;
}
private function createFallbackAnalysis(string $template): TemplateAnalysis
{
return new TemplateAnalysis(
$template,
CacheStrategy::NO_CACHE,
0,
[],
new CacheabilityScore(),
[]
);
}
private function hasUserContent(string $content): bool
{
return preg_match('/\{\{\s*(user|auth|current_user)/', $content) > 0;
}
private function hasCsrfTokens(string $content): bool
{
return preg_match('/\{\{\s*(csrf_token|_token)/', $content) > 0;
}
private function hasTimestamps(string $content): bool
{
return preg_match('/\{\{\s*(now|timestamp|date\()/', $content) > 0;
}
private function hasRandomElements(string $content): bool
{
return preg_match('/\{\{\s*(random|rand\(|uuid)/', $content) > 0;
}
private function calculateStaticRatio(string $content): float
{
$totalLength = strlen($content);
if ($totalLength === 0) return 0.0;
// Entferne alle dynamischen Teile
$staticContent = preg_replace('/\{\{.*?\}\}/', '', $content);
$staticContent = preg_replace('/@\w+.*?@end\w+/s', '', $staticContent);
return strlen($staticContent) / $totalLength;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Framework\View\Caching\Analysis;
class TemplateAnalysis
{
public function __construct(
public readonly string $template,
public readonly CacheStrategy $recommendedStrategy,
public readonly int $recommendedTtl,
public readonly array $dependencies,
public readonly CacheabilityScore $cacheability,
public readonly array $fragments
) {}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Framework\View\Caching\Analysis;
interface TemplateAnalyzer
{
public function analyze(string $template): TemplateAnalysis;
public function getDependencies(string $template): array;
public function getCacheability(string $template): CacheabilityScore;
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Framework\View\Caching;
use App\Framework\Cache\Cache;
use App\Framework\View\Caching\Analysis\TemplateAnalyzer;
class CacheDiagnostics
{
private array $metrics = [];
public function __construct(
private CacheManager $cacheManager,
private TemplateAnalyzer $analyzer,
private Cache $cache
) {}
public function getPerformanceReport(): array
{
return [
'cache_hit_rate' => $this->calculateHitRate(),
'average_render_time' => $this->getAverageRenderTime(),
'cache_size' => $this->getCacheSize(),
'most_cached_templates' => $this->getMostCachedTemplates(),
'cache_strategy_distribution' => $this->getStrategyDistribution(),
'memory_usage' => $this->getMemoryUsage(),
'recommendations' => $this->generateRecommendations()
];
}
public function analyzeTemplate(string $template): array
{
$analysis = $this->analyzer->analyze($template);
return [
'template' => $template,
'recommended_strategy' => $analysis->recommendedStrategy->name,
'recommended_ttl' => $analysis->recommendedTtl,
'cacheability_score' => $analysis->cacheability->getScore(),
'dependencies' => $analysis->dependencies,
'potential_fragments' => $analysis->fragments,
'optimization_suggestions' => $this->getOptimizationSuggestions($analysis)
];
}
public function warmupCache(array $templates): array
{
$results = [];
foreach ($templates as $template => $contexts) {
$results[$template] = [];
foreach ($contexts as $context) {
try {
$templateContext = new TemplateContext($template, $context);
// Warmup durch Rendering
$this->cacheManager->render($templateContext, function() {
return "<!-- Warmup content for {$template} -->";
});
$results[$template][] = 'success';
} catch (\Exception $e) {
$results[$template][] = 'error: ' . $e->getMessage();
}
}
}
return $results;
}
public function healthCheck(): array
{
return [
'cache_connection' => $this->testCacheConnection(),
'template_analyzer' => $this->testTemplateAnalyzer(),
'fragment_cache' => $this->testFragmentCache(),
'strategy_availability' => $this->testStrategies(),
'memory_usage' => $this->checkMemoryUsage(),
'disk_space' => $this->checkDiskSpace(),
'overall_status' => $this->determineOverallHealth()
];
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace App\Framework\View\Caching;
use App\Framework\Cache\Cache;
use App\Framework\View\Caching\Analysis\CacheStrategy;
use App\Framework\View\Caching\Analysis\TemplateAnalysis;
use App\Framework\View\Caching\Analysis\TemplateAnalyzer;
use App\Framework\View\Caching\Strategies\ComponentCacheStrategy;
use App\Framework\View\Caching\Strategies\FragmentCacheStrategy;
use App\Framework\View\Caching\Strategies\FullPageCacheStrategy;
use App\Framework\View\Caching\Strategies\NoCacheStrategy;
use App\Framework\View\Caching\Strategies\ViewCacheStrategy;
use App\Framework\View\RenderContext;
use Archive\Archived\SmartCacheEngine;
class CacheManager
{
private array $strategies = [];
private ?TemplateAnalysis $lastAnalysis = null;
public function __construct(
private Cache $cache,
private TemplateAnalyzer $analyzer,
private FragmentCache $fragmentCache,
private array $strategyMapping = []
) {
$this->initializeStrategies();
}
public function render(TemplateContext $context, callable $renderer): string
{
// 1. Template analysieren für optimale Strategy
$analysis = $this->analyzer->analyze($context->template);
$this->lastAnalysis = $analysis;
// 2. Passende Strategy auswählen
$strategy = $this->selectStrategy($analysis);
if (!$strategy->shouldCache($context)) {
return $renderer();
}
// 3. Cache-Key generieren
$cacheKey = $strategy->generateKey($context);
// 4. Cache-Lookup
$cached = $this->cache->get($cacheKey);
if ($cached->isHit) {
return $cached->value;
}
// 5. Rendern und cachen
$content = $renderer();
$ttl = $strategy->getTtl($context);
$this->cache->set($cacheKey, $content, $ttl);
return $content;
}
public function invalidateTemplate(string $template): int
{
$invalidated = 0;
// Invalidiere alle Strategies für dieses Template
foreach ($this->strategies as $strategy) {
if ($strategy->canInvalidate($template)) {
// Pattern-basierte Invalidierung je nach Strategy
$pattern = $this->buildInvalidationPattern($strategy, $template);
$invalidated += $this->invalidateByPattern($pattern);
}
}
return $invalidated;
}
private function selectStrategy(TemplateAnalysis $analysis): ViewCacheStrategy
{
return match($analysis->recommendedStrategy) {
CacheStrategy::FULL_PAGE => $this->strategies['full_page'],
CacheStrategy::COMPONENT => $this->strategies['component'],
CacheStrategy::FRAGMENT => $this->strategies['fragment'],
CacheStrategy::USER_AWARE => $this->strategies['user_aware'],
default => $this->strategies['no_cache']
};
}
private function initializeStrategies(): void
{
$this->strategies = [
'full_page' => new FullPageCacheStrategy($this->cache),
'component' => new ComponentCacheStrategy($this->cache),
'fragment' => new FragmentCacheStrategy($this->cache),
'no_cache' => new NoCacheStrategy(),
];
}
private function buildInvalidationPattern(mixed $strategy, string $template): string
{
return match(get_class($strategy)) {
FullPageCacheStrategy::class => "page:{$template}:*",
ComponentCacheStrategy::class => "component:*{$template}*",
FragmentCacheStrategy::class => "fragment:{$template}:*",
default => "*{$template}*"
};
}
private function invalidateByPattern(null $pattern): int
{
// Vereinfachte Implementation - in Realität müsste das der Cache-Driver unterstützen
// Für jetzt: Cache komplett leeren bei Pattern-Match
$invalidated = 0;
if (str_contains($pattern, '*')) {
$this->cache->clear();
$invalidated = 1;
}
return $invalidated;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Framework\View\Caching;
interface FragmentCache
{
public function fragment(string $key, callable $generator, int $ttl, array $tags = []): string;
public function invalidateFragment(string $key): bool;
public function invalidateByTags(array $tags): int;
public function hasFragment(string $key): bool;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Framework\View\Caching\Keys;
class SmartKeyGenerator
{
public function generate(string $string, string $key)
{
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Framework\View\Caching;
use App\Framework\View\RenderContext;
interface SmartCache
{
public function isEnabled(): bool;
public function render(RenderContext $context, callable $fallbackRenderer): string;
public function invalidateCache(?string $template = null): int;
public function getCacheStats(): array;
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Framework\View\Caching\Strategies;
use App\Framework\Cache\Cache;
use App\Framework\View\Caching\TemplateContext;
final readonly class ComponentCacheStrategy implements ViewCacheStrategy
{
public function __construct(private Cache $cache) {}
public function shouldCache(TemplateContext $context): bool
{
return str_contains($context->template, 'component') ||
str_contains($context->template, 'partial');
}
public function generateKey(TemplateContext $context): string
{
$component = basename($context->template);
$dataHash = md5(serialize($context->data));
return "component:{$component}:{$dataHash}";
}
public function getTtl(TemplateContext $context): int
{
// Komponenten länger cachen da sie wiederverwendbar sind
return 1800; // 30 minutes
}
public function canInvalidate(string $template): bool
{
return str_contains($template, 'component') || str_contains($template, 'partial');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Framework\View\Caching\Strategies;
use App\Framework\Cache\Cache;
use App\Framework\View\Caching\TemplateContext;
class FragmentCacheStrategy implements ViewCacheStrategy
{
public function __construct(private Cache $cache) {}
public function shouldCache(TemplateContext $context): bool
{
return isset($context->metadata['fragment_id']);
}
public function generateKey(TemplateContext $context): string
{
$fragment = $context->metadata['fragment_id'] ?? 'default';
return "fragment:{$context->template}:{$fragment}:" . md5(serialize($context->data));
}
public function getTtl(TemplateContext $context): int
{
return 600; // 10 minutes - Fragments kürzer cachen
}
public function canInvalidate(string $template): bool
{
return true;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Framework\View\Caching\Strategies;
use App\Framework\Cache\Cache;
use App\Framework\View\Caching\TemplateContext;
final readonly class FullPageCacheStrategy implements ViewCacheStrategy
{
public function __construct(private Cache $cache) {}
public function shouldCache(TemplateContext $context): bool
{
// Keine User-spezifischen Daten
return !$this->hasUserData($context->data) &&
!$this->hasVolatileData($context->data);
}
public function generateKey(TemplateContext $context): string
{
return "page:{$context->template}:" . md5(serialize($this->getNonVolatileData($context->data)));
}
public function getTtl(TemplateContext $context): int
{
return match($this->getPageType($context->template)) {
'layout' => 3600, // 1 hour
'static' => 7200, // 2 hours
'content' => 1800, // 30 minutes
default => 900 // 15 minutes
};
}
private function hasUserData(array $data): bool
{
$userKeys = ['user', 'auth', 'session', 'current_user'];
return !empty(array_intersect(array_keys($data), $userKeys));
}
private function hasVolatileData(array $data): bool
{
$volatileKeys = ['csrf_token', '_token', 'flash', 'errors', 'timestamp', 'now'];
return !empty(array_intersect(array_keys($data), $volatileKeys));
}
private function getNonVolatileData(array $data): array
{
$volatileKeys = ['csrf_token', '_token', 'flash', 'errors', 'timestamp', 'now', 'user', 'auth', 'session'];
return array_diff_key($data, array_flip($volatileKeys));
}
private function getPageType(string $template): string
{
if (str_contains($template, 'layout')) return 'layout';
if (str_contains($template, 'static')) return 'static';
if (str_contains($template, 'page')) return 'content';
return 'default';
}
public function canInvalidate(string $template): bool
{
return true;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Framework\View\Caching\Strategies;
use App\Framework\View\Caching\TemplateContext;
final readonly class NoCacheStrategy implements ViewCacheStrategy
{
public function shouldCache(TemplateContext $context): bool
{
return false;
}
public function generateKey(TemplateContext $context): string
{
return '';
}
public function getTtl(TemplateContext $context): int
{
return 0;
}
public function canInvalidate(string $template): bool
{
return false;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Framework\View\Caching\Strategies;
use App\Framework\View\Caching\TemplateContext;
interface ViewCacheStrategy
{
public function shouldCache(TemplateContext $context): bool;
public function generateKey(TemplateContext $context): string;
public function getTtl(TemplateContext $context): int;
public function canInvalidate(string $template): bool;
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Framework\View\Caching;
use App\Framework\Cache\Cache;
class TaggedFragmentCache implements FragmentCache
{
private array $tagMapping = [];
public function __construct(private Cache $cache) {}
public function fragment(string $key, callable $generator, int $ttl, array $tags = []): string
{
$fullKey = "fragment:{$key}";
$cached = $this->cache->get($fullKey);
if ($cached->isHit) {
return $cached->value;
}
$content = $generator();
$this->cache->set($fullKey, $content, $ttl);
$this->tagFragment($fullKey, $tags);
return $content;
}
public function invalidateFragment(string $key): bool
{
$fullKey = "fragment:{$key}";
return $this->cache->forget($fullKey);
}
public function invalidateByTags(array $tags): int
{
$invalidated = 0;
foreach ($tags as $tag) {
$keys = $this->getKeysByTag($tag);
foreach ($keys as $key) {
if ($this->cache->forget($key)) {
$invalidated++;
}
}
}
return $invalidated;
}
public function hasFragment(string $key): bool
{
$fullKey = "fragment:{$key}";
return $this->cache->has($fullKey);
}
private function tagFragment(string $key, array $tags): void
{
foreach ($tags as $tag) {
if (!isset($this->tagMapping[$tag])) {
$this->tagMapping[$tag] = [];
}
$this->tagMapping[$tag][] = $key;
}
}
private function getKeysByTag(string $tag): array
{
return $this->tagMapping[$tag] ?? [];
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Framework\View\Caching;
class TemplateContext
{
public function __construct(
public readonly string $template,
public readonly array $data,
public readonly ?string $controllerClass = null,
public readonly array $metadata = []
) {}
}

View File

@@ -1,18 +0,0 @@
<?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;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Framework\View;
use App\Framework\Filesystem\FileStorage;
final readonly class ComponentCache
{
public function __construct(
private string $cacheDir = __DIR__ . "/cache/components/",
private FileStorage $storage = new FileStorage,
) {}
public function get(string $componentName, array $data, string $templatePath): ?string
{
$hash = $this->generateHash($templatePath, $data);
$cacheFile = $this->getCacheFile($componentName, $hash);
return $this->storage->exists($cacheFile) ? $this->storage->get($cacheFile) : null;
}
public function set(string $componentName, array $data, string $templatePath, string $output): void
{
$hash = $this->generateHash($templatePath, $data);
$cacheFile = $this->getCacheFile($componentName, $hash);
$this->storage->createDirectory($this->cacheDir);
$this->storage->put($cacheFile, $output);
}
private function generateHash(string $templatePath, array $data): string
{
return md5_file($templatePath) . '_' . md5(serialize($data));
}
private function getCacheFile(string $componentName, string $hash): string
{
return $this->cacheDir . "/{$componentName}_{$hash}.html";
}
}

View File

@@ -2,46 +2,38 @@
namespace App\Framework\View;
final class ComponentRenderer
use App\Framework\Core\PathProvider;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Meta\MetaData;
use App\Framework\View\Loading\TemplateLoader;
final readonly 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/"
private TemplateProcessor $processor,
private ComponentCache $cache,
private TemplateLoader $loader,
private FileStorage $storage = new FileStorage,
) {}
public function render(string $componentName, array $data): string
{
$path = $this->loader->getComponentPath($componentName);
if (!file_exists($path)) {
if(!$this->storage->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);
if(!$cached = $this->cache->get($componentName, $data, $path)) {
return $cached;
}
$template = $this->storage->get($path);
$context = new RenderContext(template: $componentName, metaData: new MetaData(''), data: $data);
$output = $this->processor->render($context, $template);
$template = file_get_contents($path);
$compiled = $this->compiler->compile($template)->saveHTML();
$this->cache->set($componentName, $data, $path, $output);
$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;
}
}

View File

@@ -1,48 +0,0 @@
<?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;
}
}
}

View File

@@ -1,65 +0,0 @@
<?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}>";
}
}

View File

@@ -1,57 +0,0 @@
<?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();
}
}

View File

@@ -1,55 +0,0 @@
<?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);
}
}
}

View File

@@ -1,53 +0,0 @@
<?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;
}
}

View File

@@ -1,73 +0,0 @@
<?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;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Framework\View;
use Dom\Element;
final readonly class DomComponentService
{
public function replaceComponent(DomWrapper $dom, Element $component, string $html): void {
$dom->replaceElementWithHtml($component, $html);
}
public function processSlots(DomWrapper $dom): void {
$slots = $dom->getElementsByTagName('slot');
$slots->forEach(function($slot) use ($dom) {
$name = $slot->getAttribute('name') ?? 'default';
$content = $this->getSlotContent($dom, $name);
if ($content) {
$dom->replaceElementWithHtml($slot, $content);
}
});
}
private function getSlotContent(DomWrapper $dom, string $name): ?string {
$slotProviders = $dom->getElementsByAttribute('slot', $name);
if ($slotProviders->isEmpty()) {
return null;
}
return $slotProviders->first()->innerHTML;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Framework\View;
use Dom\Element;
final readonly class DomFormService
{
public function addCsrfTokenToForms(DomWrapper $dom, string $token): void {
$forms = $dom->getElementsByTagName('form');
$forms->forEach(function($form) use ($dom, $token) {
$this->addCsrfTokenToForm($dom, $form, $token);
});
}
public function addCsrfTokenToForm(DomWrapper $dom, Element $form, string $token): void {
// Prüfen ob bereits vorhanden
$existing = $form->querySelector('input[name="_token"]');
if ($existing) {
$existing->setAttribute('value', $token);
return;
}
$csrf = $dom->document->createElement('input');
$csrf->setAttribute('type', 'hidden');
$csrf->setAttribute('name', '_token');
$csrf->setAttribute('value', $token);
$form->insertBefore($csrf, $form->firstChild);
}
public function addHoneypotField(DomWrapper $dom, Element $form, string $fieldName = 'email_confirm'): void {
$honeypot = $dom->document->createElement('input');
$honeypot->setAttribute('type', 'text');
$honeypot->setAttribute('name', $fieldName);
$honeypot->setAttribute('style', 'display:none;position:absolute;left:-9999px;');
$honeypot->setAttribute('tabindex', '-1');
$honeypot->setAttribute('autocomplete', 'off');
$form->insertBefore($honeypot, $form->firstChild);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Framework\View;
final readonly class DomHeadService
{
public function appendToHead(DomWrapper $dom, string $html): void {
$head = $dom->getElementsByTagName('head')->first();
if (!$head) {
// Fallback: head erstellen oder zu body hinzufügen
$head = $dom->document->createElement('head');
$dom->document->documentElement->insertBefore($head, $dom->document->body);
}
$fragment = $dom->createFragmentFromHtml($html);
$head->appendChild($fragment);
}
public function prependToHead(DomWrapper $dom, string $html): void {
$head = $dom->getElementsByTagName('head')->first();
if (!$head) return;
$fragment = $dom->createFragmentFromHtml($html);
$head->insertBefore($fragment, $head->firstChild);
}
public function addStylesheet(DomWrapper $dom, string $href): void {
$link = $dom->document->createElement('link');
$link->setAttribute('rel', 'stylesheet');
$link->setAttribute('href', $href);
$head = $dom->getElementsByTagName('head')->first();
$head?->appendChild($link);
}
public function addScript(DomWrapper $dom, string $src, array $attributes = []): void {
$script = $dom->document->createElement('script');
$script->setAttribute('src', $src);
$script->setAttribute('type', 'module');
foreach ($attributes as $name => $value) {
$script->setAttribute($name, $value);
}
$head = $dom->getElementsByTagName('head')->first();
$head?->appendChild($script);
}
public function addMeta(DomWrapper $dom, string $name, string $content): void {
$meta = $dom->document->createElement('meta');
$meta->setAttribute('name', $name);
$meta->setAttribute('content', $content);
$head = $dom->getElementsByTagName('head')->first();
$head?->appendChild($meta);
}
}

View File

@@ -2,15 +2,9 @@
namespace App\Framework\View;
use Dom\HTMLDocument;
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;
public function process(DomWrapper $dom, RenderContext $context): DomWrapper;
}

View File

@@ -2,30 +2,48 @@
namespace App\Framework\View;
use Dom\HTMLDocument;
use DOMDocument;
final class DomTemplateParser
{
public function parse(string $html): DOMDocument
public function parse(string $html): HTMLDocument
{
//$html = preg_replace('#<(img|br|hr|input|meta|link|area|base|col|embed|source|track|wbr)([^>]*)>#i', '<$1$2 />', $html);
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
);
$dom = HTMLDocument::createFromString($html);
libxml_clear_errors();
return $dom;
}
public function toHtml(DOMDocument $dom): string
/**
* Erstellt einen DomWrapper aus HTML-String
*/
public function parseToWrapper(string $html): DomWrapper
{
return DomWrapper::fromString($html);
}
/**
* Erstellt einen DomWrapper aus Datei
*/
public function parseFileToWrapper(string $path): DomWrapper
{
return DomWrapper::fromFile($path);
}
public function toHtml(HTMLDocument $dom): string
{
return $dom->saveHTML();
}
public function parseFile(string $path): HTMLDocument
{
return HTMLDocument::createFromFile($path);
}
}

View File

@@ -0,0 +1,261 @@
<?php
namespace App\Framework\View;
use Dom\HTMLDocument;
use Dom\Element;
use Dom\HTMLElement;
use Dom\Node;
use Dom\DocumentFragment;
/**
* Wrapper-Klasse für HTMLDocument mit erweiterten Template-spezifischen Operationen
*/
final readonly class DomWrapper
{
public function __construct(
public HTMLDocument $document
) {}
/**
* Erstellt einen DomWrapper aus HTML-String
*/
public static function fromString(string $html): self
{
libxml_use_internal_errors(true);
$dom = HTMLDocument::createFromString($html);
libxml_clear_errors();
return new self($dom);
}
/**
* Erstellt einen DomWrapper aus Datei
*/
public static function fromFile(string $path): self
{
$dom = HTMLDocument::createFromFile($path);
return new self($dom);
}
/**
* Konvertiert zu HTML-String
*/
public function toHtml(bool $bodyOnly = false): string
{
if ($bodyOnly && $this->document->body) {
return $this->document->body->innerHTML;
}
return $this->document->saveHTML();
}
/**
* Findet Elemente nach Tag-Name
*/
public function getElementsByTagName(string $tagName): ElementCollection
{
$elements = iterator_to_array($this->document->getElementsByTagName($tagName));
return new ElementCollection($elements, $this);
}
/**
* Findet Element nach ID
*/
public function getElementById(string $id): ?Element
{
return $this->document->getElementById($id);
}
/**
* Findet Elemente nach Name-Attribut
*/
public function getElementsByName(string $name): ElementCollection
{
$elements = [];
$this->findElementsByNameRecursive($this->document->documentElement, $name, $elements);
return new ElementCollection($elements, $this);
}
private function findElementsByNameRecursive(?Node $node, string $name, array &$elements): void
{
if (!$node || $node->nodeType !== XML_ELEMENT_NODE) {
return;
}
if ($node instanceof Element && $node->getAttribute('name') === $name) {
$elements[] = $node;
}
foreach ($node->childNodes as $child) {
$this->findElementsByNameRecursive($child, $name, $elements);
}
}
/**
* Findet Elemente nach Attribut und Wert
*/
public function getElementsByAttribute(string $attribute, ?string $value = null): ElementCollection
{
$elements = [];
$this->findElementsByAttributeRecursive($this->document->documentElement, $attribute, $value, $elements);
return new ElementCollection($elements, $this);
}
private function findElementsByAttributeRecursive(?Node $node, string $attribute, ?string $value, array &$elements): void
{
if (!$node || $node->nodeType !== XML_ELEMENT_NODE) {
return;
}
if ($node instanceof Element) {
if ($value === null && $node->hasAttribute($attribute)) {
$elements[] = $node;
} elseif ($value !== null && $node->getAttribute($attribute) === $value) {
$elements[] = $node;
}
}
foreach ($node->childNodes as $child) {
$this->findElementsByAttributeRecursive($child, $attribute, $value, $elements);
}
}
/**
* Erstellt ein Document Fragment
*/
public function createFragment(): DocumentFragment
{
return $this->document->createDocumentFragment();
}
/**
* Erstellt ein Fragment aus HTML-String
*/
public function createFragmentFromHtml(string $html): DocumentFragment
{
$fragment = $this->document->createDocumentFragment();
$fragment->appendXML($html);
return $fragment;
}
/**
* Ersetzt ein Element durch HTML-Content
*/
public function replaceElementWithHtml(Element $element, string $html): void
{
$fragment = $this->createFragmentFromHtml($html);
$element->parentNode?->replaceChild($fragment, $element);
}
/**
* Entfernt leere Textknoten
*/
public function removeEmptyTextNodes(): self
{
$this->removeEmptyTextNodesRecursive($this->document->documentElement);
return $this;
}
private function removeEmptyTextNodesRecursive(?Node $node): void
{
if (!$node) {
return;
}
$childNodes = [];
foreach ($node->childNodes as $child) {
$childNodes[] = $child;
}
foreach ($childNodes as $child) {
if ($child->nodeType === XML_TEXT_NODE && trim($child->textContent) === '') {
$node->removeChild($child);
} else {
$this->removeEmptyTextNodesRecursive($child);
}
}
}
/**
* Fügt CSS-Klasse zu einem Element hinzu
*/
public function addClass(Element $element, string $className): void
{
$currentClass = $element->getAttribute('class');
$classes = $currentClass ? explode(' ', $currentClass) : [];
if (!in_array($className, $classes)) {
$classes[] = $className;
$element->setAttribute('class', implode(' ', $classes));
}
}
/**
* Entfernt CSS-Klasse von einem Element
*/
public function removeClass(Element $element, string $className): void
{
$currentClass = $element->getAttribute('class');
if (!$currentClass) {
return;
}
$classes = array_filter(explode(' ', $currentClass), fn($c) => $c !== $className);
$element->setAttribute('class', implode(' ', $classes));
}
/**
* Prüft ob Element eine CSS-Klasse hat
*/
public function hasClass(Element $element, string $className): bool
{
$currentClass = $element->getAttribute('class');
if (!$currentClass) {
return false;
}
return in_array($className, explode(' ', $currentClass));
}
/**
* Findet Elemente nach CSS-Klasse
*/
public function getElementsByClassName(string $className): ElementCollection
{
$elements = [];
$this->findElementsByClassRecursive($this->document->documentElement, $className, $elements);
return new ElementCollection($elements, $this);
}
private function findElementsByClassRecursive(?Node $node, string $className, array &$elements): void
{
if (!$node || $node->nodeType !== XML_ELEMENT_NODE) {
return;
}
if ($node instanceof Element && $this->hasClass($node, $className)) {
$elements[] = $node;
}
foreach ($node->childNodes as $child) {
$this->findElementsByClassRecursive($child, $className, $elements);
}
}
/**
* Setzt Attribut für ein Element
*/
public function setAttribute(Element $element, string $name, string $value): void
{
$element->setAttribute($name, $value);
}
/**
* Entfernt Attribut von einem Element
*/
public function removeAttribute(Element $element, string $name): void
{
$element->removeAttribute($name);
}
}

View File

@@ -0,0 +1,279 @@
<?php
namespace App\Framework\View;
use ArrayIterator;
use Countable;
use Dom\Element;
use IteratorAggregate;
/**
* Funktionale Sammlung für DOM-Elemente mit moderner API
*/
final class ElementCollection implements IteratorAggregate, Countable
{
public function __construct(
private array $elements,
private DomWrapper $wrapper
) {}
/**
* Führt eine Funktion für jedes Element aus
*/
public function forEach(callable $callback): self
{
foreach ($this->elements as $index => $element) {
$callback($element, $index);
}
return $this;
}
/**
* Transformiert jedes Element und gibt ein Array zurück
*/
public function map(callable $callback): array
{
return array_map($callback, $this->elements);
}
/**
* Filtert Elemente basierend auf einer Bedingung
*/
public function filter(callable $callback): self
{
return new self(
array_values(array_filter($this->elements, $callback)),
$this->wrapper
);
}
/**
* Reduziert die Sammlung zu einem einzelnen Wert
*/
public function reduce(callable $callback, mixed $initial = null): mixed
{
return array_reduce($this->elements, $callback, $initial);
}
/**
* Prüft ob mindestens ein Element die Bedingung erfüllt
*/
public function some(callable $callback): bool
{
foreach ($this->elements as $element) {
if ($callback($element)) {
return true;
}
}
return false;
}
/**
* Prüft ob alle Elemente die Bedingung erfüllen
*/
public function every(callable $callback): bool
{
foreach ($this->elements as $element) {
if (!$callback($element)) {
return false;
}
}
return true;
}
/**
* Findet das erste Element, das die Bedingung erfüllt
*/
public function find(callable $callback): ?Element
{
foreach ($this->elements as $element) {
if ($callback($element)) {
return $element;
}
}
return null;
}
/**
* Gibt das erste Element zurück
*/
public function first(): ?Element
{
return $this->elements[0] ?? null;
}
/**
* Gibt das letzte Element zurück
*/
public function last(): ?Element
{
return end($this->elements) ?: null;
}
/**
* Gibt das Element an der angegebenen Position zurück
*/
public function at(int $index): ?Element
{
return $this->elements[$index] ?? null;
}
/**
* Nimmt die ersten n Elemente
*/
public function take(int $count): self
{
return new self(
array_slice($this->elements, 0, $count),
$this->wrapper
);
}
/**
* Überspringt die ersten n Elemente
*/
public function skip(int $count): self
{
return new self(
array_slice($this->elements, $count),
$this->wrapper
);
}
/**
* Fügt CSS-Klasse zu allen Elementen hinzu
*/
public function addClass(string $className): self
{
return $this->forEach(fn($el) => $this->wrapper->addClass($el, $className));
}
/**
* Entfernt CSS-Klasse von allen Elementen
*/
public function removeClass(string $className): self
{
return $this->forEach(fn($el) => $this->wrapper->removeClass($el, $className));
}
/**
* Setzt Attribut für alle Elemente
*/
public function setAttribute(string $name, string $value): self
{
return $this->forEach(fn($el) => $el->setAttribute($name, $value));
}
/**
* Entfernt Attribut von allen Elementen
*/
public function removeAttribute(string $name): self
{
return $this->forEach(fn($el) => $el->removeAttribute($name));
}
/**
* Setzt innerHTML für alle Elemente
*/
public function setInnerHTML(string $html): self
{
return $this->forEach(fn($el) => $el->innerHTML = $html);
}
/**
* Setzt textContent für alle Elemente
*/
public function setTextContent(string $text): self
{
return $this->forEach(fn($el) => $el->textContent = $text);
}
/**
* Ersetzt alle Elemente mit HTML-Content
*/
public function replaceWith(string $html): void
{
$this->forEach(fn($el) => $this->wrapper->replaceElementWithHtml($el, $html));
}
/**
* Entfernt alle Elemente aus dem DOM
*/
public function remove(): void
{
$this->forEach(fn($el) => $el->parentNode?->removeChild($el));
}
/**
* Kombiniert diese Sammlung mit einer anderen
*/
public function merge(ElementCollection $other): self
{
return new self(
array_merge($this->elements, $other->elements),
$this->wrapper
);
}
/**
* Gibt eindeutige Elemente zurück
*/
public function unique(): self
{
return new self(
array_values(array_unique($this->elements, SORT_REGULAR)),
$this->wrapper
);
}
/**
* Prüft ob die Sammlung leer ist
*/
public function isEmpty(): bool
{
return empty($this->elements);
}
/**
* Prüft ob die Sammlung nicht leer ist
*/
public function isNotEmpty(): bool
{
return !$this->isEmpty();
}
/**
* Konvertiert zu Array
*/
public function toArray(): array
{
return $this->elements;
}
/**
* Gibt die Anzahl der Elemente zurück
*/
public function count(): int
{
return count($this->elements);
}
/**
* Iterator für foreach-Schleifen
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->elements);
}
/**
* Debug-Information
*/
public function __debugInfo(): array
{
return [
'count' => $this->count(),
'elements' => $this->elements
];
}
}

View File

@@ -1,53 +1,162 @@
<?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;
use App\Framework\Attributes\Singleton;
use App\Framework\Cache\Cache;
use App\Framework\Core\PathProvider;
use App\Framework\DI\Container;
use App\Framework\DI\DefaultContainer;
use App\Framework\Filesystem\FileStorage;
use App\Framework\View\Caching\Analysis\SmartTemplateAnalyzer;
use App\Framework\View\Caching\CacheManager;
use App\Framework\View\Caching\TaggedFragmentCache;
use App\Framework\View\Caching\TemplateContext;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\Processors\CsrfReplaceProcessor;
use Archive\Optimized\QuickSmartCacheReplacement;
#[Singleton]
final readonly class Engine implements TemplateRenderer
{
private ?TemplateRenderer $smartCache;
private ?CacheManager $cacheManager;
public function __construct(
private TemplateLoader $loader = new TemplateLoader(),
private Compiler $compiler = new Compiler(),
private Renderer $renderer = new Renderer(),
private TemplateProcessor $processor = new TemplateProcessor()
private TemplateLoader $loader,
private PathProvider $pathProvider,
private DomTemplateParser $parser = new DomTemplateParser,
private TemplateProcessor $processor = new TemplateProcessor,
private FileStorage $storage = new FileStorage,
private string $cachePath = __DIR__ . "/cache",
private Container $container = new DefaultContainer(),
private bool $useSmartCache = true,
private bool $legacyCacheEnabled = false,
?Cache $cache = null,
private bool $cacheEnabled = true,
) {
$this->processor->registerDom(new ComponentProcessor());
$this->processor->registerDom(new LayoutTagProcessor());
$this->processor->registerDom(new PlaceholderReplacer());
// Stelle sicher, dass das Cache-Verzeichnis existiert
if (!is_dir($this->cachePath)) {
mkdir($this->cachePath, 0755, true);
}
/*
// Initialisiere Smart Cache System
if ($this->useSmartCache && $cache) {
#$analyzer = new TemplateAnalyzer();
#$fragmentCache = new FragmentCacheManager($cache);
$this->smartCache = new QuickSmartCacheReplacement(
cache: $cache,
#analyzer : $analyzer,
#fragmentCache: $fragmentCache,
loader: $this->loader,
processor: $this->processor
);
} else {
$this->smartCache = null;
}*/
// Neues Cache-System initialisieren
if ($this->cacheEnabled && $cache) {
$analyzer = new SmartTemplateAnalyzer($this->loader);
$fragmentCache = new TaggedFragmentCache($cache);
$this->cacheManager = new CacheManager(
cache: $cache,
analyzer: $analyzer,
fragmentCache: $fragmentCache
);
} else {
$this->cacheManager = null;
}
}
public function render(RenderContext $context): string
{
// Verwende neuen CacheManager wenn verfügbar
if ($this->cacheManager) {
$templateContext = new TemplateContext(
template: $context->template,
data: $context->data,
controllerClass: $context->controllerClass,
metadata: $context->metaData ? ['meta' => $context->metaData] : []
);
$template = $context->template;
$data = $context->data;
return $this->cacheManager->render($templateContext, function() use ($context) {
return $this->renderDirect($context);
});
}
$cacheFile = __DIR__ . "/cache/{$template}.cache.html";
// Fallback ohne Cache
return $this->renderDirect($context);
$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;
// Verwende Smart Cache wenn verfügbar
if ($this->useSmartCache && $this->smartCache !== null) {
$processor = new TemplateProcessor([],[CsrfReplaceProcessor::class], $this->container);
$cachedOutput = $this->smartCache->render($context);
return $processor->render($context, $cachedOutput ?? '');
}
return $this->processor->render($context, $content);
#return $this->renderer->render($dom, $data, $this);
// Fallback zu Legacy-Caching oder direkt rendern
return $this->legacyCacheEnabled
? $this->renderWithLegacyCache($context)
: $this->renderDirect($context);
}
private function renderWithLegacyCache(RenderContext $context): string
{
$cacheKey = 'view_' . md5($context->template . '_' . ($context->controllerClass ?? ''));
$templateFile = $this->loader->getTemplatePath($context->template, $context->controllerClass);
$cacheFile = $this->cachePath . "/{$cacheKey}.cache.html";
// Prüfe ob Cache existiert und nicht älter als das Template
// FIXED: Entferne das "x" Suffix um Caching zu reaktivieren
if (file_exists($cacheFile) && filemtime($cacheFile) >= filemtime($templateFile)) {
$content = $this->storage->get($cacheFile);
} else {
// Template normal laden und kompilieren
$content = $this->loader->load($context->template, $context->controllerClass, $context);
$dom = $this->parser->parse($content);
$html = $dom->saveHTML();
$html = html_entity_decode($html);
// VOR dynamischer Verarbeitung rohe Struktur cachen
$this->storage->put($cacheFile, $html);
$content = $html;
}
return $this->processor->render($context, $content);
}
private function renderDirect(RenderContext $context): string
{
$content = $this->loader->load($context->template, $context->controllerClass, $context);
$dom = $this->parser->parse($content);
$html = html_entity_decode($dom->saveHTML());
return $this->processor->render($context, $html);
}
public function invalidateCache(?string $template = null): int
{
return $this->cacheManager?->invalidateTemplate($template) ?? 0;
}
public function getCacheStats(): array
{
if (!$this->cacheManager) {
return ['cache_enabled' => false];
}
return [
'cache_enabled' => true,
'cache_manager' => $this->cacheManager->getStats(), // Diese Methode müsstest du noch implementieren
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Framework\View;
use Dom\HTMLDocument;
/**
* Erweiterte Version des DomProcessor-Interfaces mit DomWrapper-Unterstützung
*/
interface EnhancedDomProcessor extends DomProcessor
{
/**
* Verarbeitet das DOM mit dem erweiterten DomWrapper
* @param DomWrapper $dom Zu manipulierender DOM-Wrapper
* @param RenderContext $context Kontext für die Verarbeitung
* @return DomWrapper Der veränderte DOM-Wrapper
*/
public function process(DomWrapper $dom, RenderContext $context): DomWrapper;
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Framework\View;
use App\Framework\DI\Container;
/**
* Erweiterte Version des TemplateProcessors mit DomWrapper-Unterstützung
*/
final readonly class EnhancedTemplateProcessor
{
public function __construct(
private array $domProcessors,
private array $stringProcessors,
private Container $container
) {}
public function render(RenderContext $context, string $html, bool $component = false): string
{
$parser = new DomTemplateParser();
$domWrapper = $parser->parseToWrapper($html);
// Verkettete Verarbeitung der DOM-Prozessoren
foreach ($this->domProcessors as $processorClass) {
$processor = $this->container->get($processorClass);
if ($processor instanceof EnhancedDomProcessor) {
// Verwende die erweiterte DomWrapper-Funktionalität
$domWrapper = $processor->processWrapper($domWrapper, $context);
} else {
// Fallback für bestehende Prozessoren
$dom = $processor->process($domWrapper->document, $context);
$domWrapper = new DomWrapper($dom);
}
}
$html = $domWrapper->toHtml($component);
// Verarbeitung durch String-Prozessoren
foreach ($this->stringProcessors as $processorClass) {
$processor = $this->container->get($processorClass);
$html = $processor->process($html, $context);
}
return $html;
}
/**
* Rendert direkt mit DomWrapper für erweiterte Kontrolle
*/
public function renderWithWrapper(RenderContext $context, string $html, bool $component = false): DomWrapper
{
$parser = new DomTemplateParser();
$domWrapper = $parser->parseToWrapper($html);
// Verkettete Verarbeitung der DOM-Prozessoren
foreach ($this->domProcessors as $processorClass) {
$processor = $this->container->get($processorClass);
if ($processor instanceof EnhancedDomProcessor) {
$domWrapper = $processor->processWrapper($domWrapper, $context);
} else {
$dom = $processor->process($domWrapper->document, $context);
$domWrapper = new DomWrapper($dom);
}
}
return $domWrapper;
}
public function __debugInfo(): ?array
{
return [
'domProcessors' => $this->domProcessors,
'stringProcessors' => $this->stringProcessors,
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Framework\View\Exception;
use App\Framework\Exception\FrameworkException;
class TemplateNotFound extends FrameworkException
{
protected string $template;
public function __construct(string $template, ?\Throwable $previous = null, int $code = 0, array $context = [])
{
$this->template = $template;
$message = "Das Template '$template' konnte nicht geladen werden.";
parent::__construct($message, $code, $previous, $context);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Exceptions;
use App\Framework\Exception\FrameworkException;
final class TemplateNotFoundException extends FrameworkException
{
public function __construct(
string $template,
string $file,
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct(
message: "Template \"$template\" nicht gefunden ($file).",
code: $code,
previous: $previous,
context: [
'template' => $template,
'file' => $file
]
);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Framework\View\Functions;
use App\Domain\Media\ImageSlotRepository;
use App\Domain\Media\ImageSourceSetGenerator;
use App\Framework\View\ComponentRenderer;
final readonly class ImageSlotFunction implements TemplateFunction
{
public string $functionName;
public function __construct(
private ImageSlotRepository $imageSlotRepository,
private ComponentRenderer $componentRenderer
){
$this->functionName = 'imageslot';
}
public function __invoke(string $slotName): string
{
$image = $this->imageSlotRepository->findBySlotName($slotName)->image;
$srcGen = new ImageSourceSetGenerator();
return $srcGen->generatePictureElement($image);
$data = [
'image' => $image->filename,
'alt' => $image->altText
];
return $this->componentRenderer->render('imageslot', $data);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Framework\View\Functions;
interface TemplateFunction
{
public string $functionName {get;}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Framework\View\Functions;
use App\Framework\Router\UrlGenerator;
final readonly class UrlFunction implements TemplateFunction
{
public function __construct(
private UrlGenerator $urlGenerator
){
$this->functionName = 'url';
}
public function __invoke(string $namedRoute, array $params = []): string
{
return $this->urlGenerator->route($namedRoute, $params);
}
public string $functionName;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Framework\View\Loading\Resolvers;
final readonly class ControllerResolver implements TemplateResolverStrategy
{
const string TEMPLATE_EXTENSION = '.view.php';
public function resolve(string $template, ?string $controllerClass = null): ?string
{
if(!$controllerClass) {
return null;
}
try {
$rc = new \ReflectionClass($controllerClass);
$dir = dirname($rc->getFileName());
$path = $dir . DIRECTORY_SEPARATOR . $template . self::TEMPLATE_EXTENSION;
return file_exists($path) ? $path : null;
} catch (\ReflectionException) {
return null;
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Framework\View\Loading\Resolvers;
use App\Framework\Core\PathProvider;
final readonly class DefaultPathResolver implements TemplateResolverStrategy
{
const string TEMPLATE_EXTENSION = '.view.php';
public function __construct(
private PathProvider $pathProvider,
private string $templatePath = '/src/Framework/View/templates'
){}
public function resolve(string $template, ?string $controllerClass = null): ?string
{
$path = $this->pathProvider->resolvePath(
$this->templatePath . '/' . $template . self::TEMPLATE_EXTENSION
);
return file_exists($path) ? $path : null;
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Framework\View\Loading\Resolvers;
use App\Framework\Discovery\Results\DiscoveryResults;
use App\Framework\View\TemplateDiscoveryVisitor;
final readonly class DiscoveryResolver implements TemplateResolverStrategy
{
const string TEMPLATE_EXTENSION = '.view.php';
public function __construct(
private DiscoveryResults|null $discoverySource = null
) {}
public function resolve(string $template, ?string $controllerClass = null): ?string
{
if ($this->discoverySource === null) {
return null;
}
return $this->resolveFromDiscoveryResults($template, $controllerClass);
}
private function resolveFromDiscoveryResults(string $template, ?string $controllerClass): ?string
{
$templates = $this->discoverySource->getTemplates();
// Strategie 1: Direkter Template-Name-Match
if (isset($templates[$template])) {
return $this->selectBestTemplate($templates[$template], $controllerClass);
}
// Strategie 2: Template mit Extension
$templateWithExt = $template . self::TEMPLATE_EXTENSION;
if (isset($templates[$templateWithExt])) {
return $this->selectBestTemplate($templates[$templateWithExt], $controllerClass);
}
// Strategie 3: Suche in allen Templates
foreach ($templates as $templateName => $templateData) {
if ($this->templateMatches($templateName, $template)) {
return $this->selectBestTemplate($templateData, $controllerClass);
}
}
return null;
}
private function resolveFromTemplateVisitor(string $template, ?string $controllerClass): ?string
{
$path = $this->discoverySource->getTemplatePath($template, $controllerClass);
return ($path && file_exists($path)) ? $path : null;
}
private function selectBestTemplate(mixed $templateData, ?string $controllerClass): ?string
{
// Wenn templateData ein String ist (direkter Pfad)
if (is_string($templateData)) {
return file_exists($templateData) ? $templateData : null;
}
// Wenn templateData ein Array ist (mehrere Varianten)
if (is_array($templateData)) {
// 1. Versuche Controller-spezifisches Template
if ($controllerClass && isset($templateData[$controllerClass])) {
$path = $templateData[$controllerClass];
if (is_string($path) && file_exists($path)) {
return $path;
}
}
// 2. Versuche Standard-Template (leerer Key)
if (isset($templateData[''])) {
$path = $templateData[''];
if (is_string($path) && file_exists($path)) {
return $path;
}
}
// 3. Nimm das erste verfügbare Template
foreach ($templateData as $path) {
if (is_string($path) && file_exists($path)) {
return $path;
}
}
}
return null;
}
private function templateMatches(string $templateName, string $searchTemplate): bool
{
// Entferne Extension für Vergleich
$cleanTemplateName = str_replace(self::TEMPLATE_EXTENSION, '', $templateName);
return $cleanTemplateName === $searchTemplate ||
basename($cleanTemplateName) === $searchTemplate ||
str_contains($cleanTemplateName, $searchTemplate);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Framework\View\Loading\Resolvers;
final readonly class TemplateMapResolver implements TemplateResolverStrategy
{
const string TEMPLATE_EXTENSION = '.view.php';
public function __construct(
private array $templates = []
){}
public function resolve(string $template, ?string $controllerClass = null): ?string
{
$key = $template . self::TEMPLATE_EXTENSION;
$path = $this->templates[$key] ?? null;
return ($path && file_exists($path)) ? $path : null;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Framework\View\Loading\Resolvers;
interface TemplateResolverStrategy
{
public function resolve(string $template, ?string $controllerClass = null): ?string;
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Framework\View\Loading;
final class TemplateCache
{
private array $cache = [];
public function __construct(
private readonly bool $enabled = true
) {}
public function get(string $key): ?string
{
if (!$this->enabled) {
return null;
}
return $this->cache[$key] ?? null;
}
public function set(string $key, string $content): void
{
if (!$this->enabled) {
return;
}
$this->cache[$key] = $content;
}
public function clear(): void
{
$this->cache = [];
}
public function isEnabled(): bool
{
return $this->enabled;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Framework\View\Loading;
final readonly class TemplateContentLoader
{
public function load(string $path): string
{
if (!file_exists($path)) {
throw new \RuntimeException("Template file not found: {$path}");
}
$content = @file_get_contents($path);
if ($content === false) {
throw new \RuntimeException("Template could not be read: {$path}");
}
// HTML-Entities dekodieren, damit -> nicht als &gt; erscheint
return html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading;
use App\Framework\Cache\Cache;
use App\Framework\Core\PathProvider;
use App\Framework\Discovery\Results\DiscoveryResults;
use App\Framework\Filesystem\FileStorage;
use App\Framework\View\Loading\Resolvers\ControllerResolver;
use App\Framework\View\Loading\Resolvers\DefaultPathResolver;
use App\Framework\View\Loading\Resolvers\DiscoveryResolver;
use App\Framework\View\Loading\Resolvers\TemplateMapResolver;
use App\Framework\View\Loading\Resolvers\LayoutResolver;
use App\Framework\View\Loading\Resolvers\ComponentResolver;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateDiscoveryVisitor;
final class TemplateLoader
{
private readonly TemplatePathResolver $pathResolver;
private readonly TemplateContentLoader $contentLoader;
#private readonly TemplateCache $cache;
public function __construct(
private readonly PathProvider $pathProvider,
private readonly Cache $cache,
private readonly ?DiscoveryResults $discoveryResults = null,
private readonly array $templates = [],
private readonly string $templatePath = '/src/Framework/View/templates',
private readonly FileStorage $storage = new FileStorage,
#private readonly ?TemplateDiscoveryVisitor $templateVisitor = null,
) {
$this->pathResolver = $this->createPathResolver();
$this->contentLoader = new TemplateContentLoader();
#$this->cache = new TemplateCache();
}
private function createPathResolver(): TemplatePathResolver
{
$resolvers = [
// 1. Explizite Template-Map (höchste Priorität)
new TemplateMapResolver($this->templates),
// 2. Discovery-basierte Templates (falls verfügbar)
new DiscoveryResolver($this->discoveryResults),
// 3. Layout-spezifische Suche (für "main" etc.)
new LayoutResolver($this->pathProvider, $this->templatePath),
// 4. Component-spezifische Suche
new ComponentResolver($this->pathProvider, $this->templatePath),
// 5. Controller-basierte Templates
new ControllerResolver(),
// 6. Standard-Pfad (Fallback)
new DefaultPathResolver($this->pathProvider, $this->templatePath),
];
return new TemplatePathResolver($resolvers);
}
public function load(string $template, ?string $controllerClass = null, ?RenderContext $context = null): string
{
$cacheKey = 'template_content|' . $template . '|' . ($controllerClass ?? 'default');
return $this->cache->remember($cacheKey, function() use ($template, $controllerClass, $context) {
$path = $this->pathResolver->resolve($template, $controllerClass);
$content = $this->contentLoader->load($path);
return $content;
})->value;
if ($cached = $this->cache->get($cacheKey)) {
return $cached;
}
$path = $this->pathResolver->resolve($template, $controllerClass);
$content = $this->contentLoader->load($path);
$this->cache->set($cacheKey, $content);
return $content;
}
public function getTemplatePath(string $template, ?string $controllerClass = null): string
{
return $this->pathResolver->resolve($template, $controllerClass);
}
public function clearCache(): void
{
$this->cache->clear();
}
public function getComponentPath(string $name): string
{
return __DIR__ . "/../templates/components/{$name}.html";
}
public function debugTemplatePath(string $template, ?string $controllerClass = null): array
{
$debug = ['template' => $template, 'attempts' => []];
$resolvers = [
'TemplateMap' => new TemplateMapResolver($this->templates),
'Discovery' => new DiscoveryResolver($this->discoveryResults),
'Layout' => new LayoutResolver($this->pathProvider, $this->templatePath),
'Component' => new ComponentResolver($this->pathProvider, $this->templatePath),
'Controller' => new ControllerResolver(),
'Default' => new DefaultPathResolver($this->pathProvider, $this->templatePath),
];
foreach ($resolvers as $name => $resolver) {
try {
$path = $resolver->resolve($template, $controllerClass);
$debug['attempts'][$name] = [
'path' => $path,
'exists' => $path ? file_exists($path) : false,
'success' => $path && file_exists($path)
];
} catch (\Exception $e) {
$debug['attempts'][$name] = ['error' => $e->getMessage()];
}
}
return $debug;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Framework\View\Loading;
use App\Framework\View\Exceptions\TemplateNotFoundException;
final readonly class TemplatePathResolver
{
public function __construct(
private array $resolvers = []
){}
public function resolve(string $template, ?string $controllerClass = null): string
{
foreach($this->resolvers as $resolver) {
if($path = $resolver->resolve($template, $controllerClass)) {
return $path;
}
}
throw new TemplateNotFoundException($template, $controllerClass ?? '');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Framework\View\Processing;
use App\Framework\View\DOMTemplateParser;
use App\Framework\View\DomWrapper;
use App\Framework\View\ProcessorResolver;
use App\Framework\View\RenderContext;
final class DomProcessingPipeline
{
public function __construct(
private readonly array $processors,
private ProcessorResolver $resolver,
)
{
}
public function process(RenderContext $context, string $html): DomWrapper
{
$parser = new DOMTemplateParser();
$dom = $parser->parseToWrapper($html);
foreach($this->processors as $processorClass) {
$processor = $this->resolver->resolve($processorClass);
$dom = $processor->process($dom, $context);
}
return $dom;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Framework\View\Processing;
use App\Framework\View\ProcessorResolver;
use App\Framework\View\RenderContext;
final readonly class StringProcessingPipeline
{
public function __construct(
private array $processors,
private ProcessorResolver $resolver,
)
{
}
public function process(string $html, RenderContext $context): string
{
foreach ($this->processors as $processorClass) {
$processor = $this->resolver->resolve($processorClass);
$html = $processor->process($html, $context);
}
return $html;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Framework\View;
use App\Framework\DI\Container;
final class ProcessorResolver
{
private array $resolvedProcessors = [];
public function __construct(
private readonly Container $container,
){}
public function resolve(string $processorClass): object
{
if(!isset($this->resolvedProcessors[$processorClass])) {
$this->resolvedProcessors[$processorClass] = $this->container->get($processorClass);
}
return $this->resolvedProcessors[$processorClass];
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Framework\View\Processors;
use App\Framework\Core\PathProvider;
use App\Framework\View\DomHeadService;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use Dom\HTMLDocument;
use DOMDocument;
use DOMElement;
use App\Framework\View\RenderContext;
final class AssetInjector implements DomProcessor
{
private array $manifest;
public function __construct(
PathProvider $pathProvider,
private DomHeadService $headService,
string $manifestPath = '',
)
{
$manifestPath = $pathProvider->resolvePath('/public/.vite/manifest.json');;
#$manifestPath = dirname(__DIR__, 3) . '../public/.vite/manifest.json';
if (!is_file($manifestPath)) {
throw new \RuntimeException("Vite manifest not found: $manifestPath");
}
$json = file_get_contents($manifestPath);
$this->manifest = json_decode($json, true) ?? [];
}
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
// JS-Key, wie im Manifest unter "resources/js/main.js"
$jsKey = 'resources/js/main.js';
// CSS-Key, wie im Manifest unter "resources/css/styles.css"
$cssKey = 'resources/css/styles.css';
#$head = $dom->getElementsByTagName('head')->item(0);
$head = $dom->document->head;
$insertParent = $head ?: $dom->document->getElementsByTagName('body')->item(0) ?: $dom->document->documentElement;
// --- CSS, wie von Vite empfohlen: Feld "css" beim js-Eintrag! ---
if (!empty($this->manifest[$jsKey]['css']) && is_array($this->manifest[$jsKey]['css'])) {
foreach ($this->manifest[$jsKey]['css'] as $cssFile) {
$this->headService->addStylesheet($dom, '/' . ltrim($cssFile, '/'));
/*$link = $dom->document->createElement('link');
$link->setAttribute('rel', 'stylesheet');
$link->setAttribute('href', '/' . ltrim($cssFile, '/'));
$insertParent->appendChild($link);*/
}
}
// --- JS Main Script ---
if (isset($this->manifest[$jsKey]['file']) && str_ends_with($this->manifest[$jsKey]['file'], '.js')) {
$this->headService->addScript($dom, '/' . ltrim($this->manifest[$jsKey]['file'], '/'));
/*$script = $dom->document->createElement('script');
$script->setAttribute('src', '/' . ltrim($this->manifest[$jsKey]['file'], '/'));
$script->setAttribute('type', 'module');
$insertParent->appendChild($script);*/
}
return $dom;
}
}

View File

@@ -3,16 +3,31 @@
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLDocument;
use Dom\Node;
final readonly class CommentStripProcessor implements DomProcessor
{
public function process(\DOMDocument $dom, RenderContext $context): void
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$xpath = new \DOMXPath($dom);
foreach ($xpath->query('//comment()') as $commentNode) {
$commentNode->parentNode?->removeChild($commentNode);
$this->removeComments($dom->document);
return $dom;
}
private function removeComments(Node $node): void
{
// Wir gehen rekursiv durch alle Childnodes
for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
$child = $node->childNodes->item($i);
if ($child->nodeType === XML_COMMENT_NODE) {
$node->removeChild($child);
} else {
// Rekursion in die nächste Ebene
$this->removeComments($child);
}
}
}
}

View File

@@ -3,35 +3,52 @@
namespace App\Framework\View\Processors;
use App\Framework\View\ComponentRenderer;
use App\Framework\View\DomComponentService;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLElement;
/**
* Erweiterte Version des ComponentProcessors mit DomWrapper-Unterstützung
*/
final readonly class ComponentProcessor implements DomProcessor
{
public function __construct(
private ComponentRenderer $renderer = new ComponentRenderer()
){}
private DomComponentService $componentService,
private ComponentRenderer $renderer
) {}
public function process(\DOMDocument $dom, RenderContext $context): void
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$xpath = new \DOMXPath($dom);
$components = $dom->getElementsByTagName('component');
foreach ($xpath->query('//component') as $node) {
$name = $node->getAttribute('name');
$attributes = [];
$components->forEach(function ($component) use ($context, $dom) {
/** @var HTMLElement $component */
$name = $component->getAttribute('name');
foreach ($node->attributes as $attr) {
if($attr->nodeName !== 'name') {
$attributes[$attr->nodeName] = $attr->nodeValue;
}
}
if(!$name) return;
$attributes = $this->extractAttributes($component);
$componentHtml = $this->renderer->render($name, array_merge($context->data, $attributes));
$fragment = $dom->createDocumentFragment();
$fragment->appendXML($componentHtml);
$this->componentService->replaceComponent($dom, $component, $componentHtml);
#$dom->replaceElementWithHtml($component, $componentHtml);
$node->parentNode->replaceChild($fragment, $node);
});
return $dom;
}
private function extractAttributes(HTMLElement $component): array
{
$attributes = [];
foreach ($component->attributes as $attr) {
if ($attr->nodeName !== 'name') {
$attributes[$attr->nodeName] = $attr->nodeValue;
}
}
return $attributes;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Framework\View\Processors;
use App\Framework\Http\Session\Session;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
readonly class CsrfReplaceProcessor implements StringProcessor
{
public function __construct(
private Session $session,
){}
public function process(string $html, RenderContext $context): string
{
if(!str_contains($html, '___TOKEN___')){
// No token to replace
return $html;
}
$formIds = $this->extractFormIdsFromHtml($html);
if (empty($formIds)) {
return $html;
}
// Für jede gefundene Form-ID das entsprechende Token generieren
foreach ($formIds as $formId) {
$token = $this->session->csrf->generateToken($formId);
// Token-Platzhalter für diese spezifische Form-ID ersetzen
$html = $this->replaceTokenForFormId($html, $formId, $token);
}
return $html;
#$csrf->setAttribute('value', $this->session->csrf->generateToken($formId));
}
private function extractFormIdsFromHtml(string $html): array
{
$formIds = [];
// Pattern für Form-ID: value="form_xxxxx"
if (preg_match_all('/value=["\']form_([^"\']+)["\']/', $html, $matches)) {
foreach ($matches[1] as $match) {
$formId = 'form_' . $match;
if (!in_array($formId, $formIds)) {
$formIds[] = $formId;
}
}
}
return $formIds;
}
private function replaceTokenForFormId(string $html, string $formId, string $token): string
{
// Suche nach dem Token-Platzhalter, der zur entsprechenden Form-ID gehört
// Wir müssen den Kontext der Form berücksichtigen
$pattern = '/(<form[^>]*>.*?<input[^>]*name=["\']_form_id["\'][^>]*value=["\']' . preg_quote($formId, '/') . '["\'][^>]*>.*?)<input[^>]*name=["\']_token["\'][^>]*value=["\']___TOKEN___["\'][^>]*>/s';
return preg_replace_callback($pattern, function($matches) use ($token) {
return str_replace('___TOKEN___', $token, $matches[0]);
}, $html);
}
}

View File

@@ -3,8 +3,9 @@
namespace App\Framework\View\Processors;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
final readonly class EscapeProcessor
final readonly class EscapeProcessor implements StringProcessor
{
public function process(string $html, RenderContext $context): string
{

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors\Exceptions;
use App\Framework\Exception\FrameworkException;
final class ViteManifestNotFoundException extends FrameworkException
{
public function __construct(
string $manifestPath,
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct(
message: "Vite manifest not found: $manifestPath",
code: $code,
previous: $previous,
context: ['manifestPath' => $manifestPath]
);
}
}

View File

@@ -2,58 +2,71 @@
namespace App\Framework\View\Processors;
use App\Framework\DI\Container;
use App\Framework\Meta\MetaData;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateProcessor;
final class ForProcessor implements DomProcessor
{
public function __construct(
private Container $container,
private ?TemplateProcessor $templateProcessor = null,
) {
// Falls kein TemplateProcessor übergeben wird, erstellen wir einen mit den Standard-Prozessoren
if ($this->templateProcessor === null) {
#$this->templateProcessor = new TemplateProcessor([],[PlaceholderReplacer::class], $this->container);
#$this->templateProcessor->register(PlaceholderReplacer::class);
$this->templateProcessor = $this->container->get(TemplateProcessor::class);
}
}
/**
* @inheritDoc
*/
public function process(\DOMDocument $dom, RenderContext $context): void
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$xpath = new \DOMXPath($dom);
foreach ($xpath->query('//for[@var][@in]') as $node) {
foreach ($dom->document->querySelectorAll('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) {
$items = $context->data[$in] ?? null;
if (isset($context->model->{$in}) && $items === null) {
$items = $context->model->{$in};
}
if (is_iterable($items)) {
foreach ($items 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));
}
// Neuen Kontext für die Schleifenvariable erstellen
$loopContext = new RenderContext(
template: $context->template,
metaData: new MetaData('', ''),
data: array_merge($context->data, [$var => $item]),
#model: $context->model,
controllerClass: $context->controllerClass
);
$output .= $dom->saveHTML($fragment);
// Den Inhalt der Schleife mit den bestehenden Prozessoren verarbeiten
$innerHTML = $clone->innerHTML;
$processedContent = $this->templateProcessor->render($loopContext, $innerHTML, true);
$output .= $processedContent;
}
}
$replacement = $dom->createDocumentFragment();
$replacement = $dom->document->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);
}
}
return $dom;
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Http\Session\Session;
use App\Framework\View\DomFormService;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLElement;
final readonly class FormProcessor implements DomProcessor
{
public function __construct(
private DomFormService $formService,
#private Session $session,
) {}
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$forms = $dom->document->querySelectorAll('form');
/** @var HTMLElement $form */
foreach($forms as $form) {
$formId = $this->generateFormId($form, $context);
$csrf = $dom->document->createElement('input');
$csrf->setAttribute('name', '_token');
$csrf->setAttribute('type', 'hidden');
$csrf->setAttribute('value', '___TOKEN___');
#$csrf->setAttribute('value', $this->session->csrf->generateToken($formId));
// Form ID
$formIdField = $dom->document->createElement('input');
$formIdField->setAttribute('name', '_form_id');
$formIdField->setAttribute('type', 'hidden');
$formIdField->setAttribute('value', $formId);
$form->insertBefore($csrf, $form->firstChild);
$form->insertBefore($formIdField, $form->firstChild);;
}
return $dom;
}
private function generateFormId(HTMLElement $form, RenderContext $context): string
{
// 1. Vorhandene ID verwenden, falls gesetzt
if ($existingId = $form->getAttribute('id')) {
return $existingId;
}
// 2. Hash aus charakteristischen Eigenschaften erstellen
$hashComponents = [
$context->route ?? 'unknown_route',
$form->getAttribute('action') ?? '',
$form->getAttribute('method') ?? 'post',
$this->getFormStructureHash($form)
];
return 'form_' . substr(hash('sha256', implode('|', $hashComponents)), 0, 12);
}
private function getFormStructureHash(HTMLElement $form): string
{
// Hash aus Formular-Struktur (Input-Namen) erstellen
$inputs = $form->querySelectorAll('input[name], select[name], textarea[name]');
$names = [];
foreach ($inputs as $input) {
if ($name = $input->getAttribute('name')) {
$names[] = $name;
}
}
sort($names); // Sortieren für konsistente Hashes
return hash('crc32', implode(',', $names));
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLDocument;
use Dom\HTMLElement;
final readonly class HoneypotProcessor implements DomProcessor
{
private const array HONEYPOT_NAMES = [
'email_confirm',
'website_url',
'phone_number',
'user_name',
'company_name'
];
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$forms = $dom->document->querySelectorAll('form');
/** @var HTMLElement $form */
foreach($forms as $form) {
$this->addHoneypot($dom->document, $form);
$this->addTimeValidation($dom->document, $form);
}
return $dom;
}
private function addHoneypot(HTMLDocument $dom, HTMLElement $form): void
{
$honeypotName = self::HONEYPOT_NAMES[array_rand(self::HONEYPOT_NAMES)];
// Versteckter Container
$container = $dom->createElement('div');
$container->setAttribute('style', 'position:absolute;left:-9999px;visibility:hidden;');
$container->setAttribute('aria-hidden', 'true');
// Honeypot mit realistischem Label
$label = $dom->createElement('label');
$label->textContent = 'Website (optional)';
$label->setAttribute('for', $honeypotName);
$honeypot = $dom->createElement('input');
$honeypot->setAttribute('type', 'text');
$honeypot->setAttribute('name', $honeypotName);
$honeypot->setAttribute('id', $honeypotName);
$honeypot->setAttribute('autocomplete', 'off');
$honeypot->setAttribute('tabindex', '-1');
$container->appendChild($label);
$container->appendChild($honeypot);
// Honeypot-Name als verstecktes Feld
$nameField = $dom->createElement('input');
$nameField->setAttribute('type', 'hidden');
$nameField->setAttribute('name', '_honeypot_name');
$nameField->setAttribute('value', $honeypotName);
$form->insertBefore($container, $form->firstChild);
$form->insertBefore($nameField, $form->firstChild);
}
private function addTimeValidation(HTMLDocument $dom, HTMLElement $form): void
{
$timeField = $dom->createElement('input');
$timeField->setAttribute('type', 'hidden');
$timeField->setAttribute('name', '_form_start_time');
$timeField->setAttribute('value', (string)time());
$form->insertBefore($timeField, $form->firstChild);
}
}

View File

@@ -3,14 +3,30 @@
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
final readonly class IfProcessor implements DomProcessor
{
public function process(\DOMDocument $dom, \App\Framework\View\RenderContext $context): void
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$xpath = new \DOMXPath($dom);
$dom->getELementsByAttribute('if')->forEach(function ($node) use ($context) {
$condition = $node->getAttribute('if');
foreach ($xpath->query('//*[@if]') as $node) {
$value = $context->data[$condition] ?? null;
// Entferne, wenn die Bedingung nicht erfüllt ist
if (!$this->isTruthy($value)) {
$node->parentNode?->removeChild($node);
} else {
// Entferne Attribut bei Erfolg
$node->removeAttribute('if');
}
});
/*foreach ($dom->querySelectorAll('*[if]') as $node) {
$condition = $node->getAttribute('if');
$value = $context->data[$condition] ?? null;
@@ -23,7 +39,9 @@ final readonly class IfProcessor implements DomProcessor
// Entferne Attribut bei Erfolg
$node->removeAttribute('if');
}
}*/
return $dom;
}
private function isTruthy(mixed $value): bool

View File

@@ -4,8 +4,10 @@ namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomTemplateParser;
use App\Framework\View\DomWrapper;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateLoader;
use Dom\HTMLDocument;
final readonly class IncludeProcessor implements DomProcessor
{
@@ -14,16 +16,14 @@ final readonly class IncludeProcessor implements DomProcessor
private DomTemplateParser $parser = new DomTemplateParser()
) {}
public function process(\DOMDocument $dom, RenderContext $context): void
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$xpath = new \DOMXPath($dom);
foreach ($xpath->query('//include[@file]') as $includeNode) {
foreach ($dom->querySelectorAll('include[file]') as $includeNode) {
$file = $includeNode->getAttribute('file');
try {
$html = $this->loader->load($file);
$includedDom = $this->parser->parse($html);
$includedDom = $this->parser->parseFile($html);
$fragment = $dom->createDocumentFragment();
foreach ($includedDom->documentElement->childNodes as $child) {
@@ -37,5 +37,7 @@ final readonly class IncludeProcessor implements DomProcessor
$includeNode->parentNode?->replaceChild($error, $includeNode);
}
}
return $dom;
}
}

View File

@@ -2,101 +2,55 @@
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;
use App\Framework\View\DomTemplateParser;
use App\Framework\View\DomWrapper;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\RenderContext;
use Dom\Element;
final readonly class LayoutTagProcessor implements DomProcessor
{
public function __construct(
private TemplateLoader $loader = new TemplateLoader(),
private TemplateLoader $loader,
private DomTemplateParser $parser = new DomTemplateParser()
) {}
) {
#$this->loader = new TemplateLoader($this->pathProvider);
}
public function process(\DOMDocument $dom, RenderContext $context): void
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$xpath = new \DOMXPath($dom);
$layoutTags = $xpath->query('//layout[@src]');
$layoutTag = $this->findLayoutTag($dom);
if ($layoutTags->length === 0) {
return;
if (!$layoutTag) {
return $dom;
}
$layoutTag = $layoutTags->item(0);
return $this->applyLayout($layoutTag, $dom);
}
private function findLayoutTag(DomWrapper $dom): ?Element
{
$dom = $dom->document;
$layoutTags = $dom->querySelectorAll('layout[src]');
return $layoutTags->length > 0 ? $layoutTags->item(0) : null;
}
private function applyLayout(Element $layoutTag, DomWrapper $dom): DomWrapper
{
$layoutFile = $layoutTag->getAttribute('src');
$layoutHtml = $this->loader->load('/layouts/'.$layoutFile);
$layoutDom = $this->parser->parse($layoutHtml);
$layoutPath = $this->loader->getTemplatePath($layoutFile);
// Body-Slot finden
$layoutXPath = new \DOMXPath($layoutDom);
/*$slotNodes = $layoutXPath->query('//slot[@name="body"]');
$layoutDom = $this->parser->parseFileToWrapper($layoutPath);
$slot = $layoutDom->document->querySelector('main');
if ($slotNodes->length === 0) {
return;
if (!$slot) {
return $dom; // Kein Slot verfügbar
}
$slot = $slotNodes->item(0);*/
$slot->innerHTML = $layoutTag->innerHTML;
$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
);
return $layoutDom;
}
}

View File

@@ -2,23 +2,46 @@
namespace App\Framework\View\Processors;
use App\Framework\Meta\OpenGraphType;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLDocument;
final readonly class MetaManipulator implements DomProcessor
{
public function process(\DOMDocument $dom, RenderContext $context): void
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$xpath = new \DOMXPath($dom);
$description = $context->metaData->description;
foreach ($xpath->query('//meta[@name][@content]') as $meta) {
$dom->document->head->querySelector('meta[name="description"]')?->setAttribute('content', $description);
if($dom->document->head->getElementsByTagName('title')->item(0)) {
$dom->document->head->getElementsByTagName('title')->item(0)->textContent = $context->metaData->title . " | Michael Schiemer";
}
match($context->metaData->openGraph->type)
{
OpenGraphType::WEBSITE => $dom->document->head->querySelector('meta[property="og:type"]')?->setAttribute('content', 'website'),
default => throw new \Exception('Unexpected match value'),
};
foreach ($dom->document->querySelectorAll('meta[name][content]') as $meta) {
$name = $meta->getAttribute('name');
$content = $meta->getAttribute('content');
#debug($name);
// Wenn Variable bereits im Context gesetzt ist, nicht überschreiben
if (!array_key_exists($name, $context->data)) {
$context->data[$name] = $content;
}
#if (!array_key_exists($name, $context->data)) {
# $context->data[$name] = $content;
#}
}
return $dom;
}
}

View File

@@ -0,0 +1,428 @@
<?php
namespace App\Framework\View\Processors;
use App\Framework\View\StringProcessor;
use App\Framework\View\RenderContext;
/**
* Ersetzt PHP-Variable-Syntax durch tatsächliche Werte
*/
final class PhpVariableProcessor implements StringProcessor
{
private array $allowedFunctions = [
// String-Funktionen
'strtoupper', 'strtolower', 'ucfirst', 'ucwords', 'trim',
'strlen', 'substr', 'str_replace', 'htmlspecialchars',
// Datum/Zeit
'date', 'strftime', 'gmdate',
// Formatierung
'number_format', 'sprintf',
// Array-Funktionen
'count', 'implode', 'array_keys', 'array_values',
// Custom Template-Funktionen
'format_date', 'format_currency', 'format_filesize',
'print_r', 'get_class'
];
public function process(string $html, RenderContext $context): string
{
$data = $context->data ?? [];
// Stellen wir sicher, dass HTML-Entities dekodiert sind
// Dies deckt Fälle ab, in denen das Template bereits HTML-Entities enthält
$html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// 2. Funktions-Aufrufe: date('Y-m-d'), strtoupper($name)
$html = $this->replaceFunctionCalls($html, $data);
// 1. Methoden-Aufrufe: $user->getName(), $date->format('Y-m-d')
$html = $this->replaceMethodCalls($html, $data);
// 3. Objekt-Properties: $error->name
$html = $this->replaceObjectProperties($html, $data);
// 4. Array-Zugriffe: $data['key']
$html = $this->replaceArrayAccess($html, $data);
// 5. Einfache Variablen: $name
$html = $this->replaceSimpleVariables($html, $data);
// 6. Ternäre Operatoren: $condition ? 'true' : 'false'
$html = $this->replaceTernaryOperators($html, $data);
return $html;
}
/**
* Ersetzt Methoden-Aufrufe: $user->getName(), $date->format('Y-m-d')
*/
private function replaceMethodCalls(string $html, array $data): string
{
#debug(html_entity_decode($html));
return preg_replace_callback(
'/\$([a-zA-Z_][a-zA-Z0-9_]*)((?:->|(?:-&gt;))[a-zA-Z_][a-zA-Z0-9_]*)*(?:->|(?:-&gt;))([a-zA-Z_][a-zA-Z0-9_]*)\(([^)]*)\)/',
function($matches) use ($data) {
$varName = $matches[1];
$propertyChain = $matches[2] ? str_replace('-&gt;', '->', $matches[2]) : '';
$methodName = $matches[3];
$argsString = $matches[4];
if (!array_key_exists($varName, $data)) {
return $matches[0];
}
$object = $data[$varName];
// Property-Chain navigieren falls vorhanden
if (!empty($propertyChain)) {
$properties = explode('->', trim($propertyChain, '->'));
foreach ($properties as $property) {
if (is_object($object) && property_exists($object, $property)) {
$object = $object->$property;
} else {
return $matches[0];
}
}
}
if (!is_object($object)) {
return $matches[0];
}
// Methode ausführen
try {
$args = $this->parseArguments($argsString, $data);
if (method_exists($object, $methodName)) {
$result = $object->$methodName(...$args);
return $this->formatValue($result);
}
// Getter-Fallback: getName() -> name Property
if (str_starts_with($methodName, 'get') && empty($args)) {
$propertyName = lcfirst(substr($methodName, 3));
if (property_exists($object, $propertyName)) {
return $this->formatValue($object->$propertyName);
}
}
} catch (\Throwable $e) {
// Fehler ignorieren, Original-String zurückgeben
return $matches[0];
}
return $matches[0];
},
$html
);
}
/**
* Ersetzt Funktions-Aufrufe: date('Y-m-d'), strtoupper($name)
*/
private function replaceFunctionCalls(string $html, array $data): string
{
// Verbesserte Regex, die sowohl -> als auch HTML-kodiertes -&gt; erkennt
return preg_replace_callback(
'/\b([a-zA-Z_][a-zA-Z0-9_]*)\(([^)]*)\)/',
function($matches) use ($data) {
$functionName = $matches[1];
$argsString = $matches[2];
// Nur erlaubte Funktionen ausführen
if (!in_array($functionName, $this->allowedFunctions)) {
return $matches[0];
}
try {
$args = $this->parseArguments($argsString, $data);
// Custom Template-Funktionen
if (method_exists($this, 'function_' . $functionName)) {
$result = $this->{'function_' . $functionName}(...$args);
return $this->formatValue($result);
}
// Standard PHP-Funktionen
if (function_exists($functionName)) {
$result = $functionName(...$args);
return $this->formatValue($result);
}
} catch (\Throwable $e) {
return $matches[0];
}
return $matches[0];
},
$html
);
}
/**
* Ersetzt Objekt-Properties: $error->name
*/
private function replaceObjectProperties(string $html, array $data): string
{
// Verbesserte Regex, die sowohl -> als auch HTML-kodiertes -&gt; erkennt
return preg_replace_callback(
'/\$([a-zA-Z_][a-zA-Z0-9_]*)((?:->|(?:-&gt;))[a-zA-Z_][a-zA-Z0-9_]*)+/',
function($matches) use ($data) {
$varName = $matches[1];
$propertyChain = str_replace('-&gt;', '->', $matches[2]);
if (!array_key_exists($varName, $data)) {
return $matches[0]; // Variable nicht gefunden
}
$value = $data[$varName];
// Property-Chain abarbeiten: ->name->email
$properties = explode('->', trim($propertyChain, '->'));
foreach ($properties as $property) {
if (is_object($value)) {
if (property_exists($value, $property)) {
$value = $value->$property;
} elseif (method_exists($value, '__get')) {
$value = $value->__get($property);
} else {
return $matches[0]; // Property nicht gefunden
}
} elseif (is_array($value)) {
if (array_key_exists($property, $value)) {
$value = $value[$property];
} else {
return $matches[0]; // Key nicht gefunden
}
} else {
return $matches[0]; // Kann nicht weiter navigieren
}
}
return $this->formatValue($value);
},
$html
);
}
/**
* Ersetzt Array-Zugriffe: $data['key'], $items[0]
*/
private function replaceArrayAccess(string $html, array $data): string
{
return preg_replace_callback(
"/\\$([a-zA-Z_][a-zA-Z0-9_]*)\[(['\"]?)([^'\"\]]+)\2\]/",
function($matches) use ($data) {
$varName = $matches[1];
$key = $matches[3];
if (!array_key_exists($varName, $data)) {
return $matches[0];
}
$array = $data[$varName];
if (is_array($array)) {
// Numerischer oder String-Key
$actualKey = is_numeric($key) ? (int)$key : $key;
if (array_key_exists($actualKey, $array)) {
return $this->formatValue($array[$actualKey]);
}
} elseif (is_object($array)) {
// Objekt wie Array behandeln
if (property_exists($array, $key)) {
return $this->formatValue($array->$key);
}
}
return $matches[0];
},
$html
);
}
/**
* Ersetzt einfache Variablen: $name
*/
private function replaceSimpleVariables(string $html, array $data): string
{
return preg_replace_callback(
'/\$([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_\[\-])/',
function($matches) use ($data) {
$varName = $matches[1];
if (array_key_exists($varName, $data)) {
return $this->formatValue($data[$varName]);
}
return $matches[0]; // Variable nicht gefunden
},
$html
);
}
/**
* Parst Argument-String zu Array von Werten
*/
private function parseArguments(string $argsString, array $data): array
{
if (trim($argsString) === '') {
return [];
}
$args = [];
$parts = $this->splitArguments($argsString);
foreach ($parts as $part) {
$part = trim($part);
// String-Literale: 'text' oder "text"
if (preg_match('/^[\'"](.*)[\'"]$/s', $part, $matches)) {
$args[] = $matches[1];
continue;
}
// Zahlen
if (is_numeric($part)) {
$args[] = str_contains($part, '.') ? (float)$part : (int)$part;
continue;
}
// Boolean
if ($part === 'true') {
$args[] = true;
continue;
}
if ($part === 'false') {
$args[] = false;
continue;
}
// Null
if ($part === 'null') {
$args[] = null;
continue;
}
// Variablen: $name
if (preg_match('/^\$([a-zA-Z_][a-zA-Z0-9_]*)$/', $part, $matches)) {
$varName = $matches[1];
if (array_key_exists($varName, $data)) {
$args[] = $data[$varName];
continue;
}
}
// Fallback: als String behandeln
$args[] = $part;
}
return $args;
}
/**
* Teilt Argument-String intelligent auf (berücksichtigt Quotes)
*/
private function splitArguments(string $argsString): array
{
$args = [];
$current = '';
$inQuotes = false;
$quoteChar = null;
$length = strlen($argsString);
for ($i = 0; $i < $length; $i++) {
$char = $argsString[$i];
if (!$inQuotes && ($char === '"' || $char === "'")) {
$inQuotes = true;
$quoteChar = $char;
$current .= $char;
} elseif ($inQuotes && $char === $quoteChar) {
$inQuotes = false;
$quoteChar = null;
$current .= $char;
} elseif (!$inQuotes && $char === ',') {
$args[] = trim($current);
$current = '';
} else {
$current .= $char;
}
}
if ($current !== '') {
$args[] = trim($current);
}
return $args;
}
/**
* Formatiert einen Wert für die HTML-Ausgabe
*/
private function formatValue(mixed $value): string
{
return match (true) {
is_null($value) => '',
is_bool($value) => $value ? '1' : '0',
is_scalar($value) => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'),
is_array($value) => htmlspecialchars(json_encode($value, JSON_UNESCAPED_UNICODE), ENT_QUOTES, 'UTF-8'),
is_object($value) => $this->formatObject($value),
default => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8')
};
}
/**
* Formatiert Objekte für die Ausgabe
*/
private function formatObject(object $value): string
{
if (method_exists($value, '__toString')) {
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
if (method_exists($value, 'toArray')) {
return htmlspecialchars(json_encode($value->toArray(), JSON_UNESCAPED_UNICODE), ENT_QUOTES, 'UTF-8');
}
return htmlspecialchars(get_class($value), ENT_QUOTES, 'UTF-8');
}
// Custom Template-Funktionen
private function function_format_date(string|\DateTime $date, string $format = 'Y-m-d H:i:s'): string
{
if (is_string($date)) {
$date = new \DateTime($date);
}
return $date->format($format);
}
private function function_format_currency(float $amount, string $currency = 'EUR'): string
{
return number_format($amount, 2, ',', '.') . ' ' . $currency;
}
private function function_format_filesize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$factor = floor((strlen((string)$bytes) - 1) / 3);
return sprintf("%.1f %s", $bytes / pow(1024, $factor), $units[$factor]);
}
private function replaceTernaryOperators(string $html, array $data): string
{
return $html;
}
}

View File

@@ -2,41 +2,284 @@
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\DI\Container;
use App\Framework\View\Functions\ImageSlotFunction;
use App\Framework\View\Functions\UrlFunction;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
use App\Framework\View\TemplateFunctions;
use DateTimeZone;
final readonly class PlaceholderReplacer implements DomProcessor
final class PlaceholderReplacer implements StringProcessor
{
public function process(\DOMDocument $dom, RenderContext $context): void
public function __construct(
private Container $container,
)
{
$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
);
}
// Erlaubte Template-Funktionen für zusätzliche Sicherheit
private array $allowedTemplateFunctions = [
'date', 'format_date', 'format_currency', 'format_filesize',
'strtoupper', 'strtolower', 'ucfirst', 'trim', 'count', /*'imageslot'*/
];
public function process(string $html, RenderContext $context): string
{
// Template-Funktionen: {{ date('Y-m-d') }}, {{ format_currency(100) }}
$html = $this->replaceTemplateFunctions($html, $context);
// Standard Variablen und Methoden: {{ item.getRelativeFile() }}
return preg_replace_callback(
'/{{\\s*([\\w.]+)(?:\\(\\s*([^)]*)\\s*\\))?\\s*}}/',
function($matches) use ($context) {
$expression = $matches[1];
$params = isset($matches[2]) ? trim($matches[2]) : null;
if ($params !== null) {
return $this->resolveMethodCall($context->data, $expression, $params);
} else {
return $this->resolveEscaped($context->data, $expression, ENT_QUOTES | ENT_HTML5);
}
},
$html
);
}
private function replaceTemplateFunctions(string $html, RenderContext $context): string
{
return preg_replace_callback(
'/{{\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\(([^)]*)\\)\\s*}}/',
function($matches) use ($context) {
$functionName = $matches[1];
$params = trim($matches[2]);
$functions = new TemplateFunctions($this->container, ImageSlotFunction::class, UrlFunction::class);
if($functions->has($functionName)) {
$function = $functions->get($functionName);
$args = $this->parseParams($params, $context->data);
if(is_callable($function)) {
return $function(...$args);
}
#return $function(...$args);
}
// Nur erlaubte Funktionen
if (!in_array($functionName, $this->allowedTemplateFunctions)) {
return $matches[0];
}
try {
$args = $this->parseParams($params, $context->data);
// Custom Template-Funktionen
if (method_exists($this, 'function_' . $functionName)) {
$result = $this->{'function_' . $functionName}(...$args);
return $result;
#return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
// Standard PHP-Funktionen (begrenzt)
if (function_exists($functionName)) {
$result = $functionName(...$args);
return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
} catch (\Throwable $e) {
return $matches[0];
}
return $matches[0];
},
$html
);
}
private function function_imageslot(string $slotName): string
{
$function = $this->container->get(ImageSlotFunction::class);
return ($function('slot1'));
}
// Custom Template-Funktionen
private function function_format_date(string|\DateTime $date, string $format = 'Y-m-d H:i:s'): string
{
if (is_string($date)) {
$date = new \DateTimeImmutable($date, new DateTimeZone('Europe/Berlin'));
}
return $date->format($format);
}
private function function_format_currency(float $amount, string $currency = 'EUR'): string
{
return number_format($amount, 2, ',', '.') . ' ' . $currency;
}
private function function_format_filesize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$factor = floor((strlen((string)$bytes) - 1) / 3);
return sprintf("%.1f %s", $bytes / pow(1024, $factor), $units[$factor]);
}
private function resolveMethodCall(array $data, string $expression, string $params): string
{
$parts = explode('.', $expression);
$methodName = array_pop($parts); // Letzter Teil ist der Methodenname
$objectPath = implode('.', $parts); // Pfad zum Objekt
// Objekt auflösen
$object = $this->resolveValue($data, $objectPath);
if (!is_object($object)) {
return '{{ ' . $expression . '() }}'; // Platzhalter beibehalten
}
if (!method_exists($object, $methodName)) {
return '{{ ' . $expression . '() }}'; // Platzhalter beibehalten
}
try {
// Parameter parsen (falls vorhanden)
$parsedParams = empty($params) ? [] : $this->parseParams($params, $data);
// Methode aufrufen
$result = $object->$methodName(...$parsedParams);
return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8');
} catch (\Throwable $e) {
// Bei Fehlern Platzhalter beibehalten
return '{{ ' . $expression . '() }}';
}
}
private function resolveValue(array $data, string $expr): string
private function resolveEscaped(array $data, string $expr, int $flags): string
{
$value = $this->resolveValue($data, $expr);
if ($value === null) {
// Bleibt als Platzhalter stehen
return '{{ ' . $expr . ' }}';
}
return htmlspecialchars((string)$value, $flags, 'UTF-8');
}
private function resolveValue(array $data, string $expr): mixed
{
$keys = explode('.', $expr);
$value = $data;
foreach ($keys as $key) {
if (!is_array($value) || !array_key_exists($key, $value)) {
return "{{ $expr }}"; // Platzhalter bleibt erhalten
if (is_array($value) && array_key_exists($key, $value)) {
$value = $value[$key];
} elseif (is_object($value) && isset($value->$key)) {
$value = $value->$key;
} else {
return null;
}
$value = $value[$key];
}
return is_scalar($value) ? (string)$value : '';
return $value;
}
public function supports(\DOMElement $element): bool
/**
* Parst Parameter aus einem Parameter-String
*/
private function parseParams(string $paramsString, array $data): array
{
return $element->tagName === 'text';
if (empty(trim($paramsString))) {
return [];
}
$params = [];
$parts = $this->splitParams($paramsString);
foreach ($parts as $part) {
$part = trim($part);
// String-Literale: 'text' oder "text"
if (preg_match('/^[\'\"](.*)[\'\"]/s', $part, $matches)) {
$params[] = $matches[1];
continue;
}
// Zahlen
if (is_numeric($part)) {
$params[] = str_contains($part, '.') ? (float)$part : (int)$part;
continue;
}
// Boolean
if ($part === 'true') {
$params[] = true;
continue;
}
if ($part === 'false') {
$params[] = false;
continue;
}
// Null
if ($part === 'null') {
$params[] = null;
continue;
}
// Variablen-Referenzen: $variable oder Pfade: objekt.eigenschaft
if (str_starts_with($part, '$')) {
$varName = substr($part, 1);
if (array_key_exists($varName, $data)) {
$params[] = $data[$varName];
continue;
}
} elseif (str_contains($part, '.')) {
$value = $this->resolveValue($data, $part);
if ($value !== null) {
$params[] = $value;
continue;
}
}
// Fallback: Als String behandeln
$params[] = $part;
}
return $params;
}
/**
* Teilt einen Parameter-String in einzelne Parameter auf
*/
private function splitParams(string $paramsString): array
{
$params = [];
$current = '';
$inQuotes = false;
$quoteChar = null;
$length = strlen($paramsString);
for ($i = 0; $i < $length; $i++) {
$char = $paramsString[$i];
if (!$inQuotes && ($char === '"' || $char === "'")) {
$inQuotes = true;
$quoteChar = $char;
$current .= $char;
} elseif ($inQuotes && $char === $quoteChar) {
$inQuotes = false;
$quoteChar = null;
$current .= $char;
} elseif (!$inQuotes && $char === ',') {
$params[] = trim($current);
$current = '';
} else {
$current .= $char;
}
}
if ($current !== '') {
$params[] = trim($current);
}
return $params;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLDocument;
use Dom\Node;
final class RemoveEmptyLinesProcessor implements DomProcessor
{
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$this->removeEmptyTextNodes($dom->document);
return $dom;
}
private function removeEmptyTextNodes(Node $node): void
{
for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
$child = $node->childNodes->item($i);
// Wenn es ein Text-Node ist und nur aus Whitespace besteht
if ($child->nodeType === XML_TEXT_NODE && preg_match('/^\s*$/', $child->nodeValue)) {
$node->removeChild($child);
} elseif ($child->hasChildNodes()) {
// Rekursion in die Tiefe
$this->removeEmptyTextNodes($child);
}
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
final class SingleLineHtmlProcessor implements StringProcessor
{
public function process(string $html, RenderContext $context): string
{
// Entfernt Zeilenumbrüche und doppelte Whitespaces, alles in eine Zeile
$html = preg_replace('/>(\s|\n|\r)+</', '><', $html);
$html = preg_replace('/\s+/', ' ', $html);
return trim($html);
}
}

View File

@@ -3,7 +3,9 @@
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLDocument;
/*
@@ -17,11 +19,9 @@ use App\Framework\View\RenderContext;
final readonly class SlotProcessor implements DomProcessor
{
public function process(\DOMDocument $dom, RenderContext $context): void
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$xpath = new \DOMXPath($dom);
foreach ($xpath->query('//slot[@name]') as $slotNode) {
foreach ($dom->querySelectorAll('slot[name]') as $slotNode) {
$slotName = $slotNode->getAttribute('name');
$html = $context->slots[$slotName] ?? null;
@@ -38,5 +38,7 @@ final readonly class SlotProcessor implements DomProcessor
$slotNode->parentNode?->replaceChild($replacement, $slotNode);
}
return $dom;
}
}

View File

@@ -3,8 +3,10 @@
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use DOMXPath;
use Dom\Element;
use Dom\HTMLDocument;
# Ersetzt ein eigenes `<switch value="foo">` mit inneren `<case when="bar">`-Elementen.
@@ -14,11 +16,9 @@ final readonly class SwitchCaseProcessor implements DomProcessor
/**
* @inheritDoc
*/
public function process(\DOMDocument $dom, RenderContext $context): void
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$xpath = new DOMXPath($dom);
foreach ($xpath->query('//switch[@value]') as $switchNode) {
foreach ($dom->querySelectorAll('switch[value]') as $switchNode) {
$key = $switchNode->getAttribute('value');
$value = $context->data[$key] ?? null;
@@ -26,7 +26,7 @@ final readonly class SwitchCaseProcessor implements DomProcessor
$defaultCase = null;
foreach ($switchNode->childNodes as $child) {
if (!($child instanceof \DOMElement)) {
if (!($child instanceof Element)) {
continue;
}
@@ -57,5 +57,7 @@ final readonly class SwitchCaseProcessor implements DomProcessor
$switchNode->parentNode?->replaceChild($replacement, $switchNode);
}
return $dom;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
final class VoidElementsSelfClosingProcessor implements StringProcessor
{
// Liste der HTML5-Void-Elemente
private const array VOID_ELEMENTS = [
'area', 'base', 'br', 'col', 'embed', 'hr', 'img',
'input', 'link', 'meta', 'source', 'track', 'wbr',
];
public function process(string $html, RenderContext $context): string
{
$pattern = '#<(' . implode('|', self::VOID_ELEMENTS) . ')(\s[^>/]*)?(?<!/)>#i';
return preg_replace_callback($pattern, function($m) {
$tag = $m[1];
$attrs = $m[2] ?? '';
return "<$tag$attrs />";
}, $html);
}
}

View File

@@ -0,0 +1,62 @@
# AssetInjector
## Überblick
Der `AssetInjector` ist ein Prozessor, der JavaScript- und CSS-Assets aus einem Vite-Manifest automatisch in HTML-Dokumente einfügt.
## Funktionsweise
- Liest das Vite-Manifest (JSON) für JS/CSS-Dateien
- Fügt die entsprechenden `<script>` und `<link>` Tags ins `<head>` oder `<body>` ein
- Unterstützt Vite's Hot Module Replacement (HMR) im Entwicklungsmodus
## Konfiguration
Der AssetInjector benötigt den Pfad zum Vite-Manifest:
```php
$assetInjector = new AssetInjector('/path/to/manifest.json');
```
## Manifest-Format
Das Vite-Manifest hat üblicherweise folgendes Format:
```json
{
"resources/js/main.js": {
"file": "assets/main.1234abcd.js",
"css": ["assets/main.5678efgh.css"]
},
"resources/css/styles.css": {
"file": "assets/styles.5678efgh.css"
}
}
```
## Implementierung
Der Prozessor sucht nach dem `<head>` oder `<body>` Element und fügt die Asset-Tags ein. Er verarbeitet sowohl JavaScript-Dateien (als `<script>` Tags) als auch CSS-Dateien (als `<link>` Tags).
## JavaScript-Integration
JavaScript-Assets werden als Module geladen:
```html
<script src="/assets/main.1234abcd.js" type="module"></script>
```
## CSS-Integration
CSS-Assets werden über Link-Tags eingebunden:
```html
<link rel="stylesheet" href="/assets/main.5678efgh.css">
```
## Vorteile
- Automatische Versionierung von Assets (Cache-Busting)
- Einfache Integration mit dem Vite-Build-System
- Unterstützung für HMR im Entwicklungsmodus
- Keine manuelle Verwaltung von Asset-URLs notwendig

View File

@@ -0,0 +1,54 @@
# CommentStripProcessor
## Überblick
Der `CommentStripProcessor` entfernt HTML-Kommentare aus der Ausgabe, um die Größe des resultierenden HTML zu reduzieren.
## Funktionsweise
- Findet alle HTML-Kommentare im DOM
- Entfernt diese Kommentare aus dem Dokument
- Behält optional spezielle Kommentare bei (z.B. für IE-Conditional-Comments)
## Syntax
Dieser Prozessor erfordert keine spezielle Syntax im Template. Er verarbeitet automatisch alle HTML-Kommentare:
```html
<!-- Dieser Kommentar wird entfernt -->
<div>Sichtbarer Inhalt</div>
<!--[if IE]>Dieser Kommentar könnte optional beibehalten werden<![endif]-->
```
## Implementierung
Der Prozessor verwendet `xpath->query('//comment()')`, um alle Kommentarknoten im DOM zu finden. Diese werden dann aus ihren Elternknoten entfernt.
## Konfigurationsoptionen
- **keepConditionalComments**: (Boolean) Bei `true` werden IE-Conditional-Comments beibehalten
- **keepSpecialComments**: (Boolean) Bei `true` werden Kommentare mit bestimmten Präfixen beibehalten (z.B. `<!--!` oder `<!--#`)
## Beispiel für beizubehaltende Kommentare
```html
<!--! Dieser Kommentar würde beibehalten werden, wenn keepSpecialComments=true -->
<!--[if IE]>Dieser Kommentar würde beibehalten werden, wenn keepConditionalComments=true<![endif]-->
```
## Vorteile
- Reduziert die Größe der HTML-Ausgabe
- Verbessert die Ladezeit
- Entfernt Entwickler-Kommentare aus der Produktionsumgebung
## Anwendungsfälle
- Optimierung für Produktionsumgebungen
- Reduzierung der Bandbreitennutzung
- Entfernung von Entwicklerhinweisen und Debug-Informationen
## Tipps
- Verwenden Sie diesen Prozessor als einen der letzten Schritte in der Verarbeitungskette
- Kombinieren Sie ihn mit anderen Optimierungsprozessoren wie `RemoveEmptyLinesProcessor` und `SingleLineHtmlProcessor` für maximale Komprimierung

View File

@@ -0,0 +1,55 @@
# ComponentProcessor
## Überblick
Der `ComponentProcessor` ermöglicht die Verwendung von wiederverwendbaren UI-Komponenten in Templates.
## Funktionsweise
- Findet `<component>`-Tags im DOM
- Rendert die benannte Komponente mit allen Attributen als Parameter
- Ersetzt den Tag durch das gerenderte HTML der Komponente
## Syntax
```html
<component name="komponentenName" attr1="wert1" attr2="wert2"></component>
```
## Beispiel
```html
<component name="card" title="Willkommen" image="/img/welcome.jpg">
<p>Dies ist eine Karte mit Inhalt</p>
</component>
```
## Implementierung
Der Prozessor verwendet den `ComponentRenderer`, um die angegebene Komponente zu rendern. Alle Attribute des `<component>`-Tags (außer `name`) werden als Parameter an den Renderer übergeben und mit den Daten aus dem RenderContext zusammengeführt.
## Parameter
- **name**: (Erforderlich) Der Name der zu rendernden Komponente
- **Beliebige Attribute**: Werden als Parameter an die Komponente übergeben
## Kombination mit Slots
Der `ComponentProcessor` arbeitet gut mit dem `SlotProcessor` zusammen, um die Flexibilität von Komponenten zu erhöhen:
```html
<component name="modal" title="Bestätigung">
<div slot="header">Benutzerdefinierter Header</div>
<p>Hauptinhalt des Modals</p>
<div slot="footer">
<button>Bestätigen</button>
<button>Abbrechen</button>
</div>
</component>
```
## Vorteile
- Fördert die Wiederverwendung von UI-Elementen
- Verbessert die Konsistenz in der gesamten Anwendung
- Ermöglicht eine komponentenbasierte Entwicklung ähnlich wie in modernen Frontend-Frameworks

View File

@@ -0,0 +1,65 @@
# DateFormatProcessor
## Überblick
Der `DateFormatProcessor` formatiert Datums- und Zeitwerte in Templates mit benutzerdefinierten Formaten.
## Funktionsweise
- Findet Tags mit `date-format`-Attribut
- Formatiert den Inhalt des Tags oder den angegebenen Wert nach dem gewünschten Format
- Ersetzt den Inhalt des Tags mit dem formatierten Datum
## Syntax
```html
<span date-format="format" [date-value="value"]>fallbackValue</span>
```
## Parameter
- **date-format**: (Erforderlich) Das Format für die Datumsausgabe (PHP date()-Format)
- **date-value**: (Optional) Der zu formatierende Wert, falls nicht angegeben wird der Inhalt des Tags verwendet
## Beispiele
```html
<!-- Aktuelles Datum formatieren -->
<span date-format="d.m.Y">Aktuelles Datum</span>
<!-- Spezifischen Zeitstempel formatieren -->
<span date-format="d.m.Y H:i" date-value="2023-12-31 23:59:59">31.12.2023</span>
<!-- Variable aus dem RenderContext formatieren -->
<span date-format="d.m.Y H:i" date-value="{{ createdAt }}">Erstelldatum</span>
```
## Unterstützte Eingabeformate
Der Prozessor kann verschiedene Eingabeformate verarbeiten:
- Unix-Zeitstempel (als Zahl)
- ISO 8601 Datum-/Zeitstring (z.B. "2023-12-31T23:59:59")
- Natürliche Sprachausdrücke (z.B. "next Monday")
- DateTime-Objekte (wenn im RenderContext vorhanden)
## Ausgabeformatierung
Die Formatierung erfolgt mittels PHP's `date()`-Funktion. Die wichtigsten Formatierungszeichen sind:
- **d**: Tag des Monats (01-31)
- **m**: Monat (01-12)
- **Y**: Jahr (vierstellig)
- **H**: Stunde im 24-Stunden-Format (00-23)
- **i**: Minuten (00-59)
- **s**: Sekunden (00-59)
## Localization
Für eine lokalisierte Datumsausgabe kann der Prozessor mit der IntlDateFormatter-Klasse erweitert werden, um sprachspezifische Formatierungen zu unterstützen.
## Tipps
- Verwenden Sie für konsistente Datumsformatierung im gesamten Template
- Kann mit dem PlaceholderReplacer kombiniert werden, um dynamische Datumswerte zu formatieren
- Nützlich für Blogs, Ereignislisten und alle zeitbezogenen Darstellungen

View File

@@ -0,0 +1,55 @@
# EscapeProcessor
## Überblick
Der `EscapeProcessor` ist ein String-Prozessor (im Gegensatz zu DOM-Prozessoren), der für das sichere Escaping von Variablen zum Schutz vor XSS-Angriffen zuständig ist.
## Funktionsweise
- Verarbeitet den HTML-String direkt (nicht das DOM)
- Ersetzt `{{ ... }}` durch escaped HTML (mit htmlspecialchars)
- Ersetzt `{{{ ... }}}` durch rohen HTML-Inhalt (ohne Escaping)
## Syntax
```
{{ variable }} // Mit Escaping (sicher)
{{{ variable }}} // Ohne Escaping (für vertrauenswürdigen HTML-Code)
```
## Beispiel
```html
<!-- Mit Escaping (sicher) -->
<div class="user-info">
<h2>{{ username }}</h2>
</div>
<!-- Ohne Escaping (nur für vertrauenswürdigen HTML-Code) -->
<div class="rich-content">
{{{ htmlContent }}}
</div>
```
## Implementierung
Der Prozessor verwendet reguläre Ausdrücke, um die Platzhalter zu finden und zu ersetzen:
1. Zuerst werden die rohen Platzhalter (`{{{ ... }}}`) ohne Escaping ersetzt
2. Dann werden die normalen Platzhalter (`{{ ... }}`) mit Escaping ersetzt
## Sicherheitshinweise
- Verwenden Sie `{{ ... }}` für alle Benutzerdaten oder nicht vertrauenswürdigen Inhalte
- Verwenden Sie `{{{ ... }}}` nur für vertrauenswürdigen HTML-Code oder wenn HTML-Markup explizit erlaubt sein soll
- Die Verwendung von `{{{ ... }}}` mit nicht vertrauenswürdigen Daten kann zu XSS-Sicherheitslücken führen
## Besonderheiten
Im Gegensatz zu den meisten anderen Prozessoren arbeitet der EscapeProcessor mit dem rohen HTML-String und nicht mit dem DOM. Dies ermöglicht das Ersetzen von Platzhaltern, bevor das HTML geparst wird.
## Tipps
- Verwenden Sie `{{ ... }}` als Standardmethode für die Ausgabe von Variablen
- Verwenden Sie `{{{ ... }}}` nur, wenn Sie explizit HTML-Markup zulassen möchten (z.B. für Rich-Text-Editoren)
- Dieser Prozessor kann vor der DOM-Verarbeitung verwendet werden, um Platzhalter in HTML-Attributen zu ersetzen

View File

@@ -0,0 +1,61 @@
# ForProcessor
## Überblick
Der `ForProcessor` ermöglicht Iteration über Arrays oder Sammlungen und rendert HTML für jedes Element.
## Funktionsweise
- Findet `<for>`-Tags mit `var` und `in` Attributen
- Durchläuft die angegebene Sammlung im RenderContext
- Rendert den inneren Inhalt für jedes Element der Sammlung
- Ersetzt Platzhalter mit den Werten des aktuellen Elements
## Syntax
```html
<for var="itemName" in="arrayName">...</for>
```
## Beispiel
```html
<ul>
<for var="user" in="users">
<li>{{ user.name }} ({{ user.email }})</li>
</for>
</ul>
```
## Implementierung
Der Prozessor verwendet XPath, um `<for>`-Tags zu finden. Für jedes Element in der Sammlung wird der Inhalt des `<for>`-Tags geklont und die Platzhalter werden durch die entsprechenden Werte ersetzt. Die Ergebnisse werden zusammengefügt und an der Stelle des ursprünglichen `<for>`-Tags eingefügt.
## Parameter
- **var**: (Erforderlich) Der Name der Variable, die in jeder Iteration verwendet wird
- **in**: (Erforderlich) Der Name des Arrays oder der Sammlung im RenderContext
## Verschachtelte Schleifen
Schleifen können verschachtelt werden, um mehrdimensionale Daten zu verarbeiten:
```html
<for var="category" in="categories">
<h2>{{ category.name }}</h2>
<ul>
<for var="product" in="category.products">
<li>{{ product.name }} - {{ product.price }}</li>
</for>
</ul>
</for>
```
## Rekursive Platzhalterersetzung
Der ForProcessor ersetzt Platzhalter rekursiv in allen Kind-Nodes des aktuellen Elements. Dies ermöglicht die Verwendung von Platzhaltern in verschachtelten Elementen und Attributen.
## Tipps
- Stellen Sie sicher, dass die im `in`-Attribut angegebene Variable ein Array oder eine iterable Collection ist
- Der ForProcessor kann mit dem IfProcessor kombiniert werden, um bedingte Schleifen zu erstellen

View File

@@ -0,0 +1,50 @@
# IfProcessor
## Überblick
Der `IfProcessor` ermöglicht die bedingte Darstellung von Elementen in Templates basierend auf Werten aus dem RenderContext.
## Funktionsweise
- Findet Elemente mit einem `if`-Attribut
- Prüft den Wert der angegebenen Bedingung im RenderContext
- Entfernt das Element, wenn die Bedingung nicht erfüllt ist ("falsy")
- Entfernt nur das `if`-Attribut, wenn die Bedingung erfüllt ist ("truthy")
## Syntax
```html
<div if="bedingung">...</div>
```
## Beispiel
```html
<div if="isLoggedIn" class="user-panel">
Benutzerbereich
</div>
<button if="!isLoggedIn" class="login-button">Anmelden</button>
```
## Implementierung
Der Prozessor verwendet `querySelectorAll('*[if]')`, um alle Elemente mit einem `if`-Attribut zu finden. Der Wert des Attributs wird als Schlüssel im RenderContext verwendet. Die Methode `isTruthy()` bestimmt, ob der Wert als wahr angesehen wird.
## Wahrheitswerte (Truthy/Falsy)
Die folgende Logik wird verwendet, um zu bestimmen, ob ein Wert "truthy" ist:
- Boolean `true`: Truthy
- Boolean `false`: Falsy
- `null`: Falsy
- Leerer String oder nur Leerzeichen: Falsy
- Numerischer Wert `0`: Falsy
- Leeres Array: Falsy
- Alle anderen Werte: Truthy
## Tipps
- Das `if`-Attribut kann an jedem beliebigen HTML-Element verwendet werden
- Bei Erfüllung der Bedingung bleibt das Element erhalten, nur das `if`-Attribut wird entfernt
- Für komplexere Bedingungen können vordefinierte Variablen im RenderContext verwendet werden

View File

@@ -0,0 +1,48 @@
# IncludeProcessor
## Überblick
Der `IncludeProcessor` ermöglicht das Einbinden externer HTML-Dateien in das aktuelle Template.
## Funktionsweise
- Findet alle `<include>`-Tags im DOM
- Lädt die referenzierte Datei über den TemplateLoader
- Fügt den Inhalt an der Stelle des `<include>`-Tags ein
- Bei Fehlern wird ein HTML-Kommentar mit der Fehlermeldung eingefügt
## Syntax
```html
<include file="pfad/zur/datei.html"></include>
```
## Beispiel
```html
<div class="main-content">
<include file="partials/header.html"></include>
<p>Seiteninhalt...</p>
<include file="partials/footer.html"></include>
</div>
```
## Implementierung
Der Prozessor verwendet den `TemplateLoader`, um die angegebene Datei zu laden und dann den `DomTemplateParser`, um das geladene HTML zu parsen. Anschließend wird der Inhalt als DocumentFragment in das aktuelle DOM eingefügt.
## Fehlerbehandlung
Wenn die angegebene Datei nicht gefunden wird oder andere Fehler auftreten, wird der `<include>`-Tag durch einen HTML-Kommentar mit der Fehlermeldung ersetzt:
```html
<!-- Fehler beim Laden von 'pfad/zur/datei.html': File not found -->
```
## Vorteile
- Fördert die Wiederverwendung von Code
- Ermöglicht die Aufteilung komplexer Templates in kleinere, wartbare Teile
- Unterstützt modulare Entwicklung

View File

@@ -0,0 +1,59 @@
# LayoutTagProcessor
## Überblick
Der `LayoutTagProcessor` implementiert ein Layout-System mit übergeordneten Templates, das die Strukturierung und Wiederverwendung von Seitenlayouts ermöglicht.
## Funktionsweise
- Findet `<layout>`-Tags im DOM
- Lädt das angegebene Layout aus dem Verzeichnis `/src/Framework/View/templates/layouts/`
- Platziert den Inhalt des aktuellen Templates im `<main>`-Tag des Layouts
- Das Layout wird die übergeordnete Struktur für das gesamte Dokument
## Syntax
```html
<layout src="layout-name"></layout>
```
## Beispiel
**layout.view.php:**
```html
<!DOCTYPE html>
<html>
<head>
<title>Meine Webseite</title>
</head>
<body>
<header>...</header>
<main><!-- Hier wird der Inhalt eingefügt --></main>
<footer>...</footer>
</body>
</html>
```
**template.view.php:**
```html
<layout src="layout">
<h1>Mein Inhalt</h1>
<p>Wird in das main-Element des Layouts eingefügt</p>
</layout>
```
## Implementierung
Der Prozessor verwendet den `PathProvider` und `TemplateLoader`, um das Layout zu laden. Es sucht nach einem `<main>`-Element im Layout und ersetzt dessen Inhalt mit dem Inhalt des aktuellen Templates. Anschließend wird das gesamte DOM durch das neue Layout-DOM ersetzt.
## Besonderheiten
- Layouts müssen ein `<main>`-Element enthalten, um den Seiteninhalt aufzunehmen
- Es wird nur der erste `<layout>`-Tag im Dokument verarbeitet
- Die Layouts werden aus einem festen Verzeichnis geladen: `/src/Framework/View/templates/layouts/`
## Tipps
- Layouts können verschachtelt werden
- Dies ist ideal für die Erstellung einer konsistenten Seitenstruktur in der gesamten Anwendung
- Kombiniert gut mit dem `IncludeProcessor` für modulare Teile innerhalb des Layouts

View File

@@ -0,0 +1,53 @@
# MetaManipulator
## Überblick
Der `MetaManipulator` extrahiert Metadaten aus `<meta>`-Tags und macht sie für Templates verfügbar.
## Funktionsweise
- Findet alle `<meta name="x" content="y">`-Tags im DOM
- Fügt deren Werte dem RenderContext hinzu (falls noch nicht vorhanden)
- Ermöglicht Zugriff auf Metadaten über `{{ metaName }}` in Templates
## Syntax
```html
<meta name="variableName" content="variableValue">
```
## Beispiel
**Meta-Tags im HTML:**
```html
<head>
<meta name="description" content="Eine tolle Webseite">
<meta name="keywords" content="php, html, templates">
</head>
```
**Zugriff im Template:**
```html
<h1>{{ title }}</h1>
<p>{{ description }}</p>
```
## Implementierung
Der Prozessor verwendet `querySelectorAll('meta[name][content]')`, um alle Meta-Tags mit den erforderlichen Attributen zu finden. Die Werte werden nur dann dem RenderContext hinzugefügt, wenn sie dort noch nicht existieren, um vorhandene Werte nicht zu überschreiben.
## Verwendungszwecke
- SEO-Metadaten im gesamten Template verfügbar machen
- Seitentitel und Beschreibungen zentral definieren
- Dynamische Meta-Tags für Social Media (OpenGraph, Twitter Cards)
## Priorität
Bereits im RenderContext vorhandene Werte haben Vorrang vor den Werten aus Meta-Tags. Dies ermöglicht das Überschreiben von Standard-Metadaten mit dynamischen Werten.
## Tipps
- Verwenden Sie diesen Prozessor, um gemeinsame Metadaten an einer zentralen Stelle zu definieren
- Kombinieren Sie ihn mit dem LayoutTagProcessor, um seitenspezifische Meta-Tags in übergeordnete Layouts zu integrieren
- Nützlich für dynamische Titel und Beschreibungen in sozialen Medien

View File

@@ -0,0 +1,36 @@
# PlaceholderReplacer
## Überblick
Der `PlaceholderReplacer` ist ein zentraler Prozessor, der für das Ersetzen von Platzhaltern im Text und in Attributen zuständig ist.
## Funktionsweise
- Durchsucht alle Textknoten und Attribute nach `{{ ... }}`-Mustern
- Ersetzt diese mit Werten aus dem RenderContext
- Wandelt HTML-Entitäten um für sicheres Output
## Syntax
```php
{{ variableName }}
```
## Beispiel
```html
<div class="user-info">
<h2>{{ username }}</h2>
<a href="{{ profileUrl }}">Profil ansehen</a>
</div>
```
## Implementierung
Der Prozessor verwendet reguläre Ausdrücke, um Platzhalter zu finden und zu ersetzen. Bei Attributen werden Anführungszeichen und andere spezielle Zeichen entsprechend maskiert.
## Tipps
- Für komplexe Pfade kann Punktnotation verwendet werden: `{{ user.profile.name }}`
- Die Ersetzung ist standardmäßig escaped, um XSS-Angriffe zu verhindern
- Für rohen HTML-Code verwenden Sie den EscapeProcessor mit der Syntax `{{{ variableName }}}`

View File

@@ -0,0 +1,110 @@
# DOM-Prozessoren Dokumentation
## Überblick
Die DOM-Prozessoren in `src/Framework/View/Processors` sind modulare Komponenten zur Verarbeitung und Manipulation von HTML-Dokumenten. Sie implementieren das `DomProcessor`-Interface und werden vom Template-System verwendet, um dynamische Inhalte, Layouts und Komponenten in Templates einzufügen.
## Das DomProcessor Interface
```php
interface DomProcessor
{
public function process(HTMLDocument $dom, RenderContext $context): void;
}
```
Jeder Prozessor erhält:
- Ein `HTMLDocument`-Objekt (PHP 8.4's HTML5 Parser)
- Einen `RenderContext` mit Daten und Slots
## Verfügbare Prozessoren
| Prozessor | Beschreibung | Dokumentation |
|-----------|-------------|---------------|
| PlaceholderReplacer | Ersetzt Platzhalter in Text und Attributen | [Dokumentation](PlaceholderReplacer.md) |
| IncludeProcessor | Bindet externe HTML-Dateien ein | [Dokumentation](IncludeProcessor.md) |
| LayoutTagProcessor | Implementiert ein Layout-System | [Dokumentation](LayoutTagProcessor.md) |
| ComponentProcessor | Ermöglicht wiederverwendbare UI-Komponenten | [Dokumentation](ComponentProcessor.md) |
| SlotProcessor | Implementiert ein Slot-System für Komponenten | [Dokumentation](SlotProcessor.md) |
| IfProcessor | Bedingte Darstellung von Elementen | [Dokumentation](IfProcessor.md) |
| ForProcessor | Iteration über Arrays/Sammlungen | [Dokumentation](ForProcessor.md) |
| AssetInjector | Fügt JS/CSS-Assets aus dem Vite-Manifest ein | [Dokumentation](AssetInjector.md) |
| MetaManipulator | Extrahiert Metadaten aus Meta-Tags | [Dokumentation](MetaManipulator.md) |
| EscapeProcessor | Escaping von Variablen (String-Prozessor) | [Dokumentation](EscapeProcessor.md) |
| DateFormatProcessor | Formatiert Datums- und Zeitwerte | [Dokumentation](DateFormatProcessor.md) |
| SwitchCaseProcessor | Ermöglicht switch/case-Logik in Templates | [Dokumentation](SwitchCaseProcessor.md) |
| CommentStripProcessor | Entfernt HTML-Kommentare | [Dokumentation](CommentStripProcessor.md) |
| SingleLineHtmlProcessor | Konvertiert mehrzeilige HTML-Elemente in einzeilige | [Dokumentation](SingleLineHtmlProcessor.md) |
| RemoveEmptyLinesProcessor | Entfernt leere Zeilen aus dem HTML | [Dokumentation](RemoveEmptyLinesProcessor.md) |
| VoidElementsSelfClosingProcessor | Korrigiert selbstschließende Tags | [Dokumentation](VoidElementsSelfClosingProcessor.md) |
## Anwendungsreihenfolge
Prozessoren werden typischerweise in der folgenden Reihenfolge angewendet:
1. **Strukturprozessoren**
- LayoutTagProcessor
- IncludeProcessor
2. **Komponentenprozessoren**
- ComponentProcessor
- SlotProcessor
3. **Logikprozessoren**
- IfProcessor
- ForProcessor
- SwitchCaseProcessor
4. **Inhaltsprozessoren**
- PlaceholderReplacer
- DateFormatProcessor
- MetaManipulator
5. **Asset-Prozessoren**
- AssetInjector
6. **Optimierungsprozessoren**
- CommentStripProcessor
- RemoveEmptyLinesProcessor
- SingleLineHtmlProcessor
- VoidElementsSelfClosingProcessor
## Anwendungsbeispiel
```php
// Template-Engine mit Prozessoren konfigurieren
$templateEngine = new TemplateEngine();
// Strukturprozessoren
$templateEngine->addProcessor(new LayoutTagProcessor());
$templateEngine->addProcessor(new IncludeProcessor());
// Komponentenprozessoren
$templateEngine->addProcessor(new ComponentProcessor());
$templateEngine->addProcessor(new SlotProcessor());
// Logikprozessoren
$templateEngine->addProcessor(new IfProcessor());
$templateEngine->addProcessor(new ForProcessor());
// Inhaltsprozessoren
$templateEngine->addProcessor(new PlaceholderReplacer());
$templateEngine->addProcessor(new MetaManipulator());
// Template rendern
$html = $templateEngine->render('template.html', ['key' => 'value']);
```
## Eigene Prozessoren erstellen
Um einen eigenen Prozessor zu erstellen, implementieren Sie das `DomProcessor`-Interface:
```php
class MyCustomProcessor implements DomProcessor
{
public function process(HTMLDocument $dom, RenderContext $context): void
{
// DOM-Manipulation hier
}
}
```

View File

@@ -0,0 +1,65 @@
# RemoveEmptyLinesProcessor
## Überblick
Der `RemoveEmptyLinesProcessor` entfernt leere Zeilen aus dem HTML-Output, um die Größe zu reduzieren.
## Funktionsweise
- Verarbeitet den HTML-String nach der DOM-Verarbeitung
- Entfernt Zeilen, die nur Whitespace enthalten
- Behält die HTML-Struktur bei, reduziert aber die Formatierung
## Vorher / Nachher
**Vorher:**
```html
<div>
<p>Absatz 1</p>
<p>Absatz 2</p>
</div>
```
**Nachher:**
```html
<div>
<p>Absatz 1</p>
<p>Absatz 2</p>
</div>
```
## Implementierung
Der Prozessor verwendet reguläre Ausdrücke, um leere Zeilen zu finden und zu entfernen. Eine leere Zeile wird definiert als eine Zeile, die nur Whitespace (Leerzeichen, Tabs, etc.) enthält.
## Konfigurationsoptionen
- **preserveSpecialElements**: (Boolean) Bei `true` werden leere Zeilen innerhalb bestimmter Elemente (wie `<pre>` oder `<textarea>`) beibehalten
## Erhaltene Elemente
Bei bestimmten Elementen, in denen Whitespace wichtig ist, kann die Formatierung erhalten bleiben:
- `<pre>` und `<code>` Elemente
- `<textarea>` Elemente
## Vorteile
- Reduziert die Größe der HTML-Ausgabe
- Behält die grundlegende Formatierung für bessere Lesbarkeit bei
- Minimale Auswirkung auf die Darstellung im Browser
## Anwendungsfälle
- Moderate Optimierung für Produktionsumgebungen
- Reduzierung der Bandbreitennutzung
- Säuberung von HTML-Ausgabe mit übermäßigen Leerzeilen
## Tipps
- Verwenden Sie diesen Prozessor vor dem `SingleLineHtmlProcessor` für eine stufenweise Komprimierung
- Für maximale Komprimierung kombinieren Sie ihn mit `CommentStripProcessor` und `SingleLineHtmlProcessor`
- Dieser Prozessor bietet einen guten Kompromiss zwischen Komprimierung und Lesbarkeit

View File

@@ -0,0 +1,63 @@
# SingleLineHtmlProcessor
## Überblick
Der `SingleLineHtmlProcessor` konvertiert mehrzeilige HTML-Elemente in einzeilige, um die Größe der Ausgabe zu reduzieren.
## Funktionsweise
- Verarbeitet den HTML-String nach der DOM-Verarbeitung
- Entfernt Zeilenumbrüche und überschüssige Leerzeichen
- Behält die HTML-Struktur bei, reduziert aber die Formatierung
## Vorher / Nachher
**Vorher:**
```html
<div>
<p>
Dies ist ein Absatz mit
mehreren Zeilen
</p>
</div>
```
**Nachher:**
```html
<div><p>Dies ist ein Absatz mit mehreren Zeilen</p></div>
```
## Implementierung
Der Prozessor verwendet reguläre Ausdrücke oder DOM-Traversierung, um mehrzeilige Elemente zu identifizieren und zu komprimieren. Zeilenumbrüche innerhalb von Tags werden entfernt, während Textinhalt beibehalten wird.
## Konfigurationsoptionen
- **preserveTextLineBreaks**: (Boolean) Bei `true` werden Zeilenumbrüche in Textknoten als Leerzeichen beibehalten
- **preservePreTags**: (Boolean) Bei `true` wird der Inhalt von `<pre>` Tags nicht verändert
## Erhaltene Elemente
Bei bestimmten Elementen, in denen Whitespace wichtig ist, kann die Formatierung erhalten bleiben:
- `<pre>` und `<code>` Elemente
- `<textarea>` Elemente
- Elemente mit `white-space: pre` im Stil
## Vorteile
- Reduziert die Größe der HTML-Ausgabe
- Verbessert die Ladezeit
- Minimale Auswirkung auf die Darstellung im Browser
## Anwendungsfälle
- Optimierung für Produktionsumgebungen
- Reduzierung der Bandbreitennutzung
- Vorbereitung für weitere Komprimierung (z.B. Gzip)
## Tipps
- Verwenden Sie diesen Prozessor als einen der letzten Schritte in der Verarbeitungskette
- Kombinieren Sie ihn mit `CommentStripProcessor` und `RemoveEmptyLinesProcessor` für maximale Komprimierung
- Beachten Sie, dass die Lesbarkeit des HTML-Codes für Entwickler reduziert wird

View File

@@ -0,0 +1,58 @@
# SlotProcessor
## Überblick
Der `SlotProcessor` implementiert ein Slot-System für Komponenten, das die flexible Platzierung von Inhalten innerhalb von Komponenten ermöglicht.
## Funktionsweise
- Findet `<slot>`-Tags im DOM
- Ersetzt diese durch den im RenderContext definierten Slot-Inhalt
- Falls kein Inhalt definiert ist, wird der Default-Inhalt des Slots verwendet
## Syntax
```html
<slot name="slotName">Default-Inhalt</slot>
```
## Beispiel
**Komponentendefinition:**
```html
<div class="card">
<div class="card-header">
<slot name="header">Standard-Header</slot>
</div>
<div class="card-body">
<slot>Standard-Inhalt</slot>
</div>
</div>
```
**Verwendung:**
```html
<component name="card">
<div slot="header">Mein benutzerdefinierter Header</div>
<p>Mein Inhalt für den Default-Slot</p>
</component>
```
## Implementierung
Der Prozessor sucht nach `<slot>`-Tags mit einem `name`-Attribut und ersetzt sie durch den entsprechenden Inhalt aus dem `slots`-Array des RenderContext. Wenn kein passender Slot-Inhalt gefunden wird, wird der innere Inhalt des `<slot>`-Tags als Fallback verwendet.
## Benannte vs. Default-Slots
- **Benannte Slots**: Haben ein `name`-Attribut und werden durch Inhalte mit passendem `slot`-Attribut ersetzt
- **Default-Slot**: Hat kein `name`-Attribut und wird durch den Hauptinhalt der Komponente ersetzt
## Integration mit ComponentProcessor
Der SlotProcessor wird normalerweise nach dem ComponentProcessor ausgeführt, um die Slots innerhalb der gerenderten Komponenten zu verarbeiten. Die Komponenten-Templates definieren die Slots, während der Aufruf der Komponente die Inhalte für diese Slots bereitstellt.
## Vorteile
- Ermöglicht flexible, anpassbare Komponenten
- Unterstützt komplexe UI-Strukturen mit variabler Inhaltsplatzierung
- Ähnlich dem Slot-Konzept in Web Components und modernen Frontend-Frameworks

View File

@@ -0,0 +1,63 @@
# SwitchCaseProcessor
## Überblick
Der `SwitchCaseProcessor` ermöglicht switch/case-Logik in Templates, ähnlich wie in Programmiersprachen.
## Funktionsweise
- Findet `<switch>`-Tags mit einem `value`-Attribut
- Vergleicht den Wert mit den `value`-Attributen der enthaltenen `<case>`-Tags
- Zeigt nur den Inhalt des ersten passenden `<case>`-Tags an
- Falls kein Fall passt, wird der Inhalt des `<default>`-Tags angezeigt (wenn vorhanden)
## Syntax
```html
<switch value="expression">
<case value="value1">Inhalt für value1</case>
<case value="value2">Inhalt für value2</case>
<default>Fallback-Inhalt</default>
</switch>
```
## Beispiel
```html
<switch value="{{ status }}">
<case value="active">Aktiv</case>
<case value="pending">Ausstehend</case>
<case value="suspended">Gesperrt</case>
<default>Unbekannter Status</default>
</switch>
```
## Implementierung
Der Prozessor findet alle `<switch>`-Tags und deren `<case>`- und `<default>`-Kinder. Er vergleicht den im `value`-Attribut des `<switch>`-Tags angegebenen Wert mit den Werten der `<case>`-Tags und behält nur den passenden Fall bei. Alle nicht passenden Fälle werden aus dem DOM entfernt.
## Mehrere passende Fälle
Wenn mehrere `<case>`-Tags dem Wert entsprechen, wird nur der erste passende Fall berücksichtigt. Die anderen werden entfernt, ähnlich wie bei einem `break` in einer Programmiersprache.
## Default-Fall
Der `<default>`-Tag wird nur angezeigt, wenn kein `<case>`-Tag passt. Er benötigt kein `value`-Attribut.
## Dynamische Werte
Das `value`-Attribut des `<switch>`-Tags kann mit Platzhaltern verwendet werden:
```html
<switch value="{{ userRole }}">
<case value="admin">Administratorbereich</case>
<case value="moderator">Moderatorbereich</case>
<default>Benutzerbereich</default>
</switch>
```
## Vorteile
- Elegantere Alternative zu verschachtelten `if`-Bedingungen
- Verbessert die Lesbarkeit bei mehreren Bedingungen
- Ideal für Status-basierte Anzeigen, Benutzerberechtigungen, etc.

View File

@@ -0,0 +1,80 @@
# VoidElementsSelfClosingProcessor
## Überblick
Der `VoidElementsSelfClosingProcessor` stellt sicher, dass leere Elemente (Void Elements) korrekt als selbstschließende Tags formatiert sind.
## Funktionsweise
- Identifiziert HTML-Void-Elemente (Elemente ohne Endtag)
- Korrigiert ihre Formatierung gemäß HTML5-Standard
- Stellt sicher, dass selbstschließende Tags korrekt dargestellt werden
## Betroffe Elemente
Die folgenden Elemente sind HTML5-Void-Elemente und werden vom Prozessor behandelt:
- `area`
- `base`
- `br`
- `col`
- `embed`
- `hr`
- `img`
- `input`
- `link`
- `meta`
- `param`
- `source`
- `track`
- `wbr`
## Vorher / Nachher
**Korrektur für HTML5:**
```html
<!-- Falsch (XML-Stil) -->
<img src="bild.jpg" alt="Bild" />
<!-- Korrekt (HTML5) -->
<img src="bild.jpg" alt="Bild">
```
**Oder:**
```html
<!-- Falsch (fehlendes Attribut) -->
<input>
<!-- Korrekt (mit Attribut) -->
<input type="text">
```
## Implementierung
Der Prozessor durchsucht das DOM nach Void-Elementen und stellt sicher, dass sie gemäß HTML5-Standard formatiert sind. Er kann auch prüfen, ob erforderliche Attribute vorhanden sind.
## HTML5 vs. XML/XHTML
- In HTML5 haben Void-Elemente kein schließendes Tag und keinen abschließenden Schrägstrich
- In XML/XHTML müssen Void-Elemente selbstschließend sein (`<tag />`)
Dieser Prozessor standardisiert auf HTML5-Format, es sei denn, er wird anders konfiguriert.
## Konfigurationsoptionen
- **outputMode**: Kann auf "html5" oder "xhtml" gesetzt werden, um das Ausgabeformat zu bestimmen
- **enforceRequiredAttributes**: Wenn aktiviert, werden Warnungen für fehlende erforderliche Attribute generiert
## Vorteile
- Konsistente HTML-Ausgabe
- Vermeidet potenzielle Rendering-Probleme in verschiedenen Browsern
- Verbessert die Gültigkeit des HTML-Codes
## Tipps
- Dieser Prozessor sollte vor Optimierungsprozessoren wie dem CommentStripProcessor ausgeführt werden
- In HTML5-Projekten sollten Void-Elemente ohne schließenden Schrägstrich verwendet werden
- In XML/XHTML-Projekten sollten Void-Elemente immer selbstschließend sein

View File

@@ -2,13 +2,16 @@
namespace App\Framework\View;
use App\Framework\Meta\MetaData;
final readonly class RenderContext
{
public function __construct(
public string $template, // Dateiname oder Template-Key
public MetaData $metaData,
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
public ?string $controllerClass = null,
) {}
}

View File

@@ -1,145 +0,0 @@
<?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;
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class Template
{
public function __construct(public string $path) {
}
}

View File

@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Cache\Cache;
use App\Framework\Discovery\FileVisitor;
/**
* Visitor zum Auffinden von Templates
*/
final class TemplateDiscoveryVisitor implements FileVisitor
{
private array $templates = [];
private array $scannedDirectories = [];
private string $templateExtension;
private ?array $templateLookupIndex = null;
public function __construct(string $templateExtension = '.view.php')
{
$this->templateExtension = $templateExtension;
}
public function onScanStart(): void
{
$this->templates = [];
$this->scannedDirectories = [];
$this->templateLookupIndex = null;
}
public function visitClass(string $className, string $filePath): void
{
// Hier prüfen wir nicht auf Klassen, sondern auf Template-Dateien
$directory = dirname($filePath);
// Vermeide mehrfaches Scannen desselben Verzeichnisses
if (isset($this->scannedDirectories[$directory])) {
return;
}
$this->scannedDirectories[$directory] = true;
// Suche Templates im selben Verzeichnis wie die Klasse
$templateFiles = glob($directory . '/*' . $this->templateExtension);
foreach ($templateFiles as $templateFile) {
$filename = basename($templateFile);
$templateName = substr($filename, 0, -strlen($this->templateExtension));
if (!isset($this->templates[$templateName])) {
$this->templates[$templateName] = [];
}
// Ordne das Template der Klasse zu, wenn sie im selben Verzeichnis ist
$this->templates[$templateName][$className] = $templateFile;
// Füge auch einen Standard-Eintrag ohne Klasse hinzu
$this->templates[$templateName][''] = $templateFile;
}
}
public function onScanComplete(): void
{
// Erstelle einen Index für schnelleren Zugriff
$this->createTemplateLookupIndex();
}
/**
* Wird aufgerufen, wenn ein inkrementeller Scan beginnt
*/
public function onIncrementalScanStart(): void
{
// Behalte vorhandene Templates bei, setze nur scannedDirectories zurück
$this->scannedDirectories = [];
$this->templateLookupIndex = null;
}
/**
* Wird aufgerufen, wenn ein inkrementeller Scan abgeschlossen ist
*/
public function onIncrementalScanComplete(): void
{
// Aktualisiere den Index nach inkrementellem Scan
$this->createTemplateLookupIndex();
}
/**
* Erstellt einen Index für schnelleren Template-Zugriff
*/
private function createTemplateLookupIndex(): void
{
$this->templateLookupIndex = [];
foreach ($this->templates as $templateName => $variants) {
foreach ($variants as $className => $path) {
$key = $this->buildLookupKey($templateName, $className);
$this->templateLookupIndex[$key] = $path;
}
}
}
/**
* Erstellt einen eindeutigen Lookup-Schlüssel für ein Template
*/
private function buildLookupKey(string $templateName, string $className = ''): string
{
return $templateName . '|' . $className;
}
/**
* Lädt Daten aus dem Cache
*/
public function loadFromCache(Cache $cache): void
{
$cacheItem = $cache->get($this->getCacheKey());
if ($cacheItem->isHit) {
$this->templates = $cacheItem->value;
$this->createTemplateLookupIndex();
}
}
public function getCacheKey(): string
{
return 'templates';
}
/**
* Liefert die zu cachenden Daten des Visitors
*/
public function getCacheableData(): array
{
return $this->templates;
}
/**
* Gibt den Pfad eines Templates zurück
*/
public function getTemplatePath(string $templateName, ?string $className = null): ?string
{
// Verwende den Index für schnelleren Zugriff, wenn verfügbar
if ($this->templateLookupIndex !== null) {
$key = $this->buildLookupKey($templateName, $className ?? '');
if (isset($this->templateLookupIndex[$key])) {
return $this->templateLookupIndex[$key];
}
// Fallback auf Standard-Template
$fallbackKey = $this->buildLookupKey($templateName, '');
return $this->templateLookupIndex[$fallbackKey] ?? null;
}
// Klassische Suche als Fallback
if (isset($this->templates[$templateName][$className ?? ''])) {
return $this->templates[$templateName][$className ?? ''];
}
return $this->templates[$templateName][''] ?? null;
}
/**
* Gibt alle gefundenen Templates zurück
*/
public function getAllTemplates(): array
{
return $this->templates;
}
/**
* Gibt alle Templates in einem bestimmten Verzeichnis zurück
*/
public function getTemplatesInDirectory(string $directory): array
{
$result = [];
$normalizedDirectory = rtrim($directory, '/');
foreach ($this->templates as $templateName => $variants) {
foreach ($variants as $className => $path) {
if (dirname($path) === $normalizedDirectory) {
if (!isset($result[$templateName])) {
$result[$templateName] = [];
}
$result[$templateName][$className] = $path;
}
}
}
return $result;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Framework\View;
use App\Framework\DI\Container;
use App\Framework\View\Functions\TemplateFunction;
final readonly class TemplateFunctions
{
private array $functions;
public function __construct(
private Container $container, string ...$templateFunctions)
{
$functions = [];
foreach ($templateFunctions as $function) {
$functionInstance = $this->container->get($function);
$functions[$functionInstance->functionName] = $functionInstance;
}
$this->functions = $functions;
}
public function has(string $functionName): bool
{
return isset($this->functions[$functionName]);
}
public function get(string $functionName): ?TemplateFunction
{
return $this->functions[$functionName] ?? null;
}
}

View File

@@ -1,38 +0,0 @@
<?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";
}
}

View File

@@ -1,73 +0,0 @@
<?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);
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Core\AttributeMapper;
use ReflectionMethod;
final class TemplateMapper implements AttributeMapper
{
public function getAttributeClass(): string
{
return Template::class;
}
public function map(object $reflectionTarget, object $attributeInstance): ?array
{
if (!$attributeInstance instanceof Template) {
return null;
}
return [
'class' => $reflectionTarget->getName(),
#'method' => $reflectionTarget->getName(),
'path' => $attributeInstance->path,
];
}
}

View File

@@ -2,39 +2,82 @@
namespace App\Framework\View;
class TemplateProcessor
use App\Framework\DI\Container;
use App\Framework\View\Processing\DomProcessingPipeline;
use Dom\HTMLDocument;
final class TemplateProcessor
{
/** @var DomProcessor[] */
private array $domProcessors = [];
private array $resolvedProcessors = [];
public function __construct(
private readonly array $domProcessors,
private readonly array $stringProcessors,
private readonly Container $container
) {}
/** @var StringProcessor[] */
private array $stringProcessors = [];
public function registerDom(DomProcessor $processor): void
public function render(RenderContext $context, string $html, bool $component = false): string
{
$this->domProcessors[] = $processor;
#$dom = $this->processDom($html, $context);
$domPipeline = new DomProcessingPipeline($this->domProcessors, $this->container->get(ProcessorResolver::class));
$dom = $domPipeline->process($context, $html);
$html = $component ? $this->extractBodyContent($dom->document) : $dom->document->saveHTML();
#$html = $dom->body->innerHTML;
return $this->processString($html, $context);
}
public function registerString(StringProcessor $processor): void
public function __debugInfo(): ?array
{
$this->stringProcessors[] = $processor;
return [
$this->domProcessors,
$this->stringProcessors,
];
}
public function render(RenderContext $context, string $html): string
private function processDom(string $html, RenderContext $context): HTMLDocument
{
$parser = new DomTemplateParser();
$dom = $parser->parse($html);
foreach ($this->domProcessors as $processor) {
$processor->process($dom, $context);
// Verkettete Verarbeitung der DOM-Prozessoren
foreach ($this->domProcessors as $processorClass) {
$processor = $this->resolveProcessor($processorClass);
if($processor instanceof EnhancedDomProcessor) {
$dom = $processor->process(DomWrapper::fromString($dom->saveHtml()), $context)->document;
#$dom = $dom->document;
} else {
$dom = $processor->process($dom, $context);
}
}
return $dom;
}
$html = $parser->toHtml($dom);
foreach ($this->stringProcessors as $processor) {
private function processString(string $html, RenderContext $context): string
{
// Verarbeitung durch String-Prozessoren
foreach ($this->stringProcessors as $processorClass) {
$processor = $this->resolveProcessor($processorClass);
$html = $processor->process($html, $context);
}
return $html;
}
public function resolveProcessor(string $processorClass): object
{
if (!isset($this->resolvedProcessors[$processorClass])) {
$this->resolvedProcessors[$processorClass] = $this->container->get($processorClass);;
}
return $this->resolvedProcessors[$processorClass];
}
private function extractBodyContent(HTMLDocument $dom): string
{
return $dom->body?->innerHTML ?? '';
}
}

Some files were not shown because too many files have changed in this diff Show More