Skip to content

Enforcing validation on user input

Greg Bowler edited this page May 7, 2026 · 17 revisions

Client-side validation in the browser is useful, but it is not enough on its own. Requests can be replayed, forged, edited, or submitted from code that never touched the browser at all.

DomValidation is the server-side step: we pass it the form element and the submitted data, and it checks the input against the rules declared in the HTML.

The basic flow

  1. Get a reference to the <form> element in a server-side HTMLDocument.
  2. Create a Validator.
  3. Call validate($form, $input).
  4. Catch ValidationException if anything is invalid.
  5. Read the error messages from getLastErrorList().

Minimal example

use GT\Dom\HTMLDocument;
use GT\DomValidation\ValidationException;
use GT\DomValidation\Validator;

// In this example, $input is a key-value-pair of values from the POST data.
function handleSubmit(HTMLDocument $document, array $input):void {
	$form = $document->querySelector("#example-form");
	$validator = new Validator();

	try {
		$validator->validate($form, $input);
	}
	catch(ValidationException) {
		foreach($validator->getLastErrorList() as $name => $message) {
			$field = $form->querySelector("[name='$name']");
			$field?->parentElement?->dataset->validationError = $message;
		}

		return;
	}

	// Carry on processing the valid input here.
}

What validate() expects

The Validator::validate() method takes two arguments:

1. The form element

The first argument must be the form element we want to validate against.

$form = $document->forms[0];

Use the actual form element, because the library reads the rule definitions from the current DOM:

  • attributes such as required, pattern, type, min, max, and step
  • <option> values in a <select>
  • available values in radio and checkbox groups

2. The submitted input

The second argument may be:

  • an associative array
  • any iterable object
  • an object with public properties
  • an object that provides an asArray() method

That makes the validator easy to use with plain $_POST, with PHP.GT/Input, and with other request wrappers.

There is more detail on accepted input shapes in Working with input data.

What happens on failure

If one or more fields are invalid, validate() throws a ValidationException.

The exception message is intentionally broad:

  • There is 1 invalid field
  • There are 2 invalid fields

The useful detail lives in the validator's ErrorList, which is available immediately afterwards through:

$validator->getLastErrorList()

That object can be iterated like this:

foreach($validator->getLastErrorList() as $name => $message) {
	// ...
}

The key is the form field's name. The value is the combined human-readable hint for that field.

Plain PHP example

use GT\Dom\HTMLDocument;
use GT\DomValidation\ValidationException;
use GT\DomValidation\Validator;

$document = new HTMLDocument(file_get_contents("payment-form.html"));

if(($_POST["do"] ?? "") === "buy") {
	$form = $document->querySelector("#payment-form");
	$validator = new Validator();

	try {
		$validator->validate($form, $_POST);
		echo "Payment succeeded!";
		exit;
	}
	catch(ValidationException) {
		foreach($validator->getLastErrorList() as $name => $message) {
			$field = $form->querySelector("[name='$name']");
			$field?->parentElement?->dataset->validationError = $message;
		}
	}
}

echo $document;

WebEngine example

Note

In WebEngine, the current HTMLDocument and the request Input object are already part of the usual page-logic workflow, which makes DomValidation a natural fit.

use GT\Dom\HTMLDocument;
use GT\DomValidation\ValidationException;
use GT\DomValidation\Validator;
use GT\Input\Input;

function do_submit(
	HTMLDocument $document,
	Input $input,
):void {
	$form = $document->querySelector("#example-form");
	$validator = new Validator();

	try {
		$validator->validate($form, $input);
	}
	catch(ValidationException) {
		foreach($validator->getLastErrorList() as $name => $message) {
			$field = $form->querySelector("[name='$name']");
			$field?->parentElement?->dataset->validationError = $message;
		}

		return;
	}

	// Continue with the valid submission.
}

A practical habit

Once validation fails, return early.

That keeps the code simple and avoids accidentally continuing with invalid data.

catch(ValidationException) {
	// display errors...
	return;
}

Once the basic flow is clear, continue with Working with input data to see how field names and submitted values are matched.