Skip to content

Commit

Permalink
Checkbox validity (#51)
Browse files Browse the repository at this point in the history
* ci: scrutinizer simplification

* tweak: grammar fix

* feature: allow multiple values like checkboxes
closes #22

* build: install with PHP 8.1

* tweak: test correct checkable
  • Loading branch information
g105b authored Jan 6, 2024
1 parent c5751b4 commit d5a7331
Show file tree
Hide file tree
Showing 21 changed files with 588 additions and 296 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"require": {
"php": ">=8.1",
"phpgt/cssxpath": "1.*",
"phpgt/dom": "^4.1"
"phpgt/dom": "^4.1.6"
},

"require-dev": {
Expand Down
543 changes: 288 additions & 255 deletions composer.lock

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions example/02-checkboxes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php
/**
* To run this example, from a terminal navigate to the example directory and run:
* php -S 0.0.0.0:8080
* then visit http://localhost:8080/02-checkboxes.php in your web browser.
*
* The purpose of this example is to show how an invalid state occurs when
* the user hacks the client-side to allow submitting a field that doesn't exist
* in the source HTML.
*/

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

require __DIR__ . "/../vendor/autoload.php";

$html = <<<HTML
<!doctype html>
<style>
[data-validation-error] {
border-left: 2px solid red;
}
[data-validation-error]::before {
content: attr(data-validation-error);
color: red;
font-weight: bold;
}
label {
display: block;
padding: 1rem;
}
label span {
display: block;
}
</style>
<!doctype html>
<form method="post">
<fieldset>
<legend>Available currencies</legend>
<label>
<input type="checkbox" name="currency[]" value="GBP" />
<span>£ Pound (GBP)</span>
</label>
<label>
<input type="checkbox" name="currency[]" value="USD" />
<span>$ Dollar (USD)</span>
</label>
<label>
<input type="checkbox" name="currency[]" value="EUR" />
<span>€ Pound (EUR)</span>
</label>
</fieldset>
<button name="do" value="submit">Submit</button>
</form>
HTML;

function example(HTMLDocument $document, array $input) {
$validator = new Validator();
$form = $document->forms[0];

try {
$validator->validate($form, $input);
}
catch(ValidationException $exception) {
foreach($validator->getLastErrorList() as $name => $message) {
$errorElement = $form->querySelector("[name=$name]");
$errorElement->parentNode->dataset->validationError = $message;
}
return;
}

echo "Currencies selected: ";
echo implode(", ", $input["currency"]);
exit;
}

$document = new HTMLDocument($html);

if(isset($_POST["do"]) && $_POST["do"] === "submit") {
example($document, $_POST);
}

echo $document;
2 changes: 2 additions & 0 deletions src/DefaultValidationRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Gt\DomValidation\Rule\MaxLength;
use Gt\DomValidation\Rule\MinLength;
use Gt\DomValidation\Rule\Pattern;
use Gt\DomValidation\Rule\TypeCheckbox;
use Gt\DomValidation\Rule\TypeRadio;
use Gt\DomValidation\Rule\Required;
use Gt\DomValidation\Rule\SelectElement;
Expand All @@ -25,6 +26,7 @@ protected function setRuleList():void {
new MinLength(),
new MaxLength(),
new TypeRadio(),
new TypeCheckbox(),
];
}
}
2 changes: 1 addition & 1 deletion src/Rule/MaxLength.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class MaxLength extends Rule {
"maxlength"
];

public function isValid(Element $element, string $value, array $inputKvp):bool {
public function isValid(Element $element, string|array $value, array $inputKvp):bool {
$maxLength = $element->getAttribute("maxlength");
return strlen($value) <= $maxLength;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/MinLength.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class MinLength extends Rule {
"minlength"
];

public function isValid(Element $element, string $value, array $inputKvp):bool {
public function isValid(Element $element, string|array $value, array $inputKvp):bool {
$minLength = $element->getAttribute("minlength");
return strlen($value) >= $minLength;
}
Expand Down
6 changes: 1 addition & 5 deletions src/Rule/Pattern.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ class Pattern extends Rule {
"pattern",
];

public function isValid(
Element $element,
string $value,
array $inputKvp,
):bool {
public function isValid(Element $element, string|array $value, array $inputKvp):bool {
$pattern = "/" . $element->getAttribute("pattern") . "/u";
return (bool)preg_match($pattern, $value);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/Required.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Required extends Rule {
"required",
];

public function isValid(Element $element, string $value, array $inputKvp):bool {
public function isValid(Element $element, string|array $value, array $inputKvp):bool {
return !empty($value);
}

Expand Down
7 changes: 5 additions & 2 deletions src/Rule/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ public function getAttributes():array {
return $this->attributes;
}

/** @param array<string, string> $inputKvp */
/**
* @param string|array<string> $value Either a single string or multiple string values
* @param array<string, string> $inputKvp
*/
abstract public function isValid(
Element $element,
string $value,
string|array $value,
array $inputKvp,
):bool;

Expand Down
2 changes: 1 addition & 1 deletion src/Rule/SelectElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Gt\Dom\ElementType;

class SelectElement extends Rule {
public function isValid(Element $element, string $value, array $inputKvp):bool {
public function isValid(Element $element, string|array $value, array $inputKvp):bool {
$availableValues = [];

if($element->elementType !== ElementType::HTMLSelectElement) {
Expand Down
40 changes: 40 additions & 0 deletions src/Rule/Trait/Checkable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
namespace Gt\DomValidation\Rule\Trait;

use Gt\Dom\Element;

/**
* Elements that can have the `checked` attribute (radio buttons and checkboxes)
* are considered "Checkable", and their validity is dependent on other elements
* of the same name within the form.
*/
trait Checkable {
/** @param string|array $value */
private function checkedValueIsAvailable(Element $element, string|array $value):bool {
$availableValues = [];
$name = $element->name;

/** @var Element $otherInput */
foreach($element->form->getElementsByName($name) as $otherInput) {
if($radioValue = $otherInput->value) {
array_push($availableValues, $radioValue);
}
}

$checkedValueList = [];
if(is_array($value)) {
$checkedValueList = $value;
}
else {
$checkedValueList = [$value];
}

foreach($checkedValueList as $checkedValue) {
if(!in_array($checkedValue, $availableValues)) {
return false;
}
}

return true;
}
}
37 changes: 37 additions & 0 deletions src/Rule/TypeCheckbox.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
namespace Gt\DomValidation\Rule;

use Gt\Dom\Element;
use Gt\Dom\ElementType;
use Gt\DomValidation\Rule\Trait\Checkable;

class TypeCheckbox extends Rule {
use Checkable;

public function isValid(Element $element, string|array $value, array $inputKvp):bool {
if($element->elementType !== ElementType::HTMLInputElement) {
return true;
}
if($element->type !== "checkbox") {
return true;
}

if($value === "") {
return true;
}

if(!$element->form) {
return true;
}

if(!$this->checkedValueIsAvailable($element, $value)) {
return false;
}

return true;
}

public function getHint(Element $element, string $value):string {
return "This field's value must match one of the available options";
}
}
8 changes: 2 additions & 6 deletions src/Rule/TypeDate.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,14 @@ class TypeDate extends Rule {
"type=time",
];

public function isValid(
Element $element,
string $value,
array $inputKvp
):bool {
public function isValid(Element $element, string|array $value, array $inputKvp):bool {
if($value === "") {
return true;
}

$dateTime = $this->extractDateTime(
$value,
$element->getAttribute("type")
$element->getAttribute("type") ?? ""
);

return !is_null($dateTime);
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/TypeEmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class TypeEmail extends Rule {
"type=email",
];

public function isValid(Element $element, string $value, array $inputKvp):bool {
public function isValid(Element $element, string|array $value, array $inputKvp):bool {
return $value === ""
|| filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
}
Expand Down
6 changes: 1 addition & 5 deletions src/Rule/TypeNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@ class TypeNumber extends Rule {
"type=range",
];

public function isValid(
Element $element,
string $value,
array $inputKvp,
):bool {
public function isValid(Element $element, string|array $value, array $inputKvp):bool {
if($value === "") {
return true;
}
Expand Down
17 changes: 5 additions & 12 deletions src/Rule/TypeRadio.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

use Gt\Dom\Element;
use Gt\Dom\ElementType;
use Gt\DomValidation\Rule\Trait\Checkable;

class TypeRadio extends Rule {
public function isValid(Element $element, string $value, array $inputKvp):bool {
use Checkable;

public function isValid(Element $element, string|array $value, array $inputKvp):bool {
if($element->elementType !== ElementType::HTMLInputElement) {
return true;
}
Expand All @@ -21,17 +24,7 @@ public function isValid(Element $element, string $value, array $inputKvp):bool {
return true;
}

$availableValues = [];
$name = $element->name;

/** @var Element $siblingInput */
foreach($element->form->querySelectorAll("[name='$name']") as $siblingInput) {
if($radioValue = $siblingInput->value) {
array_push($availableValues, $radioValue);
}
}

if(!in_array($value, $availableValues)) {
if(!$this->checkedValueIsAvailable($element, $value)) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Rule/TypeUrl.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class TypeUrl extends Rule {
"type=url",
];

public function isValid(Element $element, string $value, array $inputKvp):bool {
public function isValid(Element $element, string|array $value, array $inputKvp):bool {
return $value === ""
|| filter_var($value, FILTER_VALIDATE_URL) !== false;
}
Expand Down
15 changes: 12 additions & 3 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function __construct(?ValidationRules $rules = null) {
$this->rules = $rules;
}

/** @param iterable<string, string> $inputKvp Associative array of user input */
/** @param iterable<string, string|array<string>> $inputKvp Associative array of user input */
public function validate(Element $form, iterable|object $inputKvp):void {
$this->errorList = new ErrorList();

Expand Down Expand Up @@ -51,7 +51,7 @@ public function getLastErrorList():ErrorList {

/**
* @param array<Rule> $ruleArray
* @param array<string, string> $inputKvp
* @param array<string, string|array<string>> $inputKvp
*/
protected function buildErrorList(
Element $form,
Expand All @@ -62,12 +62,13 @@ protected function buildErrorList(
/** @var Element $element */
foreach ($form->querySelectorAll("[$attrString]") as $element) {
$name = $element->getAttribute("name");
$name = strtok($name, "[]");

foreach ($ruleArray as $rule) {
if (!$rule->isValid($element, $inputKvp[$name] ?? "", $inputKvp)) {
$this->errorList->add(
$element,
$rule->getHint($element, $inputKvp[$name] ?? "")
$rule->getHint($element, $this->normalise($inputKvp[$name]))
);
}
}
Expand Down Expand Up @@ -97,4 +98,12 @@ private function convertObjectToKvp(object $obj):array {
}
return $array;
}

private function normalise(array|string|null $input):string {
if(is_string($input)) {
return $input;
}

return implode(", ", $input ?? []);
}
}
Loading

0 comments on commit d5a7331

Please sign in to comment.