Home » Php » Best approach to model validation in PHP?

Best approach to model validation in PHP?

Posted by: admin April 23, 2020 Leave a comment

Questions:

I’ve learned that there are often many ways to solve one programming problem, each approach typically having its own benefits and negative side affects.

What I’m trying to determine today is the best way to do model validation in PHP. Using the example of a person, I’ve outlined four different approaches I’ve used in the past, each including the classes and a usage example, as well as what I like and dislike about each approach.

My question here is this: Which approach do you feel is best? Or do you have a better approach?

Approach #1: Validation using setter methods in model class

The good

  • Simple, only one class
  • By throwing exceptions, the class can never be in an invalid state (except for business logic, ie. death comes before birth)
  • Don’t have to remember to call a any validation methods

The bad

  • Can only return 1 error (via Exception)
  • Requires the use of exceptions, and catching them, even if the errors are not very exceptional
  • Can only act upon one paramater since other paramaters may not be set yet (no way to compare birth_date and death_date)
  • Model class can be long due to lots of validation
class Person
{
    public $name;
    public $birth_date;
    public $death_date;

    public function set_name($name)
    {
        if (!is_string($name))
        {
            throw new Exception('Not a string.');
        }

        $this->name = $name;
    }

    public function set_birth_date($birth_date)
    {
        if (!is_string($birth_date))
        {
            throw new Exception('Not a string.');
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $birth_date))
        {
            throw new Exception('Not a valid date.');
        }

        $this->birth_date = $birth_date;
    }

    public function set_death_date($death_date)
    {
        if (!is_string($death_date))
        {
            throw new Exception('Not a string.');
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $death_date))
        {
            throw new Exception('Not a valid date.');
        }

        $this->death_date = $death_date;
    }
}
// Usage:

try
{
    $person = new Person();
    $person->set_name('John');
    $person->set_birth_date('1930-01-01');
    $person->set_death_date('2010-06-06');
}
catch (Exception $exception)
{
    // Handle error with $exception
}

Approach #2: Validation using validation methods in model class

The good

  • Simple, only one class
  • Possible to validate (compare) multiple paramaters (since validation occurs after all model parameters are set)
  • Can return multiple errors (via errors() method)
  • Freedom from exceptions
  • Leaves getter and setter methods available for other tasks

The bad

  • The model can be in an invalid state
  • Developer must remember to call validation is_valid() method
  • Model class can be long due to lots of validation
class Person
{
    public $name;
    public $birth_date;
    public $death_date;

    private $errors;

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

    public function is_valid()
    {
        $this->validate_name();
        $this->validate_birth_date();
        $this->validate_death_date();

        return count($this->errors) === 0;
    }

    private function validate_name()
    {
        if (!is_string($this->name))
        {
            $this->errors['name'] = 'Not a string.';
        }
    }

    private function validate_birth_date()
    {
        if (!is_string($this->birth_date))
        {
            $this->errors['birth_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->birth_date))
        {
            $this->errors['birth_date'] = 'Not a valid date.';
        }
    }

    private function validate_death_date()
    {
        if (!is_string($this->death_date))
        {
            $this->errors['death_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->death_date))
        {
            $this->errors['death_date'] = 'Not a valid date.';
            break;
        }

        if ($this->death_date < $this->birth_date)
        {
            $this->errors['death_date'] = 'Death cannot occur before birth';
        }
    }
}
// Usage:

$person = new Person();
$person->name = 'John';
$person->birth_date = '1930-01-01';
$person->death_date = '2010-06-06';

if (!$person->is_valid())
{
    // Handle errors with $person->errors()
}

Approach #3: Validation in seperate validation class

The good

  • Very simple models (all validation happens in seperate class)
  • Possible to validate (compare) multiple paramaters (since validation occurs after all model parameters are set)
  • Can return multiple errors (via errors() method)
  • Freedom from exceptions
  • Leaves getter and setter methods available for other tasks

The bad

  • Slightly more complicated as two classes are required for each model
  • The model can be in an invalid state
  • Developer must remember to use the validation class
class Person
{
    public $name;
    public $birth_date;
    public $death_date;
}
class Person_Validator
{
    private $person;
    private $errors = array();

    public function __construct(Person $person)
    {
        $this->person = $person;
    }

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

    public function is_valid()
    {
        $this->validate_name();
        $this->validate_birth_date();
        $this->validate_death_date();

        return count($this->errors) === 0;
    }

    private function validate_name()
    {
        if (!is_string($this->person->name))
        {
            $this->errors['name'] = 'Not a string.';
        }
    }

    private function validate_birth_date()
    {
        if (!is_string($this->person->birth_date))
        {
            $this->errors['birth_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->person->birth_date))
        {
            $this->errors['birth_date'] = 'Not a valid date.';
        }
    }

    private function validate_death_date()
    {
        if (!is_string($this->person->death_date))
        {
            $this->errors['death_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->person->death_date))
        {
            $this->errors['death_date'] = 'Not a valid date.';
            break;
        }

        if ($this->person->death_date < $this->person->birth_date)
        {
            $this->errors['death_date'] = 'Death cannot occur before birth';
        }
    }
}
// Usage:

$person = new Person();
$person->name = 'John';
$person->birth_date = '1930-01-01';
$person->death_date = '2010-06-06';

$validator = new Person_Validator($person);

if (!$validator->is_valid())
{
    // Handle errors with $validator->errors()
}

Approach #4: Validation in model class and validation class

The good

  • By throwing exceptions, the class can never be in an invalid state (except for business logic, ie. death comes before birth)
  • Possible to validate (compare) multiple paramaters (since business validation occurs after all model parameters are set)
  • Can return multiple errors (via errors() method)
  • Validation is organized into two groups: type (model class) and business (validation class)
  • Leaves getter and setter methods available for other tasks

The bad

  • Error handling is more complicated are there is exceptions thrown (model class), and an error array (validation class)
  • Slightly more complicated as two classes are required for each model
  • Developer must remember to use the validation class
class Person
{
    public $name;
    public $birth_date;
    public $death_date;

    private function validate_name()
    {
        if (!is_string($this->person->name))
        {
            $this->errors['name'] = 'Not a string.';
        }
    }

    private function validate_birth_date()
    {
        if (!is_string($this->person->birth_date))
        {
            $this->errors['birth_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->person->birth_date))
        {
            $this->errors['birth_date'] = 'Not a valid date.';          
        }
    }

    private function validate_death_date()
    {
        if (!is_string($this->person->death_date))
        {
            $this->errors['death_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->person->death_date))
        {
            $this->errors['death_date'] = 'Not a valid date.';
        }
    }
}
class Person_Validator
{
    private $person;
    private $errors = array();

    public function __construct(Person $person)
    {
        $this->person = $person;
    }

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

    public function is_valid()
    {
        $this->validate_death_date();

        return count($this->errors) === 0;
    }

    private function validate_death_date()
    {
        if ($this->person->death_date < $this->person->birth_date)
        {
            $this->errors['death_date'] = 'Death cannot occur before birth';
        }
    }
}
// Usage:

try
{
    $person = new Person();
    $person->set_name('John');
    $person->set_birth_date('1930-01-01');
    $person->set_death_date('2010-06-06');

    $validator = new Person_Validator($person);

    if (!$validator->is_valid())
    {
        // Handle errors with $validator->errors()
    }
}
catch (Exception $exception)
{
    // Handle error with $exception
}
How to&Answers:

I don’t think there’s just one best approach, it depends on how you are going to use your classes. In this case, when you have just a simple data object, I’d prefer to use Approach #2: Validation using validation methods in model class.

The bad things are not so bad, in my opinion:

The model can be in an invalid state

Sometimes it’s desirable to be able to have a model in an invalid state.

For instance, if you populate the Person object from a web form and want to log it. If you use the first approach, you’d have to extend the Person class, override all setters to catch exceptions and then you’d be able to have this object in an invalid state for logging.

Developer must remember to call validation is_valid() method

If the model absolutely must not be in an invalid state, or a method requires the model to be in a valid state, you can always call is_valid() from within the class to make sure it’s in a valid state.

Model class can be long due to lots of validation

Validation code must still go somewhere. Most editors let you fold functions so that should not be a problem while reading the code. If anything, I think it’s nice to have all validation in one place.