document->querySelectorAll('select'); foreach ($selects as $idx => $select) { $html = $dom->document->saveHTML($select); $snippet = substr($html, 0, 200); } // Debug: show all 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: $in = $node->getAttribute('items'); $var = $node->getAttribute('as'); } else { // Old syntax: $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 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 tags recursively if (str_contains($processedContent, '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 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; } }