Service Layer در مقابل Action Class — کدام را در Laravel انتخاب کنیم؟
مقایسه عملی Service چندمتدی و Single Action — معیار انتخاب، ترکیب هر دو و anti-patternهای رایج.
در Laravel دو الگوی محبوب برای بیرون بردن منطق business از Controller داریم: Service Layer (یک کلاس با چند متد مرتبط مثل PostService) و Action Class (یک کلاس برای یک کار مشخص مثل PublishPostAction). هیچکدام الگوی رسمی Laravel نیست و «همیشه بهتر» هم ندارند.
جواب کوتاه: برای چند عملیات ساده و همخانواده روی یک موضوع، Service کافی و سریع است. برای کاری چندمرحلهای (transaction، event، ارسال به صف) که از چند جا — Controller، Job، Artisan command یا پنل ادمین — صدا زده میشود، Action خواناتر و قابلتستتر است. برای CRUD یکخطی، هیچکدام لازم نیست.
Service Layer چیست؟
class PostService
{
public function create(CreatePostData $data, User $author): Post
{
return $author->posts()->create([
'title' => $data->title,
'slug' => $data->slug,
'content' => $data->content,
]);
}
public function update(Post $post, UpdatePostData $data): Post
{
$post->update(['title' => $data->title, 'content' => $data->content]);
return $post;
}
public function archive(Post $post): void
{
$post->update(['archived_at' => now()]);
}
}
Service عملیات مرتبط با یک موضوع (اینجا Post) را کنار هم نگه میدارد — همان ایده لایه application در معماری لایهای. تا وقتی متدها کوتاه و همخانوادهاند، پیدا کردن و تغییرشان ساده است.
Action Class چیست؟
class PublishPostAction
{
public function handle(Post $post): Post
{
$post = DB::transaction(fn () => tap($post)->update(['published_at' => now()]));
PostPublished::dispatch($post);
return $post;
}
}
یک کلاس، یک کار، یک متد عمومی. نام فایل خودش میگوید چه اتفاقی میافتد. Event بعد از commit شدن transaction ارسال میشود تا listenerها داده ذخیرهنشده نبینند — جزئیات در Transaction در Laravel و Event و Listener. الگوی کامل در Action Class در Laravel.
مقایسه سریع Service و Action
| معیار | Service | Action |
|---|---|---|
| تعداد فایل | کمتر | بیشتر |
| یافتن use case | scroll متدها | نام فایل = use case |
| پروژه کوچک | راحتتر | شاید overkill |
| پروژه بزرگ | risk god service | scale بهتر |
| تست | mock یک service | تست یک action ایزوله |
| Route invokable | معمولاً نه | بله با __invoke |
| استفاده در Job و Command | inject کل Service | inject همان یک Action |
کی Service، کی Action، کی هیچکدام؟
Service مناسب است وقتی
- چند عملیات ساده روی یک موضوع دارید (ساخت، ویرایش، آرشیو Post)
- تیم ۱–۲ نفره است و سرعت مهمتر از ساختار است
- متدها واقعاً همخانوادهاند و کلاس کوچک میماند
Action مناسب است وقتی
- کار چند مرحله دارد (transaction، event، صف، اعلان)
- همان کار از چند نقطه ورود اجرا میشود
- میخواهید هر عملیات را جدا تست و لاگ کنید
- تیم بزرگ است و چند نفر همزمان روی یک Service بزرگ conflict میگیرند
هیچکدام لازم نیست وقتی
- کار در حد
Post::create($request->validated())است و جای دیگری تکرار نمیشود - پروژه کوچک یا نمونه اولیه است و هنوز نمیدانید منطق کجا رشد میکند
ساختن کلاس فقط برای «تمیز به نظر رسیدن» همان Anemic Action است که پایینتر آمده.
یک Action، چند نقطه ورود — Controller، Job و Command
// Controller
public function publish(Post $post, PublishPostAction $publish): RedirectResponse
{
$publish->handle($post);
return back();
}
// Job در صف
public function handle(PublishPostAction $publish): void
{
$publish->handle($this->post);
}
// Artisan command
public function handle(PublishPostAction $publish): int
{
Post::query()->whereNull('published_at')->where('scheduled_for', '<=', now())
->each(fn (Post $post) => $publish->handle($post));
return self::SUCCESS;
}
Laravel وابستگیهای متد handle را در Job و Command خودکار inject میکند. منطق انتشار یک جا نوشته شده و Controller، Job و Command فقط آن را صدا میزنند — هر کدام نازک میمانند.
ترکیب هر دو — رایج و سالم
class PostService
{
public function __construct(
private CreatePostAction $create,
private PublishPostAction $publish,
) {}
public function createAndPublish(CreatePostData $data, User $user): Post
{
$post = $this->create->handle($data, $user);
return $this->publish->handle($post);
}
}
Service نازک فقط هماهنگکننده است و کار اصلی در Actionهاست. حالت رایج دیگر: Service برای عملیات ساده هر بخش و Action فقط برای flowهای پیچیده.
Anti-patterns
- God Service — ۲۰۰۰ خط همه چیز
- Anemic Action — فقط
Post::create($data)wrap - Service در Service در Service — بدون مرز واضح
- static Service — تست و DI میشکند
ساختار پوشه پیشنهادی
app/Services/PostService.php // اختیاری facade نازک
app/Actions/Posts/CreatePostAction.php
app/Actions/Posts/PublishPostAction.php
app/DTO/Posts/CreatePostData.php
Repository کجا صدا زده شود؟
اگر از Repository استفاده میکنید، آن را داخل Action یا Service صدا بزنید، نه در Controller. اگر Eloquent را مستقیم استفاده میکنید، Repository پیشنیاز هیچکدام از این دو الگو نیست.
Filament و Service/Action
Actionهای Filament در Resource یا Page میتوانند همان Action Class را صدا بزنند — منطق برای سایت و پنل ادمین یکسان میماند.
تست Service و Action
Action با یک متد عمومی را میشود مستقیم و ایزوله تست کرد؛ در تست Controller یا Job هم میتوانید آن را mock کنید تا فقط نقطه ورود تست شود. نمونه عملی در تست Repository و Action.
جمعبندی
با Service ساده شروع کنید یا اصلاً کلاسی نسازید؛ وقتی یک متد چندمرحلهای شد یا از چند جا صدا زده شد، آن را به Action تبدیل کنید. برای ورودی تایپشده Service و Action از DTO استفاده کنید و برای مفاهیم domain از Value Object.