Skip to main content

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 readonly for DTOs
  • Validate in constructor
  • Parameter order: required → nullable → default values

2. Handler Design

  • One handler per message
  • Always implement CommandHandlerInterface or QueryHandlerInterface
  • 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: void return type
  • Queries: Return data directly (not wrapped in BusResponse)

Next Steps

After implementing the Bus System in your module, consider:

  1. Writing comprehensive tests
  2. Documenting your implementation
  3. Reviewing your code for adherence to best practices
  4. Optimizing performance if necessary