chore: complete update
This commit is contained in:
39
src/Framework/View/Caching/Analysis/CacheStrategy.php
Normal file
39
src/Framework/View/Caching/Analysis/CacheStrategy.php
Normal 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
40
src/Framework/View/Caching/Analysis/CacheabilityAnalyzer.php
Normal file
40
src/Framework/View/Caching/Analysis/CacheabilityAnalyzer.php
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
29
src/Framework/View/Caching/Analysis/CacheabilityScore.php
Normal file
29
src/Framework/View/Caching/Analysis/CacheabilityScore.php
Normal 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;
|
||||
}
|
||||
}
|
||||
171
src/Framework/View/Caching/Analysis/SmartTemplateAnalyzer.php
Normal file
171
src/Framework/View/Caching/Analysis/SmartTemplateAnalyzer.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
15
src/Framework/View/Caching/Analysis/TemplateAnalysis.php
Normal file
15
src/Framework/View/Caching/Analysis/TemplateAnalysis.php
Normal 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
|
||||
) {}
|
||||
}
|
||||
10
src/Framework/View/Caching/Analysis/TemplateAnalyzer.php
Normal file
10
src/Framework/View/Caching/Analysis/TemplateAnalyzer.php
Normal 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;
|
||||
}
|
||||
84
src/Framework/View/Caching/CacheDiagnostics.php
Normal file
84
src/Framework/View/Caching/CacheDiagnostics.php
Normal 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()
|
||||
];
|
||||
}
|
||||
}
|
||||
122
src/Framework/View/Caching/CacheManager.php
Normal file
122
src/Framework/View/Caching/CacheManager.php
Normal 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;
|
||||
}
|
||||
}
|
||||
11
src/Framework/View/Caching/FragmentCache.php
Normal file
11
src/Framework/View/Caching/FragmentCache.php
Normal 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;
|
||||
}
|
||||
11
src/Framework/View/Caching/Keys/SmartKeyGenerator.php
Normal file
11
src/Framework/View/Caching/Keys/SmartKeyGenerator.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Caching\Keys;
|
||||
|
||||
class SmartKeyGenerator
|
||||
{
|
||||
|
||||
public function generate(string $string, string $key)
|
||||
{
|
||||
}
|
||||
}
|
||||
16
src/Framework/View/Caching/SmartCache.php
Normal file
16
src/Framework/View/Caching/SmartCache.php
Normal 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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
28
src/Framework/View/Caching/Strategies/NoCacheStrategy.php
Normal file
28
src/Framework/View/Caching/Strategies/NoCacheStrategy.php
Normal 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;
|
||||
}
|
||||
}
|
||||
13
src/Framework/View/Caching/Strategies/ViewCacheStrategy.php
Normal file
13
src/Framework/View/Caching/Strategies/ViewCacheStrategy.php
Normal 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;
|
||||
}
|
||||
70
src/Framework/View/Caching/TaggedFragmentCache.php
Normal file
70
src/Framework/View/Caching/TaggedFragmentCache.php
Normal 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] ?? [];
|
||||
}
|
||||
}
|
||||
13
src/Framework/View/Caching/TemplateContext.php
Normal file
13
src/Framework/View/Caching/TemplateContext.php
Normal 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 = []
|
||||
) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
40
src/Framework/View/ComponentCache.php
Normal file
40
src/Framework/View/ComponentCache.php
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}>";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
34
src/Framework/View/DomComponentService.php
Normal file
34
src/Framework/View/DomComponentService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
43
src/Framework/View/DomFormService.php
Normal file
43
src/Framework/View/DomFormService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
57
src/Framework/View/DomHeadService.php
Normal file
57
src/Framework/View/DomHeadService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
261
src/Framework/View/DomWrapper.php
Normal file
261
src/Framework/View/DomWrapper.php
Normal 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);
|
||||
}
|
||||
}
|
||||
279
src/Framework/View/ElementCollection.php
Normal file
279
src/Framework/View/ElementCollection.php
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
19
src/Framework/View/EnhancedDomProcessor.php
Normal file
19
src/Framework/View/EnhancedDomProcessor.php
Normal 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;
|
||||
}
|
||||
78
src/Framework/View/EnhancedTemplateProcessor.php
Normal file
78
src/Framework/View/EnhancedTemplateProcessor.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
17
src/Framework/View/Exception/TemplateNotFound.php
Normal file
17
src/Framework/View/Exception/TemplateNotFound.php
Normal 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);
|
||||
}
|
||||
}
|
||||
27
src/Framework/View/Exceptions/TemplateNotFoundException.php
Normal file
27
src/Framework/View/Exceptions/TemplateNotFoundException.php
Normal 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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
35
src/Framework/View/Functions/ImageSlotFunction.php
Normal file
35
src/Framework/View/Functions/ImageSlotFunction.php
Normal 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);
|
||||
}
|
||||
}
|
||||
8
src/Framework/View/Functions/TemplateFunction.php
Normal file
8
src/Framework/View/Functions/TemplateFunction.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Functions;
|
||||
|
||||
interface TemplateFunction
|
||||
{
|
||||
public string $functionName {get;}
|
||||
}
|
||||
21
src/Framework/View/Functions/UrlFunction.php
Normal file
21
src/Framework/View/Functions/UrlFunction.php
Normal 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;
|
||||
}
|
||||
24
src/Framework/View/Loading/Resolvers/ControllerResolver.php
Normal file
24
src/Framework/View/Loading/Resolvers/ControllerResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Framework/View/Loading/Resolvers/DefaultPathResolver.php
Normal file
24
src/Framework/View/Loading/Resolvers/DefaultPathResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
101
src/Framework/View/Loading/Resolvers/DiscoveryResolver.php
Normal file
101
src/Framework/View/Loading/Resolvers/DiscoveryResolver.php
Normal 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);
|
||||
}
|
||||
}
|
||||
19
src/Framework/View/Loading/Resolvers/TemplateMapResolver.php
Normal file
19
src/Framework/View/Loading/Resolvers/TemplateMapResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View\Loading\Resolvers;
|
||||
|
||||
interface TemplateResolverStrategy
|
||||
{
|
||||
public function resolve(string $template, ?string $controllerClass = null): ?string;
|
||||
}
|
||||
40
src/Framework/View/Loading/TemplateCache.php
Normal file
40
src/Framework/View/Loading/TemplateCache.php
Normal 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;
|
||||
}
|
||||
}
|
||||
22
src/Framework/View/Loading/TemplateContentLoader.php
Normal file
22
src/Framework/View/Loading/TemplateContentLoader.php
Normal 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 > erscheint
|
||||
return html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
}
|
||||
128
src/Framework/View/Loading/TemplateLoader.php
Normal file
128
src/Framework/View/Loading/TemplateLoader.php
Normal 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;
|
||||
}
|
||||
}
|
||||
23
src/Framework/View/Loading/TemplatePathResolver.php
Normal file
23
src/Framework/View/Loading/TemplatePathResolver.php
Normal 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 ?? '');
|
||||
}
|
||||
}
|
||||
31
src/Framework/View/Processing/DomProcessingPipeline.php
Normal file
31
src/Framework/View/Processing/DomProcessingPipeline.php
Normal 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;
|
||||
}
|
||||
}
|
||||
25
src/Framework/View/Processing/StringProcessingPipeline.php
Normal file
25
src/Framework/View/Processing/StringProcessingPipeline.php
Normal 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;
|
||||
}
|
||||
}
|
||||
21
src/Framework/View/ProcessorResolver.php
Normal file
21
src/Framework/View/ProcessorResolver.php
Normal 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];
|
||||
}
|
||||
}
|
||||
74
src/Framework/View/Processors/AssetInjector.php
Normal file
74
src/Framework/View/Processors/AssetInjector.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
71
src/Framework/View/Processors/CsrfReplaceProcessor.php
Normal file
71
src/Framework/View/Processors/CsrfReplaceProcessor.php
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
82
src/Framework/View/Processors/FormProcessor.php
Normal file
82
src/Framework/View/Processors/FormProcessor.php
Normal 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));
|
||||
}
|
||||
}
|
||||
78
src/Framework/View/Processors/HoneypotProcessor.php
Normal file
78
src/Framework/View/Processors/HoneypotProcessor.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
428
src/Framework/View/Processors/PhpVariableProcessor.php
Normal file
428
src/Framework/View/Processors/PhpVariableProcessor.php
Normal 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_]*)((?:->|(?:->))[a-zA-Z_][a-zA-Z0-9_]*)*(?:->|(?:->))([a-zA-Z_][a-zA-Z0-9_]*)\(([^)]*)\)/',
|
||||
function($matches) use ($data) {
|
||||
$varName = $matches[1];
|
||||
$propertyChain = $matches[2] ? str_replace('->', '->', $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 -> 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 -> erkennt
|
||||
return preg_replace_callback(
|
||||
'/\$([a-zA-Z_][a-zA-Z0-9_]*)((?:->|(?:->))[a-zA-Z_][a-zA-Z0-9_]*)+/',
|
||||
function($matches) use ($data) {
|
||||
$varName = $matches[1];
|
||||
$propertyChain = str_replace('->', '->', $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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
35
src/Framework/View/Processors/RemoveEmptyLinesProcessor.php
Normal file
35
src/Framework/View/Processors/RemoveEmptyLinesProcessor.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Framework/View/Processors/SingleLineHtmlProcessor.php
Normal file
18
src/Framework/View/Processors/SingleLineHtmlProcessor.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
62
src/Framework/View/Processors/docs/AssetInjector.md
Normal file
62
src/Framework/View/Processors/docs/AssetInjector.md
Normal 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
|
||||
54
src/Framework/View/Processors/docs/CommentStripProcessor.md
Normal file
54
src/Framework/View/Processors/docs/CommentStripProcessor.md
Normal 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
|
||||
55
src/Framework/View/Processors/docs/ComponentProcessor.md
Normal file
55
src/Framework/View/Processors/docs/ComponentProcessor.md
Normal 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
|
||||
65
src/Framework/View/Processors/docs/DateFormatProcessor.md
Normal file
65
src/Framework/View/Processors/docs/DateFormatProcessor.md
Normal 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
|
||||
55
src/Framework/View/Processors/docs/EscapeProcessor.md
Normal file
55
src/Framework/View/Processors/docs/EscapeProcessor.md
Normal 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
|
||||
61
src/Framework/View/Processors/docs/ForProcessor.md
Normal file
61
src/Framework/View/Processors/docs/ForProcessor.md
Normal 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
|
||||
50
src/Framework/View/Processors/docs/IfProcessor.md
Normal file
50
src/Framework/View/Processors/docs/IfProcessor.md
Normal 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
|
||||
48
src/Framework/View/Processors/docs/IncludeProcessor.md
Normal file
48
src/Framework/View/Processors/docs/IncludeProcessor.md
Normal 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
|
||||
59
src/Framework/View/Processors/docs/LayoutTagProcessor.md
Normal file
59
src/Framework/View/Processors/docs/LayoutTagProcessor.md
Normal 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
|
||||
53
src/Framework/View/Processors/docs/MetaManipulator.md
Normal file
53
src/Framework/View/Processors/docs/MetaManipulator.md
Normal 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
|
||||
36
src/Framework/View/Processors/docs/PlaceholderReplacer.md
Normal file
36
src/Framework/View/Processors/docs/PlaceholderReplacer.md
Normal 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 }}}`
|
||||
110
src/Framework/View/Processors/docs/README.md
Normal file
110
src/Framework/View/Processors/docs/README.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
58
src/Framework/View/Processors/docs/SlotProcessor.md
Normal file
58
src/Framework/View/Processors/docs/SlotProcessor.md
Normal 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
|
||||
63
src/Framework/View/Processors/docs/SwitchCaseProcessor.md
Normal file
63
src/Framework/View/Processors/docs/SwitchCaseProcessor.md
Normal 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.
|
||||
@@ -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
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
14
src/Framework/View/Template.php
Normal file
14
src/Framework/View/Template.php
Normal 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) {
|
||||
|
||||
}
|
||||
}
|
||||
189
src/Framework/View/TemplateDiscoveryVisitor.php
Normal file
189
src/Framework/View/TemplateDiscoveryVisitor.php
Normal 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;
|
||||
}
|
||||
}
|
||||
31
src/Framework/View/TemplateFunctions.php
Normal file
31
src/Framework/View/TemplateFunctions.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
src/Framework/View/TemplateMapper.php
Normal file
28
src/Framework/View/TemplateMapper.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user