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:
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View\Caching\Analysis;
|
||||
|
||||
enum CacheStrategy: string
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View\Caching\Analysis;
|
||||
|
||||
class CacheabilityAnalyzer
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View\Caching\Keys;
|
||||
|
||||
class SmartKeyGenerator
|
||||
{
|
||||
|
||||
public function generate(string $string, string $key)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View\Caching;
|
||||
|
||||
use App\Framework\View\RenderContext;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = []
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
use Dom\HTMLDocument;
|
||||
|
||||
interface DomProcessor
|
||||
{
|
||||
public function process(DomWrapper $dom, RenderContext $context): DomWrapper;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View\Exception;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View\Functions;
|
||||
|
||||
interface TemplateFunction
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
35
src/Framework/View/Loading/Resolvers/ComponentResolver.php
Normal file
35
src/Framework/View/Loading/Resolvers/ComponentResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
27
src/Framework/View/Loading/Resolvers/LayoutResolver.php
Normal file
27
src/Framework/View/Loading/Resolvers/LayoutResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\View\Loading\Resolvers;
|
||||
|
||||
interface TemplateResolverStrategy
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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, '/'));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
213
src/Framework/View/Processors/CsrfTokenProcessor.php
Normal file
213
src/Framework/View/Processors/CsrfTokenProcessor.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
|
||||
@@ -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_]*)((?:->|(?:->))[a-zA-Z_][a-zA-Z0-9_]*)*(?:->|(?:->))([a-zA-Z_][a-zA-Z0-9_]*)\(([^)]*)\)/',
|
||||
function($matches) use ($data) {
|
||||
function ($matches) use ($data) {
|
||||
$varName = $matches[1];
|
||||
$propertyChain = $matches[2] ? str_replace('->', '->', $matches[2]) : '';
|
||||
$methodName = $matches[3];
|
||||
$argsString = $matches[4];
|
||||
|
||||
if (!array_key_exists($varName, $data)) {
|
||||
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 -> 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 -> erkennt
|
||||
return preg_replace_callback(
|
||||
'/\$([a-zA-Z_][a-zA-Z0-9_]*)((?:->|(?:->))[a-zA-Z_][a-zA-Z0-9_]*)+/',
|
||||
function($matches) use ($data) {
|
||||
function ($matches) use ($data) {
|
||||
$varName = $matches[1];
|
||||
$propertyChain = str_replace('->', '->', $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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/*
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
26
src/Framework/View/RawHtml.php
Normal file
26
src/Framework/View/RawHtml.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\View;
|
||||
|
||||
interface StringProcessor
|
||||
{
|
||||
public function process(string $html, RenderContext $context): string;
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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 ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,6 @@ namespace App\Framework\View;
|
||||
interface TemplateRenderer
|
||||
{
|
||||
public function render(RenderContext $context): string;
|
||||
|
||||
public function renderPartial(RenderContext $context): string;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user