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

بهترین شیوه‌های سیستم باس

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

طراحی DTO

استفاده از ویژگی‌های Readonly

همیشه از اصلاح‌کننده readonly برای ویژگی‌های DTO استفاده کنید تا تغییرناپذیری را تضمین کنید:

final readonly class GetUserByIdQuery implements QueryInterface
{
public function __construct(
public int $userId
) {
// اعتبارسنجی اینجا
}
}

اعتبارسنجی در سازنده

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

public function __construct(
public string $email,
public string $password
) {
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('آدرس ایمیل نامعتبر است');
}

if (strlen($password) < 8) {
throw new InvalidArgumentException('رمز عبور باید حداقل 8 کاراکتر باشد');
}
}

ترتیب پارامترها

این ترتیب را برای پارامترهای سازنده دنبال کنید:

  1. پارامترهای الزامی
  2. پارامترهای اختیاری بدون مقدار پیش‌فرض (nullable)
  3. پارامترهای اختیاری با مقادیر پیش‌فرض
public function __construct(
public string $name, // الزامی
public ?string $description, // اختیاری، nullable
public int $limit = 10 // اختیاری با پیش‌فرض
) {
// اعتبارسنجی
}

استفاده از آرگومان‌های نام‌گذاری شده

هنگام ایجاد DTOها، از آرگومان‌های نام‌گذاری شده برای وضوح استفاده کنید:

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

طراحی کنترل‌کننده

مسئولیت واحد

هر کنترل‌کننده باید دقیقاً یک نوع پیام را کنترل کند:

// خوب
final readonly class CreateUserHandler
{
public function handle(CreateUserCommand $command): void
{
// پیاده‌سازی
}
}

// بد - کنترل چندین نوع command
final readonly class UserHandler
{
public function handleCreate(CreateUserCommand $command): void { /* ... */ }
public function handleUpdate(UpdateUserCommand $command): void { /* ... */ }
}

تزریق وابستگی

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

final readonly class GetUserByIdHandler
{
public function __construct(
private UserRepositoryInterface $repository,
private LoggerInterface $logger
) {}

public function handle(GetUserByIdQuery $query): array
{
// پیاده‌سازی با استفاده از $this->repository و $this->logger
}
}

لاگ‌گیری مناسب

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

public function handle(CreateUserCommand $command): void
{
$this->logger->info('در حال ایجاد کاربر', [
'email' => $command->email,
// داده‌های حساس مانند رمزهای عبور را لاگ نکنید
]);

try {
// پیاده‌سازی

$this->logger->info('کاربر با موفقیت ایجاد شد', [
'userId' => $userId
]);
} catch (Exception $e) {
$this->logger->error('ایجاد کاربر شکست خورد', [
'exception' => $e->getMessage(),
'email' => $command->email
]);

throw $e;
}
}

مدیریت تراکنش

از تراکنش‌ها برای عملیات‌هایی که چندین رکورد را اصلاح می‌کنند استفاده کنید:

public function handle(CreateOrderCommand $command): void
{
DB::transaction(function () use ($command) {
// ایجاد سفارش
// به‌روزرسانی موجودی
// ایجاد رکورد پرداخت
});
}

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

استثناهای خاص دامنه

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

// به جای استثناهای عمومی
throw new Exception('کاربر یافت نشد');

// از استثناهای خاص دامنه استفاده کنید
throw new UserNotFoundException("کاربر با شناسه {$userId} یافت نشد");

سلسله مراتب استثنا

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

// استثنای پایه برای ماژول
abstract class UserModuleException extends Exception {}

// استثناهای خاص
class UserNotFoundException extends UserModuleException {}
class UserAlreadyExistsException extends UserModuleException {}
class InvalidUserDataException extends UserModuleException {}

پیام‌های خطای جامع

اطلاعات مرتبط را در پیام‌های خطا شامل کنید:

// خیلی عمومی
throw new UserNotFoundException('کاربر یافت نشد');

// بهتر
throw new UserNotFoundException("کاربر با شناسه {$userId} یافت نشد");

// حتی بهتر برای دیباگ
throw new UserNotFoundException("کاربر با شناسه {$userId} در پایگاه داده {$this->connection->getName()} یافت نشد");

آزمایش

آزمایش واحد کنترل‌کننده‌ها

کنترل‌کننده‌ها را به صورت ایزوله آزمایش کنید:

public function test_get_user_by_id_handler_returns_user_data(): void
{
// ترتیب
$userId = 1;
$expectedUser = ['id' => 1, 'name' => 'John'];

$repository = $this->createMock(UserRepositoryInterface::class);
$repository->expects($this->once())
->method('findById')
->with($userId)
->willReturn($expectedUser);

$logger = $this->createMock(LoggerInterface::class);
$handler = new GetUserByIdHandler($repository, $logger);

// اقدام
$result = $handler->handle(new GetUserByIdQuery($userId));

// ادعا
$this->assertEquals($expectedUser, $result);
}

آزمایش یکپارچگی

کل جریان از کنترلر تا کنترل‌کننده را آزمایش کنید:

public function test_can_get_user_by_id_through_bus(): void
{
// ترتیب
$user = User::factory()->create();

// اقدام
$response = $this->getJson("/api/users/{$user->id}");

// ادعا
$response->assertStatus(200)
->assertJson([
'data' => [
'id' => $user->id,
'name' => $user->name
]
]);
}

آزمایش موارد خطا

همیشه موارد خطا را آزمایش کنید:

public function test_get_user_by_id_handler_throws_exception_when_user_not_found(): void
{
// ترتیب
$userId = 999;

$repository = $this->createMock(UserRepositoryInterface::class);
$repository->expects($this->once())
->method('findById')
->with($userId)
->willReturn(null);

$logger = $this->createMock(LoggerInterface::class);
$handler = new GetUserByIdHandler($repository, $logger);

// ادعا
$this->expectException(UserNotFoundException::class);

// اقدام
$handler->handle(new GetUserByIdQuery($userId));
}

یکپارچه‌سازی ماژول

ثبت ارائه‌دهنده سرویس

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

public function boot(
QueryBusInterface $queryBus,
CommandBusInterface $commandBus
): void {
// ثبت کنترل‌کننده‌های Query
$queryBus->register(GetUserByIdQuery::class, GetUserByIdHandler::class);
$queryBus->register(GetUsersByRoleQuery::class, GetUsersByRoleHandler::class);

// ثبت کنترل‌کننده‌های Command
$commandBus->register(CreateUserCommand::class, CreateUserHandler::class);
$commandBus->register(UpdateUserCommand::class, UpdateUserHandler::class);
}

طراحی کنترلر

کنترلرها را نازک و متمرکز بر نگرانی‌های HTTP نگه دارید:

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

if ($response->isSuccess()) {
return response()->json([
'data' => $response->getData()
]);
}

return response()->json([
'error' => $response->getErrorMessage()
], 404);
} catch (Exception $e) {
return response()->json([
'error' => 'خطای داخلی سرور'
], 500);
}
}

ارتباط بین ماژولی

هرگز وابستگی‌های مستقیم ایجاد نکنید

هرگز وابستگی‌های مستقیم بین ماژول‌ها ایجاد نکنید:

// بد - وابستگی مستقیم به ماژول دیگر
use App\Modules\Products\Models\Product;

class OrderHandler
{
public function handle(CreateOrderCommand $command): void
{
$product = Product::find($command->productId);
// ...
}
}

// خوب - از سیستم باس برای ارتباط بین ماژولی استفاده کنید
class OrderHandler
{
public function __construct(
private QueryBusInterface $queryBus
) {}

public function handle(CreateOrderCommand $command): void
{
$response = $this->queryBus->dispatch(
new GetProductByIdQuery($command->productId)
);

if (!$response->isSuccess()) {
throw new ProductNotFoundException($response->getErrorMessage());
}

$product = $response->getData();
// ...
}
}

از رویدادهای دامنه برای ارتباط ناهمزمان استفاده کنید

برای ارتباط ناهمزمان بین ماژول‌ها، از رویدادهای دامنه استفاده کنید:

// در ماژول User
public function handle(CreateUserCommand $command): void
{
// ایجاد کاربر

// ارسال رویداد دامنه
event(new UserCreatedEvent($userId, $command->email));
}

// در ماژول دیگر
class UserCreatedListener
{
public function __construct(
private CommandBusInterface $commandBus
) {}

public function handle(UserCreatedEvent $event): void
{
$this->commandBus->dispatch(
new SendWelcomeEmailCommand($event->userId, $event->email)
);
}
}

ملاحظات عملکرد

بارگذاری تنبل

از بارگذاری تنبل برای وابستگی‌هایی که همیشه مورد نیاز نیستند استفاده کنید:

public function handle(GetUserByIdQuery $query): array
{
// $this->expensiveService فقط در صورت نیاز ایجاد می‌شود
if ($query->includeDetails) {
$details = $this->container->make(ExpensiveService::class)->getDetails($query->userId);
return ['user' => $user, 'details' => $details];
}

return ['user' => $user];
}

کش کردن

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

public function handle(GetUserByIdQuery $query): array
{
$cacheKey = "user:{$query->userId}";

if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}

$user = $this->repository->findById($query->userId);

$this->cache->put($cacheKey, $user, 3600); // کش برای 1 ساعت

return $user;
}

پردازش دسته‌ای

برای عملیات روی چندین آیتم، از پردازش دسته‌ای استفاده کنید:

public function handle(DeleteUsersCommand $command): void
{
DB::transaction(function () use ($command) {
// به جای حذف یکی یکی
$this->repository->deleteMany($command->userIds);
});
}

مستندسازی

توضیحات کد

توضیحات معنادار به منطق پیچیده اضافه کنید:

public function handle(CalculatePricingCommand $command): void
{
// اعمال قیمت پایه
$price = $command->basePrice;

// اعمال تخفیف مقدار
if ($command->quantity > 10) {
// برای مقادیر بیش از 10، 5٪ تخفیف اعمال کنید
$price *= 0.95;
}

// اعمال تخفیف فصلی در صورت وجود
if ($this->isSeasonalDiscountPeriod()) {
// 10٪ تخفیف اضافی در طول تبلیغات فصلی
$price *= 0.9;
}

// ذخیره قیمت نهایی
$this->repository->updatePrice($command->productId, $price);
}

بلوک‌های PHPDoc

بلوک‌های PHPDoc را به متدها اضافه کنید:

/**
* کنترل کردن query دریافت کاربر با شناسه.
*
* @param GetUserByIdQuery $query Query حاوی شناسه کاربر
* @return array داده‌های کاربر
* @throws UserNotFoundException اگر کاربر یافت نشود
*/
public function handle(GetUserByIdQuery $query): array
{
// پیاده‌سازی
}

به‌روزرسانی README

README ماژول را با موارد زیر به‌روز نگه دارید:

  • هدف ماژول
  • commandها و queryهای موجود
  • مثال‌های یکپارچه‌سازی
  • مسائل رایج و راه‌حل‌ها

ملاحظات امنیتی

اعتبارسنجی ورودی

همیشه ورودی را در DTOها اعتبارسنجی کنید:

public function __construct(
public string $email,
public string $password
) {
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('آدرس ایمیل نامعتبر است');
}

if (strlen($password) < 8) {
throw new InvalidArgumentException('رمز عبور باید حداقل 8 کاراکتر باشد');
}
}

بررسی‌های مجوز

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

public function handle(UpdateUserCommand $command): void
{
// بررسی اینکه آیا کاربر فعلی می‌تواند کاربر هدف را به‌روزرسانی کند
if (!$this->authorizationService->canUpdate(Auth::id(), $command->userId)) {
throw new UnauthorizedException('شما مجاز به به‌روزرسانی این کاربر نیستید');
}

// ادامه با به‌روزرسانی
}

مدیریت داده‌های حساس

با داده‌های حساس مراقب باشید:

// داده‌های حساس را لاگ نکنید
$this->logger->info('کاربر ایجاد شد', [
'userId' => $user->id,
'email' => $user->email,
// رمز عبور یا سایر داده‌های حساس را شامل نکنید
]);

// داده‌های حساس را برنگردانید
public function handle(GetUserByIdQuery $query): array
{
$user = $this->repository->findById($query->userId);

// حذف فیلدهای حساس قبل از برگرداندن
unset($user['password']);
unset($user['securityQuestionAnswer']);

return $user;
}

الگوهای ضد رایج برای اجتناب

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

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

// بد
public function store(Request $request): JsonResponse
{
$user = new User();
$user->name = $request->input('name');
$user->email = $request->input('email');
$user->password = Hash::make($request->input('password'));
$user->save();

return response()->json(['message' => 'کاربر ایجاد شد'], 201);
}

// خوب
public function store(Request $request): JsonResponse
{
$command = new CreateUserCommand(
$request->input('name'),
$request->input('email'),
$request->input('password')
);

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

return response()->json(['message' => 'کاربر ایجاد شد'], 201);
}

دسترسی مستقیم به پایگاه داده در کنترل‌کننده‌ها

به جای دسترسی مستقیم به پایگاه داده از مخازن استفاده کنید:

// بد
public function handle(GetUserByIdQuery $query): array
{
return DB::table('users')->where('id', $query->userId)->first();
}

// خوب
public function handle(GetUserByIdQuery $query): array
{
return $this->userRepository->findById($query->userId);
}

برگرداندن Void از Queryها

Queryها همیشه باید داده برگردانند:

// بد
public function handle(GetUserByIdQuery $query): void
{
$user = $this->repository->findById($query->userId);
event(new UserViewedEvent($user));
}

// خوب
public function handle(GetUserByIdQuery $query): array
{
$user = $this->repository->findById($query->userId);
event(new UserViewedEvent($user));
return $user;
}

تغییر وضعیت در Queryها

Queryها هرگز نباید وضعیت را تغییر دهند:

// بد
public function handle(GetUserByIdQuery $query): array
{
$user = $this->repository->findById($query->userId);
$user->last_viewed_at = now();
$user->save();

return $user->toArray();
}

// خوب
public function handle(GetUserByIdQuery $query): array
{
$user = $this->repository->findById($query->userId);

// اگر نیاز به پیگیری بازدیدها دارید، از یک command استفاده کنید
$this->commandBus->dispatch(new TrackUserViewCommand($query->userId));

return $user->toArray();
}

برگرداندن داده از Commandها

Commandها هرگز نباید داده برگردانند:

// بد
public function handle(CreateUserCommand $command): int
{
$user = new User();
// تنظیم ویژگی‌ها
$user->save();

return $user->id;
}

// خوب
public function handle(CreateUserCommand $command): void
{
$user = new User();
// تنظیم ویژگی‌ها
$user->save();

// اگر نیاز به انتقال شناسه دارید، از یک رویداد استفاده کنید
event(new UserCreatedEvent($user->id));
}