Bus System Implementation Guide
This guide provides step-by-step instructions for implementing the Bus System in your modules. Following these guidelines ensures that your implementation adheres to the Planet project's architectural principles.
Module Structure
When implementing the Bus System in a new module, use the following directory structure:
app/Modules/[ModuleName]/
├── Bus/
│ ├── Commands/ # Command DTOs
│ ├── Queries/ # Query DTOs
│ └── Handlers/ # Command and Query handlers
├── Exceptions/ # Domain-specific exceptions
├── Http/Controllers/ # Controllers that use the Bus
└── Providers/ # Service providers for registration
Step 1: Create Query and Handler
Query DTO
Create a Query DTO that implements the QueryInterface:
<?php
declare(strict_types=1);
namespace App\Modules\[ModuleName]\Bus\Queries;
use App\Core\Bus\Contracts\QueryInterface;
use App\Core\Bus\Exceptions\InvalidQueryArgumentException;
final readonly class Get[Entity]ByIdQuery implements QueryInterface
{
public function __construct(
public int $entityId
) {
if ($entityId <= 0) {
throw new InvalidQueryArgumentException('Entity ID must be positive integer');
}
}
}
Query Handler
Create a handler for the Query:
<?php
declare(strict_types=1);
namespace App\Modules\[ModuleName]\Bus\Handlers;
use App\Core\Bus\Contracts\QueryHandlerInterface;
use App\Modules\[ModuleName]\Bus\Queries\Get[Entity]ByIdQuery;
use App\Modules\[ModuleName]\Exceptions\[Entity]NotFoundException;
use App\Modules\[ModuleName]\Services\EntityServiceInterface;
use Psr\Log\LoggerInterface;
final readonly class Get[Entity]ByIdHandler implements QueryHandlerInterface
{
public function __construct(
private LoggerInterface $logger,
private EntityServiceInterface $entityService
) {}
public function handle(Get[Entity]ByIdQuery $query): array
{
$this->logger->info('Retrieving entity by ID', [
'entityId' => $query->entityId
]);
// Handler should NOT contain business logic directly
// Instead, delegate to a dedicated service/repository that contains the actual business logic
$result = $this->entityService->findById($query->entityId);
// Return data directly - QueryBus wraps it in BusResponse
return $entityData;
}
}
Step 2: Create Command and Handler
Command DTO
Create a Command DTO that implements the CommandInterface:
<?php
declare(strict_types=1);
namespace App\Modules\[ModuleName]\Bus\Commands;
use App\Core\Bus\Contracts\CommandInterface;
use InvalidArgumentException;
final readonly class Create[Entity]Command implements CommandInterface
{
public function __construct(
public string $requiredField,
public ?string $optionalField = null
) {
if (empty($requiredField)) {
throw new InvalidArgumentException('Required field cannot be empty');
}
}
}
Command Handler
Create a handler for the Command:
<?php
declare(strict_types=1);
namespace App\Modules\[ModuleName]\Bus\Handlers;
use App\Core\Bus\Contracts\CommandHandlerInterface;
use App\Modules\[ModuleName]\Bus\Commands\Create[Entity]Command;
use App\Modules\[ModuleName]\Services\EntityServiceInterface;
use Psr\Log\LoggerInterface;
final readonly class Create[Entity]Handler implements CommandHandlerInterface
{
public function __construct(
private LoggerInterface $logger,
private EntityServiceInterface $entityService
) {}
public function handle(Create[Entity]Command $command): void
{
$this->logger->info('Creating entity', [
'requiredField' => $command->requiredField
]);
// Handler should NOT contain business logic directly
// Instead, delegate to a dedicated service that contains the actual business logic
$this->entityService->create($command->requiredField, $command->optionalField);
$this->logger->info('Entity created successfully');
}
}
Step 3: Create Domain Exceptions
Create domain-specific exceptions for your module:
<?php
declare(strict_types=1);
namespace App\Modules\[ModuleName]\Exceptions;
use Exception;
class [Entity]NotFoundException extends Exception
{
//
}
class [Entity]AlreadyExistsException extends Exception
{
//
}
Step 4: Create Service Provider
Create a service provider to register your handlers:
<?php
declare(strict_types=1);
namespace App\Modules\[ModuleName]\Providers;
use App\Core\Bus\Contracts\CommandBusInterface;
use App\Core\Bus\Contracts\QueryBusInterface;
use App\Modules\[ModuleName]\Bus\Commands\Create[Entity]Command;
use App\Modules\[ModuleName]\Bus\Handlers\Create[Entity]Handler;
use App\Modules\[ModuleName]\Bus\Queries\Get[Entity]ByIdQuery;
use App\Modules\[ModuleName]\Bus\Handlers\Get[Entity]ByIdHandler;
use Illuminate\Support\ServiceProvider;
class [ModuleName]ServiceProvider extends ServiceProvider
{
public function register(): void
{
// Register any module-specific services here
}
public function boot(
QueryBusInterface $queryBus,
CommandBusInterface $commandBus
): void {
// Register Query handlers
$queryBus->register(
queryClass: Get[Entity]ByIdQuery::class,
handlerClass: Get[Entity]ByIdHandler::class
);
// Register Command handlers
$commandBus->register(
commandClass: Create[Entity]Command::class,
handlerClass: Create[Entity]Handler::class
);
}
}
Step 5: Register Service Provider
Register your service provider in config/app.php:
'providers' => [
// ...
App\Core\Bus\Providers\BusServiceProvider::class,
App\Modules\[ModuleName]\Providers\[ModuleName]ServiceProvider::class,
],
Step 6: Create Controller
Create a controller that uses the Bus System:
<?php
declare(strict_types=1);
namespace App\Modules\[ModuleName]\Http\Controllers;
use App\Core\Bus\Contracts\CommandBusInterface;
use App\Core\Bus\Contracts\QueryBusInterface;
use App\Http\Controllers\Controller;
use App\Modules\[ModuleName]\Bus\Commands\Create[Entity]Command;
use App\Modules\[ModuleName]\Bus\Queries\Get[Entity]ByIdQuery;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
class [Entity]Controller extends Controller
{
public function __construct(
private readonly CommandBusInterface $commandBus,
private readonly QueryBusInterface $queryBus,
private readonly LoggerInterface $logger
) {}
public function show(int $id): JsonResponse
{
try {
$query = new Get[Entity]ByIdQuery($id);
$response = $this->queryBus->dispatch($query);
if ($response->isSuccess()) {
return response()->json([
'success' => true,
'data' => $response->getData()
]);
}
return response()->json([
'success' => false,
'message' => $response->getErrorMessage()
], 404);
} catch (\Exception $e) {
$this->logger->error('Entity retrieval failed', [
'entityId' => $id,
'exception' => $e->getMessage()
]);
return response()->json([
'success' => false,
'message' => 'Internal server error'
], 500);
}
}
public function store(Request $request): JsonResponse
{
try {
$command = new Create[Entity]Command(
requiredField: $request->input('required_field'),
optionalField: $request->input('optional_field')
);
$this->commandBus->dispatch($command);
return response()->json([
'success' => true,
'message' => 'Entity created successfully'
], 201);
} catch (\InvalidArgumentException $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 400);
} catch (\Exception $e) {
$this->logger->error('Entity creation failed', [
'exception' => $e->getMessage()
]);
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
}
Step 7: Write Tests
Create tests for your implementation:
<?php
declare(strict_types=1);
namespace App\Modules\[ModuleName]\Tests;
use App\Core\Bus\Contracts\CommandBusInterface;
use App\Core\Bus\Contracts\QueryBusInterface;
use App\Core\Bus\Contracts\BusMessage;
use App\Core\Bus\Contracts\CommandInterface;
use App\Core\Bus\Contracts\QueryInterface;
use App\Core\Bus\Contracts\CommandHandlerInterface;
use App\Core\Bus\Contracts\QueryHandlerInterface;
use App\Core\Bus\Contracts\BusResponseInterface;
use App\Modules\[ModuleName]\Bus\Commands\Create[Entity]Command;
use App\Modules\[ModuleName]\Bus\Queries\Get[Entity]ByIdQuery;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class [ModuleName]BusTest extends TestCase
{
use RefreshDatabase;
private CommandBusInterface $commandBus;
private QueryBusInterface $queryBus;
protected function setUp(): void
{
parent::setUp();
$this->commandBus = $this->app->make(CommandBusInterface::class);
$this->queryBus = $this->app->make(QueryBusInterface::class);
}
/** @test */
public function can_create_entity(): void
{
$command = new Create[Entity]Command('test value');
$this->commandBus->dispatch($command);
// Assert entity was created
$this->assertDatabaseHas('[entities]', [
'required_field' => 'test value'
]);
}
/** @test */
public function can_retrieve_entity(): void
{
// Create test entity
$entity = [Entity]::factory()->create([
'required_field' => 'test value'
]);
$query = new Get[Entity]ByIdQuery($entity->id);
$response = $this->queryBus->dispatch($query);
$this->assertTrue($response->isSuccess());
$this->assertIsArray($response->getData());
$this->assertEquals('test value', $response->getData()['required_field']);
}
}
Implementation Checklist
Use this checklist to ensure you've completed all the necessary steps:
- Create Query and Handler
- Create Command and Handler
- Create Domain exceptions
- Create Service Provider
- Register in config/app.php
- Create Controller
- Write tests
- Update documentation
Key Principles
1. DTO Design
- Always use
readonlyfor DTOs - Validate in constructor
- Parameter order: required → nullable → default values
2. Handler Design
- One handler per message
- Always implement
CommandHandlerInterfaceorQueryHandlerInterface - Handlers should NOT contain business logic directly - delegate to services or repositories
- Handlers are responsible for coordination, logging, and calling appropriate services
- Use dependency injection
- Proper logging with context
3. Exception Handling
- Use domain exceptions
- Complete logging with stack trace
- Follow Planet project error handling principles
Common Pitfalls
1. Business Logic in Controllers
Never put business logic in controllers. Controllers should only:
- Create DTOs from request data
- Dispatch commands/queries
- Format responses
2. Direct Module Dependencies
Never create direct dependencies between modules. Always use the Bus System for inter-module communication.
3. Missing Validation
Always validate input in DTO constructors to fail fast.
4. Insufficient Logging
Always include proper logging with context information.
5. Ignoring Return Types
Always use proper return types and follow the CQS principle:
- Commands:
voidreturn type - Queries: Return data directly (not wrapped in BusResponse)
Next Steps
After implementing the Bus System in your module, consider:
- Writing comprehensive tests
- Documenting your implementation
- Reviewing your code for adherence to best practices
- Optimizing performance if necessary