chore: complete update

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

View File

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