Skip to content

Instantly share code, notes, and snippets.

@thepsion5
Created November 23, 2021 20:43
Show Gist options
  • Save thepsion5/4cdcb2de3619639eb446a1588d21f1bb to your computer and use it in GitHub Desktop.
Save thepsion5/4cdcb2de3619639eb446a1588d21f1bb to your computer and use it in GitHub Desktop.
Example of A Flexible Validation Solution Using Classes For Validation Rules
<?php
/**
* Validates a value based on its length as a string
*
* ```php
* $toBeValidated = 17;
* $validationParameters = [
* 'min' => 10
* ];
* $errorMessage = '';
*
* $rule = new MinLengthValidationRule();
* $isValid = $rule->isValid($toBeValidated, $validationParameters, $errorMessage);
*
* // true
* echo $isValid;
* ```
*/
class MinLengthValidationRule implements ValidationRule
{
/**
* The name of the rule
*/
private CONST RULE_NAME = 'min_length';
/**
* The message used when validation fails, with replacements for the field
* name and parameters
*/
private CONST FAILURE_MESSAGE = '%s must have a length of %d or greater.';
/**
* @return string The name of this rule as a string
*/
public function getRuleName(): string
{
return $this::RULE_NAME;
}
/**
* Validates the provided value by checking its string length against a
* specified minimum
*
* @param mixed $value The value to be validated
* @param array $ruleParameters
* An associative array containing:
* - min: An integer with the minimum required length
* @param string $errorMessage Error message string populated if validation fails
* @return bool True if the provided value is valid, false otherwise
* @throws LogicException If the provided "min" rule parameter is invalid
*/
public function isValid($value, array $ruleParameters, string &$errorMessage): bool
{
$minLength = isset($ruleParameters['min']) ? (int) $ruleParameters['min'] : 0;
if (!array_key_exists('min', $ruleParameters)) {
throw new LogicException('The "min" parameter is required and must be an integer greater than zero');
}
$valid = strlen((string) $value) >= $minLength;
if (!$valid) {
$errorMessage = sprintf($this::FAILURE_MESSAGE, '%s', $minLength);
}
return $valid;
}
}
<?php
/**
* Defines a rule for validating a single value including an arbitrary set of parameters
*/
interface ValidationRule
{
/**
* The name of the validation rule
*
* @return string
*/
public function getRuleName(): string;
/**
* Checks whether the provided value is valid based on the given parameters,
* if applicable. If the value does not pass validation $errorMessage is
* populated with an error message containing "%s" as a replacement for the
* field name
*
* NOTE: The choice to use a parameter to hold the error message is an unusual
* one, but I wanted to keep this class entirely stateless and felt this was
* the best way
*
* @param mixed $value Some value to be validated
* @param array $ruleParameters Named parameters required for the validation,
* if any
* @param string $errorMessage Empty string passed to the validation method,
* only populated if validation fails
* @return bool True if $value is valid based on the rule and supplied
* parameters, false otherwise
*/
public function isValid($value, array $ruleParameters, string &$errorMessage): bool;
}
<?php
/**
* Validates some payload based on a given set of rules and stores any errors
* associated with the last payload
*
* ```php
* $payload = [
* 'first_name' => 'Sean',
* 'middle_name' => '',
* 'last_name' => 'Mumford',
* ];
* $ruleset = [
* 'first_name' => [
* 'min_length' => ['min' => 2]
* ],
* 'last_name' => [
* 'min_length' => ['min' => 2]
* ],
* ];
*
* $validator = new Validator();
* $validator->addRule(new MinLengthValidationRule);
*
* $isValid = $validator->validate($ruleset, $payload);
* if (!$isValid) {
* $_SESSION['message'] = 'Please correct the error with your submission before proceeding.';
* $_SESSION['errors'] = $validator->getErrors();
* header('Location: form.php');
* die();
* }
* ```
*/
class Validator
{
/**
* Current validation rules
*
* @var ValidationRule[]
*/
private $rules = [];
/**
* Set of errors, grouped by field name and then rule name
*
* @var array[]
*/
private $errors = [];
/**
* Sets a rule, replacing any previously set rule with that name
*
* @param ValidationRule $rule
*/
public function setRule(ValidationRule $rule): void
{
$ruleName = $rule->getRuleName();
$this->rules[$ruleName] = $rule;
}
/**
* Checks to see whether there is already an existing rule with the same
* set with this validator
*
* @param string|ValidationRule $rule A rule name of ValidationRule instance
* to check
* @return bool true if a rule of the same name already exists, false
* otherwise
*/
public function hasRule($rule): bool
{
$ruleName = ($rule instanceof ValidationRule) ? $rule->getRuleName() : $rule;
return array_key_exists($ruleName, $this->rules);
}
/**
* Adds an error to the list of errors for a particular field for a particular
* rule violation
*
* @param string $paramName The name of the parameter to which the error applies
* @param string $ruleName The rule to which the error applies
* @param string $errorMessage The text of the error message
*/
public function addError(string $paramName, string $ruleName, string $errorMessage)
{
if (!isset($this->errors[$paramName][$ruleName])) {
$this->errors[$paramName][$ruleName] = [];
}
$this->errors[$paramName][$ruleName][] = $errorMessage;
}
/**
* Removes all currently-stored errors
*/
public function clearErrors(): void
{
$this->errors = [];
}
/**
* Returns whether there are currently any errors stored
*
* @return bool
*/
public function hasErrors(): bool
{
return count($this->errors) > 0;
}
/**
* Validates an array of data based on the provided ruleset
*
* @param array $ruleset An associative array with the field name as the key
* and an array of rules and their parameters as a value:
* ```php
* $ruleset = [
* 'first_name' => [
* 'min_length' => ['min' => 2]
* ],
* 'last_name' => [
* 'min_length' => ['min' => 2]
* ],
* ];
* ```
*
* @param array $payload An associative array of values to validate
* @return bool True if there are no errors associated with the provided values and ruleset, false otherwise
*/
public function validate(array $ruleset, array $payload): bool
{
$this->clearErrors();
foreach($ruleset as $fieldName => $fieldRules) {
$fieldValue = $payload[$fieldName] ?? null;
$this->validateField($fieldName, $fieldValue, $fieldRules);
}
return $this->hasErrors();
}
/**
* Validates a single field based on the provided validation rules
*
* @param string $fieldName The name of the field to validate
* @param mixed $fieldValue The value of the field
* @param array $fieldRules An associative array containing a rule name as the
* key and an array of rule parameters
*/
private function validateField(string $fieldName, $fieldValue, array $fieldRules)
{
foreach($fieldRules as $ruleName => $ruleParams) {
if (!array_key_exists($ruleName, $this->rules)) {
throw new LogicException("Unknown rule '$ruleName'.");
}
$errorMessage = '';
$valid = $this->rules[$ruleName]->validate($fieldValue, $ruleParams, $errorMessage);
if(!$valid) {
$this->addError($fieldName, $ruleName, $errorMessage);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment