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?