dddominio/common
Common utilities for the DDDominio ecosystem: shared helpers, base classes, and cross-package abstractions to standardize behavior and reduce duplication across domain-driven Laravel/PHP packages.
Installation
composer require dddominio/common
Verify compatibility with your Laravel version (e.g., PHP 8.1+, Laravel 9+) in composer.json.
First Use Case: Immutable Value Objects
Define a value object (e.g., Money) in your domain layer:
use DDDominio\Common\ValueObject;
class Money extends ValueObject
{
public function __construct(
public string $amount,
public string $currency
) {
$this->validate();
}
protected function validate(): void
{
if ($this->amount <= 0) {
throw new \InvalidArgumentException("Amount must be positive.");
}
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount &&
$this->currency === $other->currency;
}
}
ValueObject and implement equals() for domain-specific comparison logic.Second Use Case: Entity with Domain Events
Create an entity (e.g., Order) that emits events:
use DDDominio\Common\Entity;
use DDDominio\Common\Domain\Event\DomainEvent;
class Order extends Entity
{
public function __construct(
public string $id,
public Money $total,
public array $items = []
) {
$this->recordThat(new OrderCreated($id, $total));
}
public function addItem(Product $product): void
{
$this->items[] = $product;
$this->recordThat(new ItemAdded($this->id, $product->id));
}
}
recordThat() to publish domain events. Configure event dispatching in Laravel’s EventServiceProvider or via the package’s config.Third Use Case: Repository Interface
Define a repository contract for your aggregate root (e.g., Order):
use DDDominio\Common\Repository\RepositoryInterface;
class OrderRepository implements RepositoryInterface
{
public function find(string $id): ?Order
{
// Implement using Eloquent or custom logic
return Order::query()->find($id);
}
public function persist(Order $order): void
{
$order->save();
}
}
AppServiceProvider:
$this->app->bind(OrderRepository::class, function ($app) {
return new OrderRepository(new OrderModel());
});
Where to Look First
src/Domain for core classes (Entity.php, ValueObject.php).tests for usage examples (e.g., EntityTest.php).config/dddominio.php (if published) for event dispatching or repository defaults.Domain Layer Structure
Organize code by bounded contexts (e.g., app/Domain/Order, app/Domain/User):
/Domain
/Order
- Order.php (AggregateRoot)
- OrderCreated.php (DomainEvent)
- Money.php (ValueObject)
- OrderRepository.php
/User
- User.php
- Email.php (ValueObject)
Entity Lifecycle
Money validation).OrderCreated triggers a notification).OrderRepository::persist($order)).Value Objects
readonly or set only in constructors.equals() for domain-specific comparisons (e.g., Money ignores minor currency fluctuations).validate() methods.Repositories
RepositoryInterface for aggregate roots only.class OrderRepository implements RepositoryInterface
{
public function __construct(private OrderModel $model) {}
public function find(string $id): ?Order
{
return $this->model->find($id)?->toDomain();
}
}
DB facade or a custom UnitOfWork pattern.Domain Events
// Option 1: Laravel Events
event(new OrderCreated($order->id));
// Option 2: DDDominio Dispatcher (if supported)
$this->eventDispatcher->dispatch($event);
EventServiceProvider:
protected $listen = [
OrderCreated::class => [
SendOrderConfirmation::class,
],
];
Services
OrderCalculator).class CreateOrderService
{
public function __construct(
private OrderRepository $repository,
private EventDispatcher $dispatcher
) {}
public function execute(array $data): Order
{
$order = new Order($data['id'], new Money($data['total']));
$this->repository->persist($order);
return $order;
}
}
New Feature Development
Order, Money).OrderCreated, PaymentProcessed).Testing
$repository = Mockery::mock(OrderRepository::class);
$repository->shouldReceive('find')->andReturn($order);
$this->assertTrue($order->isPaid());
$this->assertDatabaseHas('orders', ['id' => $order->id]);
$this->assertEquals(1, OrderCreated::dispatches()->count());
Migration from Eloquent
Entity/ValueObject classes.Laravel Eloquent
class OrderModel extends Model
{
public function toDomain(): Order
{
return new Order(
$this->id,
new Money($this->total_amount, $this->currency)
);
}
}
Validation
FormRequest for API input validation, then validate again in domain objects:
// API Layer
$request->validate(['total' => 'required|numeric']);
// Domain Layer
$money = new Money($request->total, 'USD'); // Throws if invalid
APIs
class OrderResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->resource->id,
'total' => $this->resource->total->amount,
];
}
}
CQRS (Optional)
ReadModel package:
class OrderReadModel
{
public function findById(string $id): array
{
return Order::query()
->where('id', $id)
->with('items')
->first()
->toArray();
}
}
Over-Engineering
Repository Implementation Leaks
How can I help you explore Laravel packages today?