Joe's
Digital Garden

Validator

The Interface

interface IValidator
{
    /**
     * Normalize the request
     * @return $this
     */
    public function filter(): IValidator;

    /**
     * Retrieve any validation errors
     * @return ErrorCollection
     */
    public function getErrors(): array;

    /**
     * Retrieve the request
     * @return array
     */
    public function getRequest(): array;

    /**
     * Validate the request
     * @return $this
     */
    public function validate(): IValidator;
}

A request validator handles the evaluation and normalization of an HTTP request. In this manner it checks if the request array contains valid parameters. Common validation rules are

  • Are all required fields present and set?
  • Are values representing integers contain strings that evaluate to integers?
  • Are values representing dates as strings evaluate to valid dates?
  • Do values that must exist in a specified range do so?
  • Do values that can only be set equal to a set of defined constants do so?

The validate method performs these validations, but may also delegate to strategies, closures, or other methods on the class. When an error occurrs, an exception is created and set on an internal array.

Post validation, the request is then filtered. This cleans up the request, removing superflous fields and setting default values for optional, missing parameters. In this way we always have a normalized request and do not need to evaluate if a missing key is present.

The validator should not touch the domain model. Concerns like whether an id represents an existing entity in the domain, or whether a particular account can access a specific entity should occur further down the stack. Likewise, whether the authenticated user is authorized for this request should occur earlier in the router. This is only to ensure that the request is well formed. Not that it is allowed or can be executed.

Concrete Example

class UpdateCustomerValidator implements IValidator
{
    const 
        FIELD_MISSING = 'The %s field is required',
        INTEGER_INVALID = 'The %s field must be an integer value'
    ;

    private $errors;
    private $request;

    public function __construct(array $request)
    {
        $this->request = $request;
        $this->errors = [];
    }

    public function filter(): IValidator
    {
        $this->request = [
            'id'        => (int)$this->request['id'] ?? null,
            'firstName' => $this->request['firstName'] ?? '',
            'lastName'  => $this->request['lastName'] ?? '',
        ];

        return $this;
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    public function getRequest(): array
    {
        return $this->request;
    }

    public function validate(): IValidator
    {
        if (empty($this->request['id'])) {
            $this->errors[] = new \RuntimeException(
                sprintf(self::FIELD_MISSING, 'id')
            );
        }

        if (!is_numeric($this->request['id'])) {
            $this->errors[] = new \RuntimeException(
                sprintf(self::INTEGER_INVALID, 'id')
            );
        }

        return $this;
    }
}

$UpdateCustomerValidator = new UpdateCustomerValidator($_POST);
$UpdateCustomerValidator->validate()->filter();

if (!empty($UpdateCustomerValidator->getErrors())) {
    // Handle Errors
}

$request = $UpdateCustomerValidator->getRequest();

This validator validates an attempt to update an existing Customer entity. It receives the values posted to the server out of the $_POST superglobal, validates the id field exists and is numeric. If the errors array is not empty, we know validation failed and can execute on this knowlegde. We then filter the request so we have an array that always has an id, firstName and lastName with default empty strings for firstName an lastName. After fetching this array from the validator code further down can act with the assurance that these properties exist and contain a valid value.

Finally, note that exception error messages should always be a constant value and not just an arbitrary string in the exception constructor. Where these constants are set varies by context and necessity for covering multiple languages.

Linked References

  • layered-web-application-architecture
    • Controller uses a [[Validator]] to validate and normalize the request
  • php-application-service

    In practice, the Application Service is an instance of the Command pattern -- a closure in the form of a class. It's constructor wraps several utilities and only a single public method exists to accept the request array (previously normalized by a [[Validator]] instance).

  • php-application-service

    The Application Service executes on an request (not shown, this request is retrieved from a [[Validator]] object). Here we validate that the request is possible against the model itself, that is, does the necessary entities exist in our system (in this case a pre-existing Customer entity) and are they in a state that can accept the request itself (necessary if the state change requires some predicate). The Customer state is then updated per the request and returned.