0
\$\begingroup\$

This question is a sub-post to this question

This post is a follow up for this and this.

"In this particular question I need the reviewers help to judge my Validator class"


Validator.class.php

a class designed to get used by the controllers. it has one purpose "handle client inputs"

<?php
namespace aap;

// flags
define('F_REQUIRED', 1);
define('F_MUSTBESET', 2);
define('F_ESCAPE', 4); // if data is valid it can work only with STRING and EMAIL
define('F_SANITIZE', 8); // if data is valid it can work only with STRING and EMAIL

/*
 * define('F_SETIFNOT', 16); // this flag is uselss since passing the $data var
 * by reference will make php define it as null anyway
 * see my question at :http://stackoverflow.com/questions/41483976
 */

// filter types
define('INT', 1);
define('STRING', 2);
define('FLOAT', 3);
define('DATE', 4);
define('EMAIL', 5);
// define('LONGSTRING', 5);

// error types
define('ER_NOT_SET', 10);
define('ER_EMPTY', 11);
define('ER_DATA', 12);

class Validator
{

    protected $options = [];

    protected $errors = [];

    protected $sysModel;

    public function __construct(SystemModel $sysModel, array $options = [])
    {
        $this->sysModel = $sysModel;
        $this->resetOptions();
        $this->setOptions($options);
    }

    public function resetOptions()
    {
        // defaults
        $this->options[INT]['min'] = - 1 * PHP_INT_MAX - 1;
        $this->options[INT]['max'] = PHP_INT_MAX;
        $this->options[STRING]['length'] = 80;
        $this->options[STRING]['regex'] = "/[^a-zA-Z0-9_ أ-ي]/u";
        $this->options[STRING]['ischarsinvalid'] = true;
        $this->options[FLOAT]['min'] = 0.0;
        $this->options[FLOAT]['max'] = INF;
        $this->options[DATE]['min'] = "1970-01-01"; // mysql timestamp range
        $this->options[DATE]['max'] = "2038-01-18"; // mysql timestamp range
        $this->options[DATE]['format'] = "Y-m-d"; // 2 formats only are supported Y-m-d H:i:s
    }

    /*
     * function validate is designed to report errors in the object $error array
     * @return: the variable $data if it is valid or return false if it is not valid
     * @note: passing by reference will make the
     */
    public function validate(&$data, $varName, $filterType, $flags = null)
    {
        if (! isset($data) && ($flags & F_MUSTBESET)) {
            $this->errors[$varName]['type'] = ER_NOT_SET;
            return false;
        }

        if (empty($data) && ($flags & F_REQUIRED)) {
            $this->errors[$varName]['type'] = ER_EMPTY;
            return false;
        }

        switch ($filterType) {
            case INT:
                if (isset($data) && ($data < $this->options[INT]['min'] || $data > $this->options[INT]['max'])) {
                    $this->errors[$varName]['type'] = ER_DATA;
                    $data = $this->checkEscapeAndSanitize($data, $flags);
                    return false;
                }
                return $data;
                break;
            case STRING:
                // @formatter:off
                if (! empty($data) && (strlen($data) > $this->options[STRING]['length'] || (preg_match($this->options[STRING]['regex'], $data) && $this->options[STRING]['ischarsinvalid']) || (! preg_match($this->options[STRING]['regex'], $data) && ! $this->options[STRING]['ischarsinvalid']))) {
                    // @formatter:on
                    $this->errors[$varName]['type'] = ER_DATA;
                    $data = $this->checkEscapeAndSanitize($data, $flags);
                    return false;
                }
                $data = $this->checkEscapeAndSanitize($data, $flags);
                return $data;
                break;
            case FLOAT:
                if (! empty($data) && ($data < $this->options[FLOAT]['min'] || $data > $this->options[FLOAT]['max'])) {
                    $this->errors[$varName]['type'] = ER_DATA;
                    $data = $this->checkEscapeAndSanitize($data, $flags);
                    return false;
                }
                return $data;
                break;
            case DATE:
                if (! empty($data) && ! Validator::isDate($data, $this->options[DATE]['min'], $this->options[DATE]['max'], $this->options[DATE]['format'])) {
                    $this->errors[$varName]['type'] = ER_DATA;
                    $data = $this->checkEscapeAndSanitize($data, $flags);
                    return false;
                }
                return $data;
                break;
            case EMAIL:
                if (! empty($data) && filter_var($data, FILTER_VALIDATE_EMAIL) === false) {
                    $this->errors[$varName]['type'] = ER_DATA;
                    $data = $this->checkEscapeAndSanitize($data, $flags);
                    return false;
                }
                $data = $this->checkEscapeAndSanitize($data, $flags);
                return $data;
                break;
            default:
                throw new \InvalidArgumentException("'{$filterType}'filterType value is an unknown filter type!");
                break;
        }
    }

    protected function checkEscapeAndSanitize($data, $flags)
    {
        if ($flags & F_ESCAPE) {
            $data = $this->sysModel->escape($data);
        }
        if ($flags & F_SANITIZE) {
            $data = htmlspecialchars($data);
        }
        return $data;
    }

    public function flushErrors()
    {
        $this->errors = [];
    }

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

    public function setOptions(array $options)
    {
        foreach ($options as $type => $value) {
            switch ($type) {
                case INT:
                    foreach ($value as $option => $optionValue) {
                        switch ($option) {
                            case "min":
                                if (! filter_var($options[INT]['min'], FILTER_VALIDATE_INT)) {
                                    throw new \InvalidArgumentException("'{$options[INT]['min']}'[int][min] value is not an integer!");
                                } else {
                                    $this->options[INT]['min'] = $options[INT]['min'];
                                }
                                break;
                            case "max":
                                if (! filter_var($options[INT]['max'], FILTER_VALIDATE_INT)) {
                                    throw new \InvalidArgumentException("'{$options[INT]['max']}'[int][max] value is not an integer!");
                                } else {
                                    $this->options[INT]['max'] = $options[INT]['max'];
                                }
                                break;
                            default:
                                throw new \InvalidArgumentException("'$option' is not a valid option name for int data types!");
                                break;
                        }
                    }
                    break;
                case STRING:
                    foreach ($value as $option => $optionValue) {
                        switch ($option) {
                            case "length":
                                if (! filter_var($options[STRING]['length'], FILTER_VALIDATE_INT, array(
                                    "options" => array(
                                        "min_range" => 0
                                    )
                                ))) {
                                    throw new \InvalidArgumentException("'{$options[STRING]['length']}'[string][length] value is not a positive integer!");
                                } else {
                                    $this->options[STRING]['length'] = $options[STRING]['length'];
                                }
                                break;
                            case "regex":
                                $this->options[STRING]['regex'] = $options[STRING]['regex'];
                                break;
                            case "ischarsinvalid":
                                if (! is_bool($options[STRING]['ischarsinvalid'])) {
                                    throw new \InvalidArgumentException("'{$options[STRING]['ischarsinvalid']}'[string][ischarsinvalid] value is not a bool!");
                                } else {
                                    $this->options[STRING]['ischarsinvalid'] = $options[STRING]['ischarsinvalid'];
                                }
                                break;
                            default:
                                throw new \InvalidArgumentException("'$option' is not a valid option name for string data types!");
                                break;
                        }
                    }
                    break;
                case FLOAT:
                    foreach ($value as $option => $optionValue) {
                        switch ($option) {
                            case "min":
                                if (! is_numeric($options[FLOAT]['min'])) {
                                    throw new \InvalidArgumentException("'{$options[FLOAT]['min']}'[float][min] value is not numeric");
                                } else {
                                    $this->options[FLOAT]['min'] = $options[FLOAT]['min'];
                                }
                                break;
                            case "max":
                                if (! is_numeric($options[FLOAT]['max'])) {
                                    throw new \InvalidArgumentException("'{$options[FLOAT]['max']}'[float][max] value is not numeric!");
                                } else {
                                    $this->options[FLOAT]['max'] = $options[FLOAT]['max'];
                                }
                                break;
                            default:
                                throw new \InvalidArgumentException("'$option' is not a valid option name for float data types!");
                                break;
                        }
                    }
                    break;
                case DATE:
                    foreach ($value as $option => $optionValue) {
                        switch ($option) {
                            case "min":
                                if (! Validator::isDate($options[DATE]['min'])) {
                                    throw new \InvalidArgumentException("'{$options[DATE]['min']}'[date][min] value is not a date");
                                } else {
                                    $this->options[DATE]['min'] = $options[DATE]['min'];
                                }
                                break;
                            case "max":
                                if (! Validator::isDate($options[DATE]['max'])) {
                                    throw new \InvalidArgumentException("'{$options[DATE]['max']}'[date][max] value is not a date!");
                                } else {
                                    $this->options[DATE]['max'] = $options[DATE]['max'];
                                }
                                break;
                            case "format":
                                if ($options[DATE]['format'] != "Y-m-d H:i:s" && $options[DATE]['format'] != "Y-m-d") {
                                    throw new \InvalidArgumentException("'{$options[DATE]['format']}'[date][format] value is not a valid date format");
                                } else {
                                    $this->options[DATE]['format'] = $options[DATE]['format'];
                                }
                                break;
                            default:
                                throw new \InvalidArgumentException("'$option' is not a valid option name for date data types!");
                                break;
                        }
                    }
                    break;
                default:
                    throw new \InvalidArgumentException("'$type' is not a valid type name!");
                    break;
            }
            ;
        }
    }

    public static function compareDatetime($date1, $date2, $unit)
    {
        /*
         * compare 2 dates and return the difference between date1 and date2 \
         * (positive value or nigative)
         * @return date1- date2 or false on failure
         */
        //
        if ($unit != "h" && $unit != "i" && $unit != "s" && $unit != "d") {
            return false;
        }
        ;
        $date1 = strtotime($date1);
        $date2 = strtotime($date2);

        if ($date1 === false || $date2 === false) {
            return false;
        }
        ;
        $diff = $date1 - $date2;
        switch ($unit) {
            case "d":
                return (int) round($diff / (3600 * 24));
            case "h":
                return (int) round($diff / 3600);
            case "i":
                return (int) round($diff / 60);
            case "s":
                return (int) $diff;
        }
    }

    public static function isDate($date, $from = "0001-01-01", $to = "9999-12-31", $format = "Y-m-d")
    {
        // 2 formats only are supported Y-m-d H:i:s Y-m-d
        if ($format != "Y-m-d" && $format != "Y-m-d H:i:s") {
            return false;
        }

        if ($format == "Y-m-d") {
            if (! $from) {
                $from = "0001-01-01";
            }
            ;
            if (! $to) {
                $to = "9999-12-31";
            }
            ;
        } else {
            if (! $from) {
                $from = "0001-01-01 00:00:00";
            }
            ;
            if (! $to) {
                $to = "9999-12-31 00:00:00";
            }
            ;
        }

        if (($date = \DateTime::createFromFormat($format, $date)) === false) {
            return false;
        }
        if (\DateTime::getLastErrors()['warning_count'] > 0) {
            return false;
        }

        if (($from = \DateTime::createFromFormat($format, $from)) === false) {
            return false;
        }
        if (\DateTime::getLastErrors()['warning_count'] > 0) {
            return false;
        }

        if (($to = \DateTime::createFromFormat($format, $to)) === false) {
            return false;
        }
        if (\DateTime::getLastErrors()['warning_count'] > 0) {
            return false;
        }

        if ($date->format("U") < $from->format("U")) {
            return false;
        }
        if ($date->format("U") > $to->format("U")) {
            return false;
        }

        return true;
    }
}

an example of how I plan to use the class

    $validator = new Validator($sysModel);

    //validating inputs
    $options[STRING]['length'] = 20;
    $validator->setOptions($options);
    $validator->validate($_POST['name'], "name", STRING, F_MUSTBESET );
    $options[STRING]['regex'] = "/[^0-9 +]/u";
    $options[STRING]['length'] = 15;
    $validator->setOptions($options);
    $validator->validate($_POST['phone'], "phone", STRING, F_MUSTBESET);
    $validator->resetOptions();
    $options[STRING]['regex'] = "/.*/u";
    $options[STRING]['ischarsinvalid'] = false;
    $options[STRING]['length'] = 500;
    $validator->setOptions($options);
    $validator->validate($_POST['message'], "message", STRING, F_REQUIRED | F_MUSTBESET | F_ESCAPE | F_SANITIZE);
    $validator->validate($_POST['email'], "email", EMAIL, F_MUSTBESET | F_ESCAPE | F_SANITIZE);
    $validator->resetOptions();
    $validator->validate($_POST['title'], "title", STRING, F_MUSTBESET | F_ESCAPE | F_SANITIZE);
    $validator->validate($_POST['subject'], "subject", STRING, F_REQUIRED | F_MUSTBESET | F_ESCAPE | F_SANITIZE);
    $errorMessage = "";
    foreach ($validator->getErrors() as $error => $value) {
        switch ($error) {
            case "name":
                $contact->set("nameInput-class", "incorrect-field");
                $errorMessage .= "<li>name field is not correct!</li>\n";
                // switch ($value['type']) {case ER_NOT_SET:
                break;
            case "phone":
                $contact->set("phoneInput-class", "incorrect-field");
                $errorMessage .= "<li>phone field is not correct!</li>\n";
                break;
            case "email":
                $contact->set("emailInput-class", "incorrect-field");
                $errorMessage .= "<li>email field is not correct!</li>\n";
                break;
            case "subject":
                $contact->set("subjectInput-class", "incorrect-field");
                $errorMessage .= "<li>invalid input!</li>\n";
                break;
            case "title":
                $contact->set("titleInput-class", "incorrect-field");
                $errorMessage .= "<li>title field is not correct!</li>\n";
                break;
            case "message":
                $contact->set("messageInput-class", "incorrect-field");
                $errorMessage .= "<li>message field is not correct!</li>\n";
                break;
        }
    }
\$\endgroup\$
1
\$\begingroup\$

I see what you want to achieve, but I guess I would take a different approach.

First you might run into probems with the $options variable in your example usage. If you forget to reset it, older set values will remain and lead to unexpected results.

I would not use such a universial validate() method. I would split it into different methods for text/string, integers, floats, email, ... . That would make calling the validator more intuitive. Because you do not need to remember the correct way to check if the value is a valid email, you just need to call $validator->validateEmail($email);

If you check for a number, I would go for

public function validateInteger($value, $min = null, $max = null) { 
    // code to check here 
}

See how that make using it a lot easier.

Also you could add some very simple validations like notEmpty(), isSet(), isPositive(), isNumber(), ... . But for all of them php functions exist. So you might reinvent the wheel.

But make calling your validations as simple as possible.

The other thing is that in this way it's simple to add another validation method by just inheriting from your class and adding another validateSomethingVeryComplex()-method. Adding validations in your method would require to rewrite your code.

And every validateSomething()-method should return true or false, depending on the result of the validation. An additional error could be included in a by reference param. But you will run into situations, where you do not want all errors at the end, but need a result right when you validate.

And sanitizing/escaping is not part of validation, but a seperate thing to do. I would not mix these two topics.

\$\endgroup\$
  • \$\begingroup\$ Yes that is what happened with me, I forgot to resetOptions() yesterday and it takes about an hour till I figured out the cause of the unexpected results. I will separate the validate function as you said . I think that will also save some switch logic in the global validate function.. and as for sanitizing and escaping the input, I liked the idea of having 1 class to do all the logic related to the client inputs (sanitizing, escaping, validating) . thank you for your time \$\endgroup\$ – Accountant م Jan 10 '17 at 22:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.