Files
michaelschiemer/src/Framework/View/Processors/ForProcessor.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

275 lines
9.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\DI\Container;
use App\Framework\Meta\MetaData;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomWrapper;
use App\Framework\View\RawHtml;
use App\Framework\View\RenderContext;
final class ForProcessor implements DomProcessor
{
public function __construct(
private Container $container,
) {
}
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
{
// Debug: show a snippet of HTML around select elements
$selects = $dom->document->querySelectorAll('select');
foreach ($selects as $idx => $select) {
$html = $dom->document->saveHTML($select);
$snippet = substr($html, 0, 200);
}
// Debug: show all <for> elements in document
$allForElements = $dom->document->querySelectorAll('for');
foreach ($allForElements as $idx => $forEl) {
$attrs = [];
foreach ($forEl->attributes as $attr) {
$attrs[] = $attr->name . '="' . $attr->value . '"';
}
}
// Support both syntaxes: var/in (old) and items/as (new)
$forNodesOld = $dom->document->querySelectorAll('for[var][in]');
$forNodesNew = $dom->document->querySelectorAll('for[items][as]');
// Merge both nodesets
$forNodes = [];
foreach ($forNodesOld as $node) {
$forNodes[] = $node;
}
foreach ($forNodesNew as $node) {
$forNodes[] = $node;
}
foreach ($forNodes as $node) {
// Detect which syntax is being used
if ($node->hasAttribute('items') && $node->hasAttribute('as')) {
// New syntax: <for items="arrayName" as="itemVar">
$in = $node->getAttribute('items');
$var = $node->getAttribute('as');
} else {
// Old syntax: <for var="itemVar" in="arrayName">
$var = $node->getAttribute('var');
$in = $node->getAttribute('in');
}
$output = '';
// Resolve items from context data or model
$items = $this->resolveValue($context->data, $in);
// Fallback to model if not found in data
if ($items === null && isset($context->model)) {
if (str_starts_with($in, 'model.')) {
$items = $this->resolveValue(['model' => $context->model], $in);
} else {
$items = $this->resolveValue(['model' => $context->model], 'model.' . $in);
}
}
if (is_iterable($items)) {
foreach ($items as $item) {
$clone = $node->cloneNode(true);
// Create loop context with loop variable
$loopContext = new RenderContext(
template: $context->template,
metaData: new MetaData('', ''),
data: array_merge($context->data, [$var => $item]),
controllerClass: $context->controllerClass
);
// Get innerHTML from cloned node
$innerHTML = $clone->innerHTML;
// Handle case where DOM parser treats <for> as self-closing
if (trim($innerHTML) === '') {
$innerHTML = $this->collectSiblingContent($node, $dom);
}
// Replace loop variable placeholders
$innerHTML = $this->replaceLoopVariables($innerHTML, $var, $item);
// Process placeholders in loop content
$placeholderReplacer = $this->container->get(PlaceholderReplacer::class);
$processedContent = $placeholderReplacer->process($innerHTML, $loopContext);
// Handle nested <for> tags recursively
if (str_contains($processedContent, '<for ')) {
try {
$tempWrapper = DomWrapper::fromString($processedContent);
$this->process($tempWrapper, $loopContext);
$processedContent = $tempWrapper->toHtml(true);
} catch (\Exception $e) {
// Continue with unprocessed content on error
}
}
$output .= $processedContent;
}
}
// Replace for node with processed output
if (! empty($output)) {
try {
$replacement = $dom->document->createDocumentFragment();
@$replacement->appendXML($output);
$node->parentNode?->replaceChild($replacement, $node);
} catch (\Exception $e) {
// Fallback: Use innerHTML approach
$tempDiv = $dom->document->createElement('div');
$tempDiv->innerHTML = $output;
$parent = $node->parentNode;
$nextSibling = $node->nextSibling;
$parent->removeChild($node);
while ($tempDiv->firstChild) {
if ($nextSibling) {
$parent->insertBefore($tempDiv->firstChild, $nextSibling);
} else {
$parent->appendChild($tempDiv->firstChild);
}
}
}
} else {
// Remove empty for node
$node->parentNode?->removeChild($node);
}
}
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)) {
// Try property access first
if (isset($value->$key)) {
$value = $value->$key;
} elseif (method_exists($value, $key)) {
// Try method call
$value = $value->$key();
} elseif (method_exists($value, 'get' . ucfirst($key))) {
// Try getter method
$getterMethod = 'get' . ucfirst($key);
$value = $value->$getterMethod();
} else {
return null;
}
} else {
return null;
}
}
return $value;
}
/**
* Replaces loop variable placeholders in the HTML content
*/
private function replaceLoopVariables(string $html, string $varName, mixed $item): string
{
$pattern = '/{{\\s*' . preg_quote($varName, '/') . '\\.([\\w]+)\\s*}}/';
return preg_replace_callback(
$pattern,
function ($matches) use ($item) {
$property = $matches[1];
if (is_array($item) && array_key_exists($property, $item)) {
$value = $item[$property];
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if ($value instanceof RawHtml) {
return $value->content;
}
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
} elseif (is_object($item) && isset($item->$property)) {
$value = $item->$property;
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if ($value instanceof RawHtml) {
return $value->content;
}
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
// Return placeholder unchanged if property not found
return $matches[0];
},
$html
);
}
/**
* Collects content from sibling nodes when <for> is treated as self-closing
*/
private function collectSiblingContent($forNode, DomWrapper $dom): string
{
$content = '';
$currentNode = $forNode->nextSibling;
while ($currentNode !== null) {
if ($currentNode->nodeType === XML_ELEMENT_NODE) {
// Check for loop content elements (TR for tables, DIV for other structures)
if ($currentNode->tagName === 'TR') {
$content .= $dom->document->saveHTML($currentNode);
$nextNode = $currentNode->nextSibling;
$currentNode->parentNode->removeChild($currentNode);
$currentNode = $nextNode;
break; // One TR per iteration
} elseif ($currentNode->tagName === 'TABLE') {
// Look for template TR inside table
$tableRows = $currentNode->querySelectorAll('tr');
foreach ($tableRows as $row) {
$rowHtml = $dom->document->saveHTML($row);
// Find row with placeholders (template row)
if (str_contains($rowHtml, '{{')) {
$content = $rowHtml;
break 2;
}
}
$currentNode = $currentNode->nextSibling;
} else {
$currentNode = $currentNode->nextSibling;
}
} else {
$currentNode = $currentNode->nextSibling;
}
}
return $content;
}
}