- 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.
275 lines
9.9 KiB
PHP
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;
|
|
}
|
|
}
|