Skip to content

fix(HeadNode): addChild() should throw on invalid children instead of silent discard #70

@usernane

Description

@usernane

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions