I am seeing some weird behavior that I have not been able to debug. This code is from a simple implementation of a variation on wordpress shortcodes.
The code loops through a DOMNodeList (of custom html tags) and calls another function to process each DOMElement in turn. That function replaces the DOMElement with another DOMElement, and returns the resulting text.
The first time through that loop and function call, everything works exactly as expected. However, the second time through, the call to DOMElement->replaceWith() fails without any exception being thrown. I have checked that each loop and call is working on a distinct DOMElement (tag). I am really stumped.
Any ideas greatly appreciated. Simplified code segments below:
EDIT: here is a more complete code example, trying to avoid pasting too much of my spaghetti here. 😉
EDIT: added one more bit to make reproducing easier
// for demonstration: this comes from the database
$content = '
<div>
<p>This is a test ...</p>
<sc-snippet id="Test Snippet 1"/>
<sc-snippet id="test-snippet-2"/>
</div>
';
function processContent($content)
{
$scodes = loadScodes();
$snames = array_keys($scodes);
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->loadHTML(htmlspecialchars_decode($content)); // decode the entities in custom tags
foreach($snames as $sname)
{
$scTags = $dom->getElementsByTagName($sname);
foreach($scTags as $tag)
{
$parsed = parseScodeTag($tag, $scodes[$sname]);
$scode = $parsed['scode'];
$handler = $scodes[$scode]['handler'];
$content = $handler($parsed['attrs'], $dom, $tag);
} // end foreach found tag
} // end foreach known scode
return(html_entity_decode($content)); // this should be the HTML segment with tags replaced by snippets
} // end processContent()
function loadScodes()
{
//$config = config(SCODE_CONFIG);
//$scodes = $config->get(SCODE_SCODES);
// for demonstration:
$scodes = array(
'sc-snippet' => array(
'handler' => 'handleSnippet',
'attrs' => array('id'),
),
);
return($scodes);
}
// for demonstration, snippets are stored in the database and handled in a different module
function loadSnippet($id)
{
$snippets = array(
'Test Snippet 1' => '<p>This is a test snippet 1</p>',
'test-snippet-2' => '<p>This is a test snippet 2</p>',
);
return($snippets[$id]);
}
function parseScodeTag($tag, $scode)
{
$parsed = array();
$parsed['scode'] = $tag->nodeName;
$parsed['handler'] = $scode['handler'];
$scodeAttrs = array();
if ( isset($scode['attrs']) && count($scode['attrs']) > 0 )
{
foreach ($scode['attrs'] as $attr)
{
$attrVal = $tag->getAttribute($attr);
$scodeAttrs[$attr] = $attrVal;
}
}
$parsed['attrs'] = $scodeAttrs;
return($parsed);
}
function handleSnippet($attrs, $dom, $tag)
{ // return a block of html
// check for attributes
if (!isset($attrs['id']))
{
return($dom->saveHTML());
}
$snip = loadSnippet($attrs['id']);
$e = $dom->createElement('div', $snip);
try {
$tag->replaceWith($e);
} catch(Exception $e) { // never get here
_dd('CAUGHT', false);
} finally { // correction, do not get here on the second time through
_dd('FINALLY', false);
}
return($dom->saveHTML());
}
4
C3roe put me on the right track, the problem was the fact that getElementsByTagName() is “live”. i used their suggestion to go through the elements in reverse order, and that seems to have done the trick.
thank you C3roe!!!
foreach($snames as $sname)
{
$scTags = $dom->getElementsByTagName($sname);
$nTags = $scTags->length;
for($i = $nTags; $i > 0 ; $i--)
{
$tag = $scTags[$i - 1];
$parsed = parseScodeTag($tag, $scodes[$sname]);
$scode = $parsed['scode'];
$handler = $scodes[$scode]['handler'];
$content = $handler($parsed['attrs'], $dom, $tag);
} // end for each tag, in reverse order
} // end foreach known scode