Skip to content

Extending custom rules

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

The built-in rules cover common HTML constraints, but real applications usually need a few rules that are specific to their own data and workflows.

Typical examples include:

  • a password must not contain the username
  • a username must not already exist
  • one date must be later than another
  • an invite code must match a value stored elsewhere

These are all beyond what a generic HTML attribute can express, which is why DomValidation lets us add our own rule objects.

The Rule and ValidationRules classes

Custom validation uses two related types:

  • Rule: a single validation rule
  • ValidationRules: a collection of rules used by the validator

Most projects will start from DefaultValidationRules so that the standard HTML rules still apply.

A small custom rule

This example makes a password invalid if it contains the submitted username.

use GT\Dom\Element;
use GT\DomValidation\Rule\Rule;

class UsernameNotWithinPassword extends Rule {
	public function isValid(
		Element $element,
		string|array $value,
		array $inputKvp,
	):bool {
		if($element->type !== "password") {
			return true;
		}

		if(!is_string($value)) {
			return true;
		}

		return !str_contains($value, $inputKvp["username"] ?? "");
	}

	public function getHint(Element $element, string $value):string {
		return "The password must not contain the username";
	}
}

There are three things to notice here:

  1. The rule receives the current element.
  2. It also receives the current field value.
  3. It receives the whole submitted input array, which makes cross-field checks possible.

Adding the custom rule to the defaults

use GT\DomValidation\DefaultValidationRules;

$rules = new DefaultValidationRules();
$rules->addRule(new UsernameNotWithinPassword());

Then pass that rule collection to the validator:

use GT\DomValidation\Validator;

$validator = new Validator($rules);

Full example

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

$document = new HTMLDocument($html);
$form = $document->forms[0];

$rules = new DefaultValidationRules();
$rules->addRule(new UsernameNotWithinPassword());

$validator = new Validator($rules);

try {
	$validator->validate($form, [
		"username" => "cody",
		"password" => "cody-password",
	]);
}
catch(ValidationException) {
	foreach($validator->getLastErrorList() as $name => $message) {
		// Render the error message however the application prefers.
	}
}

When to override getExceptionClass()

Every rule may optionally return a more specific exception class.

If we do not override getExceptionClass(), the base Rule class returns ValidityStateException.

That is usually enough if we only care about the error messages. Override it when we want to distinguish one category of validation problem from another at exception level.

A good rule-writing habit

Try to keep each custom rule small and easy to name.

This is better:

  • UsernameNotWithinPassword
  • EndDateAfterStartDate
  • InviteCodeIsKnown

Than one large rule object that tries to validate half the form at once.

Small rules are easier to test, easier to reuse, and easier to explain when someone reads the code later.


If you want to understand the exception types and grouped messages that come back from the validator, go to Understanding validation errors.