Domain driven design
This document explains how to design and implement a Product and Group relationship using the principles of Domain-Driven Design (DDD) in a Symfony application. The example demonstrates creating a Product object while adhering to DDD principles.
Key DDD Concepts
1. Aggregate Root
- The
Groupentity is the Aggregate Root in this scenario. - A
Productis part of theGroupaggregate and cannot exist independently. - All operations on
Productobjects must go through theGroupentity.
2. Encapsulation of Logic
- Business rules for managing
Productobjects are encapsulated within theGroupentity. - Direct instantiation of
Productis not allowed outside theGroupcontext.
3. Value Objects (Optional)
- Value objects can be used for attributes like
Priceto encapsulate validation and logic.
Implementation Steps
1. Group Entity
The Group entity is responsible for managing its associated Product objects.
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Group
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\OneToMany(mappedBy: 'group', targetEntity: Product::class, cascade: ['persist', 'remove'])]
private Collection $products;
public function __construct(string $name)
{
$this->name = $name;
$this->products = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(string $name, float $price, string $description): Product
{
$product = new Product($this, $name, $price, $description);
$this->products->add($product);
return $product;
}
}
2. Product Entity
The Product entity is designed to ensure it is always created within the context of a Group.
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(type: 'float')]
private ?float $price = null;
#[ORM\Column(type: 'text')]
private ?string $description = null;
#[ORM\ManyToOne(targetEntity: Group::class, inversedBy: 'products')]
#[ORM\JoinColumn(nullable: false)]
private ?Group $group = null;
public function __construct(Group $group, string $name, float $price, string $description)
{
$this->group = $group;
$this->name = $name;
$this->price = $price;
$this->description = $description;
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function getPrice(): ?float
{
return $this->price;
}
public function getDescription(): ?string
{
return $this->description;
}
public function getGroup(): ?Group
{
return $this->group;
}
}
3. Service Layer for Product Creation
All operations involving domain logic should be handled in a service or application layer.
use App\Entity\Group;
use Doctrine\ORM\EntityManagerInterface;
class ProductService
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function createProductForGroup(int $groupId, string $name, float $price, string $description): void
{
// Fetch the Group
$groupRepository = $this->entityManager->getRepository(Group::class);
$group = $groupRepository->find($groupId);
if (!$group) {
throw new \DomainException('Group not found');
}
// Add Product via Group
$group->addProduct($name, $price, $description);
// Persist changes
$this->entityManager->persist($group); // Products are persisted via cascade
$this->entityManager->flush();
}
}
4. Advantages of This Design
- Encapsulation: Business logic for creating
Productobjects is centralized in theGroupentity. - Aggregate Consistency: Ensures
Productobjects are always created within a validGroup. - Validation: Business rules for
Productcreation can be enforced in theaddProductmethod of theGroupentity.
Example Usage
$productService = new ProductService($entityManager);
$productService->createProductForGroup(1, 'Sample Product', 100.0, 'Sample Description');
Conclusion
By following the principles of DDD, this approach ensures:
- Clear separation of concerns.
- Consistent and valid domain models.
- Adherence to the Aggregate Root pattern, making the domain more maintainable and expressive.
