Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching\Analysis;
enum CacheStrategy: string

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching\Analysis;
class CacheabilityAnalyzer

View File

@@ -1,23 +1,37 @@
<?php
declare(strict_types=1);
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;
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));
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching\Analysis;
use App\Framework\View\Loading\TemplateLoader;
@@ -8,7 +10,8 @@ final readonly class SmartTemplateAnalyzer implements TemplateAnalyzer
{
public function __construct(
private TemplateLoader $loader,
) {}
) {
}
public function analyze(string $template): TemplateAnalysis
{
@@ -72,7 +75,7 @@ final readonly class SmartTemplateAnalyzer implements TemplateAnalyzer
private function determineOptimalStrategy(string $template, CacheabilityScore $cacheability, array $dependencies): CacheStrategy
{
// Nicht cacheable = NoCache
if (!$cacheability->isCacheable()) {
if (! $cacheability->isCacheable()) {
return CacheStrategy::NO_CACHE;
}
@@ -111,11 +114,11 @@ final readonly class SmartTemplateAnalyzer implements TemplateAnalyzer
// 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, '{{')) {
if (strlen($match) > 200 && ! str_contains($match, '{{')) {
$fragments["static_block_{$i}"] = [
'type' => 'static',
'size' => strlen($match),
'cacheable' => true
'cacheable' => true,
];
}
}
@@ -159,7 +162,9 @@ final readonly class SmartTemplateAnalyzer implements TemplateAnalyzer
private function calculateStaticRatio(string $content): float
{
$totalLength = strlen($content);
if ($totalLength === 0) return 0.0;
if ($totalLength === 0) {
return 0.0;
}
// Entferne alle dynamischen Teile
$staticContent = preg_replace('/\{\{.*?\}\}/', '', $content);
@@ -167,5 +172,4 @@ final readonly class SmartTemplateAnalyzer implements TemplateAnalyzer
return strlen($staticContent) / $totalLength;
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching\Analysis;
class TemplateAnalysis
@@ -11,5 +13,6 @@ class TemplateAnalysis
public readonly array $dependencies,
public readonly CacheabilityScore $cacheability,
public readonly array $fragments
) {}
) {
}
}

View File

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

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching;
use App\Framework\Cache\Cache;
@@ -13,7 +15,8 @@ class CacheDiagnostics
private CacheManager $cacheManager,
private TemplateAnalyzer $analyzer,
private Cache $cache
) {}
) {
}
public function getPerformanceReport(): array
{
@@ -24,7 +27,7 @@ class CacheDiagnostics
'most_cached_templates' => $this->getMostCachedTemplates(),
'cache_strategy_distribution' => $this->getStrategyDistribution(),
'memory_usage' => $this->getMemoryUsage(),
'recommendations' => $this->generateRecommendations()
'recommendations' => $this->generateRecommendations(),
];
}
@@ -39,7 +42,7 @@ class CacheDiagnostics
'cacheability_score' => $analysis->cacheability->getScore(),
'dependencies' => $analysis->dependencies,
'potential_fragments' => $analysis->fragments,
'optimization_suggestions' => $this->getOptimizationSuggestions($analysis)
'optimization_suggestions' => $this->getOptimizationSuggestions($analysis),
];
}
@@ -55,7 +58,7 @@ class CacheDiagnostics
$templateContext = new TemplateContext($template, $context);
// Warmup durch Rendering
$this->cacheManager->render($templateContext, function() {
$this->cacheManager->render($templateContext, function () {
return "<!-- Warmup content for {$template} -->";
});
@@ -78,7 +81,7 @@ class CacheDiagnostics
'strategy_availability' => $this->testStrategies(),
'memory_usage' => $this->checkMemoryUsage(),
'disk_space' => $this->checkDiskSpace(),
'overall_status' => $this->determineOverallHealth()
'overall_status' => $this->determineOverallHealth(),
];
}
}

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\View\Caching\Analysis\CacheStrategy;
use App\Framework\View\Caching\Analysis\TemplateAnalysis;
use App\Framework\View\Caching\Analysis\TemplateAnalyzer;
@@ -11,12 +14,11 @@ 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(
@@ -37,24 +39,34 @@ class CacheManager
// 2. Passende Strategy auswählen
$strategy = $this->selectStrategy($analysis);
if (!$strategy->shouldCache($context)) {
return $renderer();
if (! $strategy->shouldCache($context)) {
$content = $renderer();
if (! is_string($content)) {
throw new \RuntimeException('Renderer must return a string, got: ' . get_debug_type($content));
}
return $content;
}
// 3. Cache-Key generieren
$cacheKey = $strategy->generateKey($context);
// 4. Cache-Lookup
$cached = $this->cache->get($cacheKey);
if ($cached->isHit) {
$result = $this->cache->get($cacheKey);
$cached = $result->getItem($cacheKey);
if ($cached->isHit && is_string($cached->value)) {
return $cached->value;
}
// 5. Rendern und cachen
$content = $renderer();
$ttl = $strategy->getTtl($context);
$this->cache->set($cacheKey, $content, $ttl);
if (! is_string($content)) {
throw new \RuntimeException('Renderer must return a string, got: ' . get_debug_type($content));
}
$ttl = $strategy->getTtl($context);
$this->cache->set(CacheItem::forSet($cacheKey, $content, \App\Framework\Core\ValueObjects\Duration::fromSeconds($ttl)));
return $content;
}
@@ -106,7 +118,7 @@ class CacheManager
};
}
private function invalidateByPattern(null $pattern): int
private function invalidateByPattern(string $pattern): int
{
// Vereinfachte Implementation - in Realität müsste das der Cache-Driver unterstützen
// Für jetzt: Cache komplett leeren bei Pattern-Match

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching;
use App\Framework\View\RenderContext;

View File

@@ -1,24 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching\Strategies;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\View\Caching\TemplateContext;
final readonly class ComponentCacheStrategy implements ViewCacheStrategy
{
public function __construct(private Cache $cache) {}
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
public function generateKey(TemplateContext $context): CacheKey
{
$component = basename($context->template);
$dataHash = md5(serialize($context->data));
return "component:{$component}:{$dataHash}";
return CacheKey::fromString("component:{$component}:{$dataHash}");
}
public function getTtl(TemplateContext $context): int

View File

@@ -1,23 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching\Strategies;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\View\Caching\TemplateContext;
class FragmentCacheStrategy implements ViewCacheStrategy
{
public function __construct(private Cache $cache) {}
public function __construct(private Cache $cache)
{
}
public function shouldCache(TemplateContext $context): bool
{
return isset($context->metadata['fragment_id']);
}
public function generateKey(TemplateContext $context): string
public function generateKey(TemplateContext $context): CacheKey
{
$fragment = $context->metadata['fragment_id'] ?? 'default';
return "fragment:{$context->template}:{$fragment}:" . md5(serialize($context->data));
return CacheKey::fromString("fragment:{$context->template}:{$fragment}:" . md5(serialize($context->data)));
}
public function getTtl(TemplateContext $context): int

View File

@@ -1,24 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching\Strategies;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\View\Caching\TemplateContext;
final readonly class FullPageCacheStrategy implements ViewCacheStrategy
{
public function __construct(private Cache $cache) {}
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);
return ! $this->hasUserData($context->data) &&
! $this->hasVolatileData($context->data);
}
public function generateKey(TemplateContext $context): string
public function generateKey(TemplateContext $context): CacheKey
{
return "page:{$context->template}:" . md5(serialize($this->getNonVolatileData($context->data)));
return CacheKey::fromString("page:{$context->template}:" . md5(serialize($this->getNonVolatileData($context->data))));
}
public function getTtl(TemplateContext $context): int
@@ -34,26 +39,36 @@ final readonly class FullPageCacheStrategy implements ViewCacheStrategy
private function hasUserData(array $data): bool
{
$userKeys = ['user', 'auth', 'session', 'current_user'];
return !empty(array_intersect(array_keys($data), $userKeys));
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));
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';
if (str_contains($template, 'layout')) {
return 'layout';
}
if (str_contains($template, 'static')) {
return 'static';
}
if (str_contains($template, 'page')) {
return 'content';
}
return 'default';
}

View File

@@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching\Strategies;
use App\Framework\Cache\CacheKey;
use App\Framework\View\Caching\TemplateContext;
final readonly class NoCacheStrategy implements ViewCacheStrategy
@@ -11,9 +14,9 @@ final readonly class NoCacheStrategy implements ViewCacheStrategy
return false;
}
public function generateKey(TemplateContext $context): string
public function generateKey(TemplateContext $context): CacheKey
{
return '';
return CacheKey::fromString('no-cache');
}
public function getTtl(TemplateContext $context): int

View File

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

View File

@@ -1,27 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
class TaggedFragmentCache implements FragmentCache
{
private array $tagMapping = [];
public function __construct(private Cache $cache) {}
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);
$result = $this->cache->get(CacheKey::fromString($fullKey));
$cached = $result->getItem(CacheKey::fromString($fullKey));
if ($cached->isHit) {
return $cached->value;
}
$content = $generator();
$this->cache->set($fullKey, $content, $ttl);
$this->cache->set(CacheItem::forSet(CacheKey::fromString($fullKey), $content, \App\Framework\Core\ValueObjects\Duration::fromSeconds($ttl)));
$this->tagFragment($fullKey, $tags);
return $content;
@@ -30,7 +37,8 @@ class TaggedFragmentCache implements FragmentCache
public function invalidateFragment(string $key): bool
{
$fullKey = "fragment:{$key}";
return $this->cache->forget($fullKey);
return $this->cache->forget(CacheKey::fromString($fullKey));
}
public function invalidateByTags(array $tags): int
@@ -39,24 +47,26 @@ class TaggedFragmentCache implements FragmentCache
foreach ($tags as $tag) {
$keys = $this->getKeysByTag($tag);
foreach ($keys as $key) {
if ($this->cache->forget($key)) {
if ($this->cache->forget(CacheKey::fromString($key))) {
$invalidated++;
}
}
}
return $invalidated;
}
public function hasFragment(string $key): bool
{
$fullKey = "fragment:{$key}";
return $this->cache->has($fullKey);
return $this->cache->has(CacheKey::fromString($fullKey));
}
private function tagFragment(string $key, array $tags): void
{
foreach ($tags as $tag) {
if (!isset($this->tagMapping[$tag])) {
if (! isset($this->tagMapping[$tag])) {
$this->tagMapping[$tag] = [];
}
$this->tagMapping[$tag][] = $key;

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Caching;
class TemplateContext
@@ -9,5 +11,6 @@ class TemplateContext
public readonly array $data,
public readonly ?string $controllerClass = null,
public readonly array $metadata = []
) {}
) {
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Filesystem\FileStorage;
@@ -8,8 +10,9 @@ final readonly class ComponentCache
{
public function __construct(
private string $cacheDir = __DIR__ . "/cache/components/",
private FileStorage $storage = new FileStorage,
) {}
private FileStorage $storage = new FileStorage('/'),
) {
}
public function get(string $componentName, array $data, string $templatePath): ?string
{

View File

@@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Core\PathProvider;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Meta\MetaData;
use App\Framework\View\Loading\TemplateLoader;
@@ -13,18 +14,19 @@ final readonly class ComponentRenderer
private TemplateProcessor $processor,
private ComponentCache $cache,
private TemplateLoader $loader,
private FileStorage $storage = new FileStorage,
) {}
private FileStorage $storage = new FileStorage('/'),
) {
}
public function render(string $componentName, array $data): string
{
$path = $this->loader->getComponentPath($componentName);
if(!$this->storage->exists($path)) {
if (! $this->storage->exists($path)) {
return "<!-- Komponente '$componentName' nicht gefunden -->";
}
if(!$cached = $this->cache->get($componentName, $data, $path)) {
if ($cached = $this->cache->get($componentName, $data, $path)) {
return $cached;
}
@@ -32,6 +34,11 @@ final readonly class ComponentRenderer
$context = new RenderContext(template: $componentName, metaData: new MetaData(''), data: $data);
$output = $this->processor->render($context, $template);
// Ensure we never return null
if ($output === null) {
$output = '';
}
$this->cache->set($componentName, $data, $path, $output);
return $output;

View File

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

View File

@@ -1,24 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use Dom\Element;
final readonly class DomFormService
{
public function addCsrfTokenToForms(DomWrapper $dom, string $token): void {
public function addCsrfTokenToForms(DomWrapper $dom, string $token): void
{
$forms = $dom->getElementsByTagName('form');
$forms->forEach(function($form) use ($dom, $token) {
$forms->forEach(function ($form) use ($dom, $token) {
$this->addCsrfTokenToForm($dom, $form, $token);
});
}
public function addCsrfTokenToForm(DomWrapper $dom, Element $form, string $token): void {
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;
}
@@ -30,7 +35,8 @@ final readonly class DomFormService
$form->insertBefore($csrf, $form->firstChild);
}
public function addHoneypotField(DomWrapper $dom, Element $form, string $fieldName = 'email_confirm'): void {
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);

View File

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

View File

@@ -1,10 +0,0 @@
<?php
namespace App\Framework\View;
use Dom\HTMLDocument;
interface DomProcessor
{
public function process(DomWrapper $dom, RenderContext $context): DomWrapper;
}

View File

@@ -1,49 +0,0 @@
<?php
namespace App\Framework\View;
use Dom\HTMLDocument;
use DOMDocument;
final class DomTemplateParser
{
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 = HTMLDocument::createFromString($html);
libxml_clear_errors();
return $dom;
}
/**
* Erstellt einen DomWrapper aus HTML-String
*/
public function parseToWrapper(string $html): DomWrapper
{
return DomWrapper::fromString($html);
}
/**
* Erstellt einen DomWrapper aus Datei
*/
public function parseFileToWrapper(string $path): DomWrapper
{
return DomWrapper::fromFile($path);
}
public function toHtml(HTMLDocument $dom): string
{
return $dom->saveHTML();
}
public function parseFile(string $path): HTMLDocument
{
return HTMLDocument::createFromFile($path);
}
}

View File

@@ -1,12 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use Dom\HTMLDocument;
use Dom\Element;
use Dom\HTMLElement;
use Dom\Node;
use Dom\DocumentFragment;
use Dom\Element;
use Dom\HTMLDocument;
use Dom\Node;
/**
* Wrapper-Klasse für HTMLDocument mit erweiterten Template-spezifischen Operationen
@@ -15,7 +16,8 @@ final readonly class DomWrapper
{
public function __construct(
public HTMLDocument $document
) {}
) {
}
/**
* Erstellt einen DomWrapper aus HTML-String
@@ -35,6 +37,7 @@ final readonly class DomWrapper
public static function fromFile(string $path): self
{
$dom = HTMLDocument::createFromFile($path);
return new self($dom);
}
@@ -56,6 +59,7 @@ final readonly class DomWrapper
public function getElementsByTagName(string $tagName): ElementCollection
{
$elements = iterator_to_array($this->document->getElementsByTagName($tagName));
return new ElementCollection($elements, $this);
}
@@ -74,12 +78,13 @@ final readonly class DomWrapper
{
$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) {
if (! $node || $node->nodeType !== XML_ELEMENT_NODE) {
return;
}
@@ -99,12 +104,13 @@ final readonly class DomWrapper
{
$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) {
if (! $node || $node->nodeType !== XML_ELEMENT_NODE) {
return;
}
@@ -136,6 +142,7 @@ final readonly class DomWrapper
{
$fragment = $this->document->createDocumentFragment();
$fragment->appendXML($html);
return $fragment;
}
@@ -154,12 +161,13 @@ final readonly class DomWrapper
public function removeEmptyTextNodes(): self
{
$this->removeEmptyTextNodesRecursive($this->document->documentElement);
return $this;
}
private function removeEmptyTextNodesRecursive(?Node $node): void
{
if (!$node) {
if (! $node) {
return;
}
@@ -185,7 +193,7 @@ final readonly class DomWrapper
$currentClass = $element->getAttribute('class');
$classes = $currentClass ? explode(' ', $currentClass) : [];
if (!in_array($className, $classes)) {
if (! in_array($className, $classes)) {
$classes[] = $className;
$element->setAttribute('class', implode(' ', $classes));
}
@@ -197,11 +205,11 @@ final readonly class DomWrapper
public function removeClass(Element $element, string $className): void
{
$currentClass = $element->getAttribute('class');
if (!$currentClass) {
if (! $currentClass) {
return;
}
$classes = array_filter(explode(' ', $currentClass), fn($c) => $c !== $className);
$classes = array_filter(explode(' ', $currentClass), fn ($c) => $c !== $className);
$element->setAttribute('class', implode(' ', $classes));
}
@@ -211,7 +219,7 @@ final readonly class DomWrapper
public function hasClass(Element $element, string $className): bool
{
$currentClass = $element->getAttribute('class');
if (!$currentClass) {
if (! $currentClass) {
return false;
}
@@ -225,12 +233,13 @@ final readonly class DomWrapper
{
$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) {
if (! $node || $node->nodeType !== XML_ELEMENT_NODE) {
return;
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use ArrayIterator;
@@ -15,7 +17,8 @@ final class ElementCollection implements IteratorAggregate, Countable
public function __construct(
private array $elements,
private DomWrapper $wrapper
) {}
) {
}
/**
* Führt eine Funktion für jedes Element aus
@@ -25,6 +28,7 @@ final class ElementCollection implements IteratorAggregate, Countable
foreach ($this->elements as $index => $element) {
$callback($element, $index);
}
return $this;
}
@@ -65,6 +69,7 @@ final class ElementCollection implements IteratorAggregate, Countable
return true;
}
}
return false;
}
@@ -74,10 +79,11 @@ final class ElementCollection implements IteratorAggregate, Countable
public function every(callable $callback): bool
{
foreach ($this->elements as $element) {
if (!$callback($element)) {
if (! $callback($element)) {
return false;
}
}
return true;
}
@@ -91,6 +97,7 @@ final class ElementCollection implements IteratorAggregate, Countable
return $element;
}
}
return null;
}
@@ -145,7 +152,7 @@ final class ElementCollection implements IteratorAggregate, Countable
*/
public function addClass(string $className): self
{
return $this->forEach(fn($el) => $this->wrapper->addClass($el, $className));
return $this->forEach(fn ($el) => $this->wrapper->addClass($el, $className));
}
/**
@@ -153,7 +160,7 @@ final class ElementCollection implements IteratorAggregate, Countable
*/
public function removeClass(string $className): self
{
return $this->forEach(fn($el) => $this->wrapper->removeClass($el, $className));
return $this->forEach(fn ($el) => $this->wrapper->removeClass($el, $className));
}
/**
@@ -161,7 +168,7 @@ final class ElementCollection implements IteratorAggregate, Countable
*/
public function setAttribute(string $name, string $value): self
{
return $this->forEach(fn($el) => $el->setAttribute($name, $value));
return $this->forEach(fn ($el) => $el->setAttribute($name, $value));
}
/**
@@ -169,7 +176,7 @@ final class ElementCollection implements IteratorAggregate, Countable
*/
public function removeAttribute(string $name): self
{
return $this->forEach(fn($el) => $el->removeAttribute($name));
return $this->forEach(fn ($el) => $el->removeAttribute($name));
}
/**
@@ -177,7 +184,7 @@ final class ElementCollection implements IteratorAggregate, Countable
*/
public function setInnerHTML(string $html): self
{
return $this->forEach(fn($el) => $el->innerHTML = $html);
return $this->forEach(fn ($el) => $el->innerHTML = $html);
}
/**
@@ -185,7 +192,7 @@ final class ElementCollection implements IteratorAggregate, Countable
*/
public function setTextContent(string $text): self
{
return $this->forEach(fn($el) => $el->textContent = $text);
return $this->forEach(fn ($el) => $el->textContent = $text);
}
/**
@@ -193,7 +200,7 @@ final class ElementCollection implements IteratorAggregate, Countable
*/
public function replaceWith(string $html): void
{
$this->forEach(fn($el) => $this->wrapper->replaceElementWithHtml($el, $html));
$this->forEach(fn ($el) => $this->wrapper->replaceElementWithHtml($el, $html));
}
/**
@@ -201,7 +208,7 @@ final class ElementCollection implements IteratorAggregate, Countable
*/
public function remove(): void
{
$this->forEach(fn($el) => $el->parentNode?->removeChild($el));
$this->forEach(fn ($el) => $el->parentNode?->removeChild($el));
}
/**
@@ -239,7 +246,7 @@ final class ElementCollection implements IteratorAggregate, Countable
*/
public function isNotEmpty(): bool
{
return !$this->isEmpty();
return ! $this->isEmpty();
}
/**
@@ -273,7 +280,7 @@ final class ElementCollection implements IteratorAggregate, Countable
{
return [
'count' => $this->count(),
'elements' => $this->elements
'elements' => $this->elements,
];
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
@@ -9,54 +10,55 @@ use App\Framework\Core\PathProvider;
use App\Framework\DI\Container;
use App\Framework\DI\DefaultContainer;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceService;
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 ?TemplateRenderer $smartCache;
private ?CacheManager $cacheManager;
public function __construct(
private TemplateLoader $loader,
private PathProvider $pathProvider,
private DomTemplateParser $parser = new DomTemplateParser,
private TemplateProcessor $processor = new TemplateProcessor,
private FileStorage $storage = new FileStorage,
private PerformanceService $performanceService,
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,
#private bool $useSmartCache = true,
#private bool $legacyCacheEnabled = false,
?Cache $cache = null,
private bool $cacheEnabled = true,
) {
// Stelle sicher, dass das Cache-Verzeichnis existiert
if (!is_dir($this->cachePath)) {
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);
/*
// 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;
}*/
$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) {
@@ -84,62 +86,51 @@ final readonly class Engine implements TemplateRenderer
metadata: $context->metaData ? ['meta' => $context->metaData] : []
);
return $this->cacheManager->render($templateContext, function() use ($context) {
return $this->cacheManager->render($templateContext, function () use ($context) {
return $this->renderDirect($context);
});
}
// Fallback ohne Cache
return $this->renderDirect($context);
// 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 ?? '');
}
// Fallback zu Legacy-Caching oder direkt rendern
return $this->legacyCacheEnabled
/*return $this->legacyCacheEnabled
? $this->renderWithLegacyCache($context)
: $this->renderDirect($context);
: $this->renderDirect($context);*/
}
private function renderWithLegacyCache(RenderContext $context): string
/*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)) {
// Optimized cache check - avoid expensive filemtime calls
if (file_exists($cacheFile)) {
$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);
}
return $this->processor->render($context, $content);
}
// Cache miss - render and cache
$content = $this->renderDirect($context);
$this->storage->put($cacheFile, $content);
return $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);
// Optimized single-pass rendering
return $this->performanceService->measure(
'template_render',
function () use ($context) {
// Load template content
$content = $this->loader->load($context->template, $context->controllerClass, $context);
// Direct processing without intermediate DOM parsing
return $this->processor->render($context, $content);
},
PerformanceCategory::VIEW,
['template' => $context->template]
);
}
public function invalidateCache(?string $template = null): int
@@ -149,7 +140,7 @@ final readonly class Engine implements TemplateRenderer
public function getCacheStats(): array
{
if (!$this->cacheManager) {
if (! $this->cacheManager) {
return ['cache_enabled' => false];
}
@@ -159,4 +150,8 @@ final readonly class Engine implements TemplateRenderer
];
}
public function renderPartial(RenderContext $context): string
{
return $this->renderDirect($context);
}
}

View File

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

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\DI\Container;
use App\Framework\Template\Parser\DomTemplateParser;
/**
* Erweiterte Version des TemplateProcessors mit DomWrapper-Unterstützung
@@ -13,7 +16,8 @@ final readonly class EnhancedTemplateProcessor
private array $domProcessors,
private array $stringProcessors,
private Container $container
) {}
) {
}
public function render(RenderContext $context, string $html, bool $component = false): string
{

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Exception;
use App\Framework\Exception\FrameworkException;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\View\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class TemplateNotFoundException extends FrameworkException
@@ -16,12 +17,16 @@ final class TemplateNotFoundException extends FrameworkException
) {
parent::__construct(
message: "Template \"$template\" nicht gefunden ($file).",
context: new ExceptionContext(
operation: 'template_render',
component: 'view_engine',
data: [
'template' => $template,
'file' => $file,
]
),
code: $code,
previous: $previous,
context: [
'template' => $template,
'file' => $file
]
previous: $previous
);
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Functions;
use App\Domain\Media\ImageSlotRepository;
@@ -13,7 +15,7 @@ final readonly class ImageSlotFunction implements TemplateFunction
public function __construct(
private ImageSlotRepository $imageSlotRepository,
private ComponentRenderer $componentRenderer
){
) {
$this->functionName = 'imageslot';
}
@@ -27,7 +29,7 @@ final readonly class ImageSlotFunction implements TemplateFunction
$data = [
'image' => $image->filename,
'alt' => $image->altText
'alt' => $image->altText,
];
return $this->componentRenderer->render('imageslot', $data);

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Functions;
interface TemplateFunction

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Functions;
use App\Framework\Router\UrlGenerator;
@@ -8,7 +10,7 @@ final readonly class UrlFunction implements TemplateFunction
{
public function __construct(
private UrlGenerator $urlGenerator
){
) {
$this->functionName = 'url';
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading\Resolvers;
use App\Framework\Core\PathProvider;
final readonly class ComponentResolver implements TemplateResolverStrategy
{
public function __construct(
private PathProvider $pathProvider,
private string $templatePath = '/src/Framework/View/templates'
) {
}
public function resolve(string $template, ?string $controllerClass = null): ?string
{
// Versuche .html Extension für Components
$htmlPath = $this->pathProvider->resolvePath(
$this->templatePath . '/components/' . $template . '.html'
);
if (file_exists($htmlPath)) {
return $htmlPath;
}
// Fallback: .view.php Extension
$phpPath = $this->pathProvider->resolvePath(
$this->templatePath . '/components/' . $template . '.view.php'
);
return file_exists($phpPath) ? $phpPath : null;
}
}

View File

@@ -1,22 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading\Resolvers;
final readonly class ControllerResolver implements TemplateResolverStrategy
{
const string TEMPLATE_EXTENSION = '.view.php';
public const string TEMPLATE_EXTENSION = '.view.php';
public function resolve(string $template, ?string $controllerClass = null): ?string
{
if(!$controllerClass) {
if (! $controllerClass) {
return null;
}
try {
$rc = new \ReflectionClass($controllerClass);
$dir = dirname($rc->getFileName());
// First try direct controller directory
$path = $dir . DIRECTORY_SEPARATOR . $template . self::TEMPLATE_EXTENSION;
return file_exists($path) ? $path : null;
if (file_exists($path)) {
return $path;
}
// Then try templates subdirectory
$templatesPath = $dir . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . $template . self::TEMPLATE_EXTENSION;
if (file_exists($templatesPath)) {
return $templatesPath;
}
return null;
} catch (\ReflectionException) {
return null;
}

View File

@@ -1,17 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading\Resolvers;
use App\Framework\Core\PathProvider;
final readonly class DefaultPathResolver implements TemplateResolverStrategy
{
const string TEMPLATE_EXTENSION = '.view.php';
public 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
{

View File

@@ -1,17 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading\Resolvers;
use App\Framework\Discovery\Results\DiscoveryResults;
use App\Framework\View\TemplateDiscoveryVisitor;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Filesystem\FilePath;
use App\Framework\Filesystem\FileSystemService;
final readonly class DiscoveryResolver implements TemplateResolverStrategy
{
const string TEMPLATE_EXTENSION = '.view.php';
public const string TEMPLATE_EXTENSION = '.view.php';
public function __construct(
private DiscoveryResults|null $discoverySource = null
) {}
private FileSystemService $fileSystem,
private DiscoveryRegistry|null $discoverySource = null,
) {
}
public function resolve(string $template, ?string $controllerClass = null): ?string
{
@@ -19,83 +24,47 @@ final readonly class DiscoveryResolver implements TemplateResolverStrategy
return null;
}
return $this->resolveFromDiscoveryResults($template, $controllerClass);
return $this->resolveFromDiscoveryRegistry($template, $controllerClass);
}
private function resolveFromDiscoveryResults(string $template, ?string $controllerClass): ?string
private function resolveFromDiscoveryRegistry(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);
$templateMapping = $this->discoverySource->templates->get($template);
if ($templateMapping) {
return $this->validateTemplatePath($templateMapping->path);
}
// Strategie 2: Template mit Extension
$templateWithExt = $template . self::TEMPLATE_EXTENSION;
if (isset($templates[$templateWithExt])) {
return $this->selectBestTemplate($templates[$templateWithExt], $controllerClass);
$templateMapping = $this->discoverySource->templates->get($templateWithExt);
if ($templateMapping) {
return $this->validateTemplatePath($templateMapping->path);
}
// Strategie 3: Suche in allen Templates
foreach ($templates as $templateName => $templateData) {
if ($this->templateMatches($templateName, $template)) {
return $this->selectBestTemplate($templateData, $controllerClass);
foreach ($this->discoverySource->templates->getAll() as $templateMapping) {
if ($this->templateMatches($templateMapping->name, $template)) {
return $this->validateTemplatePath($templateMapping->path);
}
}
return null;
}
private function resolveFromTemplateVisitor(string $template, ?string $controllerClass): ?string
private function validateTemplatePath(FilePath $path): ?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;
return $this->fileSystem->fileExists($path) ? $path->toString() : null;
#return file_exists($path) ? $path : null;
}
private function templateMatches(string $templateName, string $searchTemplate): bool
{
// Entferne Extension für Vergleich
// Entferne Extension für Vergleich (.view.php oder nur .view)
$cleanTemplateName = str_replace(self::TEMPLATE_EXTENSION, '', $templateName);
$cleanTemplateName = str_replace('.view', '', $cleanTemplateName);
return $cleanTemplateName === $searchTemplate ||
basename($cleanTemplateName) === $searchTemplate ||
str_contains($cleanTemplateName, $searchTemplate);
basename($cleanTemplateName) === $searchTemplate;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading\Resolvers;
use App\Framework\Core\PathProvider;
final readonly class LayoutResolver implements TemplateResolverStrategy
{
public 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 . '/layouts/' . $template . self::TEMPLATE_EXTENSION
);
return file_exists($path) ? $path : null;
}
}

View File

@@ -1,13 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading\Resolvers;
final readonly class TemplateMapResolver implements TemplateResolverStrategy
{
const string TEMPLATE_EXTENSION = '.view.php';
public const string TEMPLATE_EXTENSION = '.view.php';
public function __construct(
private array $templates = []
){}
) {
}
public function resolve(string $template, ?string $controllerClass = null): ?string
{

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading\Resolvers;
interface TemplateResolverStrategy

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading;
final class TemplateCache
@@ -8,11 +10,12 @@ final class TemplateCache
public function __construct(
private readonly bool $enabled = true
) {}
) {
}
public function get(string $key): ?string
{
if (!$this->enabled) {
if (! $this->enabled) {
return null;
}
@@ -21,7 +24,7 @@ final class TemplateCache
public function set(string $key, string $content): void
{
if (!$this->enabled) {
if (! $this->enabled) {
return;
}

View File

@@ -1,12 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading;
final readonly class TemplateContentLoader
{
public function load(string $path): string
{
if (!file_exists($path)) {
if (! file_exists($path)) {
throw new \RuntimeException("Template file not found: {$path}");
}

View File

@@ -1,35 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\PathProvider;
use App\Framework\Discovery\Results\DiscoveryResults;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Filesystem\FileSystemService;
use App\Framework\View\Loading\Resolvers\ComponentResolver;
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\Loading\Resolvers\TemplateMapResolver;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateDiscoveryVisitor;
final class TemplateLoader
final readonly class TemplateLoader
{
private readonly TemplatePathResolver $pathResolver;
private readonly TemplateContentLoader $contentLoader;
private TemplatePathResolver $pathResolver;
private 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,
private PathProvider $pathProvider,
private Cache $cache,
private ?DiscoveryRegistry $discoveryRegistry = null,
private array $templates = [],
private string $templatePath = '/src/Framework/View/templates',
private bool $useMtimeInvalidation = true,
) {
$this->pathResolver = $this->createPathResolver();
$this->contentLoader = new TemplateContentLoader();
@@ -43,7 +46,7 @@ final class TemplateLoader
new TemplateMapResolver($this->templates),
// 2. Discovery-basierte Templates (falls verfügbar)
new DiscoveryResolver($this->discoveryResults),
new DiscoveryResolver(new FileSystemService(), $this->discoveryRegistry),
// 3. Layout-spezifische Suche (für "main" etc.)
new LayoutResolver($this->pathProvider, $this->templatePath),
@@ -63,22 +66,31 @@ final class TemplateLoader
public function load(string $template, ?string $controllerClass = null, ?RenderContext $context = null): string
{
$cacheKey = 'template_content|' . $template . '|' . ($controllerClass ?? 'default');
$resolvedPath = null;
$mtimeSegment = '';
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;
if ($this->useMtimeInvalidation) {
$resolvedPath = $this->pathResolver->resolve($template, $controllerClass);
$mtime = is_string($resolvedPath) && is_file($resolvedPath) ? (int) @filemtime($resolvedPath) : 0;
$mtimeSegment = $mtime > 0 ? (string) $mtime : '';
}
$path = $this->pathResolver->resolve($template, $controllerClass);
$cacheKey = $this->generateCacheKey($template, $controllerClass, $mtimeSegment);
// Try cache first
$cached = $this->cache->get($cacheKey)->getItem($cacheKey);
if ($cached->isHit && is_string($cached->value)) {
return $cached->value;
}
// Load from filesystem
$path = $resolvedPath ?? $this->pathResolver->resolve($template, $controllerClass);
$content = $this->contentLoader->load($path);
$this->cache->set($cacheKey, $content);
// Cache for future use (24 hours TTL)
$ttl = Duration::fromSeconds(86400);
$this->cache->set(CacheItem::forSet($cacheKey, $content, $ttl));
return $content;
}
@@ -94,29 +106,31 @@ final class TemplateLoader
public function getComponentPath(string $name): string
{
return __DIR__ . "/../templates/components/{$name}.html";
return $this->pathProvider->resolvePath($this->templatePath . "/components/{$name}.html");
}
public function debugTemplatePath(string $template, ?string $controllerClass = null): array
{
$debug = ['template' => $template, 'attempts' => []];
$resolvers = [
/*$resolvers = [
'TemplateMap' => new TemplateMapResolver($this->templates),
'Discovery' => new DiscoveryResolver($this->discoveryResults),
'Discovery' => new DiscoveryResolver(new FileSystemService(), $this->discoveryRegistry),
'Layout' => new LayoutResolver($this->pathProvider, $this->templatePath),
'Component' => new ComponentResolver($this->pathProvider, $this->templatePath),
'Controller' => new ControllerResolver(),
'Default' => new DefaultPathResolver($this->pathProvider, $this->templatePath),
];
];*/
$resolvers = $this->createPathResolver();
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)
'exists' => $path && is_file($path),
'success' => $path && is_file($path),
];
} catch (\Exception $e) {
$debug['attempts'][$name] = ['error' => $e->getMessage()];
@@ -125,4 +139,17 @@ final class TemplateLoader
return $debug;
}
private function generateCacheKey(string $template, ?string $controllerClass, string $mtimeSegment = ''): CacheKey
{
$parts = ['template', 'v1', $template];
if ($controllerClass) {
$parts[] = $controllerClass;
}
if ($mtimeSegment !== '') {
$parts[] = $mtimeSegment;
}
return CacheKey::fromString(implode(':', $parts));
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Loading;
use App\Framework\View\Exceptions\TemplateNotFoundException;
@@ -8,12 +10,13 @@ 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)) {
foreach ($this->resolvers as $resolver) {
if ($path = $resolver->resolve($template, $controllerClass)) {
return $path;
}
}

View File

@@ -1,31 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processing;
use App\Framework\View\DOMTemplateParser;
use App\Framework\Template\Parser\DomTemplateParser;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\Template\Processing\ProcessorResolver;
use App\Framework\View\DomWrapper;
use App\Framework\View\ProcessorResolver;
use App\Framework\View\RenderContext;
final class DomProcessingPipeline
final readonly class DomProcessingPipeline
{
/** @param class-string[] $processors */
public function __construct(
private readonly array $processors,
private array $processors,
private ProcessorResolver $resolver,
)
{
) {
}
public function process(RenderContext $context, string $html): DomWrapper
{
$parser = new DOMTemplateParser();
$parser = new DomTemplateParser();
$dom = $parser->parseToWrapper($html);
foreach($this->processors as $processorClass) {
foreach ($this->processors as $processorClass) {
/** @var DomProcessor $processor */
$processor = $this->resolver->resolve($processorClass);
$dom = $processor->process($dom, $context);
}
return $dom;
}
}

View File

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

View File

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

View File

@@ -1,14 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Core\PathProvider;
use App\Framework\Template\Processing\DomProcessor;
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
@@ -19,15 +18,15 @@ final class AssetInjector implements DomProcessor
PathProvider $pathProvider,
private DomHeadService $headService,
string $manifestPath = '',
)
{
) {
$manifestPath = $pathProvider->resolvePath('/public/.vite/manifest.json');;
$manifestPath = $pathProvider->resolvePath('/public/.vite/manifest.json');
;
#$manifestPath = dirname(__DIR__, 3) . '../public/.vite/manifest.json';
if (!is_file($manifestPath)) {
if (! is_file($manifestPath)) {
throw new \RuntimeException("Vite manifest not found: $manifestPath");
}
$json = file_get_contents($manifestPath);
@@ -47,7 +46,7 @@ final class AssetInjector implements DomProcessor
$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'])) {
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, '/'));

View File

@@ -1,19 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\Template\Processing\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(DomWrapper $dom, RenderContext $context): DomWrapper
{
$this->removeComments($dom->document);
return $dom;
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Template\Processing\DomProcessor;
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;
@@ -17,7 +19,8 @@ final readonly class ComponentProcessor implements DomProcessor
public function __construct(
private DomComponentService $componentService,
private ComponentRenderer $renderer
) {}
) {
}
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
@@ -27,7 +30,9 @@ final readonly class ComponentProcessor implements DomProcessor
/** @var HTMLElement $component */
$name = $component->getAttribute('name');
if(!$name) return;
if (! $name) {
return;
}
$attributes = $this->extractAttributes($component);
@@ -49,6 +54,7 @@ final readonly class ComponentProcessor implements DomProcessor
$attributes[$attr->nodeName] = $attr->nodeValue;
}
}
return $attributes;
}
}

View File

@@ -1,59 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Http\Session\Session;
use App\Framework\DI\Container;
use App\Framework\Http\Session\FormDataStorage;
use App\Framework\Http\Session\FormIdGenerator;
use App\Framework\Http\Session\SessionInterface;
use App\Framework\Http\Session\ValidationErrorBag;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
readonly class CsrfReplaceProcessor implements StringProcessor
{
public function __construct(
private Session $session,
){}
private Container $container,
private FormIdGenerator $formIdGenerator,
) {
}
public function process(string $html, RenderContext $context): string
{
if(!str_contains($html, '___TOKEN___')){
// No token to replace
return $html;
// Check if we have any form-related placeholders
$hasToken = str_contains($html, '___TOKEN___');
$hasOldInput = str_contains($html, '___OLD_INPUT_');
$hasError = str_contains($html, '___ERROR_');
if (! $hasToken && ! $hasOldInput && ! $hasError) {
return $html;
}
/* OLD CODE - DEACTIVATED
error_log("CsrfReplaceProcessor: process() called");
$formIds = $this->extractFormIdsFromHtml($html);
// Check if we have any form-related placeholders
$hasToken = str_contains($html, '___TOKEN___');
$hasOldInput = str_contains($html, '___OLD_INPUT_');
$hasError = str_contains($html, '___ERROR_');
error_log("CsrfReplaceProcessor: Placeholders - Token: " . ($hasToken ? 'YES' : 'NO') .
", OldInput: " . ($hasOldInput ? 'YES' : 'NO') .
", Error: " . ($hasError ? 'YES' : 'NO'));
if (!$hasToken && !$hasOldInput && !$hasError) {
error_log("CsrfReplaceProcessor: No placeholders found, skipping");
return $html;
}
*/
// Get session from container (available after SessionMiddleware)
if (! $this->container->has(SessionInterface::class)) {
return $html;
}
$session = $this->container->get(SessionInterface::class);
$formIds = $this->formIdGenerator->extractFormIdsFromHtml($html);
if (empty($formIds)) {
return $html;
}
// Für jede gefundene Form-ID das entsprechende Token generieren
// Process each form
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);
// 1. Replace CSRF tokens
if (str_contains($html, '___TOKEN___')) {
$token = $session->csrf->generateToken($formId);
$html = $this->replaceTokenForFormId($html, $formId, $token->toString());
}
// 2. Replace old input values
$html = $this->replaceOldInputForForm($html, $formId, $session);
// 3. Replace error messages
$html = $this->replaceErrorsForForm($html, $formId, $session);
}
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
@@ -62,10 +86,154 @@ readonly class CsrfReplaceProcessor implements StringProcessor
// 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 preg_replace_callback($pattern, function ($matches) use ($token) {
return str_replace('___TOKEN___', $token, $matches[0]);
}, $html);
}
private function replaceOldInputForForm(string $html, string $formId, SessionInterface $session): string
{
$formDataStorage = new FormDataStorage($session);
if (! $formDataStorage->has($formId)) {
// No old data, remove placeholders
return $this->cleanupOldInputPlaceholders($html);
}
$oldData = $formDataStorage->get($formId);
foreach ($oldData as $fieldName => $value) {
// Text inputs and textareas
$placeholder = "___OLD_INPUT_{$fieldName}___";
if (str_contains($html, $placeholder)) {
$escapedValue = htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
$html = str_replace($placeholder, $escapedValue, $html);
}
// Select options
if (is_scalar($value)) {
$selectPattern = "___OLD_SELECT_{$fieldName}_{$value}___";
$html = str_replace(
"data-selected-if=\"{$selectPattern}\"",
'selected="selected"',
$html
);
}
// Radio buttons
if (is_scalar($value)) {
$radioPattern = "___OLD_RADIO_{$fieldName}_{$value}___";
$html = str_replace(
"data-checked-if=\"{$radioPattern}\"",
'checked="checked"',
$html
);
}
// Checkboxes
if (is_array($value)) {
foreach ($value as $checkboxValue) {
$checkboxPattern = "___OLD_CHECKBOX_{$fieldName}_{$checkboxValue}___";
$html = str_replace(
"data-checked-if=\"{$checkboxPattern}\"",
'checked="checked"',
$html
);
}
} elseif ($value) {
$checkboxValue = is_bool($value) ? '1' : (string) $value;
$checkboxPattern = "___OLD_CHECKBOX_{$fieldName}_{$checkboxValue}___";
$html = str_replace(
"data-checked-if=\"{$checkboxPattern}\"",
'checked="checked"',
$html
);
}
}
return $this->cleanupOldInputPlaceholders($html);
}
private function replaceErrorsForForm(string $html, string $formId, SessionInterface $session): string
{
$errorBag = new ValidationErrorBag($session);
if (! $errorBag->has($formId)) {
// No errors, remove error placeholders
return $this->cleanupErrorPlaceholders($html);
}
$errors = $errorBag->get($formId);
foreach ($errors as $fieldName => $fieldErrors) {
$placeholder = "___ERROR_{$fieldName}___";
if (! str_contains($html, $placeholder)) {
continue;
}
if (empty($fieldErrors)) {
// Remove error display
$html = $this->removeErrorDisplay($html, $fieldName);
continue;
}
// Build error HTML
$errorHtml = '';
foreach ($fieldErrors as $error) {
$escapedError = htmlspecialchars($error, ENT_QUOTES, 'UTF-8');
$errorHtml .= "<span class=\"error-message\">{$escapedError}</span>";
}
// Replace placeholder with actual errors
$html = str_replace($placeholder, $errorHtml, $html);
// Add error class to the field
$html = $this->addErrorClassToField($html, $fieldName);
}
return $this->cleanupErrorPlaceholders($html);
}
private function cleanupOldInputPlaceholders(string $html): string
{
// Remove remaining old input placeholders
$html = preg_replace('/___OLD_INPUT_[^_]+___/', '', $html);
// Remove remaining data attributes
$html = preg_replace('/\s*data-selected-if="[^"]*"/', '', $html);
$html = preg_replace('/\s*data-checked-if="[^"]*"/', '', $html);
return $html;
}
private function cleanupErrorPlaceholders(string $html): string
{
// Remove empty error displays
$html = preg_replace('/<div[^>]*class="[^"]*error-display[^"]*"[^>]*>___ERROR_[^_]+___<\/div>/', '', $html);
$html = preg_replace('/___ERROR_[^_]+___/', '', $html);
return $html;
}
private function removeErrorDisplay(string $html, string $fieldName): string
{
$pattern = '/<div[^>]*class="[^"]*error-display[^"]*"[^>]*data-field="' . preg_quote($fieldName, '/') . '"[^>]*>.*?<\/div>/s';
return preg_replace($pattern, '', $html);
}
private function addErrorClassToField(string $html, string $fieldName): string
{
// Add 'error' class to field - handle existing class attribute
$pattern = '/(<(?:input|select|textarea)[^>]*name="' . preg_quote($fieldName, '/') . '"[^>]*class="[^"]*)(")([^>]*>)/';
$html = preg_replace($pattern, '$1 error$2$3', $html);
// Handle fields without existing class attribute
$pattern = '/(<(?:input|select|textarea)[^>]*name="' . preg_quote($fieldName, '/') . '"[^>]*?)(?:\s|>)/';
$html = preg_replace($pattern, '$1 class="error" ', $html);
return $html;
}
}

View File

@@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\RenderContext;
/**
* CSRF-Token-Processor für Cache-sichere CSRF-Token-Behandlung
*
* Dieser Processor löst das Problem mit gecachten CSRF-Tokens:
* 1. Beim Caching: Ersetzt CSRF-Tokens mit Platzhaltern
* 2. Beim Rendern: Ersetzt Platzhalter mit frischen Tokens
*/
final class CsrfTokenProcessor implements StringProcessor
{
private const string CSRF_PLACEHOLDER = '__CSRF_TOKEN_PLACEHOLDER__';
private const string META_PLACEHOLDER = '__CSRF_META_PLACEHOLDER__';
private const string FIELD_PLACEHOLDER = '__CSRF_FIELD_PLACEHOLDER__';
public function __construct(
private readonly bool $cacheMode = false,
private readonly bool $debugMode = false
) {
}
public function process(string $html, RenderContext $context): string
{
return $html;
if (! $this->shouldProcessCsrf($context)) {
return $html;
}
if ($this->cacheMode) {
// Cache-Modus: Ersetze echte Tokens mit Platzhaltern
return $this->replaceTokensWithPlaceholders($html, $context);
} else {
// Render-Modus: Ersetze Platzhalter mit echten Tokens
return $this->replacePlaceholdersWithTokens($html, $context);
}
}
/**
* Prüft ob das Template CSRF-Token-Processing benötigt
*/
private function shouldProcessCsrf(RenderContext $context): bool
{
// Check 1: Data enthält CSRF-Token
if (isset($context->data['csrf_token']) || isset($context->data['_token'])) {
return true;
}
// Check 2: Template-Name deutet auf Formulare hin
$formKeywords = ['form', 'contact', 'login', 'register', 'checkout', 'admin'];
foreach ($formKeywords as $keyword) {
if (str_contains($context->template, $keyword)) {
return true;
}
}
return false;
}
/**
* Cache-Modus: Ersetzt echte CSRF-Tokens mit Platzhaltern
*/
private function replaceTokensWithPlaceholders(string $html, RenderContext $context): string
{
$this->debugLog("Converting CSRF tokens to placeholders for caching");
// 1. Ersetze Template-Syntax CSRF-Aufrufe
$html = $this->replaceTemplateCsrfCalls($html);
// 2. Ersetze echte Token-Werte falls vorhanden
if (isset($context->data['csrf_token'])) {
$html = str_replace($context->data['csrf_token'], self::CSRF_PLACEHOLDER, $html);
}
if (isset($context->data['_token'])) {
$html = str_replace($context->data['_token'], self::CSRF_PLACEHOLDER, $html);
}
// 3. Ersetze HTML-Pattern
$html = $this->replaceHtmlCsrfPatterns($html);
return $html;
}
/**
* Render-Modus: Ersetzt Platzhalter mit echten CSRF-Tokens
*/
private function replacePlaceholdersWithTokens(string $html, RenderContext $context): string
{
$this->debugLog("Converting placeholders to fresh CSRF tokens");
$csrfToken = $this->getCsrfToken($context);
// Ersetze alle Platzhalter-Types
$replacements = [
self::CSRF_PLACEHOLDER => $csrfToken,
self::META_PLACEHOLDER => $csrfToken,
self::FIELD_PLACEHOLDER => $this->generateCsrfField($csrfToken),
];
return str_replace(array_keys($replacements), array_values($replacements), $html);
}
/**
* Ersetzt Template-Syntax CSRF-Aufrufe mit Platzhaltern
*/
private function replaceTemplateCsrfCalls(string $html): string
{
$patterns = [
// {{ csrf_token() }} oder {{ csrf_token }}
'/\{\{\s*csrf_token\(\s*\)\s*\}\}/' => self::CSRF_PLACEHOLDER,
'/\{\{\s*csrf_token\s*\}\}/' => self::CSRF_PLACEHOLDER,
// {!! csrf_token() !!}
'/\{!!\s*csrf_token\(\s*\)\s*!!\}/' => self::CSRF_PLACEHOLDER,
// {{ _token }}
'/\{\{\s*_token\s*\}\}/' => self::CSRF_PLACEHOLDER,
// @csrf Blade-Direktive
'/@csrf/' => self::FIELD_PLACEHOLDER,
// csrf_field() Helper
'/\{\{\s*csrf_field\(\s*\)\s*\}\}/' => self::FIELD_PLACEHOLDER,
'{!! csrf_field() !!}' => self::FIELD_PLACEHOLDER,
];
return preg_replace(array_keys($patterns), array_values($patterns), $html);
}
/**
* Ersetzt HTML-CSRF-Patterns mit Platzhaltern
*/
private function replaceHtmlCsrfPatterns(string $html): string
{
// Hidden Input Fields mit CSRF-Token
$html = preg_replace(
'/<input[^>]*name=["\']_token["\'][^>]*value=["\']([^"\']+)["\'][^>]*>/',
'<input type="hidden" name="_token" value="' . self::CSRF_PLACEHOLDER . '">',
$html
);
// Meta Tags mit CSRF-Token
$html = preg_replace(
'/<meta[^>]*name=["\']csrf-token["\'][^>]*content=["\']([^"\']+)["\'][^>]*>/',
'<meta name="csrf-token" content="' . self::META_PLACEHOLDER . '">',
$html
);
return $html;
}
/**
* Holt den aktuellen CSRF-Token
*/
private function getCsrfToken(RenderContext $context): string
{
// Priorität: Context-Daten > Generierter Token
return $context->data['csrf_token'] ??
$context->data['_token'] ??
$this->generateFreshCsrfToken();
}
/**
* Generiert einen neuen CSRF-Token
*/
private function generateFreshCsrfToken(): string
{
// Integration mit deinem CSRF-System
// Das könnte auch ein Service-Call sein
return bin2hex(random_bytes(32));
}
/**
* Generiert ein komplettes CSRF-Hidden-Field
*/
private function generateCsrfField(string $token): string
{
return '<input type="hidden" name="_token" value="' . $token . '">';
}
/**
* Debug-Logging
*/
private function debugLog(string $message): void
{
// Debug logging removed for production
// Use proper logger in production environment
}
/**
* Factory-Methode für Cache-Modus
*/
public static function forCaching(bool $debugMode = false): self
{
return new self(cacheMode: true, debugMode: $debugMode);
}
/**
* Factory-Methode für Render-Modus
*/
public static function forRendering(bool $debugMode = false): self
{
return new self(cacheMode: false, debugMode: $debugMode);
}
}

View File

@@ -1,15 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
# Formatiert Datumsfelder etwa mit einem Tag `<date value="iso_date" format="d.m.Y" />`.
final readonly class DateFormatProcessor implements StringProcessor
{
/**
* @inheritDoc
*/
@@ -19,7 +20,7 @@ final readonly class DateFormatProcessor implements StringProcessor
$key = $matches[1];
$format = $matches[2];
if (!isset($context->data[$key]) || !($context->data[$key] instanceof \DateTimeInterface)) {
if (! isset($context->data[$key]) || ! ($context->data[$key] instanceof \DateTimeInterface)) {
return $matches[0]; // Unverändert lassen
}

View File

@@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
final readonly class EscapeProcessor implements StringProcessor
{
@@ -17,6 +19,7 @@ final readonly class EscapeProcessor implements StringProcessor
// Dann alle übrigen Variablen escapen
$html = preg_replace_callback('/{{\s*(\w+)\s*}}/', function ($matches) use ($context) {
$value = $context->data[$matches[1]] ?? '';
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_HTML5);
}, $html);

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\View\Processors\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class ViteManifestNotFoundException extends FrameworkException
@@ -15,9 +16,10 @@ final class ViteManifestNotFoundException extends FrameworkException
) {
parent::__construct(
message: "Vite manifest not found: $manifestPath",
context: ExceptionContext::forOperation('vite_manifest_load', 'View')
->withData(['manifestPath' => $manifestPath]),
code: $code,
previous: $previous,
context: ['manifestPath' => $manifestPath]
previous: $previous
);
}
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\DI\Container;
use App\Framework\Meta\MetaData;
use App\Framework\View\DomProcessor;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateProcessor;
@@ -14,7 +16,6 @@ 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) {
@@ -35,9 +36,12 @@ final class ForProcessor implements DomProcessor
$output = '';
$items = $context->data[$in] ?? null;
if (isset($context->model->{$in}) && $items === null) {
$items = $context->model->{$in};
// Handle nested property paths (e.g., "redis.key_sample")
$items = $this->resolveValue($context->data, $in);
// Fallback to model if not found in data
if ($items === null && isset($context->model)) {
$items = $this->resolveValue(['model' => $context->model], 'model.' . $in);
}
if (is_iterable($items)) {
@@ -69,4 +73,25 @@ final class ForProcessor implements DomProcessor
return $dom;
}
/**
* Resolves nested property paths like "redis.key_sample"
*/
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)) {
$value = $value[$key];
} elseif (is_object($value) && isset($value->$key)) {
$value = $value->$key;
} else {
return null;
}
}
return $value;
}
}

View File

@@ -1,82 +1,205 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\FormIdGenerator;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomFormService;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLElement;
use Dom\Element;
final readonly class FormProcessor implements DomProcessor
{
public function __construct(
private DomFormService $formService,
#private Session $session,
) {}
private FormIdGenerator $formIdGenerator,
) {
}
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
$forms = $dom->document->querySelectorAll('form');
#error_log("FormProcessor: Found " . count($forms) . " forms to process");
/** @var HTMLElement $form */
foreach($forms as $form) {
// Debug: Track when FormProcessor runs and what it processes
$html = $dom->document->saveHTML();
$containsForm = str_contains($html, '<form');
$formElements = $dom->document->getElementsByTagName('form');
$formId = $this->generateFormId($form, $context);
/*error_log(sprintf(
"FormProcessor[%s]: template=%s, forms=%d, hasFormTag=%s, length=%d",
$context->controllerClass ?? 'unknown',
$context->template,
$formElements->length,
$containsForm ? 'YES' : 'NO',
strlen($html)
));
$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));
// Show a snippet if it's likely an error template
if (!$containsForm && strlen($html) < 1000) {
error_log("FormProcessor: HTML snippet (no forms): " . substr($html, 0, 200));
}
// Form ID
$formIdField = $dom->document->createElement('input');
$formIdField->setAttribute('name', '_form_id');
$formIdField->setAttribute('type', 'hidden');
$formIdField->setAttribute('value', $formId);
// Show context if we unexpectedly find no forms
if (!$containsForm && str_contains(strtolower($context->template), 'contact')) {
error_log("FormProcessor: WARNING - Contact template has no forms! HTML: " . substr($html, 0, 300));
}*/
$form->insertBefore($csrf, $form->firstChild);
$form->insertBefore($formIdField, $form->firstChild);;
foreach ($forms as $form) {
$formId = $this->formIdGenerator->generateFromRenderContext($form, $context);
// 1. Add CSRF token placeholder
$this->addCsrfTokenPlaceholder($dom, $form);
// 2. Add form ID
$this->addFormId($dom, $form, $formId);
// 3. Add old input placeholders to form fields
$this->addOldInputPlaceholders($form);
// 4. Add error display placeholders
$this->addErrorDisplayPlaceholders($dom, $form);
}
return $dom;
}
private function generateFormId(HTMLElement $form, RenderContext $context): string
private function addCsrfTokenPlaceholder(DomWrapper $dom, Element $form): void
{
// 1. Vorhandene ID verwenden, falls gesetzt
if ($existingId = $form->getAttribute('id')) {
return $existingId;
// Check if CSRF token already exists
$existing = $form->querySelector('input[name="_token"]');
if ($existing) {
$existing->setAttribute('value', '___TOKEN___');
return;
}
// 2. Hash aus charakteristischen Eigenschaften erstellen
$hashComponents = [
$context->route ?? 'unknown_route',
$form->getAttribute('action') ?? '',
$form->getAttribute('method') ?? 'post',
$this->getFormStructureHash($form)
];
$csrf = $dom->document->createElement('input');
$csrf->setAttribute('name', '_token');
$csrf->setAttribute('type', 'hidden');
$csrf->setAttribute('value', '___TOKEN___');
return 'form_' . substr(hash('sha256', implode('|', $hashComponents)), 0, 12);
$form->insertBefore($csrf, $form->firstChild);
}
private function getFormStructureHash(HTMLElement $form): string
private function addFormId(DomWrapper $dom, Element $form, string $formId): void
{
// Hash aus Formular-Struktur (Input-Namen) erstellen
$inputs = $form->querySelectorAll('input[name], select[name], textarea[name]');
$names = [];
// Check if form ID already exists
$existing = $form->querySelector('input[name="_form_id"]');
if ($existing) {
$existing->setAttribute('value', $formId);
foreach ($inputs as $input) {
if ($name = $input->getAttribute('name')) {
$names[] = $name;
return;
}
$formIdField = $dom->document->createElement('input');
$formIdField->setAttribute('name', '_form_id');
$formIdField->setAttribute('type', 'hidden');
$formIdField->setAttribute('value', $formId);
$form->insertBefore($formIdField, $form->firstChild);
}
private function addOldInputPlaceholders(Element $form): void
{
// Text inputs, email, password, etc.
$textInputs = $form->querySelectorAll('input[type="text"], input[type="email"], input[type="password"], input[type="url"], input[type="tel"], input[type="number"], input[type="search"], input[name]:not([type])');
foreach ($textInputs as $input) {
$name = $input->getAttribute('name');
if ($name && $input->getAttribute('type') !== 'hidden') {
// Only set placeholder if no value is already set or if value is empty
$currentValue = $input->getAttribute('value');
if (! $currentValue || $currentValue === '') {
$input->setAttribute('value', "___OLD_INPUT_{$name}___");
}
}
}
sort($names); // Sortieren für konsistente Hashes
return hash('crc32', implode(',', $names));
// Textareas
$textareas = $form->querySelectorAll('textarea[name]');
foreach ($textareas as $textarea) {
$name = $textarea->getAttribute('name');
if ($name) {
// Only set placeholder if textarea is empty
$currentContent = trim($textarea->textContent);
if (! $currentContent || $currentContent === '') {
$textarea->textContent = "___OLD_INPUT_{$name}___";
}
}
}
// Select dropdowns - add placeholder to each option
$selects = $form->querySelectorAll('select[name]');
foreach ($selects as $select) {
$name = $select->getAttribute('name');
if ($name) {
$options = $select->querySelectorAll('option');
foreach ($options as $option) {
$value = $option->getAttribute('value');
if ($value) {
$option->setAttribute('data-selected-if', "___OLD_SELECT_{$name}_{$value}___");
}
}
}
}
// Radio buttons
$radios = $form->querySelectorAll('input[type="radio"][name]');
foreach ($radios as $radio) {
$name = $radio->getAttribute('name');
$value = $radio->getAttribute('value');
if ($name && $value) {
$radio->setAttribute('data-checked-if', "___OLD_RADIO_{$name}_{$value}___");
}
}
// Checkboxes
$checkboxes = $form->querySelectorAll('input[type="checkbox"][name]');
foreach ($checkboxes as $checkbox) {
$name = $checkbox->getAttribute('name');
$value = $checkbox->getAttribute('value') ?: '1';
if ($name) {
$checkbox->setAttribute('data-checked-if', "___OLD_CHECKBOX_{$name}_{$value}___");
}
}
}
private function addErrorDisplayPlaceholders(DomWrapper $dom, Element $form): void
{
// Find all form fields that could have errors
$fields = $form->querySelectorAll('input[name], select[name], textarea[name]');
foreach ($fields as $field) {
$name = $field->getAttribute('name');
if (! $name || $field->getAttribute('type') === 'hidden') {
continue;
}
// Check if error display already exists
$existingError = $form->querySelector(sprintf('.error-display[data-field="%s"]', $name));
if ($existingError) {
continue;
}
// Create error display placeholder
$errorDisplay = $dom->document->createElement('div');
$errorDisplay->setAttribute('class', 'error-display');
$errorDisplay->setAttribute('data-field', $name);
$errorDisplay->textContent = "___ERROR_{$name}___";
// Insert error display after the field
$parent = $field->parentNode;
if ($parent && $field->nextSibling) {
$parent->insertBefore($errorDisplay, $field->nextSibling);
} else {
$parent?->appendChild($errorDisplay);
}
}
}
}

View File

@@ -1,11 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\Element;
use Dom\HTMLDocument;
use Dom\HTMLElement;
@@ -16,7 +18,7 @@ final readonly class HoneypotProcessor implements DomProcessor
'website_url',
'phone_number',
'user_name',
'company_name'
'company_name',
];
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
@@ -24,7 +26,7 @@ final readonly class HoneypotProcessor implements DomProcessor
$forms = $dom->document->querySelectorAll('form');
/** @var HTMLElement $form */
foreach($forms as $form) {
foreach ($forms as $form) {
$this->addHoneypot($dom->document, $form);
$this->addTimeValidation($dom->document, $form);
}
@@ -32,7 +34,7 @@ final readonly class HoneypotProcessor implements DomProcessor
return $dom;
}
private function addHoneypot(HTMLDocument $dom, HTMLElement $form): void
private function addHoneypot(HTMLDocument $dom, Element $form): void
{
$honeypotName = self::HONEYPOT_NAMES[array_rand(self::HONEYPOT_NAMES)];
@@ -66,7 +68,7 @@ final readonly class HoneypotProcessor implements DomProcessor
$form->insertBefore($nameField, $form->firstChild);
}
private function addTimeValidation(HTMLDocument $dom, HTMLElement $form): void
private function addTimeValidation(HTMLDocument $dom, Element $form): void
{
$timeField = $dom->createElement('input');
$timeField->setAttribute('type', 'hidden');

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
@@ -13,10 +15,11 @@ final readonly class IfProcessor implements DomProcessor
$dom->getELementsByAttribute('if')->forEach(function ($node) use ($context) {
$condition = $node->getAttribute('if');
$value = $context->data[$condition] ?? null;
// Handle nested property paths (e.g., "performance.opcacheMemoryUsage")
$value = $this->resolveValue($context->data, $condition);
// Entferne, wenn die Bedingung nicht erfüllt ist
if (!$this->isTruthy($value)) {
if (! $this->isTruthy($value)) {
$node->parentNode?->removeChild($node);
} else {
// Entferne Attribut bei Erfolg
@@ -24,33 +27,47 @@ final readonly class IfProcessor implements DomProcessor
}
});
/*foreach ($dom->querySelectorAll('*[if]') as $node) {
$condition = $node->getAttribute('if');
$value = $context->data[$condition] ?? null;
// Entferne, wenn die Bedingung nicht erfüllt ist
if (!$this->isTruthy($value)) {
$node->parentNode?->removeChild($node);
continue;
}
// Entferne Attribut bei Erfolg
$node->removeAttribute('if');
}*/
return $dom;
}
/**
* Resolves nested property paths like "performance.opcacheMemoryUsage"
*/
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)) {
$value = $value[$key];
} elseif (is_object($value) && isset($value->$key)) {
$value = $value->$key;
} else {
return null;
}
}
return $value;
}
private function isTruthy(mixed $value): bool
{
if (is_bool($value)) return $value;
if (is_null($value)) return false;
if (is_string($value)) return trim($value) !== '';
if (is_numeric($value)) return $value != 0;
if (is_array($value)) return count($value) > 0;
if (is_bool($value)) {
return $value;
}
if (is_null($value)) {
return false;
}
if (is_string($value)) {
return trim($value) !== '';
}
if (is_numeric($value)) {
return $value != 0;
}
if (is_array($value)) {
return count($value) > 0;
}
return true;
}

View File

@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomTemplateParser;
use App\Framework\Template\Parser\DomTemplateParser;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\RenderContext;
use Dom\HTMLDocument;
final readonly class IncludeProcessor implements DomProcessor
{
public function __construct(
private TemplateLoader $loader,
private DomTemplateParser $parser = new DomTemplateParser()
) {}
) {
}
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{

View File

@@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\View\DomTemplateParser;
use App\Framework\Template\Parser\DomTemplateParser;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\RenderContext;
@@ -15,14 +17,17 @@ final readonly class LayoutTagProcessor implements DomProcessor
private TemplateLoader $loader,
private DomTemplateParser $parser = new DomTemplateParser()
) {
#$this->loader = new TemplateLoader($this->pathProvider);
}
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
if ($context->isPartial) {
return $this->removeLayoutTag($dom);
}
$layoutTag = $this->findLayoutTag($dom);
if (!$layoutTag) {
if (! $layoutTag) {
return $dom;
}
@@ -33,6 +38,7 @@ final readonly class LayoutTagProcessor implements DomProcessor
{
$dom = $dom->document;
$layoutTags = $dom->querySelectorAll('layout[src]');
return $layoutTags->length > 0 ? $layoutTags->item(0) : null;
}
@@ -45,7 +51,7 @@ final readonly class LayoutTagProcessor implements DomProcessor
$layoutDom = $this->parser->parseFileToWrapper($layoutPath);
$slot = $layoutDom->document->querySelector('main');
if (!$slot) {
if (! $slot) {
return $dom; // Kein Slot verfügbar
}
@@ -53,4 +59,27 @@ final readonly class LayoutTagProcessor implements DomProcessor
return $layoutDom;
}
private function removeLayoutTag(DomWrapper $dom): DomWrapper
{
$layoutTag = $this->findLayoutTag($dom);
if (! $layoutTag) {
return $dom;
}
// Den Inhalt des Layout-Tags bewahren und an dessen Stelle einfügen
$parent = $layoutTag->parentNode;
if ($parent) {
// Alle Kinder des Layout-Tags vor dem Layout-Tag selbst einfügen
while ($layoutTag->firstChild) {
$parent->insertBefore($layoutTag->firstChild, $layoutTag);
}
}
// Jetzt das leere Layout-Tag entfernen
$layoutTag->remove();
return $dom;
}
}

View File

@@ -1,12 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Meta\OpenGraphType;
use App\Framework\View\DomProcessor;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLDocument;
final readonly class MetaManipulator implements DomProcessor
{
@@ -17,13 +18,12 @@ final readonly class MetaManipulator implements DomProcessor
$dom->document->head->querySelector('meta[name="description"]')?->setAttribute('content', $description);
if($dom->document->head->getElementsByTagName('title')->item(0)) {
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)
{
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'),
};

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\StringProcessor;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\RenderContext;
/**
@@ -27,7 +29,7 @@ final class PhpVariableProcessor implements StringProcessor
// Custom Template-Funktionen
'format_date', 'format_currency', 'format_filesize',
'print_r', 'get_class'
'print_r', 'get_class',
];
public function process(string $html, RenderContext $context): string
@@ -70,20 +72,20 @@ final class PhpVariableProcessor implements StringProcessor
return preg_replace_callback(
'/\$([a-zA-Z_][a-zA-Z0-9_]*)((?:->|(?:-&gt;))[a-zA-Z_][a-zA-Z0-9_]*)*(?:->|(?:-&gt;))([a-zA-Z_][a-zA-Z0-9_]*)\(([^)]*)\)/',
function($matches) use ($data) {
function ($matches) use ($data) {
$varName = $matches[1];
$propertyChain = $matches[2] ? str_replace('-&gt;', '->', $matches[2]) : '';
$methodName = $matches[3];
$argsString = $matches[4];
if (!array_key_exists($varName, $data)) {
if (! array_key_exists($varName, $data)) {
return $matches[0];
}
$object = $data[$varName];
// Property-Chain navigieren falls vorhanden
if (!empty($propertyChain)) {
if (! empty($propertyChain)) {
$properties = explode('->', trim($propertyChain, '->'));
foreach ($properties as $property) {
if (is_object($object) && property_exists($object, $property)) {
@@ -94,7 +96,7 @@ final class PhpVariableProcessor implements StringProcessor
}
}
if (!is_object($object)) {
if (! is_object($object)) {
return $matches[0];
}
@@ -104,6 +106,7 @@ final class PhpVariableProcessor implements StringProcessor
if (method_exists($object, $methodName)) {
$result = $object->$methodName(...$args);
return $this->formatValue($result);
}
@@ -134,12 +137,12 @@ final class PhpVariableProcessor implements StringProcessor
// Verbesserte Regex, die sowohl -> als auch HTML-kodiertes -&gt; erkennt
return preg_replace_callback(
'/\b([a-zA-Z_][a-zA-Z0-9_]*)\(([^)]*)\)/',
function($matches) use ($data) {
function ($matches) use ($data) {
$functionName = $matches[1];
$argsString = $matches[2];
// Nur erlaubte Funktionen ausführen
if (!in_array($functionName, $this->allowedFunctions)) {
if (! in_array($functionName, $this->allowedFunctions)) {
return $matches[0];
}
@@ -149,12 +152,14 @@ final class PhpVariableProcessor implements StringProcessor
// 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);
}
@@ -176,11 +181,11 @@ final class PhpVariableProcessor implements StringProcessor
// Verbesserte Regex, die sowohl -> als auch HTML-kodiertes -&gt; erkennt
return preg_replace_callback(
'/\$([a-zA-Z_][a-zA-Z0-9_]*)((?:->|(?:-&gt;))[a-zA-Z_][a-zA-Z0-9_]*)+/',
function($matches) use ($data) {
function ($matches) use ($data) {
$varName = $matches[1];
$propertyChain = str_replace('-&gt;', '->', $matches[2]);
if (!array_key_exists($varName, $data)) {
if (! array_key_exists($varName, $data)) {
return $matches[0]; // Variable nicht gefunden
}
@@ -222,11 +227,11 @@ final class PhpVariableProcessor implements StringProcessor
{
return preg_replace_callback(
"/\\$([a-zA-Z_][a-zA-Z0-9_]*)\[(['\"]?)([^'\"\]]+)\2\]/",
function($matches) use ($data) {
function ($matches) use ($data) {
$varName = $matches[1];
$key = $matches[3];
if (!array_key_exists($varName, $data)) {
if (! array_key_exists($varName, $data)) {
return $matches[0];
}
@@ -259,7 +264,7 @@ final class PhpVariableProcessor implements StringProcessor
{
return preg_replace_callback(
'/\$([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_\[\-])/',
function($matches) use ($data) {
function ($matches) use ($data) {
$varName = $matches[1];
if (array_key_exists($varName, $data)) {
@@ -290,28 +295,33 @@ final class PhpVariableProcessor implements StringProcessor
// 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;
}
@@ -320,6 +330,7 @@ final class PhpVariableProcessor implements StringProcessor
$varName = $matches[1];
if (array_key_exists($varName, $data)) {
$args[] = $data[$varName];
continue;
}
}
@@ -345,7 +356,7 @@ final class PhpVariableProcessor implements StringProcessor
for ($i = 0; $i < $length; $i++) {
$char = $argsString[$i];
if (!$inQuotes && ($char === '"' || $char === "'")) {
if (! $inQuotes && ($char === '"' || $char === "'")) {
$inQuotes = true;
$quoteChar = $char;
$current .= $char;
@@ -353,7 +364,7 @@ final class PhpVariableProcessor implements StringProcessor
$inQuotes = false;
$quoteChar = null;
$current .= $char;
} elseif (!$inQuotes && $char === ',') {
} elseif (! $inQuotes && $char === ',') {
$args[] = trim($current);
$current = '';
} else {
@@ -406,6 +417,7 @@ final class PhpVariableProcessor implements StringProcessor
if (is_string($date)) {
$date = new \DateTime($date);
}
return $date->format($format);
}
@@ -418,6 +430,7 @@ final class PhpVariableProcessor implements StringProcessor
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$factor = floor((strlen((string)$bytes) - 1) / 3);
return sprintf("%.1f %s", $bytes / pow(1024, $factor), $units[$factor]);
}

View File

@@ -1,24 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\DI\Container;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\Functions\ImageSlotFunction;
use App\Framework\View\Functions\UrlFunction;
use App\Framework\View\RawHtml;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
use App\Framework\View\TemplateFunctions;
use DateTime;
use DateTimeZone;
final class PlaceholderReplacer implements StringProcessor
{
public function __construct(
private Container $container,
)
{
private readonly Container $container,
) {
}
// Erlaubte Template-Funktionen für zusätzliche Sicherheit
/**
* @var string[]
*/
private array $allowedTemplateFunctions = [
'date', 'format_date', 'format_currency', 'format_filesize',
'strtoupper', 'strtolower', 'ucfirst', 'trim', 'count', /*'imageslot'*/
@@ -32,7 +39,7 @@ final class PlaceholderReplacer implements StringProcessor
// Standard Variablen und Methoden: {{ item.getRelativeFile() }}
return preg_replace_callback(
'/{{\\s*([\\w.]+)(?:\\(\\s*([^)]*)\\s*\\))?\\s*}}/',
function($matches) use ($context) {
function ($matches) use ($context) {
$expression = $matches[1];
$params = isset($matches[2]) ? trim($matches[2]) : null;
@@ -50,22 +57,22 @@ final class PlaceholderReplacer implements StringProcessor
{
return preg_replace_callback(
'/{{\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\(([^)]*)\\)\\s*}}/',
function($matches) use ($context) {
function ($matches) use ($context) {
$functionName = $matches[1];
$params = trim($matches[2]);
$functions = new TemplateFunctions($this->container, ImageSlotFunction::class, UrlFunction::class);
if($functions->has($functionName)) {
if ($functions->has($functionName)) {
$function = $functions->get($functionName);
$args = $this->parseParams($params, $context->data);
if(is_callable($function)) {
if (is_callable($function)) {
return $function(...$args);
}
#return $function(...$args);
}
// Nur erlaubte Funktionen
if (!in_array($functionName, $this->allowedTemplateFunctions)) {
if (! in_array($functionName, $this->allowedTemplateFunctions)) {
return $matches[0];
}
@@ -75,6 +82,7 @@ final class PlaceholderReplacer implements StringProcessor
// 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');
}
@@ -82,6 +90,7 @@ final class PlaceholderReplacer implements StringProcessor
// Standard PHP-Funktionen (begrenzt)
if (function_exists($functionName)) {
$result = $functionName(...$args);
return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
@@ -98,15 +107,17 @@ final class PlaceholderReplacer implements StringProcessor
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
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);
}
@@ -119,6 +130,7 @@ final class PlaceholderReplacer implements StringProcessor
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$factor = floor((strlen((string)$bytes) - 1) / 3);
return sprintf("%.1f %s", $bytes / pow(1024, $factor), $units[$factor]);
}
@@ -131,11 +143,11 @@ final class PlaceholderReplacer implements StringProcessor
// Objekt auflösen
$object = $this->resolveValue($data, $objectPath);
if (!is_object($object)) {
if (! is_object($object)) {
return '{{ ' . $expression . '() }}'; // Platzhalter beibehalten
}
if (!method_exists($object, $methodName)) {
if (! method_exists($object, $methodName)) {
return '{{ ' . $expression . '() }}'; // Platzhalter beibehalten
}
@@ -160,6 +172,12 @@ final class PlaceholderReplacer implements StringProcessor
// Bleibt als Platzhalter stehen
return '{{ ' . $expr . ' }}';
}
// RawHtml-Objekte nicht escapen
if ($value instanceof RawHtml) {
return $value->content;
}
return htmlspecialchars((string)$value, $flags, 'UTF-8');
}
@@ -199,28 +217,33 @@ final class PlaceholderReplacer implements StringProcessor
// 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;
}
@@ -229,12 +252,22 @@ final class PlaceholderReplacer implements StringProcessor
$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;
}
} else {
// Einfache Variablennamen (ohne $ oder .) aus Template-Daten auflösen
$value = $this->resolveValue($data, $part);
if ($value !== null) {
$params[] = $value;
continue;
}
}
@@ -260,7 +293,7 @@ final class PlaceholderReplacer implements StringProcessor
for ($i = 0; $i < $length; $i++) {
$char = $paramsString[$i];
if (!$inQuotes && ($char === '"' || $char === "'")) {
if (! $inQuotes && ($char === '"' || $char === "'")) {
$inQuotes = true;
$quoteChar = $char;
$current .= $char;
@@ -268,7 +301,7 @@ final class PlaceholderReplacer implements StringProcessor
$inQuotes = false;
$quoteChar = null;
$current .= $char;
} elseif (!$inQuotes && $char === ',') {
} elseif (! $inQuotes && $char === ',') {
$params[] = trim($current);
$current = '';
} else {

View File

@@ -1,12 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLDocument;
use Dom\Node;
final class RemoveEmptyLinesProcessor implements DomProcessor

View File

@@ -1,10 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
final class SingleLineHtmlProcessor implements StringProcessor
{
@@ -13,6 +14,7 @@ final class SingleLineHtmlProcessor implements StringProcessor
// Entfernt Zeilenumbrüche und doppelte Whitespaces, alles in eine Zeile
$html = preg_replace('/>(\s|\n|\r)+</', '><', $html);
$html = preg_replace('/\s+/', ' ', $html);
return trim($html);
}
}

View File

@@ -1,11 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLDocument;
/*

View File

@@ -1,18 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\View\DomProcessor;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\Element;
use Dom\HTMLDocument;
# Ersetzt ein eigenes `<switch value="foo">` mit inneren `<case when="bar">`-Elementen.
final readonly class SwitchCaseProcessor implements DomProcessor
{
/**
* @inheritDoc
*/
@@ -26,7 +26,7 @@ final readonly class SwitchCaseProcessor implements DomProcessor
$defaultCase = null;
foreach ($switchNode->childNodes as $child) {
if (!($child instanceof Element)) {
if (! ($child instanceof Element)) {
continue;
}
@@ -34,6 +34,7 @@ final readonly class SwitchCaseProcessor implements DomProcessor
$caseValue = $child->getAttribute('value');
if ((string)$value === $caseValue) {
$matchingCase = $child;
break;
}
}

View File

@@ -1,10 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
final class VoidElementsSelfClosingProcessor implements StringProcessor
{
@@ -18,9 +19,10 @@ final class VoidElementsSelfClosingProcessor implements StringProcessor
{
$pattern = '#<(' . implode('|', self::VOID_ELEMENTS) . ')(\s[^>/]*)?(?<!/)>#i';
return preg_replace_callback($pattern, function($m) {
return preg_replace_callback($pattern, function ($m) {
$tag = $m[1];
$attrs = $m[2] ?? '';
return "<$tag$attrs />";
}, $html);
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
/**
* Wrapper für HTML-Inhalte, die nicht escaped werden sollen
*/
final readonly class RawHtml
{
public function __construct(
public string $content
) {
}
public function __toString(): string
{
return $this->content;
}
public static function from(string $content): self
{
return new self($content);
}
}

View File

@@ -1,10 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Meta\MetaData;
use App\Framework\Template\TemplateContext;
final readonly class RenderContext
final readonly class RenderContext implements TemplateContext
{
public function __construct(
public string $template, // Dateiname oder Template-Key
@@ -13,5 +16,8 @@ final readonly class RenderContext
public ?string $layout = null, // Optionales Layout
public array $slots = [], // Benannte Slots wie ['main' => '<p>...</p>']
public ?string $controllerClass = null,
) {}
public ?string $route = null, // Route name for form ID generation
public bool $isPartial = false,
) {
}
}

View File

@@ -1,8 +0,0 @@
<?php
namespace App\Framework\View;
interface StringProcessor
{
public function process(string $html, RenderContext $context): string;
}

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\DI\Container;
@@ -8,9 +10,11 @@ use App\Framework\View\Functions\TemplateFunction;
final readonly class TemplateFunctions
{
private array $functions;
public function __construct(
private Container $container, string ...$templateFunctions)
{
private Container $container,
string ...$templateFunctions
) {
$functions = [];
foreach ($templateFunctions as $function) {
$functionInstance = $this->container->get($function);

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Core\AttributeMapper;
use ReflectionMethod;
use App\Framework\Reflection\WrappedReflectionClass;
use App\Framework\Reflection\WrappedReflectionMethod;
final class TemplateMapper implements AttributeMapper
{
@@ -13,15 +15,19 @@ final class TemplateMapper implements AttributeMapper
return Template::class;
}
public function map(object $reflectionTarget, object $attributeInstance): ?array
public function map(WrappedReflectionClass|WrappedReflectionMethod $reflectionTarget, object $attributeInstance): ?array
{
if (!$attributeInstance instanceof Template) {
if (! $attributeInstance instanceof Template) {
return null;
}
// Template can be applied to classes or methods
$className = $reflectionTarget instanceof WrappedReflectionClass
? $reflectionTarget->getFullyQualified()
: $reflectionTarget->getDeclaringClass()->getFullyQualified();
return [
'class' => $reflectionTarget->getName(),
#'method' => $reflectionTarget->getName(),
'class' => $className,
'path' => $attributeInstance->path,
];
}

View File

@@ -1,32 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\DI\Container;
use App\Framework\Template\Processing\ProcessorResolver;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\Processing\DomProcessingPipeline;
use Dom\HTMLDocument;
final class TemplateProcessor
{
private array $resolvedProcessors = [];
private ?DomProcessingPipeline $domPipeline = null;
private ?ProcessorResolver $processorResolver = null;
public function __construct(
private readonly array $domProcessors,
private readonly array $stringProcessors,
private readonly Container $container
) {}
) {
}
public function render(RenderContext $context, string $html, bool $component = false): string
{
#$dom = $this->processDom($html, $context);
// Skip DOM processing if no DOM processors configured
if (empty($this->domProcessors)) {
return $this->processString($html, $context);
}
$domPipeline = new DomProcessingPipeline($this->domProcessors, $this->container->get(ProcessorResolver::class));
// Lazy initialize DOM pipeline
if ($this->domPipeline === null) {
$this->processorResolver ??= $this->container->get(ProcessorResolver::class);
$this->domPipeline = new DomProcessingPipeline($this->domProcessors, $this->processorResolver);
}
$dom = $domPipeline->process($context, $html);
$dom = $this->domPipeline->process($context, $html);
$html = $component ? $this->extractBodyContent($dom->document) : $dom->document->saveHTML();
#$html = $dom->body->innerHTML;
$processedHtml = $component
? $this->extractBodyContent($dom->document)
: $dom->document->saveHTML();
return $this->processString($html, $context);
return $this->processString($processedHtml, $context);
}
public function __debugInfo(): ?array
@@ -37,25 +55,7 @@ final class TemplateProcessor
];
}
private function processDom(string $html, RenderContext $context): HTMLDocument
{
$parser = new DomTemplateParser();
$dom = $parser->parse($html);
// 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;
}
// Removed unused processDom method - using DomProcessingPipeline instead
private function processString(string $html, RenderContext $context): string
{
@@ -68,16 +68,19 @@ final class TemplateProcessor
return $html;
}
public function resolveProcessor(string $processorClass): object
/** @param class-string<StringProcessor> $processorClass */
public function resolveProcessor(string $processorClass): StringProcessor
{
if (!isset($this->resolvedProcessors[$processorClass])) {
$this->resolvedProcessors[$processorClass] = $this->container->get($processorClass);;
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 ?? '';
return $dom->body->innerHTML ?? '';
}
}

View File

@@ -7,4 +7,6 @@ namespace App\Framework\View;
interface TemplateRenderer
{
public function render(RenderContext $context): string;
public function renderPartial(RenderContext $context): string;
}

View File

@@ -1,41 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Cache\Cache;
use App\Framework\Core\PathProvider;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\Initializer;
use App\Framework\Discovery\FileScanner;
use App\Framework\Discovery\Results\DiscoveryResults;
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\Discovery\Results\DiscoveryRegistry;
use App\Framework\Performance\PerformanceService;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\Loading\TemplatePathResolver;
use App\Framework\View\Processors\AssetInjector;
use App\Framework\View\Processors\CommentStripProcessor;
use App\Framework\View\Processors\ComponentProcessor;
use App\Framework\View\Processors\CsrfReplaceProcessor;
use App\Framework\View\Processors\FormProcessor;
use App\Framework\View\Processors\ForProcessor;
use App\Framework\View\Processors\HoneypotProcessor;
use App\Framework\View\Processors\IfProcessor;
use App\Framework\View\Processors\LayoutTagProcessor;
use App\Framework\View\Processors\MetaManipulator;
use App\Framework\View\Processors\PlaceholderReplacer;
use App\Framework\View\Processors\RemoveEmptyLinesProcessor;
use App\Framework\View\Processors\SingleLineHtmlProcessor;
use App\Framework\View\Processors\VoidElementsSelfClosingProcessor;
final readonly class TemplateRendererInitializer
{
public function __construct(
private DefaultContainer $container,
private FileScanner $scanner,
private PathProvider $pathProvider,
private DiscoveryResults $results,
) {}
#private FileScanner $scanner,
#private PathProvider $pathProvider,
private DiscoveryRegistry $results,
) {
}
#[Initializer]
public function __invoke(): TemplateRenderer
@@ -44,6 +43,7 @@ final readonly class TemplateRendererInitializer
ComponentProcessor::class,
LayoutTagProcessor::class,
MetaManipulator::class,
IfProcessor::class,
ForProcessor::class,
AssetInjector::class,
CommentStripProcessor::class,
@@ -54,14 +54,24 @@ final readonly class TemplateRendererInitializer
$strings = [
PlaceholderReplacer::class,
SingleLineHtmlProcessor::class,
#SingleLineHtmlProcessor::class,
VoidElementsSelfClosingProcessor::class,
CsrfReplaceProcessor::class,
# CsrfReplaceProcessor::class, // DEACTIVATED - FormDataResponseMiddleware handles form processing
];
$domImplementations = [];
foreach ($this->results->interfaces->get(DomProcessor::class) as $className) {
$domImplementations[] = $className->getFullyQualified();
}
$stringImplementations = [];
foreach ($this->results->interfaces->get(StringProcessor::class) as $className) {
$stringImplementations[] = $className->getFullyQualified();
}
$templateProcessor = new TemplateProcessor(
[ComponentProcessor::class, ...$this->results->getInterfaceImplementations(DomProcessor::class)],
$this->results->getInterfaceImplementations(StringProcessor::class),
[ComponentProcessor::class, ...$domImplementations],
$stringImplementations,
$this->container,
);
@@ -69,25 +79,18 @@ final readonly class TemplateRendererInitializer
$this->container->singleton(TemplateProcessor::class, $templateProcessor);
#ViewCacheBootstrap::quickEnable($this->container);
/*$files = $this->scanner->findFiles($this->pathProvider->getSourcePath(), '*.view.php');
$templates = [];
foreach ($files as $file) {
$templates[$file->getFilename()] = $file->getPathname();
}*/
$pathProvider = $this->container->get(PathProvider::class);
$cache = $this->container->get(Cache::class);
$performanceService = $this->container->get(PerformanceService::class);
$loader = new TemplateLoader(pathProvider: $pathProvider, cache: $cache, discoveryResults: $this->results/*, templates: $templates*/);
$loader = new TemplateLoader(pathProvider: $pathProvider, cache: $cache, discoveryRegistry: $this->results/*, templates: $templates*/);
$this->container->singleton(TemplateLoader::class, $loader);
return new Engine(
$loader,
$pathProvider,
$performanceService,
processor: $templateProcessor,
container: $this->container,
cache: $cache,

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔍 Run All Checks
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}📊 Export Report
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}📄 View Logs
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔍 Run All Checks
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔄 Refresh All
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔄 Refresh All
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔍 Run All Checks
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔍 Run All Checks
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔄 Refresh All
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔄 Refresh All
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔍 Run All Checks
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔍 Run All Checks
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}📄 View Logs
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔍 Run All Checks
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}📄 View Logs
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔍 Run All Checks
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}📄 View Logs
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}📊 Export Report
</button></body></html>

View File

@@ -0,0 +1,3 @@
<html><head><link rel="stylesheet" href="/assets/css-x5fXjerd.css"><script src="/assets/js-CDDJMAPM.js" type="module"></script></head><body><button class="admin-button {{ variant ? 'admin-button--' + variant : '' }} {{ size ? 'admin-button--' + size : '' }}" {{="" disabled="" ?="" 'disabled'="" :="" ''="" }}="" onclick="" 'onclick="' + onclick + '" '="">
{{ icon }}🔍 Run All Checks
</button></body></html>

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