I am struggling to find the proper DDD way to have a parent/child oneToMany relationship that:
- Ensures entities cannot exist in invalid state
- Exposes no unwanted methods (i.e. a clean API)
I am using PHP and Doctrine2 but I guess this applies to many other languages/platforms as well. Here's my base entity code. I have Parent
and Child
objects. A Child
cannot exist without a parent.
/**
* @ORM\Entity
*/
class ParentClass
{
/**
* @ORM\OneToMany(targetEntity="Child", mappedBy="parent", orphanRemoval=true, cascade={"persist", "remove"})
*/
private $children;
}
/**
* @ORM\Entity
*/
class Child
{
/**
* @ORM\ManyToOne(targetEntity="Base", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
private $parent;
}
But how should I create and remove child entities?
To enfore consistency I could have the parent as a constructor parameter for Child:
class ParentClass
{
public function addChild(Child $child)
{
$this->children[] = $child;
}
public function removeChild(Child $child)
{
$this->children->removeElement($child);
}
}
class Child
{
public function __construct(ParentClass $parent)
{
$this->parent = $parent;
$this->parent->addChild($this);
}
}
$parent = new ParentClass();
$child = new Child($parent);
The problem with this is that it exposes addChild which really shouldn't be used by developers now. It would need a whole load of extra checks to ensure you cannot move children between parents.
As an alternative I could use a setter:
class ParentClass
{
public function addChild(Child $child)
{
$child->setParent($this);
$this->children[] = $child;
}
public function removeChild(Child $child)
{
$this->children->removeElement($child);
}
}
class Child
{
public function setParent(ParentClass $parent)
{
$this->parent = $parent;
}
}
$parent = new ParentClass();
$parent->addChild(new Child());
The problem here is that Child would be in an invalid state until you call addChild.
A third option could be to have addChild create a new child:
class ParentClass
{
public function addChild()
{
$child = new Child($parent);
$this->children[] = $child;
return $child;
}
public function removeChild(Child $child)
{
$this->children->removeElement($child);
}
}
class Child
{
public function __construct(ParentClass $parent)
{
$this->parent = $parent;
}
}
$parent = new ParentClass();
$child = $parent->addChild();
The problem with this is that the child constructor is exposed to developers. Also, my (Symfony) form library is probably going to hate me, causing me to have a bunch of DTO's and mappers just for a simple use case.
There are probably even more possible ways to handle this. What is the preferred way to ensure a clean domain model?