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?
Exception
)birth_date
and death_date
)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 }
errors()
method)is_valid()
methodclass 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() }
errors()
method)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() }
errors()
method)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 }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With