Description
HeadNode::addChild() silently discards children that are not allowed in <head>. This violates LSP (parent HTMLNode::addChild() always adds) and makes debugging impossible — the child simply vanishes with no feedback.
Additionally, $chainOnParent defaults to true (parent defaults to false), causing surprising behavior where $head->addChild(...) returns $head instead of the child.
Current Behavior (silent discard)
$head->addChild(new HTMLNode('div')); // Silently ignored, no error
$head->addChild(new HTMLNode('title')); // Silently ignored
$head->addChild(new HTMLNode('meta', ['charset' => 'UTF-8'])); // Silently ignored
$meta = $head->addChild('meta', ['name' => 'x']);
// $meta is $head (not the meta node!) due to $chainOnParent=true default
Proposed Behavior (throw with guidance)
$head->addChild(new HTMLNode('div'));
// InvalidArgumentException: Element 'div' is not allowed in <head>. Allowed: base, title, meta, link, script, noscript, #COMMENT, style
$head->addChild(new HTMLNode('title'));
// InvalidArgumentException: Use setPageTitle() to set the document title.
$head->addChild(new HTMLNode('meta', ['charset' => 'UTF-8']));
// InvalidArgumentException: Use setCharSet() to set the document charset.
$head->addChild(new HTMLNode('link', ['rel' => 'canonical']));
// InvalidArgumentException: Use setCanonical() to set the canonical URL.
// Duplicate meta:
$head->addMeta('description', 'first');
$head->addChild(new HTMLNode('meta', ['name' => 'description']));
// InvalidArgumentException: Meta 'description' already exists. Use addMeta() with $override=true.
Changes Required
1. addChild() throws on invalid input
Validation rules:
- Not in
ALLOWED_CHILDREN → throw listing allowed elements
title → throw pointing to setPageTitle()
base → throw pointing to setBase()
meta[charset] → throw pointing to setCharSet()
link[rel=canonical] → throw pointing to setCanonical()
- Duplicate
meta[name] → throw pointing to addMeta($name, $val, true)
2. Fix $chainOnParent default to false
Match parent class behavior. $head->addChild('script', [...]) should return the script node, not $head.
3. Remove addChildHelper() private method
Its logic is inlined into addChild() with exceptions replacing silent returns.
4. Internal callers use parent::addChild()
Methods that already validate internally (addMeta, cssJsInsertHelper, insertMetaInCorrectOrder) should call parent::addChild() to bypass the public validation they already performed.
Breaking Changes
| Change |
Impact |
| Invalid children throw |
Code adding <div> etc. to <head> will crash (was always a bug) |
| Managed nodes throw with guidance |
Code using addChild() for title/base/canonical/charset will crash (should use dedicated methods) |
| Duplicate meta throws |
Code adding same meta twice will crash (use addMeta($name, $val, true)) |
$chainOnParent default → false |
$head->addChild(...) returns child node, not $head |
All breaking changes surface existing bugs or align with parent class contract.
Acceptance Criteria
addChild() throws InvalidArgumentException for all invalid cases
- Exception messages include guidance on the correct method to use
$chainOnParent defaults to false
addCSS(), addJs(), addMeta(), addAlternate(), addLink() still work correctly
- Existing HeadNode tests updated to reflect new behavior
- New tests for each exception case
Description
HeadNode::addChild()silently discards children that are not allowed in<head>. This violates LSP (parentHTMLNode::addChild()always adds) and makes debugging impossible — the child simply vanishes with no feedback.Additionally,
$chainOnParentdefaults totrue(parent defaults tofalse), causing surprising behavior where$head->addChild(...)returns$headinstead of the child.Current Behavior (silent discard)
Proposed Behavior (throw with guidance)
Changes Required
1.
addChild()throws on invalid inputValidation rules:
ALLOWED_CHILDREN→ throw listing allowed elementstitle→ throw pointing tosetPageTitle()base→ throw pointing tosetBase()meta[charset]→ throw pointing tosetCharSet()link[rel=canonical]→ throw pointing tosetCanonical()meta[name]→ throw pointing toaddMeta($name, $val, true)2. Fix
$chainOnParentdefault tofalseMatch parent class behavior.
$head->addChild('script', [...])should return the script node, not$head.3. Remove
addChildHelper()private methodIts logic is inlined into
addChild()with exceptions replacing silent returns.4. Internal callers use
parent::addChild()Methods that already validate internally (
addMeta,cssJsInsertHelper,insertMetaInCorrectOrder) should callparent::addChild()to bypass the public validation they already performed.Breaking Changes
<div>etc. to<head>will crash (was always a bug)addChild()for title/base/canonical/charset will crash (should use dedicated methods)addMeta($name, $val, true))$chainOnParentdefault →false$head->addChild(...)returns child node, not$headAll breaking changes surface existing bugs or align with parent class contract.
Acceptance Criteria
addChild()throwsInvalidArgumentExceptionfor all invalid cases$chainOnParentdefaults tofalseaddCSS(),addJs(),addMeta(),addAlternate(),addLink()still work correctly