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;
});
}
}