Firehed's Blog

MVC Guide - In-model Data Validation

Note: This is written for PHP, but the concepts should apply to (nearly) any programming language.

When writing MVC-based code (or any other time for that matter), it's important to validate your data. I won't go into great detail as to why - that should be fairly straightforward. However, an MVC architecture is flexible enough to allow for that validation to occur in either the model or the controller.

Use the controller?

When you're first getting started with MVC, you may be tempted to perform data validation in your controller - after all, it's where you're receiving data and passing it to the model. You want to make sure it's valid, right? Wrong! While you do want to make sure that you're not being given an empty ZIP code to store in your addresses table, you should not be performing that validation in the controller. Why should the controller care what a ZIP code looks like? What if your validation requirements change down the road? You then have to go through all of your controllers that accept that data type and update each of them. Bad stuff.

Validate in the model!

There are a plethora of different ways you can go about performing data validation directly in the model. One approach that I've been using for a while is as follows:

  • All of the data is stored in a protected array within the object, appropriately named $data.
  • Use of the __get and __set magic methods allow easy access to that data, while simultaneously allowing me to wrap validation around that access
  • Individual data models extend from an abstract base class, which also handles saving and loading data as needed
  • When __set($key, $value) is automatically called, it checks for the presence of a setKey($value) method in the model, and if found uses its return value for $data. If not, the original value is used.
  • The setKey($value) call is wrapped in a try/catch block looking for thrown ValidationExceptions. If one is caught, the message is stored in an $exceptions array.
  • As you might suspect, the setKey($value) method performs the actual validation - if the data fails validation, it throws a ValidationException with an appropriate error message. Since the return value is what's actually stored, it's also a great place to normalize data - for example, removing all non-numeric characters from a phone number

So enough about how it works, here's a code sample:

abstract class Model {
    protected $data = array();
    protected $exceptions = array();

    public function __get($key) {
        if (method_exists($this, $method = 'get'.ucfirst($key))) {
            return $this->$method();
        }

        // If extending an ORM class, call parent::__get($key);
        return isset($this->data[$key]) ? $this->data[$key] : null;
    }

    public function __set($key, $value) {
        if (method_exists($this, $method = 'set'. ucfirst($key))) {
            try {
                $this->data[$key] = $this->$method($value);
                return;
            }
            catch (ValidationException $e) {
                $this->exceptions[] = $e->getMessage();
            }
        }

        // If extending an ORM class, call parent::__set($key, $value);
        $this->data[$key] = $value;
    }

    protected function getExceptions() {
        return $this->exceptions;
    }

    public function save() {
        if ($this->exceptions) {
            return false;
        }

        // If extending an ORM class, call parent::save()
        // Otherwise, implement your own save routine here
        return true;
    }
}

class ValidationException extends Exception {}

This is obviously not a complete tool, but rather a basic starting point. My own code has this extend an ORM class, which enables easy mapping of data values to DB columns, and handles the saving and loading of data for me.

Usage

As an abstract class, the code sample above isn't going to do much for you. Here's a very basic example - let's assume we have a User model, and Users have usernames, passwords, phone numbers, and street addresses.

class User extends Model {
    protected function setUsername($value) {
        // This would be some sort of database lookup
        if (userExists($value)) {
            throw new ValidationException('That username is already in use.');
        }
        return $value;
    }

    protected function setPassword($value) {
        if (strlen($value) < 6) {
            throw new ValidationException('Passwords have a 6-character minimum.');
        }
        return sha1($value); 
        // We don't want to store the password as plaintext!

        // SECURITY NOTE: passwords should be salted for added
        // security - I do not recommending using SHA1 alone. 
        // This is a basic example!
    }

    protected function setPhoneNumber($value) {
        $value = preg_replace('/[^0-9]*/, '', $value);
        // Basic RegEx to strip non-numeric characters

        if (strlen($value) != 10) {
            throw new ValidationException('You must provide a 10-digit phone number.');
        }
        return $value;
    }
}

For brevity's sake, I haven't included any examples >` as his username and wreak havoc on your visitors. That's a post for another day, but be aware of it.

How this works in the controller

So, you have your "fat model" defined - so here's an example of a "skinny controller". Note that this is is highly generalized, since all MVC frameworks are going to work a little bit differently.

class UserController extends Controller {
    public function edit() {
        // Let's assume $this->user is set up in the core controller, finding the logged-in user

        $this->user->username = $_POST['username'];
        $this->user->password = $_POST['password'];
        $this->user->phoneNumber = $_POST['phoneNumber'];

        if ($this->user->save()) {
            $this->message('Profile updated!');
            // Redirect to the home page
        }
        else {
            $this->error($this->user->exceptions);
        }
    }
}

You'll spot the use of Controller::message() and Controller::error() in there - those are methods that will flash up messages or errors in the next page load. Many frameworks provide the ability to do this easily, at least in some capacity. Again - this is a highly generalized example, so adapt to taste. This simply sets the values, and if any validation failed (which will be evidenced by save() returning false), the errors are stored in User::$exceptions which can then easily be passed to your error displaying method.

What if I want to have a user confirm a new password?

Great point! Often, we want to have a "confirm password" field - but that may not apply to every situation, such as resetting a user's password via your administrative control panel. This is a place where it does make sense to put some validation in the controller.

class UserController extends Controller {
    public function password() {
        if ($_POST['password'] === $_POST['confirmPassword']) {
            $this->user->password = $_POST['password'];
            if ($this->user->save()) {
                $this->message('Password changed.');
                // redirect
            }
            else {
                $this->error($this->user->exceptions);
            }
        }
        else {
            $this->error('Those passwords don\'t match.');
        }
    }
}

This approach allows you make sure that the two passwords from the form are identical before trying to update the user. If they are, then you set the password (which will then be passed to the standard setPassword($value) method, ensuring it meets requirements) and save as normal. If not, you display an error to the user. The net result is the password being validated twice - once to ensure the user typed it correctly, and once to ensure that it meets your site's requirements.

Where do I go from here?

Wherever you like, of course. Get coding! The important take-aways from this are:

  • The majority of your data validation should take place in your data model
  • Validating some data in the controller is OK, but it's important to understand when and why.
  • Make it easy to code with! When I first started out, I didn't have a good approach to making data validation easy. As a result, I'd get lazy and plenty of fields wouldn't get validated! When the validation resides in the model, all controllers that interact with that model inherit that validation. And by making it take only a couple of lines of code to find and display validation issues, my users get a better experience.
  • This approach displays the exceptions thrown during validation directly to the user as-is - so ensure that any ValidationExceptions thrown are user-friendly!
  • By normalizing data on the way into the validators (see User::setPhoneNumber above), I can allow users to input the data however they feel comfortable without forcing them to adhere to a specific format. As a user once told me, "If you want the data a certain way, you take care of the formatting. Don't make me worry about it."