Pure PHP DDD Foundation for building Domain-Driven Design projects. This package provides the core building blocks: Domain layer (ValueObjects, Repository interfaces, Exceptions), Application layer (Command/Query/Handler), and InMemory infrastructure for testing.
composer require alexandrebulete/ddd-foundationsrc/
├── Domain/
│ ├── Exception/
│ │ └── EntityNotFoundException.php
│ ├── Repository/
│ │ ├── PaginatorInterface.php
│ │ └── RepositoryInterface.php
│ ├── Trait/
│ │ └── AsSelectableEnum.php
│ └── ValueObject/
│ ├── DatetimeVO.php
│ ├── EmailVO.php
│ ├── IdentifierInterface.php
│ ├── IdentifierVO.php
│ └── StringVO.php
├── Application/
│ ├── Command/
│ │ ├── AsCommandHandler.php
│ │ ├── CommandBusInterface.php
│ │ └── CommandInterface.php
│ ├── Criteria/
│ │ ├── CriteriaBuilder.php
│ │ ├── CriteriaBuilderInterface.php
│ │ ├── CriteriaNormalizer.php
│ │ └── CriteriaNormalizerInterface.php
│ ├── Handler/
│ │ ├── QueryCollectionHandler.php
│ │ └── QuerySingleHandler.php
│ └── Query/
│ ├── AsQueryHandler.php
│ ├── QueryBusInterface.php
│ └── QueryInterface.php
└── Infrastructure/
└── InMemory/
├── InMemoryPaginator.php
└── InMemoryRepository.php
use AlexandreBulete\DddFoundation\Domain\ValueObject\IdentifierVO;
use AlexandreBulete\DddFoundation\Domain\ValueObject\StringVO;
// Generate a new identifier
$id = IdentifierVO::generate();
// Create from string
$id = IdentifierVO::fromString('01HZXYZ...');
// String value object
$title = StringVO::fromString('My Title');A trait for PHP enums that provides a choices() method, useful for form select fields or grid filters:
use AlexandreBulete\DddFoundation\Domain\Trait\AsSelectableEnum;
enum StatusEnum: string
{
use AsSelectableEnum;
case DRAFT = 'draft';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
}
// Usage
StatusEnum::choices();
// Returns: ['draft' => 'draft', 'published' => 'published', 'archived' => 'archived']Perfect for Sylius Grid filters or Symfony form choices:
// Sylius Grid
->addFilter(SelectFilter::create('status', StatusEnum::choices()))
// Symfony Form
->add('status', ChoiceType::class, [
'choices' => array_flip(StatusEnum::choices()),
])use AlexandreBulete\DddFoundation\Domain\Repository\RepositoryInterface;
class PostRepository implements RepositoryInterface
{
// Implement the interface methods
}use AlexandreBulete\DddFoundation\Application\Command\CommandInterface;
use AlexandreBulete\DddFoundation\Application\Command\AsCommandHandler;
// Define a command
readonly class CreatePostCommand implements CommandInterface
{
public function __construct(
public string $title,
public string $content,
) {}
}
// Define a handler
#[AsCommandHandler]
readonly class CreatePostHandler
{
public function __invoke(CreatePostCommand $command): void
{
// Handle the command
}
}The Criteria system provides a standardized way to build and normalize query criteria for filtering data. It consists of two main components:
Provides fluent methods to build typed criteria arrays:
use AlexandreBulete\DddFoundation\Application\Criteria\CriteriaBuilder;
$builder = new CriteriaBuilder();
// Equality
$builder->eq('status', 'published'); // ['status' => ['type' => 'eq', 'value' => 'published']]
$builder->neq('status', 'draft'); // ['status' => ['type' => 'neq', 'value' => 'draft']]
// Comparison
$builder->lt('price', 100); // ['price' => ['type' => 'lt', 'value' => 100]]
$builder->lte('price', 100); // ['price' => ['type' => 'lte', 'value' => 100]]
$builder->gt('price', 50); // ['price' => ['type' => 'gt', 'value' => 50]]
$builder->gte('price', 50); // ['price' => ['type' => 'gte', 'value' => 50]]
// Collections
$builder->in('category', ['a', 'b']); // ['category' => ['type' => 'in', 'value' => ['a', 'b']]]
$builder->notIn('category', ['c']); // ['category' => ['type' => 'notIn', 'value' => ['c']]]
// Pattern matching
$builder->like('title', '%keyword%'); // ['title' => ['type' => 'like', 'value' => '%keyword%']]
$builder->notLike('title', '%spam%'); // ['title' => ['type' => 'notLike', 'value' => '%spam%']]Abstract class to normalize and transform raw criteria before passing them to repositories. Extend this class to add domain-specific normalization logic:
use AlexandreBulete\DddFoundation\Application\Criteria\CriteriaNormalizer;
use AlexandreBulete\DddFoundation\Application\Criteria\CriteriaNormalizerInterface;
final readonly class PostCriteriaNormalizer extends CriteriaNormalizer implements CriteriaNormalizerInterface
{
public function normalize(array $criteria): array
{
// First call parent to drop empty values
$criteria = parent::normalize($criteria);
// Then apply domain-specific transformations
$criteria = $this->normalizeStatusCriteria($criteria);
return $criteria;
}
private function normalizeStatusCriteria(array $criteria): array
{
$status = $criteria['status'] ?? null;
if (!is_string($status)) {
return $criteria;
}
$now = new \DateTimeImmutable();
return match ($status) {
'published' => $this->mergeCriteria($criteria, [
'status' => ['type' => 'eq', 'value' => 'published'],
'publishedAt' => ['type' => 'lte', 'value' => $now],
]),
'scheduled' => $this->mergeCriteria($criteria, [
'status' => ['type' => 'eq', 'value' => 'published'],
'publishedAt' => ['type' => 'gt', 'value' => $now],
]),
default => $criteria,
};
}
}The base CriteriaNormalizer class provides:
normalize(array $criteria): array- Entry point, drops empty values by defaultdropEmptyValues(array $criteria): array- Removes null or empty string valuesmergeCriteria(array $criteria, array $overrides): array- Merges override criteria into existing criteria
alexandrebulete/ddd-doctrine-bridge- Doctrine ORM integrationalexandrebulete/ddd-apiplatform-bridge- API Platform integration
alexandrebulete/ddd-symfony-bundle- Symfony Messenger integrationalexandrebulete/ddd-doctrine-bundle- Doctrine bundle for Symfonyalexandrebulete/ddd-apiplatform-bundle- API Platform bundle for Symfonyalexandrebulete/ddd-sylius-bundle- Sylius Stack integration