3

Using Data Mapper Pattern:

  • an object/entity is unaware of data mapper and storage (e.g. RDBMS).
  • storage is unaware of data mapper and object/entity.
  • data mapper is of course aware and bridge the object/entity and storage.

How do I validate a unique field in an object/entity (e.g. $user->name) without it be aware of data mapper and storage (i.e. $user cannot simply call $userDataMapper->count('name='.$this->name))?

class User
{
    private $name; // unique

    public function validate(): bool
    {
        // what to put here to validate that $this->name
        // is unique in column `users`.`name`?
    }
}

There are two potential solutions (one was suggested by tereško) that I know so far, but both have shortcomings.

The first as suggested by tereško is to catch the PDOException.

class UserDataMapper
{
    public function store($user)
    {
        $sql = 'INSERT INTO `users` SET `name` = :name, `email_address` = :emailAddress...';
        $params =
        [
            'name' => $user->getName(),
            'emailAddress' => $user->getEmailAddress(),
            // ...
        ];

        $statement = $this->connection->prepare($sql);

        try
        {
            $statement->execute($params);
        }
        catch (\PDOException $e)
        {
            if ($e->getCode() === 23000)
            {
                // problem: can only receive one unique error at a time.

                // parse error message and throw corresponding exception.
                if (...name error...)
                {
                    thrown new \NameAlreadyRegistered;
                }
                elseif (...email address error...)
                {
                    thrown new \EmailAlreadyRegistered;
                }
            }

            throw $e; // because if this happens, you missed something
        }
    }
}

// Controller
class Register
{
    public function run()
    {
        if ($user->validate()) // first step of validation
        {
            // second step of validation
            try
            {
                $this->userDataMapper->store($this->user);
            }
            catch (\NameAlreadyRegistered $e)
            {
                $this->errors->add(... NameAlreadyRegistered ...)
            }
            catch (\EmailAlreadyRegistered $e)
            {
                $this->errors->add(... EmailAlreadyRegistered ...)
            }
            // ...other catches...
        }
        else
        {
            $this->errors = $user->getErrors();
        }
    }
}

The problem is that this will split validation in two places, namely within the entity (User) and DataMapper/Controller (detected by DataMapper and passed on to Controller to be logged). Alternatively, DataMapper could catch and handle the Exception/MySQL error code but this violates single responsibily princinple, while not alleviating the "split validation" problem.

Additionally, PDO/MySQL can only throw one error at time. If there two or more unique columns, we can only "validate" one of them at a time.

Another consequence of splitting validation in two places is that if later, we want to add more unique columns, then in addtion to User entity we also have to modify Register controller (and ChangeEmailAddress and ChangeProfile controllers and so on).

The second approach is one I'm using at the moment which is to separate validation into a separate object.

Class UserValidation
{
    public function validate()
    {
        if ($this->userDataMapper->count('name='.$user->getName() > 0))
        {
            $this->errors->add(...NameAlreadyRegistered...);
        }

        if ($this->userDataMapper->count('email_address='.$user->getEmailAddress() > 0))
        {
            $this->errors->add(...EmailAlreadyRegistered...);
        }
    }
}

// Controller
class Register
{
    public function run()
    {
        if ($this->userValidation()->validate())
        {
            $this->userDataMapper()->store($user);
        }
        else
        {
            $this->errors = $this->userValidation()->getErrors();
        }
    }
}

This works. Until the entity is extended.

class SpecialUser extends User
{
    private $someUniqueField;
}

// need to extend the UserValidation to incorporate the new field(s) too.
class SpecialUserValidation extends UserValidation
{
    public function validate()
    {
        parent::validate();

        // ...validate $this->user->someUniqueField...
    }
}

For each entity subclass, a validation subclass is required.

So, we're back to my original question. How to (properly) validate uniqueness in Data Mapper Pattern?

iseng
  • 31
  • 2

2 Answers2

6

Why do you want to do the RDBMS's job? Unless you are using some outdated SQL connection abstraction API (like the dead-but-not-forgotten ext/mysql), attempting to violate the UNIQUE constraint will cause an exception to be thrown.

So, you data mapper should just catch that exception (lets say use PDO, so it would be PDOException), find out the error code and then re-throw it as a proper business-domain exception. That's it.

That domain exception then can be handled at the service layer.

Your datamapper should not be responsible for data integrity checks. Those are handled by RDBMS's CONSTRAINT definitions. The extent of the available constraints will of course depend on which RDBMS you use.

namespace Model\Mapper;

use Model\Entity;
use Model\Exception;
use Component\DataMapper

class User extends DataMapper 
{
    // DB $this->connection passing is probably shared, so it's nice to just leave it in superclass

    public function store(Entity\User $user)
    {
        $statement = $this->connection->prepare('INSERT INTO ...');
        $statement->bindValue(':email', $user->getEmailAddress());
        try {
            $statement->execute();
        } catch (\PDOException $e) {
            if ($e->getCode() === 23000) {
                thrown new Exception\EmailAlreadyRegistered;
            }
            throw $e; // because if this happens, you missed something
        }
    }

}
tereško
  • 58,060
  • 25
  • 98
  • 150
  • Thanks for answering. Doesn't this split the validation into two places? I've rewritten the question and elaborated upon this there (since I can't reply sufficiently in this comment). – iseng Dec 23 '17 at 04:41
  • 1
    Actually not just two places. You check for data integrity in data mappers, you check for business rules in domain entities (or domain objects) and you [check for valid input](https://stackoverflow.com/a/47535617/727208) in in the value objects. Your data mapper does not care about the other two aspects. – tereško Dec 23 '17 at 07:46
1

I think it's tricky. Based on martin fowlers definition:

A layer of Mappers (473) that moves data between objects and a database while keeping them independent of each other and the mapper itself.

It seems like your solution is correct, as not only your business domain User is not aware of ANY sql, the mapper is not aware of your business domain User, it's completely decoupled in that neither knows of the other.

IMO the UserMapper should absolutely be aware of the db since that is it's job:

class UserDataMapper
{
    private $db; // this should be avoided?
    private $name;

    public function __construct($db, $name)
    {
        $this->db; // bad? NOPE!
        $this->name = $name;
    }

    public function validate()
    {

        if ($this->db->count('name='.$this->name) > 0)
            return false;
    }
}

But then comes the issue of interchange data ie, what's passed from your entity User to your data mapper? Right now it only needs to pass a name but in the future there will most probably be many other fields. Luckily clean architecture has recommendations on what to pass.


Now the problem of validation is of concurrency, if two concurrent apache threads/processes are creating a user with the same name, both could get the count == 0, in which case there'd need to be some sort of unique constraint on table.name for creation so only one of the inserts succeed!

dm03514
  • 54,664
  • 18
  • 108
  • 145
  • 1
    Thanks for the answer. But you may have combined the entity/object (e.g. User) with the data mapper (e.g. UserDataMapper). This violates the single responsibility principle.User deals with its business logic (concerning anything about User, including validation) and Data Mapper deals translating/mapping objects with storage during storing and retrieving process. – iseng Dec 23 '17 at 04:39