Install the Package
composer require alexandrebulete/ddd-foundation
Add to composer.json under require-dev if only for testing:
"alexandrebulete/ddd-foundation": "^1.1"
Define a Value Object
Create a domain-specific value object (e.g., EmailVO) in app/Domain/ValueObject/EmailVO.php:
use AlexandreBulete\DddFoundation\Domain\ValueObject\StringVO;
final class EmailVO extends StringVO
{
public static function fromString(string $value): self
{
return parent::fromString($value);
}
}
Use in a Model
Integrate into a Laravel model (e.g., User.php):
use App\Domain\ValueObject\EmailVO;
class User
{
private EmailVO $email;
public function __construct(EmailVO $email)
{
$this->email = $email;
}
public function getEmail(): EmailVO
{
return $this->email;
}
}
Create a Command/Handler
Define a simple command and handler in app/Application/Command/CreateUserCommand.php and app/Application/Handler/CreateUserHandler.php:
// Command
use AlexandreBulete\DddFoundation\Application\Command\CommandInterface;
readonly class CreateUserCommand implements CommandInterface
{
public function __construct(
public string $email,
public string $name,
) {}
}
// Handler
use AlexandreBulete\DddFoundation\Application\Command\AsCommandHandler;
#[AsCommandHandler]
readonly class CreateUserHandler
{
public function __invoke(CreateUserCommand $command): User
{
return new User(EmailVO::fromString($command->email));
}
}
Register the Command Bus
Bind the command bus in a service provider (e.g., AppServiceProvider.php):
use AlexandreBulete\DddFoundation\Application\Command\CommandBusInterface;
use AlexandreBulete\DddFoundation\Infrastructure\CommandBus;
public function register(): void
{
$this->app->singleton(CommandBusInterface::class, fn () => new CommandBus());
}
Dispatch a Command Use the command bus in a controller or service:
use App\Application\Command\CreateUserCommand;
use Illuminate\Support\Facades\App;
$commandBus = App::make(CommandBusInterface::class);
$user = $commandBus->dispatch(new CreateUserCommand('test@example.com', 'John Doe'));
Replace Laravel’s form validation with domain-level validation:
use AlexandreBulete\DddFoundation\Domain\ValueObject\EmailVO;
final class EmailVO extends StringVO
{
public static function fromString(string $value): self
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email format.');
}
return parent::fromString($value);
}
}
Now validation is enforced at the domain layer, not just the application layer.
Value Objects as Immutables
Use StringVO, IdentifierVO, or custom VOs to enforce immutability and validation:
final class PriceVO
{
private function __construct(private float $amount) {}
public static function fromFloat(float $amount): self
{
if ($amount < 0) {
throw new \InvalidArgumentException('Price cannot be negative.');
}
return new self($amount);
}
public function getAmount(): float { return $this->amount; }
}
Entities with Identity
Use IdentifierVO for entity IDs to decouple from database concerns:
use AlexandreBulete\DddFoundation\Domain\ValueObject\IdentifierVO;
final class User
{
private function __construct(
private IdentifierVO $id,
private EmailVO $email,
) {}
public static function create(EmailVO $email): self
{
return new self(IdentifierVO::generate(), $email);
}
public function getId(): IdentifierVO { return $this->id; }
}
Domain Events
Emit events from entities (e.g., UserCreated, OrderShipped) and handle them via Laravel’s event system:
use Illuminate\Support\Facades\Event;
class User
{
public function __construct(EmailVO $email)
{
Event::dispatch(new UserCreated($this));
}
}
CQRS with Command/Query Handlers Separate read (queries) and write (commands) operations:
// Query
use AlexandreBulete\DddFoundation\Application\Query\QueryInterface;
readonly class GetUserQuery implements QueryInterface
{
public function __construct(public IdentifierVO $userId) {}
}
// Handler
use AlexandreBulete\DddFoundation\Application\Query\AsQueryHandler;
#[AsQueryHandler]
readonly class GetUserHandler
{
public function __invoke(GetUserQuery $query): User
{
return $this->userRepository->find($query->userId);
}
}
Criteria for Filtering
Use CriteriaBuilder to construct complex filters:
$criteria = (new CriteriaBuilder())
->eq('status', 'published')
->gte('createdAt', now()->subDays(7))
->in('category', ['news', 'tech']);
$users = $this->userRepository->findAll($criteria);
Normalizing Criteria
Extend CriteriaNormalizer to add domain logic:
final class UserCriteriaNormalizer extends CriteriaNormalizer
{
public function normalize(array $criteria): array
{
$criteria = parent::normalize($criteria);
return $this->normalizeSearchCriteria($criteria);
}
private function normalizeSearchCriteria(array $criteria): array
{
if (isset($criteria['search'])) {
return $this->mergeCriteria($criteria, [
'name' => ['type' => 'like', 'value' => '%' . $criteria['search'] . '%'],
'email' => ['type' => 'like', 'value' => '%' . $criteria['search'] . '%'],
]);
}
return $criteria;
}
}
In-Memory Repository for Testing
Use InMemoryRepository in PHPUnit tests:
use AlexandreBulete\DddFoundation\Infrastructure\InMemory\InMemoryRepository;
$repository = new InMemoryRepository();
$repository->save(new User(EmailVO::fromString('test@example.com')));
$user = $repository->find(IdentifierVO::fromString('user-id'));
Repository Interface Implementation
Implement RepositoryInterface for Eloquent or other ORMs:
use AlexandreBulete\DddFoundation\Domain\Repository\RepositoryInterface;
use App\Models\User as EloquentUser;
class UserRepository implements RepositoryInterface
{
public function find(IdentifierVO $id): ?User
{
$eloquentUser = EloquentUser::find($id->toString());
return $eloquentUser ? new User($eloquentUser->id, EmailVO::fromString($eloquentUser->email)) : null;
}
public function save(User $user): void
{
EloquentUser::updateOrCreate(
['id' => $user->getId()->toString()],
[
'email' => $user->getEmail()->toString(),
]
);
}
}
Service Provider Setup
Register the package in AppServiceProvider:
public function register(): void
{
$this->app->singleton(CommandBusInterface::class, fn () => new CommandBus());
$this->app->singleton(QueryBusInterface::class, fn () => new QueryBus());
}
Command/Query Dispatching Use dependency injection to dispatch commands/queries:
use App\Application\Command\CreateUserCommand;
use App\Application\Query\GetUserQuery;
class UserController extends Controller
{
public function __construct(
private CommandBusInterface $commandBus,
private QueryBusInterface $queryBus,
) {}
public function store(CreateUserRequest $request)
{
$command = new CreateUserCommand(
$request->email,
$request->name,
);
$this->commandBus->dispatch($command);
How can I help you explore Laravel packages today?