در پروژههای PHP معمولاً با عملیات زیادی سروکار داریم؛ عملیاتی مانند ایجاد سفارش، ارسال ایمیل، حذف محصول، ثبت پرداخت، ارسال پیامک، تولید فاکتور و موارد مشابه.
در سادهترین حالت، کافی است متد مربوط به عملیات را مستقیماً صدا بزنیم:
$orderService->create($data);
اما همیشه شرایط به همین سادگی نیست.
گاهی لازم است یک عملیات را:
- در زمان دیگری اجرا کنیم.
- داخل Queue قرار دهیم.
- ثبت و لاگ کنیم.
- دوباره اجرا کنیم.
- تاریخچه اجرای آن را نگه داریم.
- امکان Undo داشته باشیم.
- بدون اطلاع از جزئیات اجرا، آن را به یک بخش دیگر سیستم بسپاریم.
در چنین شرایطی Command Pattern در PHP میتواند راهکار مناسبی باشد.
Command یکی از الگوهای طراحی رفتاری است و ایده اصلی آن این است که یک درخواست یا عملیات را به یک شیء مستقل تبدیل کنیم. در ساختار کلاسیک Command معمولاً مفاهیمی مانند Command، Invoker و Receiver وجود دارند و Invoker فقط از قرارداد Command برای اجرای عملیات استفاده میکند.
# Command Pattern چیست؟
Command Pattern یا الگوی Command الگویی است که یک درخواست یا عملیات را به شکل یک Object کپسوله میکند.
به زبان ساده، بهجای اینکه بگوییم:
$emailService->send($user);
میتوانیم یک شیء بسازیم که نماینده درخواست «ارسال ایمیل» باشد:
$command = new SendEmailCommand($user);
و بعد:
$command->execute();
در این حالت خود عملیات تبدیل به یک شیء شده است.
این موضوع امکانات بسیار بیشتری در اختیار ما قرار میدهد.
مثلاً میتوانیم Command را:
- ذخیره کنیم.
- به Queue بفرستیم.
- لاگ کنیم.
- زمان اجرای آن را کنترل کنیم.
- دوباره اجرا کنیم.
- در بعضی سناریوها Undo کنیم.
# یک مثال ساده از دنیای واقعی
فرض کنید یک کنترل از راه دور داریم.
روی کنترل چند دکمه وجود دارد:
Remote Control
│
├── روشن کردن تلویزیون
├── خاموش کردن تلویزیون
├── افزایش صدا
└── کاهش صدا
کنترل از راه دور لازم نیست بداند تلویزیون دقیقاً چگونه روشن میشود.
فقط میداند:
> وقتی دکمه روشن را فشار دادم، Command مربوط به روشن کردن تلویزیون را اجرا کن.
این دقیقاً یکی از کاربردهای مهم Command Pattern است.
# مشکل اجرای مستقیم متدها
فرض کنید یک سیستم مدیریت سفارش داریم:
<?php
final class OrderService
{
public function create(array $data): void
{
// Create order
}
}
در Controller:
<?php
$orderService = new OrderService();
$orderService->create($data);
این کد کاملاً قابل قبول است.
اما فرض کنید چند ماه بعد نیاز داشته باشیم:
- عملیات ایجاد سفارش Queue شود.
- عملیات ثبت شود.
- امکان Retry داشته باشد.
- از چند نقطه مختلف قابل اجرا باشد.
- تاریخچه عملیات را نگهداری کنیم.
در این شرایط، تبدیل عملیات به یک Command میتواند ساختار بهتری ایجاد کند.
# اجزای اصلی Command Pattern
Command Pattern معمولاً از چند نقش اصلی تشکیل میشود:
۱. Command
Interface یا قرارداد مشترک Commandها.
۲. Concrete Command
پیادهسازی واقعی یک دستور.
۳. Receiver
کلاسی که عملیات واقعی را انجام میدهد.
۴. Invoker
کلاسی که Command را اجرا میکند.
۵. Client
بخشی از برنامه که Command و Receiver را ایجاد و به یکدیگر متصل میکند.
ساختار کلی:
Client
│
▼
Command
│
▼
Invoker
│
▼
Concrete Command
│
▼
Receiver
نکته مهم این است که Invoker لازم نیست جزئیات Receiver را بداند؛ فقط Command را اجرا میکند. این جداسازی یکی از اهداف اصلی Command Pattern است.
# پیادهسازی Command Pattern در PHP
بیایید یک مثال واقعی ایجاد کنیم.
فرض کنید میخواهیم سیستم ارسال ایمیل داشته باشیم.
ابتدا Interface مربوط به Command را ایجاد میکنیم:
<?php
declare(strict_types=1);
interface Command
{
public function execute(): void;
}
تمام Commandهای ما باید متد execute() را داشته باشند.
# ساخت Receiver
حالا سرویس واقعی ارسال ایمیل را ایجاد میکنیم:
<?php
declare(strict_types=1);
final class EmailService
{
public function send(
string $email,
string $message
): void {
echo "Email sent to {$email}: {$message}\n";
}
}
این کلاس همان Receiver است.
Receiver مسئول انجام عملیات واقعی است.
# ساخت Concrete Command
حالا Command مربوط به ارسال ایمیل را ایجاد میکنیم:
<?php
declare(strict_types=1);
final class SendEmailCommand implements Command
{
public function __construct(
private readonly EmailService $emailService,
private readonly string $email,
private readonly string $message
) {
}
public function execute(): void
{
$this->emailService->send(
$this->email,
$this->message
);
}
}
حالا SendEmailCommand نماینده یک درخواست مشخص است:
> برای این ایمیل، این پیام را ارسال کن.
# ساخت Invoker
حالا یک Invoker ایجاد میکنیم:
<?php
declare(strict_types=1);
final class CommandInvoker
{
public function run(Command $command): void
{
$command->execute();
}
}
نکته مهم اینجاست که Invoker اصلاً نمیداند Command مربوط به ایمیل است یا سفارش یا پرداخت.
فقط میداند:
$command->execute();
این همان جداسازیای است که Command Pattern ایجاد میکند.
# اجرای Command
حالا همه اجزا را کنار هم قرار میدهیم:
<?php
$emailService = new EmailService();
$command = new SendEmailCommand(
emailService: $emailService,
email: 'user@example.com',
message: 'Welcome to our website'
);
$invoker = new CommandInvoker();
$invoker->run($command);
جریان اجرای برنامه:
Client
↓
SendEmailCommand
↓
CommandInvoker
↓
execute()
↓
EmailService
↓
send()
# چرا Command را مستقیم اجرا نکنیم؟
ممکن است بپرسید:
اگر در نهایت execute() همان send() را صدا میزند، چرا از همان ابتدا این کار را انجام ندهیم؟
سؤال بسیار خوبی است.
مزیت Command زمانی مشخص میشود که بخواهیم خود درخواست را مدیریت کنیم.
مثلاً:
$command = new SendEmailCommand(...);
حالا این Command یک Object است.
میتوانیم آن را:
Create
↓
Store
↓
Queue
↓
Execute
↓
Log
↓
Retry
کنیم.
در حالی که یک فراخوانی مستقیم متد، فقط همان لحظه اجرا میشود.
Command Pattern دقیقاً با تبدیل عملیات به Object، امکانهایی مانند Queue، نگهداری تاریخچه و اجرای Undo را در معماری مناسبتر میکند.
# مثال دوم: Command برای سفارش
فرض کنید یک فروشگاه اینترنتی داریم.
Receiver:
<?php
declare(strict_types=1);
final class OrderService
{
public function create(int $userId): void
{
echo "Order created for user {$userId}\n";
}
}
Command:
<?php
declare(strict_types=1);
final class CreateOrderCommand implements Command
{
public function __construct(
private readonly OrderService $orderService,
private readonly int $userId
) {
}
public function execute(): void
{
$this->orderService->create(
$this->userId
);
}
}
حالا:
<?php
$orderService = new OrderService();
$command = new CreateOrderCommand(
orderService: $orderService,
userId: 15
);
$invoker = new CommandInvoker();
$invoker->run($command);
Invoker همچنان هیچ اطلاعی از OrderService ندارد.
این نکته بسیار مهم است.
# اجرای چند Command
یکی از مزیتهای جالب Command Pattern این است که میتوان چند Command مختلف را در یک مجموعه قرار داد.
<?php
$commands = [
new CreateOrderCommand(
$orderService,
15
),
new SendEmailCommand(
$emailService,
'user@example.com',
'Your order has been created.'
),
];
سپس:
<?php
foreach ($commands as $command) {
$invoker->run($command);
}
در این حالت Invoker فقط با Interface کار میکند.
بنابراین میتواند Commandهای مختلف را بدون اطلاع از نوع دقیق آنها اجرا کند.
# Composite Command چیست؟
گاهی اوقات یک عملیات بزرگ از چند Command کوچک تشکیل میشود.
مثلاً تکمیل سفارش:
Complete Order
│
├── Create Invoice
├── Update Inventory
├── Send Email
└── Send SMS
میتوانیم یک Command داشته باشیم که خودش چند Command دیگر را اجرا کند.
<?php
declare(strict_types=1);
final class CompositeCommand implements Command
{
/**
* @param Command[] $commands
*/
public function __construct(
private readonly array $commands
) {
}
public function execute(): void
{
foreach ($this->commands as $command) {
$command->execute();
}
}
}
حالا:
<?php
$command = new CompositeCommand([
$createOrderCommand,
$invoiceCommand,
$emailCommand,
]);
$command->execute();
این روش امکان ترکیب چند عملیات مستقل را فراهم میکند.
# Undo در Command Pattern
یکی از قابلیتهای شناختهشده Command Pattern امکان ایجاد عملیات معکوس یا undo() است.
البته تمام Commandها الزاماً به Undo نیاز ندارند.
اگر چنین قابلیتی لازم باشد، میتوانیم Interface را تغییر دهیم:
<?php
interface UndoableCommand extends Command
{
public function undo(): void;
}
مثلاً Command مربوط به اضافه کردن محصول به سبد خرید:
<?php
declare(strict_types=1);
final class AddProductCommand implements UndoableCommand
{
public function __construct(
private readonly Cart $cart,
private readonly int $productId
) {
}
public function execute(): void
{
$this->cart->add(
$this->productId
);
}
public function undo(): void
{
$this->cart->remove(
$this->productId
);
}
}
حالا:
$command->execute();
محصول را اضافه میکند و:
$command->undo();
آن را حذف میکند.
مستندات DesignPatternsPHP نیز undo() را بهعنوان یکی از کاربردهای Command قابل پیادهسازی معرفی میکند.
# ساخت Command History
وقتی Commandها Object باشند، میتوانیم تاریخچه آنها را نگهداری کنیم.
مثلاً:
<?php
declare(strict_types=1);
final class CommandHistory
{
/**
* @var UndoableCommand[]
*/
private array $history = [];
public function push(
UndoableCommand $command
): void {
$this->history[] = $command;
}
public function undoLast(): void
{
$command = array_pop($this->history);
if ($command === null) {
return;
}
$command->undo();
}
}
حالا:
$command->execute();
$history->push($command);
و بعداً:
$history->undoLast();
این مدل در نرمافزارهایی که نیاز به Undo/Redo دارند بسیار کاربردی است.
# Command Pattern و Queue
یکی از مهمترین کاربردهای مدرن Command، اجرای عملیات در زمان دیگری است.
فرض کنید ارسال ایمیل عملیاتی زمانبر است.
بهجای اینکه درخواست HTTP کاربر منتظر ارسال ایمیل بماند، میتوانیم درخواست را به یک Queue بسپاریم.
ساختار مفهومی:
HTTP Request
↓
Create Command
↓
Queue
↓
Worker
↓
Execute Command
↓
Send Email
در این حالت Command به یک واحد قابل اجرا تبدیل شده است.
در معماریهای مدرن، Queue Jobها نیز از همین ایده استفاده میکنند: اطلاعات لازم برای انجام یک عملیات در یک Object قرار میگیرد و بعداً توسط Worker اجرا میشود.
# Command Pattern در Laravel
Laravel مفاهیم مختلفی دارد که به ایده Command نزدیک هستند.
یکی از مهمترین آنها Artisan Command است.
Artisan رابط خط فرمان Laravel است و امکان ساخت Commandهای اختصاصی را فراهم میکند. در مستندات Laravel میتوان با make:command یک Command اختصاصی ایجاد کرد.
مثلاً:
php artisan make:command SendWelcomeEmails
Laravel کلاس Command را برای ما ایجاد میکند.
ساختار Command میتواند چیزی شبیه این باشد:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
final class SendWelcomeEmails extends Command
{
protected $signature = 'users:send-welcome-emails';
protected $description = 'Send welcome emails to users';
public function handle(): int
{
// Business logic
$this->info('Emails sent successfully.');
return self::SUCCESS;
}
}
حالا میتوانیم اجرا کنیم:
php artisan users:send-welcome-emails
Laravel همچنین اجازه میدهد Commandها از طریق Service Container ساخته شوند و در ساختارهای مختلف پروژه ثبت شوند.
# نکته مهم: Artisan Command دقیقاً همان Command Pattern نیست
این نکته برای یک مقاله آموزشی بسیار مهم است.
وقتی در Laravel میگوییم:
php artisan users:send-welcome-emails
منظورمان الزاماً این نیست که تمام ساختار کلاسیک GoF Command Pattern را پیادهسازی کردهایم.
Artisan یک قابلیت Framework برای ساخت دستورات CLI است.
اما از نظر مفهومی، این ساختار با ایده Command ارتباط نزدیکی دارد:
Command
↓
یک عملیات مشخص
↓
قابل اجرا از طریق یک Interface مشخص
بنابراین نباید هر کلاسی که نام Command دارد را بهصورت خودکار یک پیادهسازی کامل از GoF Command Pattern بدانیم.
# اجرای Commandهای Laravel از داخل برنامه
Laravel امکان اجرای Programmatic Command را نیز فراهم میکند.
مثلاً:
<?php
use Illuminate\Support\Facades\Artisan;
Artisan::call(
'users:send-welcome-emails'
);
طبق مستندات Laravel، Artisan::call() میتواند یک Command را از داخل برنامه اجرا کند و حتی آرگومانها و Optionهای آن را نیز دریافت کند.
مثلاً:
<?php
Artisan::call(
'mail:send',
[
'user' => '15',
'--queue' => 'default',
]
);
# زمانبندی Command در Laravel
یکی دیگر از کاربردهای مهم Command در Laravel، اجرای آن توسط Scheduler است.
برای مثال:
<?php
use Illuminate\Support\Facades\Schedule;
Schedule::command(
'users:send-welcome-emails'
)->daily();
در Laravel میتوان Artisan Commandها را با Scheduler اجرا کرد و حتی Command Class و آرگومانهای آن را مشخص کرد.
در نتیجه معماری میتواند اینگونه باشد:
Scheduler
↓
Artisan Command
↓
Service
↓
Business Operation
این ساختار برای کارهایی مانند موارد زیر بسیار کاربردی است:
- تمدید سرویسها
- بررسی سفارشهای منقضی
- ارسال اعلانها
- پاکسازی دادههای قدیمی
- بهروزرسانی قیمتها
- همگامسازی اطلاعات
- تولید گزارش
# Command Pattern و Service Layer
یکی از اشتباهات رایج این است که تمام Business Logic را داخل Command قرار دهیم.
مثلاً:
public function handle(): int
{
// 300 lines of business logic
}
این ساختار معمولاً مناسب نیست.
بهتر است Command نقش هماهنگکننده داشته باشد:
public function handle(
OrderService $orderService
): int {
$orderService->process();
return self::SUCCESS;
}
در این معماری:
Command
↓
Service
↓
Domain / Business Logic
↓
Repository / Model / External Service
Command بیشتر وظیفه ورود به عملیات را دارد و منطق پیچیده کسبوکار بهتر است در Service یا لایه مناسب Domain قرار گیرد.
# Command و Dependency Injection
Command Pattern بهخوبی با Dependency Injection ترکیب میشود.
مثلاً:
<?php
declare(strict_types=1);
final class GenerateInvoiceCommand implements Command
{
public function __construct(
private readonly InvoiceService $invoiceService,
private readonly int $orderId
) {
}
public function execute(): void
{
$this->invoiceService->generate(
$this->orderId
);
}
}
در اینجا Command خودش Invoice را تولید نمیکند.
بلکه عملیات را به InvoiceService میسپارد.
این طراحی باعث میشود وابستگیها واضح و قابل تست باشند.
# تفاوت Command و Strategy
Command و Strategy هر دو میتوانند رفتار را در قالب Object قرار دهند، اما هدف آنها متفاوت است.
Strategy
Strategy به این سؤال پاسخ میدهد:
> این عملیات را با کدام الگوریتم انجام بدهم؟
مثلاً:
Calculate Discount
├── Percentage Strategy
├── Fixed Strategy
└── VIP Strategy
Command
Command بیشتر به این سؤال پاسخ میدهد:
> چه عملیاتی باید انجام شود؟
مثلاً:
Create Order
Send Email
Delete Product
Generate Invoice
بنابراین:
Strategy = انتخاب روش انجام یک کار
Command = تبدیل خود درخواست/عملیات به یک Object
این تفاوت در منابع مربوط به Command نیز بهصورت مفهومی مطرح شده است؛ Strategy بیشتر روی انتخاب الگوریتم تمرکز دارد، در حالی که Command یک درخواست را کپسوله میکند.
# تفاوت Command و Observer
Observer برای اطلاعرسانی یک رخداد به چند بخش مختلف استفاده میشود.
مثلاً:
OrderCreated
│
├── Email
├── SMS
└── Accounting
اما Command یک عملیات مشخص را به یک Object تبدیل میکند:
SendEmailCommand
↓
execute()
پس:
Observer = چه کسانی باید از رخداد مطلع شوند؟
Command = چه عملیاتی باید اجرا شود؟
این دو الگو حتی میتوانند کنار هم استفاده شوند.
# تفاوت Command و Facade
Facade یک Interface ساده برای یک سیستم پیچیده ارائه میکند.
مثلاً:
$orderFacade->completeOrder();
ممکن است پشت این متد چند سرویس مختلف اجرا شوند.
اما Command بیشتر یک درخواست مشخص را به Object تبدیل میکند:
$command->execute();
به زبان ساده:
- Facade پیچیدگی سیستم را پنهان میکند.
- Command یک درخواست را کپسوله میکند.
# تفاوت Command و Job در Laravel
این بخش برای برنامهنویسان Laravel بسیار مهم است.
یک Laravel Job میتواند از نظر مفهومی شبیه یک Command باشد؛ چون اطلاعات لازم برای انجام یک عملیات را در یک Object قرار میدهد و آن عملیات میتواند بعداً توسط Queue Worker اجرا شود.
مثلاً:
<?php
final class SendInvoiceEmail implements ShouldQueue
{
public function __construct(
public readonly int $invoiceId
) {
}
public function handle(): void
{
// Send invoice email
}
}
وقتی این Job را Queue میکنیم:
SendInvoiceEmail::dispatch($invoiceId);
در واقع یک عملیات را برای اجرای بعدی بستهبندی کردهایم.
البته Laravel Job و GoF Command Pattern دقیقاً یک مفهوم نیستند، اما از نظر معماری شباهت بسیار زیادی دارند.
# مزایای Command Pattern
۱. کاهش وابستگی
Invoker فقط Command را میشناسد و نیازی به شناخت جزئیات Receiver ندارد.
۲. امکان Queue کردن عملیات
وقتی عملیات به Object تبدیل شود، میتوان آن را برای اجرای بعدی مدیریت کرد.
۳. امکان ثبت تاریخچه
میتوان Commandهای اجراشده را ذخیره کرد.
۴. امکان Undo
در صورت نیاز میتوان Interface مربوط به undo() ایجاد کرد.
۵. ترکیب چند عملیات
میتوان چند Command را در یک Composite Command قرار داد.
۶. تستپذیری بهتر
هر Command را میتوان جداگانه تست کرد.
۷. اجرای مجدد
در سناریوهایی که عملیات قابل تکرار طراحی شده باشد، میتوان Command را دوباره اجرا کرد.
# معایب Command Pattern
Command نیز مانند سایر Design Patternها همیشه بهترین انتخاب نیست.
افزایش تعداد کلاسها
بهجای یک متد ساده ممکن است یک Interface و چند کلاس ایجاد شود.
افزایش پیچیدگی
برای عملیات ساده ممکن است ساخت Command فقط باعث پیچیدهتر شدن پروژه شود.
نیاز به مدیریت State
اگر Commandها قابل Queue یا Retry باشند، باید وضعیت آنها و اثر اجرای مجدد بهدرستی مدیریت شود.
مشکلات Serialization
اگر Command قرار است در Queue ذخیره شود، اطلاعات داخل آن باید قابلیت Serialize شدن داشته باشند و بهتر است بهجای نگهداشتن Objectهای سنگین یا Connectionها، دادههای ساده مانند ID، String و مقدارهای موردنیاز را نگه داریم.
# Idempotency در Commandها
در سیستمهای واقعی، مخصوصاً وقتی Commandها Queue یا Retry میشوند، موضوع Idempotency اهمیت زیادی پیدا میکند.
فرض کنید Command زیر پرداخت را انجام میدهد:
$paymentCommand->execute();
اگر Worker به هر دلیل دوباره آن را اجرا کند، نباید یک تراکنش را دوبار ثبت کنیم.
بنابراین باید بررسی کنیم که آیا عملیات قبلاً انجام شده است یا خیر.
مثلاً:
if ($payment->isCompleted()) {
return;
}
سپس عملیات انجام شود.
این موضوع در سیستمهای مالی، پرداخت، سفارش و ارسال پیام بسیار مهم است.
# یک مثال Command برای پرداخت
<?php
declare(strict_types=1);
final class ProcessPaymentCommand implements Command
{
public function __construct(
private readonly PaymentService $paymentService,
private readonly int $transactionId
) {
}
public function execute(): void
{
$transaction = $this->paymentService
->find($this->transactionId);
if ($transaction->isCompleted()) {
return;
}
$this->paymentService->process(
$transaction
);
}
}
در اینجا اگر Command دوباره اجرا شود، وضعیت تراکنش بررسی میشود.
این طراحی برای سیستمهایی که امکان Retry دارند اهمیت زیادی دارد.
# چه زمانی از Command Pattern استفاده کنیم؟
Command Pattern زمانی انتخاب مناسبی است که:
- عملیاتها پیچیده هستند.
- باید عملیات را Queue کنیم.
- نیاز به Retry داریم.
- نیاز به تاریخچه عملیات داریم.
- نیاز به Undo داریم.
- چند نوع عملیات مختلف داریم.
- میخواهیم Invoker از Receiver مستقل باشد.
- عملیات باید قابل ذخیره یا انتقال باشد.
- نیاز به اجرای مجدد عملیات داریم.
مثلاً در یک سیستم فروشگاهی:
Commands
│
├── CreateOrder
├── CancelOrder
├── GenerateInvoice
├── SendInvoice
├── UpdateInventory
└── RefundPayment
هرکدام میتوانند Command مستقل باشند.
# چه زمانی از Command استفاده نکنیم؟
اگر عملیات بسیار ساده است، لازم نیست حتماً Command بسازیم.
مثلاً:
$user->activate();
برای چنین عملیات سادهای ساختن:
ActivateUserCommand
CommandInterface
CommandInvoker
ActivateUserReceiver
احتمالاً بیش از حد پیچیده است.
اصل مهم این است:
> Design Pattern باید یک مشکل واقعی را حل کند، نه اینکه صرفاً برای زیباتر شدن معماری استفاده شود.
# اشتباهات رایج در Command Pattern
قرار دادن تمام Business Logic داخل Command
Command نباید به یک کلاس چندصدخطی تبدیل شود.
بهتر است منطق پیچیده در Service یا Domain قرار گیرد.
وابسته کردن Invoker به Concrete Command
Invoker بهتر است فقط Interface را بشناسد:
public function run(Command $command): void
{
$command->execute();
}
نه اینکه:
public function run(
SendEmailCommand $command
): void {
// ...
}
استفاده از Command برای هر متد ساده
هر عملیات سادهای الزاماً به Command نیاز ندارد.
بیتوجهی به Retry
اگر Command قرار است چند بار اجرا شود، باید بررسی کنیم اجرای مجدد آن چه اثری خواهد داشت.
نگهداری Objectهای نامناسب در Command
خصوصاً در Queue، بهتر است Command دادههای ساده و قابل Serialize را نگهداری کند.
مثلاً:
public function __construct(
private readonly int $userId
) {
}
اغلب انتخاب بهتری از نگهداشتن یک Object بسیار پیچیده است.
# تست Command با PHPUnit
یکی از مزایای Command این است که میتوان آن را مستقل تست کرد.
مثلاً:
<?php
use PHPUnit\Framework\TestCase;
final class SendEmailCommandTest extends TestCase
{
public function test_it_sends_email(): void
{
$emailService = $this->createMock(
EmailService::class
);
$emailService
->expects(self::once())
->method('send')
->with(
'user@example.com',
'Hello'
);
$command = new SendEmailCommand(
emailService: $emailService,
email: 'user@example.com',
message: 'Hello'
);
$command->execute();
}
}
در این تست، لازم نیست ایمیل واقعی ارسال شود.
فقط بررسی میکنیم که Command، Receiver را به شکل صحیح فراخوانی کرده است.
# معماری پیشنهادی Command در یک پروژه Laravel
برای پروژههای بزرگ Laravel میتوان ساختار منظمی برای Commandها ایجاد کرد:
app/
├── Commands/
│ ├── Orders/
│ │ ├── CreateOrderCommand.php
│ │ ├── CancelOrderCommand.php
│ │ └── CompleteOrderCommand.php
│ │
│ ├── Payments/
│ │ ├── ProcessPaymentCommand.php
│ │ └── RefundPaymentCommand.php
│ │
│ └── Users/
│ ├── ActivateUserCommand.php
│ └── SendWelcomeEmailCommand.php
│
├── Services/
│ ├── OrderService.php
│ ├── PaymentService.php
│ └── UserService.php
البته ساختار دقیق پوشهها باید بر اساس معماری پروژه انتخاب شود.
Laravel نیز امکان قرار دادن Commandها در مسیرهای سفارشی و ثبت آنها از طریق withCommands را فراهم میکند.
# Command Pattern در یک پروژه واقعی فروشگاهی
فرض کنید فروشگاه بعد از پرداخت سفارش باید این عملیات را انجام دهد:
Payment Successful
│
▼
CompleteOrderCommand
│
├── Update Inventory
├── Generate Invoice
├── Send Email
├── Send SMS
└── Notify Accounting
بهجای اینکه Controller تمام این عملیات را انجام دهد، میتوانیم یک Command داشته باشیم:
<?php
final class CompleteOrderCommand implements Command
{
public function __construct(
private readonly OrderService $orderService,
private readonly int $orderId
) {
}
public function execute(): void
{
$this->orderService->complete(
$this->orderId
);
}
}
و سپس Service مسئول هماهنگی عملیات داخلی باشد.
این ساختار باعث میشود Controller سبکتر بماند:
<?php
$commandBus->dispatch(
new CompleteOrderCommand(
$orderService,
$orderId
)
);
در پروژههای بزرگ حتی میتوان یک Command Bus ایجاد کرد که وظیفه اجرای Commandها را بر عهده بگیرد.
# Command Bus چیست؟
Command Bus سیستمی است که Commandها را دریافت و اجرا میکند.
مثلاً:
Controller
↓
Command Bus
↓
Command
↓
Handler / Receiver
بهجای اینکه هر Controller مستقیماً Serviceهای مختلف را صدا بزند، میتواند یک Command ارسال کند.
مثلاً:
$commandBus->dispatch(
new CreateOrderCommand($userId)
);
Command Bus میتواند قابلیتهای دیگری نیز اضافه کند:
- Logging
- Authorization
- Transaction
- Retry
- Queue
- Metrics
- Exception Handling
البته ساخت Command Bus اختصاصی برای پروژههای کوچک ضروری نیست.
# جمعبندی
Command Pattern در PHP یکی از الگوهای مهم Behavioral Design Patterns است که یک درخواست یا عملیات را به یک Object مستقل تبدیل میکند.
در ساختار کلاسیک آن معمولاً با مفاهیم زیر روبهرو هستیم:
Client
↓
Command
↓
Invoker
↓
Receiver
مهمترین مزیت این الگو این است که عملیات دیگر فقط یک فراخوانی ساده متد نیست؛ بلکه به یک Object تبدیل شده و میتوان آن را مدیریت کرد.
این موضوع امکانهایی مانند:
- Queue
- Retry
- Logging
- History
- Undo
- Scheduling
- ترکیب چند عملیات
را سادهتر میکند.
در PHP میتوان Command Pattern را با Interface و Dependency Injection پیادهسازی کرد و در Laravel نیز مفاهیمی مانند Artisan Command، Job و Queue ارتباط نزدیکی با ایده اجرای عملیات بهعنوان یک واحد مستقل دارند. Laravel بهصورت رسمی امکان ساخت Commandهای اختصاصی، اجرای Programmatic آنها و زمانبندی Artisan Commandها را فراهم میکند.
با این حال، نباید برای هر متد ساده یک Command ایجاد کنیم.
اگر عملیات ساده است، کد ساده معمولاً انتخاب بهتری است.
اما اگر یک عملیات باید قابل Queue شدن، Retry، ثبت، زمانبندی، Undo یا مدیریت مستقل باشد، Command Pattern میتواند ارزش بسیار زیادی ایجاد کند.
در نهایت، هدف Design Patternها اضافه کردن کلاس و کد بیشتر نیست؛ هدف اصلی آنها ایجاد ساختاری است که در برابر تغییرات آینده، قابل نگهداری و توسعه باقی بماند.
سوالات متداول
Command Pattern در PHP چیست؟
Command Pattern الگویی رفتاری است که یک درخواست یا عملیات را به یک Object تبدیل میکند تا بتوان آن را مستقل از اجراکننده مدیریت، ذخیره، Queue یا در صورت نیاز معکوس کرد.
اجزای اصلی Command Pattern چیست؟
اجزای اصلی شامل Command، Concrete Command، Receiver، Invoker و Client هستند.
Invoker در Command Pattern چه کاری انجام میدهد؟
Invoker مسئول اجرای Command است و بهتر است بدون اطلاع از جزئیات Receiver، فقط از قرارداد Command استفاده کند.
Receiver در Command Pattern چیست؟
Receiver کلاسی است که عملیات واقعی را انجام میدهد. Command معمولاً درخواست را به Receiver منتقل میکند.
آیا Command Pattern در Laravel کاربرد دارد؟
بله. علاوه بر Artisan Command، مفاهیمی مانند Job و Queue نیز از نظر معماری با ایده کپسولهسازی عملیات و اجرای مستقل آن ارتباط نزدیکی دارند.
تفاوت Command و Strategy چیست؟
Strategy مشخص میکند یک کار با کدام الگوریتم انجام شود؛ Command یک عملیات یا درخواست مشخص را به یک Object تبدیل میکند.
آیا همه Commandها باید متد undo داشته باشند؟
خیر. undo() فقط زمانی لازم است که سیستم واقعاً به قابلیت بازگرداندن عملیات نیاز داشته باشد.
آیا Command Pattern برای پروژههای کوچک مناسب است؟
معمولاً نه، مگر اینکه پروژه واقعاً به ویژگیهایی مانند Queue، Retry، History یا Undo نیاز داشته باشد. برای عملیاتهای ساده، استفاده از ساختار مستقیم معمولاً خواناتر است.
آیا Laravel Job همان Command Pattern است؟
خیر، دقیقاً یکسان نیستند؛ اما Laravel Job از نظر مفهومی شباهت زیادی به Command دارد، زیرا دادهها و عملیات موردنیاز برای اجرای یک کار را در یک Object قرار میدهد و میتواند آن را برای اجرای بعدی به Queue بسپارد.





