alexandrebulete/ddd-symfony-bundle
composer require spatie/laravel-package-tools laravel-queue
npm install @tom-select/tom-select
app/
├── Domains/
│ ├── Post/
│ │ ├── Infrastructure/
│ │ │ ├── Laravel/
│ │ │ │ ├── ServiceProviders/
│ │ │ │ │ └── PostServiceProvider.php
│ │ │ │ ├── Routes/
│ │ │ │ │ └── api.php
│ │ │ │ └── Console/
│ │ │ │ └── commands.php
AppServiceProvider:
public function boot()
{
$this->registerBoundedContext('Post');
$this->registerBoundedContext('User');
}
protected function registerBoundedContext(string $context): void
{
$provider = "App\\Domains\\{$context}\\Infrastructure\\Laravel\\ServiceProviders\\{$context}ServiceProvider";
if (class_exists($provider)) {
$this->app->register($provider);
}
}
// app/Application/Command/CommandBus.php
class CommandBus
{
public function dispatch(CommandInterface $command): void
{
$handler = app()->make($command::class . 'Handler');
$handler($command);
}
}
#[AsCommandHandler]):
use App\Application\Command\CommandHandler;
#[CommandHandler]
class CreatePostHandler
{
public function __invoke(CreatePostCommand $command)
{
// Handle command
}
}
// In your ServiceProvider
$this->app->bind(CommandBus::class, function ($app) {
$bus = new CommandBus();
$bus->setHandlers($this->discoverHandlers());
return $bus;
});
protected function discoverHandlers(): array
{
return collect(app()->getBindings())
->filter(fn ($_, $key) => str_ends_with($key, 'Handler'))
->mapWithKeys(fn ($handler, $key) => [
str_replace('Handler', '', $key) => $handler
])
->toArray();
}
Symfony Pattern (via DddKernel):
// src/Post/Infrastructure/Symfony/routes/api.yaml
app_post:
path: /posts
controller: App\Domains\Post\Infrastructure\Symfony\Controller\PostController::index
Laravel Equivalent:
// app/Domains/Post/Infrastructure/Laravel/Routes/api.php
Route::prefix('posts')->group(function () {
Route::get('/', [PostController::class, 'index']);
});
Workflow:
DddKernel from src/*/Infrastructure/Symfony/routes/.ServiceProvider or use Laravel Package Tools for auto-discovery.Symfony Pattern:
#[AsCommandHandler]
class CreatePostHandler
{
public function __invoke(CreatePostCommand $command)
{
// ...
}
}
Laravel Equivalent:
// Using Laravel Macros (PHP 8.1+)
CommandBus::macro('handle', function ($command) {
$handler = app()->make($command::class . 'Handler');
return $handler($command);
});
// Usage
$bus->handle(new CreatePostCommand(...));
Workflow:
class CreatePostCommand implements CommandInterface
{
public function __construct(public string $title) {}
}
#[AsCommandHandler].doctrine/annotations) or macros to auto-discover handlers.$bus->dispatch(new CreatePostCommand('Hello World'));
Symfony Pattern:
$builder->add('authorId', AutocompleteType::class, [
'remote_url' => route('admin_user_autocomplete'),
]);
Laravel Equivalent (Livewire):
use Livewire\WithFileUploads;
class PostForm extends Component
{
public $authorId;
public $searchQuery = '';
public function updatedSearchQuery()
{
$this->authorId = null;
$this->dispatch('search-authors', query: $this->searchQuery);
}
public function render()
{
return view('livewire.post-form', [
'authors' => Author::where('name', 'like', "%{$this->searchQuery}%")
->limit(5)
->get()
->map(fn ($author) => [
'id' => $author->id,
'text' => $author->email,
]),
]);
}
}
Blade Template:
<div>
<input
wire:model="searchQuery"
wire:ignore
x-data="{ open: false }"
x-on:search-authors.window="open = true"
x-on:click.away="open = false"
>
<div x-show="open" x-transition>
<ul>
@foreach($authors as $author)
<li wire:click="authorId = {{ $author['id'] }}">{{ $author['text'] }}</li>
@endforeach
</ul>
</div>
</div>
Symfony Pattern:
# config/packages/messenger.yaml
framework:
messenger:
buses:
command.bus:
middleware:
- doctrine_transaction
Laravel Equivalent:
// app/Providers/EventServiceProvider.php
public function boot()
{
Queue::before(function ($job, $data) {
if ($job instanceof CommandJob) {
DB::beginTransaction();
}
});
Queue::after(function ($job, $data) {
if ($job instanceof CommandJob) {
DB::commit();
}
});
}
Kernel Auto-Import Limitations:
DddKernel only loads files from src/*/Infrastructure/Symfony/. Misplaced configs (e.g., in config/) are ignored.configureContainer() to merge additional configs:
protected function configureContainer(ContainerConfigurator $container): void
{
$container->import($this->getProjectDir().'/config/custom/*.yaml');
}
Messenger Bus Separation:
command.bus is for writes and query.bus for reads. Mixing them can lead to race conditions (e.g., queries in a transactional command bus).if ($this->bus->getName() === 'command.bus' && $command instanceof QueryInterface) {
throw new \RuntimeException('Queries must use query.bus');
}
Autocomplete Endpoint:
{ "results": [...] } with id/text fields. Deviations (e.g., nested objects) break Tom Select.return response()->json([
'results' => $users->map(fn ($user) => [
'id' => $user->id,
'text' => $user->email,
]),
]);
Handler Discovery:
#[AsCommandHandler]).// Using doctrine/annotations
$reflection = new ReflectionClass($handler);
$annotation = $reflection->getAnnotation(CommandHandler::class);
if ($annotation) {
$bus->register($annotation->command, $handler);
}
Query Bus Responses:
How can I help you explore Laravel packages today?