Where should validation take place?

2020-03-26 08:36发布

问题:

I want to use MVC pattern in my PHP project. I also want to use Dao-Service pattern for Model layer since it makes database engines easily interchangeable and keeps business logic out of DB interaction.

Now, I heard that validation should happen in Model layer since Controllers are only responsible for transporting data. That's pretty reasonable.

Should it, however, be implemented in Service layer, or entities themselves?

Approach 1: validation in entities

class Post extends Entity
{
    protected $title;

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

    public function setTitle($newTitle)
    {
        if (strlen($newTitle) == 0)
            throw new ValidationException('Title cannot be empty.');
        $this->title = $newTitle;
    }
}

class PostService
{
    public static function saveOrUpdate(Post $post)
    {
        PostDao::saveOrUpdate($post);
    }
}

Pros:

  • I know immediately if I do something wrong,
  • everything is in one place, which seems like a good thing.

Cons:

  • lots of boilerplate code due to fancy setters and getters,
  • adds some business logic to entity, which seems like a bad thing (especially if validation is very complex - e.g. needs to query database / other services),
  • serializing and deserializing might become difficult.

Approach 2: validation in service

class Post extends Entity
{
    public $title;
}

class PostService
{
    public static function saveOrUpdate(Post $post)
    {
        if (strlen($post->title) == 0)
            throw new ValidationException('Title cannot be empty.');
        PostDao::saveOrUpdate($post);
    }
}

Pros:

  • keeps business logic to service, which seems like a good thing,
  • boilerplate is kept to minimum.

Cons:

  • I don't know immediately when things go wrong and what did actually go wrong.
  • I have no guarantee that validation will actually take place before saving it to database. An example: I have to save post in two routines, forget to use PostService::saveOrUpdate proxy in one of them and do it directly via PostDao::saveOrUpdate. Poof, validation doesn't take place in that routine and project's only hope are now unit tests or myself spotting it in the code. Thus, code is more difficult to maintain in this regard.

Do you have any hints, SO? Am I missing something? So far the project is on drawing board so I'm ready for anything.

回答1:

NOTE: Controller is not responsible for "transporting data". Controller's responsibility is altering model's (and in special cases - current view instance's) state.

There is actually third approach: have a separate isValid() method for the domain object (you call them "entities"). The setter validation becomes messy, when you have validation rules across multiple data entries.

Example: validation of repeated password for user registration form.

Having validation in setter for this will turn out quite messy. Especially if you opt to use exceptions for each failing validation.

Also, instead of data access objects I would recommend for you to use data mappers. The code would basically look something like this:

$post = new Model\Domain\Post;
$mapper = new Model\Mappers\Post($pdo);

$post->setId(42);
$mapper->fetch($post);

$post->setTitle($title);
if ($post->isValid()) {
    $mapper->store($post);
}

This approach also lets you externalize the validation by injecting some kind of Validator instance in the Model\Domain\Post instance through constructor, if you want to reuse some validation rules.

Though, when making a larger app, you will probably notice, that there are very few repeated checks, which go beyond existing php filters.

NOTE: please don't use static classes. This forces you to use procedural paradigm, where it's not needed.

Another thing that you have to pay attention to is: what kind of validation are you doing ?

Business rules should be validated in domain objects, but data integrity checks (like: "is this email address unique") are part of persistence logic. And persistence logic should (according to SRP be handled by a separate instance. In the given example that part was managed by data mapper.



回答2:

I think you should have two layers of validation, your "model" validation which validates invariant conditions which apply globally to the data model. For example, "name" is required due to that being a not-null field in the database. In this instance approach 2 is appropriate.

The other layer of validation is "form" validation, and that would go in your form model, this tests for conditions which are specific to the form. Say for example you have an is_admin field on your user model, it might be valid to set this in the admin panel update user form, but invalid on the users "change password" form. This approach is probably closer to the first.

With regard to implementing it, I probably wouldn't validate it in the setter unless you want to have a test around every single "set" or you're happy to set and save only the fields which are valid. Usually you'd reject the entire update if one of the fields is invalid, so it would make more sense to have an isValid() function, which could be called automatically before saving or to test whether it's valid without saving.