Skip to content
Greg Bowler edited this page May 18, 2026 · 11 revisions

WebEngine renders HTML by working with a real document model on the server. That means the page can be manipulated using familiar DOM ideas rather than only through string concatenation.

The DOM package itself is documented in more detail at https://www.php.gt/docs/Dom/.

Direct DOM manipulation vs. binding

Direct DOM manipulation is useful when you need to change the structure of the document, move nodes around, or work with a one-off element relationship.

Example of direct DOM manipulation:

use GT\Dom\HTMLDocument;
use Gt\Http\Uri;

function go(HTMLDocument $document, Uri $uri):void {
	// Mark the link's parent that matches the current URI in as "selected":
	foreach($document->querySelectorAll("main-menu a") as $link) {
		if($link->href === $uri->getPath()) {
			$link->parentElement->classList->add("selected");
		}
	}
}

Binding is usually cleaner when the structure is already present in HTML and you only need to fill in values. The two approaches work well together, but binding should usually be the default because it keeps the page view and the PHP more loosely coupled.

Example of binding:

use GT\DomTemplate\Binder;
use GT\Database\Database;

function go(Binder $binder, Database $db):void {
	$row = $db->fetch("currentEntity");
	$binder->bindData($row);
}

In the above two examples, notice how when using the Binder, the PHP code doesn't necessarily need to know anything about the page structure, whereas when using the HTMLDocument directly, the PHP is more tightly coupled to the structure of the HTML.

Typical DOM work

When using HTMLDocument in WebEngine, the most common operations fall into four groups:

  • finding the elements we need
  • changing text, attributes, or classes
  • creating or moving nodes
  • inspecting relationships such as parents, children, and nearest matches

The API is intentionally close to the browser DOM, so methods such as querySelector(), querySelectorAll(), closest(), createElement(), appendChild(), before(), after(), and replaceWith() work in familiar ways.

Finding elements

Selector-based traversal is usually the starting point:

use GT\Dom\HTMLDocument;

function go(HTMLDocument $document):void {
	$title = $document->querySelector("main h1");
	$buttons = $document->querySelectorAll("form[action] button");

	$title?->classList->add("is-ready");

	foreach($buttons as $button) {
		$button->disabled = false;
	}
}

querySelector() returns the first matching element or null. querySelectorAll() returns a NodeList of all matches. In PHP.GT/Dom, that list is static, so if you later add matching elements, the original list does not update itself.

Read more in the main DOM docs: https://www.php.gt/docs/Dom/Document-traversal/

Changing text, attributes, and classes

Once we have the right element, the next step is usually to adjust content or element state.

use GT\Dom\HTMLDocument;

function go(HTMLDocument $document):void {
	$document->querySelector(".customer-name")->innerText = "Cody";
	$document->querySelector(".customer-email")->textContent = "cody@example.com";

	$profileLink = $document->querySelector("a.profile-link");
	$profileLink->href = "/user/cody";
	$profileLink->setAttribute("data-user-id", "42");

	$status = $document->querySelector(".status");
	$status->classList->remove("is-offline");
	$status->classList->add("is-online");
}

innerText is usually the most natural choice for visible text in HTML. Properties such as href, src, value, hidden, title, and tabIndex can also be manipulated directly, and classList gives a clean interface for adding, removing, replacing, and toggling classes.

Read more in the main DOM docs: https://www.php.gt/docs/Dom/Working-with-HTML-specific-helpers/

Creating and inserting elements

Direct DOM work becomes especially useful when the page structure itself has to change.

use GT\Dom\HTMLDocument;

function go(HTMLDocument $document):void {
	$list = $document->createElement("ul");

	foreach(["Draft", "Review", "Published"] as $label) {
		$item = $document->createElement("li");
		$item->innerText = $label;
		$list->appendChild($item);
	}

	$panel = $document->querySelector("#status-panel");
	$panel?->appendChild($list);
}

The same approach works for adding banners, wrapping existing content, inserting separators, or replacing one structure with another. Because the page is a real document tree, those structural changes stay explicit and local.

Navigating relationships

DOM work is not only about selectors from the root document. Often the useful move is relative navigation from an element we already have:

use GT\Dom\HTMLDocument;

function go(HTMLDocument $document):void {
	$error = $document->querySelector(".field-error");
	$group = $error?->closest(".form-group");
	$group?->classList->add("has-error");

	$notice = $document->querySelector(".notice");
	$notice?->before($document->createElement("hr"));
}

Properties such as parentElement, children, nextElementSibling, and methods such as closest() make it possible to express document-local logic without repeatedly searching the whole page.

Practical guidance

Prefer binding for routine content output where the HTML structure already exists and only the values change. Reach for direct DOM manipulation when:

  • the page structure must be inserted, removed, or rearranged
  • one element's state depends on another element's location or relationship
  • repeated binding keys would be more awkward than just editing the relevant nodes directly
  • the behaviour is naturally expressed as document operations rather than data mapping

If the PHP keeps reaching deep into many unrelated parts of the page, that is usually a sign the view structure should be improved, or that some of the work should move back into binding data to the DOM, custom HTML components, or page partials.

For deeper DOM coverage, the best entry points in the main docs are:


See how binding data to the DOM can keep dynamic pages clean and maintainable, or move on to work with user input.

Clone this wiki locally