پرش به مطلب اصلی

راهنمای پیاده‌سازی سیستم باس

این راهنما دستورالعمل‌های گام به گام برای پیاده‌سازی سیستم باس در ماژول‌های شما را ارائه می‌دهد. پیروی از این دستورالعمل‌ها اطمینان می‌دهد که پیاده‌سازی شما با اصول معماری پروژه Planet مطابقت دارد.

ساختار ماژول

هنگام پیاده‌سازی سیستم باس در یک ماژول جدید، از ساختار دایرکتوری زیر استفاده کنید:

app/Modules/[ModuleName]/
├── Bus/
│ ├── Commands/ # Command DTOs
│ ├── Queries/ # Query DTOs
│ └── Handlers/ # Command and Query handlers
├── Exceptions/ # استثناهای خاص دامنه
├── Http/Controllers/ # کنترلرهایی که از باس استفاده می‌کنند
└── Providers/ # ارائه‌دهنده‌های سرویس برای ثبت

گام 1: ایجاد Query و Handler

Query DTO

یک Query DTO که 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('شناسه موجودیت باید عدد صحیح مثبت باشد');
}
}
}

Query Handler

یک کنترل‌کننده برای 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('در حال بازیابی موجودیت با شناسه', [
'entityId' => $query->entityId
]);

// کنترل‌کننده نباید مستقیماً حاوی منطق کسب‌وکار باشد
// در عوض، به یک سرویس/مخزن اختصاصی که حاوی منطق کسب‌وکار واقعی است، واگذار کنید
$result = $this->entityService->findById($query->entityId);

// داده را مستقیماً برگردانید - QueryBus آن را در BusResponse قرار می‌دهد
return $entityData;
}
}

گام 2: ایجاد Command و Handler

Command DTO

یک Command DTO که 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('فیلد الزامی نمی‌تواند خالی باشد');
}
}
}

Command Handler

یک کنترل‌کننده برای 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('در حال ایجاد موجودیت', [
'requiredField' => $command->requiredField
]);

// کنترل‌کننده نباید مستقیماً حاوی منطق کسب‌وکار باشد
// در عوض، به یک سرویس اختصاصی که حاوی منطق کسب‌وکار واقعی است، واگذار کنید
$this->entityService->create($command->requiredField, $command->optionalField);

$this->logger->info('موجودیت با موفقیت ایجاد شد');
}
}

گام 3: ایجاد استثناهای دامنه

استثناهای خاص دامنه برای ماژول خود ایجاد کنید:

<?php
declare(strict_types=1);

namespace App\Modules\[ModuleName]\Exceptions;

use Exception;

class [Entity]NotFoundException extends Exception
{
//
}

class [Entity]AlreadyExistsException extends Exception
{
//
}

گام 4: ایجاد ارائه‌دهنده سرویس

یک ارائه‌دهنده سرویس برای ثبت کنترل‌کننده‌های خود ایجاد کنید:

<?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
{
// سرویس‌های خاص ماژول را اینجا ثبت کنید
}

public function boot(
QueryBusInterface $queryBus,
CommandBusInterface $commandBus
): void {
// ثبت کنترل‌کننده‌های Query
$queryBus->register(
queryClass: Get[Entity]ByIdQuery::class,
handlerClass: Get[Entity]ByIdHandler::class
);

// ثبت کنترل‌کننده‌های Command
$commandBus->register(
commandClass: Create[Entity]Command::class,
handlerClass: Create[Entity]Handler::class
);
}
}

گام 5: ثبت ارائه‌دهنده سرویس

ارائه‌دهنده سرویس خود را در config/app.php ثبت کنید:

'providers' => [
// ...
App\Core\Bus\Providers\BusServiceProvider::class,
App\Modules\[ModuleName]\Providers\[ModuleName]ServiceProvider::class,
],

گام 6: ایجاد کنترلر

یک کنترلر که از سیستم باس استفاده می‌کند ایجاد کنید:

<?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('بازیابی موجودیت با شکست مواجه شد', [
'entityId' => $id,
'exception' => $e->getMessage()
]);

return response()->json([
'success' => false,
'message' => 'خطای داخلی سرور'
], 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' => 'موجودیت با موفقیت ایجاد شد'
], 201);

} catch (\InvalidArgumentException $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 400);

} catch (\Exception $e) {
$this->logger->error('ایجاد موجودیت با شکست مواجه شد', [
'exception' => $e->getMessage()
]);

return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
}

گام 7: نوشتن تست‌ها

تست‌هایی برای پیاده‌سازی خود ایجاد کنید:

<?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('مقدار تست');

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

// بررسی ایجاد موجودیت
$this->assertDatabaseHas('[entities]', [
'required_field' => 'مقدار تست'
]);
}

/** @test */
public function can_retrieve_entity(): void
{
// ایجاد موجودیت تست
$entity = [Entity]::factory()->create([
'required_field' => 'مقدار تست'
]);

$query = new Get[Entity]ByIdQuery($entity->id);
$response = $this->queryBus->dispatch($query);

$this->assertTrue($response->isSuccess());
$this->assertIsArray($response->getData());
$this->assertEquals('مقدار تست', $response->getData()['required_field']);
}
}

چک‌لیست پیاده‌سازی

از این چک‌لیست برای اطمینان از تکمیل تمام مراحل لازم استفاده کنید:

  • ایجاد Query و Handler
  • ایجاد Command و Handler
  • ایجاد استثناهای دامنه
  • ایجاد ارائه‌دهنده سرویس
  • ثبت در config/app.php
  • ایجاد کنترلر
  • نوشتن تست‌ها
  • به‌روزرسانی مستندات

اصول کلیدی

1. طراحی DTO

  • همیشه از readonly برای DTOها استفاده کنید
  • در سازنده اعتبارسنجی کنید
  • ترتیب پارامتر: الزامی → nullable → مقادیر پیش‌فرض

2. طراحی Handler

  • یک کنترل‌کننده برای هر پیام
  • همیشه CommandHandlerInterface یا QueryHandlerInterface را پیاده‌سازی کنید
  • کنترل‌کننده‌ها نباید مستقیماً حاوی منطق کسب‌وکار باشند - به سرویس‌ها یا مخازن واگذار کنید
  • کنترل‌کننده‌ها مسئول هماهنگی، لاگ‌گیری و فراخوانی سرویس‌های مناسب هستند
  • از تزریق وابستگی استفاده کنید
  • لاگ‌گیری مناسب با زمینه

3. مدیریت استثنا

  • از استثناهای دامنه استفاده کنید
  • لاگ‌گیری کامل با stack trace
  • از اصول مدیریت خطای پروژه Planet پیروی کنید

اشتباهات رایج

1. منطق کسب‌وکار در کنترلرها

هرگز منطق کسب‌وکار را در کنترلرها قرار ندهید. کنترلرها فقط باید:

  • DTOها را از داده‌های درخواست ایجاد کنند
  • commandها/queryها را ارسال کنند
  • پاسخ‌ها را قالب‌بندی کنند

2. وابستگی‌های مستقیم ماژول

هرگز وابستگی‌های مستقیم بین ماژول‌ها ایجاد نکنید. همیشه از سیستم باس برای ارتباط بین ماژولی استفاده کنید.

3. اعتبارسنجی گم شده

همیشه ورودی را در سازنده‌های DTO اعتبارسنجی کنید تا سریع شکست بخورد.

4. لاگ‌گیری ناکافی

همیشه لاگ‌گیری مناسب با اطلاعات زمینه را شامل کنید.

5. نادیده گرفتن انواع بازگشتی

همیشه از انواع بازگشتی مناسب استفاده کنید و از اصل CQS پیروی کنید:

  • Commandها: نوع بازگشتی void
  • Queryها: داده را مستقیماً برگردانید (نه پیچیده شده در BusResponse)

مراحل بعدی

پس از پیاده‌سازی سیستم باس در ماژول خود، موارد زیر را در نظر بگیرید:

  1. نوشتن تست‌های جامع
  2. مستندسازی پیاده‌سازی خود
  3. بررسی کد خود برای پایبندی به بهترین شیوه‌ها
  4. بهینه‌سازی عملکرد در صورت لزوم