A Domain Model is not necessarily an object that can be directly translated to a database row.
Your Person
example does fit this description, and I like to call such an object an Entity (adopted from the Doctrine 2 ORM).
But, like Martin Fowler describes, a Domain Model is any object that incorporates both behavior and data.
a strict solution
Here's a quite strict solution to the problem you describe:
Say your Person
Domain Model (or Entity) must have a first name and last name, and optionally a maiden name. These must be strings, but for simplicity may contain any character.
You want to enforce that whenever such a Person
exists, these prerequisites are met. The class would look like this:
class Person
{
/**
* @var string
*/
protected $firstname;
/**
* @var string
*/
protected $lastname;
/**
* @var string|null
*/
protected $maidenname;
/**
* @param string $firstname
* @param string $lastname
* @param string|null $maidenname
*/
public function __construct($firstname, $lastname, $maidenname = null)
{
$this->setFirstname($firstname);
$this->setLastname($lastname);
$this->setMaidenname($maidenname);
}
/**
* @param string $firstname
*/
public function setFirstname($firstname)
{
if (!is_string($firstname)) {
throw new InvalidArgumentException('Must be a string');
}
$this->firstname = $firstname;
}
/**
* @return string
*/
public function getFirstname()
{
return $this->firstname;
}
/**
* @param string $lastname
*/
public function setLastname($lastname)
{
if (!is_string($lastname)) {
throw new InvalidArgumentException('Must be a string');
}
$this->lastname = $lastname;
}
/**
* @return string
*/
public function getLastname()
{
return $this->lastname;
}
/**
* @param string|null $maidenname
*/
public function setMaidenname($maidenname)
{
if (!is_string($maidenname) or !is_null($maidenname)) {
throw new InvalidArgumentException('Must be a string or null');
}
$this->maidenname = $maidenname;
}
/**
* @return string|null
*/
public function getMaidenname()
{
return $this->maidenname;
}
}
As you can see there is no way (disregarding Reflection) that you can instantiate a Person
object without having the prerequisites met.
This is a good thing, because whenever you encounter a Person
object, you can be a 100% sure about what kind of data you are dealing with.
Now you need a second Domain Model to handle user input, lets call it PersonForm
(because it often represents a form being filled out on a website).
It has the same properties as Person
, but blindly accepts any kind of data.
It will also have a list of validation rules, a method like isValid()
that uses those rules to validate the data, and a method to fetch any violations.
I'll leave the definition of the class to your imagination :)
Last you need a Controller (or Service) to tie these together. Here's some pseudo-code:
class PersonController
{
/**
* @param Request $request
* @param PersonMapper $mapper
* @param ViewRenderer $view
*/
public function createAction($request, $mapper, $view)
{
if ($request->isPost()) {
$data = $request->getPostData();
$personForm = new PersonForm();
$personForm->setData($data);
if ($personForm->isValid()) {
$person = new Person(
$personForm->getFirstname(),
$personForm->getLastname(),
$personForm->getMaidenname()
);
$mapper->insert($person);
// redirect
} else {
$view->setErrors($personForm->getViolations());
$view->setData($data);
}
}
$view->render('create/add');
}
}
As you can see the PersonForm
is used to intercept and validate user input. And only if that input is valid a Person
is created and saved in the database.
business rules
This does mean that certain business logic will be duplicated:
In Person
you'll want to enforce business rules, but it can simple throw an exception when something is off.
In PersonForm
you'll have validators that apply the same rules to prevent invalid user input from reaching Person
. But here those validators can be more advanced. Think off things like human error messages, breaking on the first rule, etc. You can also apply filters that change the input slightly (like lowercasing a username for example).
In other words: Person
will enforce business rules on a low level, while PersonForm
is more about handling user input.
more convenient
A less strict approach, but maybe more convenient:
Limit the validation done in Person
to enforce required properties, and enforce the type of properties (string, int, etc). No more then that.
You can also have a list of constraints in Person
. These are the business rules, but without actual validation code. So it's just a bit of configuration.
Have a Validator
service that is capable of receiving data along with a list of constraints. It should be able to validate that data according to the constraints. You'll probably want a small validator class for each type of constraint. (Have a look at the Symfony 2 validator component).
PersonForm
can have the Validator
service injected, so it can use that service to validate the user input.
Lastly, have a PersonManager
service that's responsible for any actions you want to perform on a Person
(like create/update/delete, and maybe things like register/activate/etc). The PersonManager
will need the PersonMapper
as dependency.
When you need to create a Person
, you call something like $personManager->create($userInput);
That call will create a PersonForm
, validate the data, create a Person
(when the data is valid), and persist the Person
using the PersonMapper
.
The key here is this:
You could draw a circle around all these classes and call it your "Person domain" (DDD). And the interface (entry point) to that domain is the PersonManager
service. Every action you want to perform on a Person
must go through PersonManager
.
If you stick to that in your application, you should be safe regarding to ensuring business rules :)
PersonForm
andPerson
models would have to check and enforce that logic? The easiest approach I see is removing that check fromPerson
, but then the domain model is no longer defining business constraints. – Wallraff