chore: complete update
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user