Skip to main content

Bus System Examples

This guide provides practical examples of using the Bus System in various scenarios. These examples demonstrate how to implement common patterns and solve real-world problems using the Bus architecture.

Basic CRUD Operations

Creating a Resource

Command Definition

namespace App\Modules\Users\Commands;

use App\Core\Bus\CommandInterface;

final readonly class CreateUserCommand implements CommandInterface
{
public function __construct(
public string $name,
public string $email,
public string $password
) {
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email address');
}

if (strlen($password) < 8) {
throw new \InvalidArgumentException('Password must be at least 8 characters');
}
}
}

Command Handler

namespace App\Modules\Users\Handlers;

use App\Core\Bus\CommandHandlerInterface;
use App\Modules\Users\Commands\CreateUserCommand;
use App\Modules\Users\Models\User;
use Illuminate\Support\Facades\Hash;
use Psr\Log\LoggerInterface;

final readonly class CreateUserHandler implements CommandHandlerInterface
{
public function __construct(
private LoggerInterface $logger
) {}

public function handle(CreateUserCommand $command): void
{
$this->logger->info('Creating new user', ['email' => $command->email]);

$user = new User();
$user->name = $command->name;
$user->email = $command->email;
$user->password = Hash::make($command->password);
$user->save();

$this->logger->info('User created successfully', ['id' => $user->id]);

// Dispatch domain event
event(new UserCreatedEvent($user->id, $user->email));
}
}

Controller Implementation

namespace App\Modules\Users\Controllers;

use App\Core\Bus\CommandBusInterface;
use App\Modules\Users\Commands\CreateUserCommand;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class UserController extends Controller
{
public function __construct(
private CommandBusInterface $commandBus
) {}

public function store(Request $request): JsonResponse
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:8',
]);

try {
$command = new CreateUserCommand(
name: $request->input('name'),
email: $request->input('email'),
password: $request->input('password')
);

$this->commandBus->dispatch($command);

return response()->json(['message' => 'User created successfully'], 201);
} catch (\InvalidArgumentException $e) {
return response()->json(['error' => $e->getMessage()], 422);
} catch (\Exception $e) {
return response()->json(['error' => 'Failed to create user'], 500);
}
}
}

Retrieving a Resource

Query Definition

namespace App\Modules\Users\Queries;

use App\Core\Bus\QueryInterface;

final readonly class GetUserByIdQuery implements QueryInterface
{
public function __construct(
public int $userId
) {
if ($userId <= 0) {
throw new \InvalidArgumentException('User ID must be a positive integer');
}
}
}

Query Handler

namespace App\Modules\Users\Handlers;

use App\Core\Bus\QueryHandlerInterface;
use App\Modules\Users\Exceptions\UserNotFoundException;
use App\Modules\Users\Models\User;
use App\Modules\Users\Queries\GetUserByIdQuery;
use Psr\Log\LoggerInterface;

final readonly class GetUserByIdHandler implements QueryHandlerInterface
{
public function __construct(
private LoggerInterface $logger
) {}

public function handle(GetUserByIdQuery $query): array
{
$this->logger->info('Fetching user by ID', ['userId' => $query->userId]);

$user = User::find($query->userId);

if (!$user) {
$this->logger->warning('User not found', ['userId' => $query->userId]);
throw new UserNotFoundException("User with ID {$query->userId} not found");
}

// Remove sensitive data
$userData = $user->toArray();
unset($userData['password']);

return $userData;
}
}

Controller Implementation

namespace App\Modules\Users\Controllers;

use App\Core\Bus\QueryBusInterface;
use App\Modules\Users\Exceptions\UserNotFoundException;
use App\Modules\Users\Queries\GetUserByIdQuery;
use Illuminate\Http\JsonResponse;

class UserController extends Controller
{
public function __construct(
private QueryBusInterface $queryBus
) {}

public function show(int $id): JsonResponse
{
try {
$query = new GetUserByIdQuery($id);
$user = $this->queryBus->dispatch($query);

return response()->json(['data' => $user]);
} catch (UserNotFoundException $e) {
return response()->json(['error' => $e->getMessage()], 404);
} catch (\Exception $e) {
return response()->json(['error' => 'Failed to retrieve user'], 500);
}
}
}

Complex Business Logic

Order Processing System

Command Definition

namespace App\Modules\Orders\Commands;

use App\Core\Bus\CommandInterface;

final readonly class CreateOrderCommand implements CommandInterface
{
/**
* @param int $userId
* @param array $items Array of items with format [['product_id' => 1, 'quantity' => 2], ...]
* @param string $shippingAddress
*/
public function __construct(
public int $userId,
public array $items,
public string $shippingAddress
) {
if (empty($items)) {
throw new \InvalidArgumentException('Order must contain at least one item');
}

foreach ($items as $item) {
if (!isset($item['product_id']) || !isset($item['quantity'])) {
throw new \InvalidArgumentException('Each item must have product_id and quantity');
}

if ($item['quantity'] <= 0) {
throw new \InvalidArgumentException('Item quantity must be positive');
}
}

if (empty($shippingAddress)) {
throw new \InvalidArgumentException('Shipping address is required');
}
}
}

Command Handler

namespace App\Modules\Orders\Handlers;

use App\Core\Bus\CommandBusInterface;
use App\Core\Bus\CommandHandlerInterface;
use App\Core\Bus\QueryBusInterface;
use App\Modules\Orders\Commands\CreateOrderCommand;
use App\Modules\Orders\Exceptions\InsufficientInventoryException;
use App\Modules\Orders\Models\Order;
use App\Modules\Orders\Models\OrderItem;
use App\Modules\Products\Queries\GetProductByIdQuery;
use App\Modules\Users\Queries\GetUserByIdQuery;
use Illuminate\Support\Facades\DB;
use Psr\Log\LoggerInterface;

final readonly class CreateOrderHandler implements CommandHandlerInterface
{
public function __construct(
private QueryBusInterface $queryBus,
private CommandBusInterface $commandBus,
private LoggerInterface $logger
) {}

public function handle(CreateOrderCommand $command): void
{
$this->logger->info('Creating new order', [
'userId' => $command->userId,
'itemCount' => count($command->items)
]);

// Verify user exists
$user = $this->queryBus->dispatch(new GetUserByIdQuery($command->userId));

// Calculate total and verify inventory
$orderTotal = 0;
$orderItems = [];

foreach ($command->items as $item) {
$product = $this->queryBus->dispatch(
new GetProductByIdQuery($item['product_id'])
);

// Check inventory
if ($product['stock'] < $item['quantity']) {
throw new InsufficientInventoryException(
"Insufficient inventory for product {$product['name']}"
);
}

$itemTotal = $product['price'] * $item['quantity'];
$orderTotal += $itemTotal;

$orderItems[] = [
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'unit_price' => $product['price'],
'total' => $itemTotal
];
}

// Create order in transaction
DB::transaction(function () use ($command, $orderTotal, $orderItems) {
// Create order
$order = new Order();
$order->user_id = $command->userId;
$order->total = $orderTotal;
$order->shipping_address = $command->shippingAddress;
$order->status = 'pending';
$order->save();

// Create order items
foreach ($orderItems as $item) {
$orderItem = new OrderItem();
$orderItem->order_id = $order->id;
$orderItem->product_id = $item['product_id'];
$orderItem->quantity = $item['quantity'];
$orderItem->unit_price = $item['unit_price'];
$orderItem->total = $item['total'];
$orderItem->save();

// Update inventory
$this->commandBus->dispatch(
new DecrementProductStockCommand(
$item['product_id'],
$item['quantity']
)
);
}

// Create payment record
$this->commandBus->dispatch(
new CreatePaymentCommand(
$order->id,
$orderTotal
)
);

$this->logger->info('Order created successfully', ['orderId' => $order->id]);

// Dispatch domain event
event(new OrderCreatedEvent($order->id, $command->userId));
});
}
}

Controller Implementation

namespace App\Modules\Orders\Controllers;

use App\Core\Bus\CommandBusInterface;
use App\Modules\Orders\Commands\CreateOrderCommand;
use App\Modules\Orders\Exceptions\InsufficientInventoryException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class OrderController extends Controller
{
public function __construct(
private CommandBusInterface $commandBus
) {}

public function store(Request $request): JsonResponse
{
$request->validate([
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|integer|exists:products,id',
'items.*.quantity' => 'required|integer|min:1',
'shipping_address' => 'required|string'
]);

try {
$command = new CreateOrderCommand(
userId: auth()->id(),
items: $request->input('items'),
shippingAddress: $request->input('shipping_address')
);

$this->commandBus->dispatch($command);

return response()->json(['message' => 'Order created successfully'], 201);
} catch (InsufficientInventoryException $e) {
return response()->json(['error' => $e->getMessage()], 422);
} catch (\InvalidArgumentException $e) {
return response()->json(['error' => $e->getMessage()], 422);
} catch (\Exception $e) {
return response()->json(['error' => 'Failed to create order'], 500);
}
}
}

Cross-Module Communication

User Registration with Email Notification

Command Definition (Users Module)

namespace App\Modules\Users\Commands;

use App\Core\Bus\CommandInterface;

final readonly class RegisterUserCommand implements CommandInterface
{
public function __construct(
public string $name,
public string $email,
public string $password
) {
// Validation omitted for brevity
}
}

Command Handler (Users Module)

namespace App\Modules\Users\Handlers;

use App\Core\Bus\CommandBusInterface;
use App\Core\Bus\CommandHandlerInterface;
use App\Modules\Users\Commands\RegisterUserCommand;
use App\Modules\Users\Models\User;
use Illuminate\Support\Facades\Hash;
use Psr\Log\LoggerInterface;

final readonly class RegisterUserHandler implements CommandHandlerInterface
{
public function __construct(
private CommandBusInterface $commandBus,
private LoggerInterface $logger
) {}

public function handle(RegisterUserCommand $command): void
{
$this->logger->info('Registering new user', ['email' => $command->email]);

// Create user
$user = new User();
$user->name = $command->name;
$user->email = $command->email;
$user->password = Hash::make($command->password);
$user->save();

$this->logger->info('User registered successfully', ['id' => $user->id]);

// Cross-module communication via command bus
$this->commandBus->dispatch(
new \App\Modules\Notifications\Commands\SendWelcomeEmailCommand(
$user->id,
$user->email,
$user->name
)
);
}
}

Command Definition (Notifications Module)

namespace App\Modules\Notifications\Commands;

use App\Core\Bus\CommandInterface;

final readonly class SendWelcomeEmailCommand implements CommandInterface
{
public function __construct(
public int $userId,
public string $email,
public string $name
) {}
}

Command Handler (Notifications Module)

namespace App\Modules\Notifications\Handlers;

use App\Core\Bus\CommandHandlerInterface;
use App\Modules\Notifications\Commands\SendWelcomeEmailCommand;
use App\Modules\Notifications\Services\EmailService;
use Psr\Log\LoggerInterface;

final readonly class SendWelcomeEmailHandler implements CommandHandlerInterface
{
public function __construct(
private EmailService $emailService,
private LoggerInterface $logger
) {}

public function handle(SendWelcomeEmailCommand $command): void
{
$this->logger->info('Sending welcome email', [
'userId' => $command->userId,
'email' => $command->email
]);

$this->emailService->sendWelcomeEmail(
$command->email,
$command->name
);

$this->logger->info('Welcome email sent successfully');
}
}

Event-Driven Architecture

Domain Event and Listener

Domain Event

namespace App\Modules\Orders\Events;

use Illuminate\Foundation\Events\Dispatchable;

class OrderShippedEvent
{
use Dispatchable;

public function __construct(
public int $orderId,
public int $userId,
public string $trackingNumber
) {}
}

Event Listener

namespace App\Modules\Notifications\Listeners;

use App\Core\Bus\CommandBusInterface;
use App\Modules\Notifications\Commands\SendOrderShippedNotificationCommand;
use App\Modules\Orders\Events\OrderShippedEvent;

class OrderShippedListener
{
public function __construct(
private CommandBusInterface $commandBus
) {}

public function handle(OrderShippedEvent $event): void
{
$this->commandBus->dispatch(
new SendOrderShippedNotificationCommand(
$event->orderId,
$event->userId,
$event->trackingNumber
)
);
}
}

Event Registration

// In EventServiceProvider.php
protected $listen = [
\App\Modules\Orders\Events\OrderShippedEvent::class => [
\App\Modules\Notifications\Listeners\OrderShippedListener::class,
\App\Modules\Analytics\Listeners\TrackOrderShippedListener::class,
],
];

Advanced Patterns

Decorator Pattern

Logging Decorator for Command Bus

namespace App\Core\Bus\Decorators;

use App\Core\Bus\CommandBusInterface;
use App\Core\Bus\CommandInterface;
use Psr\Log\LoggerInterface;

class LoggingCommandBusDecorator implements CommandBusInterface
{
public function __construct(
private CommandBusInterface $commandBus,
private LoggerInterface $logger
) {}

public function dispatch(CommandInterface $command): void
{
$commandClass = get_class($command);

$this->logger->info("Dispatching command", [
'command' => $commandClass,
'data' => $this->sanitizeCommandData($command)
]);

$startTime = microtime(true);

try {
$this->commandBus->dispatch($command);

$executionTime = microtime(true) - $startTime;

$this->logger->info("Command executed successfully", [
'command' => $commandClass,
'execution_time' => $executionTime
]);
} catch (\Throwable $e) {
$executionTime = microtime(true) - $startTime;

$this->logger->error("Command execution failed", [
'command' => $commandClass,
'execution_time' => $executionTime,
'exception' => get_class($e),
'message' => $e->getMessage()
]);

throw $e;
}
}

public function register(string $commandClass, string $handlerClass): void
{
$this->commandBus->register($commandClass, $handlerClass);
}

private function sanitizeCommandData(CommandInterface $command): array
{
$data = [];
$reflection = new \ReflectionClass($command);

foreach ($reflection->getProperties() as $property) {
$property->setAccessible(true);
$propertyName = $property->getName();
$propertyValue = $property->getValue($command);

// Sanitize sensitive data
if (in_array($propertyName, ['password', 'token', 'secret'])) {
$data[$propertyName] = '***REDACTED***';
} else {
$data[$propertyName] = $propertyValue;
}
}

return $data;
}
}

Registration in Service Provider

namespace App\Providers;

use App\Core\Bus\CommandBusInterface;
use App\Core\Bus\Decorators\LoggingCommandBusDecorator;
use App\Core\Bus\Implementations\SimpleCommandBus;
use Illuminate\Support\ServiceProvider;
use Psr\Log\LoggerInterface;

class BusServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(CommandBusInterface::class, function ($app) {
$commandBus = new SimpleCommandBus($app);

// Apply decorator
return new LoggingCommandBusDecorator(
$commandBus,
$app->make(LoggerInterface::class)
);
});
}
}

Chain of Responsibility Pattern

Validation Middleware

namespace App\Core\Bus\Middleware;

use App\Core\Bus\CommandInterface;
use Closure;

class ValidationMiddleware
{
public function handle(CommandInterface $command, Closure $next)
{
// Validate command if it implements ValidatableInterface
if ($command instanceof ValidatableInterface) {
$command->validate();
}

return $next($command);
}
}

Transaction Middleware

namespace App\Core\Bus\Middleware;

use App\Core\Bus\CommandInterface;
use Closure;
use Illuminate\Support\Facades\DB;

class TransactionMiddleware
{
public function handle(CommandInterface $command, Closure $next)
{
// Only apply transaction for commands that need it
if ($command instanceof TransactionalInterface) {
return DB::transaction(function () use ($command, $next) {
return $next($command);
});
}

return $next($command);
}
}

Middleware Registration

namespace App\Providers;

use App\Core\Bus\CommandBusInterface;
use App\Core\Bus\Implementations\MiddlewareCommandBus;
use App\Core\Bus\Middleware\ValidationMiddleware;
use App\Core\Bus\Middleware\TransactionMiddleware;
use App\Core\Bus\Middleware\LoggingMiddleware;
use Illuminate\Support\ServiceProvider;

class BusServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(CommandBusInterface::class, function ($app) {
$commandBus = new MiddlewareCommandBus($app);

// Register middleware in execution order
$commandBus->addMiddleware(new LoggingMiddleware());
$commandBus->addMiddleware(new ValidationMiddleware());
$commandBus->addMiddleware(new TransactionMiddleware());

return $commandBus;
});
}
}