Serviceها در لاراول؛ راهنمای کامل Service Layer
Serviceها در لاراول یکی از مهمترین الگوهای معماری برای تمیزتر کردن کد، کاهش وابستگی Controllerها، جداسازی منطق تجاری و افزایش تستپذیری پروژه هستند. برخلاف Model، Controller یا Middleware، لاراول بهصورت پیشفرض پوشه یا دستور اختصاصی برای Serviceها ارائه نمیکند؛ اما در پروژههای حرفهای، ساخت Service Layer یکی از بهترین روشها برای مدیریت منطق پیچیده، تعامل با سیستمهای خارجی، پردازش سفارش، پرداخت، ارسال پیامک، مدیریت فایل، گزارشگیری و عملیات چندمرحلهای است. در این مقاله، بهصورت کامل و فنی با مفهوم Service در Laravel، ساختار پیشنهادی، نحوه استفاده در Controller، ارتباط با Service Container، Dependency Injection، Service Provider، تستنویسی و بهترین روشهای پیادهسازی Serviceها آشنا میشویم.
برای شنیدن متن، روی «پخش صوت مقاله» بزنید.
مقدمه
در پروژههای کوچک لاراولی، معمولاً توسعهدهنده منطق برنامه را مستقیم داخل Controller یا Model مینویسد. برای مثال، یک متد store در کنترلر ممکن است داده ورودی را اعتبارسنجی کند، محصول را بسازد، موجودی انبار را تغییر دهد، فاکتور ایجاد کند، پیامک ارسال کند، ایمیل بفرستد، Cache را پاک کند و در نهایت کاربر را Redirect کند. این روش در ابتدا سریع به نظر میرسد، اما با رشد پروژه به یکی از اصلیترین دلایل پیچیدگی، تکرار کد، سختی تستنویسی و کاهش کیفیت نرمافزار تبدیل میشود.
اینجاست که مفهوم Service Layer یا Service Class اهمیت پیدا میکند. Serviceها در لاراول کلاسهایی هستند که منطق تجاری برنامه را از Controller، Model، Job و سایر بخشها جدا میکنند. هدف Service این است که یک فرآیند مشخص از کسبوکار یا برنامه را در یک کلاس مستقل، قابل استفاده مجدد و قابل تست پیادهسازی کند.
برای مثال، در یک نرمافزار فروشگاهی، عملیات «ثبت سفارش» فقط یک Order::create() ساده نیست. این عملیات ممکن است شامل بررسی موجودی، محاسبه تخفیف، ثبت آیتمهای سفارش، محاسبه مالیات، اتصال به درگاه پرداخت، ایجاد تراکنش، ارسال پیامک، ثبت لاگ و ارسال اعلان باشد. اگر همه این منطق داخل Controller نوشته شود، کنترلر خیلی زود به یک کلاس سنگین و غیرقابل نگهداری تبدیل میشود. اما اگر این منطق داخل OrderService قرار بگیرد، کنترلر سبکتر و معماری برنامه تمیزتر خواهد شد.
نکته مهم این است که Laravel بهصورت رسمی شما را مجبور نمیکند پوشهای به نام Services داشته باشید. اما امکاناتی مثل Service Container، Dependency Injection و Service Provider در لاراول بهخوبی از این نوع معماری پشتیبانی میکنند. طبق مستندات رسمی Laravel، Service Container ابزار قدرتمند لاراول برای مدیریت وابستگیهای کلاسها و انجام Dependency Injection است و بسیاری از کلاسهای برنامه مانند Controllerها، Event Listenerها، Middlewareها و Jobها میتوانند وابستگیهای خود را بهصورت خودکار از Container دریافت کنند.
در این مقاله، بهصورت کامل و فنی بررسی میکنیم که Serviceها در لاراول چه هستند، چه زمانی باید از آنها استفاده کنیم، چه تفاوتی با Controller و Model دارند، چطور ساخته میشوند، چگونه با Dependency Injection و Service Container کار میکنند و در پروژههای شرکتی چه Best Practiceهایی برای طراحی آنها وجود دارد. 🚀
Service در لاراول چیست؟
Service در Laravel معمولاً یک کلاس PHP مستقل است که بخشی از منطق تجاری یا فرآیندهای کاربردی برنامه را مدیریت میکند. این کلاسها معمولاً در مسیر زیر قرار داده میشوند:
app/Services
البته این مسیر اجباری نیست. شما میتوانید بر اساس ساختار پروژه از مسیرهایی مثل موارد زیر استفاده کنید:
app/Services
app/Domain/Order/Services
app/Actions
app/Application/Services
اما در بسیاری از پروژههای شرکتی لاراول، پوشه app/Services یک انتخاب ساده، قابل فهم و رایج است.
نمونه یک Service ساده:
<?php
namespace App\Services;
use App\Models\Product;
class ProductService
{
public function create(array $data): Product
{
return Product::create($data);
}
public function update(Product $product, array $data): Product
{
$product->update($data);
return $product;
}
}
و استفاده در Controller:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreProductRequest;
use App\Services\ProductService;
class ProductController extends Controller
{
public function __construct(
protected ProductService $productService
) {}
public function store(StoreProductRequest $request)
{
$product = $this->productService->create(
$request->validated()
);
return redirect()->route('products.show', $product);
}
}
در اینجا Controller فقط درخواست را دریافت میکند، داده اعتبارسنجیشده را به Service میدهد و پاسخ مناسب برمیگرداند. منطق ایجاد محصول داخل ProductService قرار گرفته است.
چرا Serviceها در لاراول مهم هستند؟
Serviceها بهخصوص در پروژههای متوسط و بزرگ اهمیت زیادی دارند. دلیل اصلی این است که با رشد پروژه، منطق برنامه بهمرور پیچیدهتر میشود. اگر این منطق در Controllerها، Modelها یا Routeها پراکنده شود، نگهداری پروژه سخت خواهد شد.
مزایای استفاده از Serviceها
| مزیت | توضیح |
|---|---|
| کاهش حجم Controller | کنترلر فقط نقش هماهنگکننده دارد و منطق اصلی به Service منتقل میشود |
| افزایش تستپذیری | Serviceها را میتوان مستقل از HTTP Request تست کرد |
| کاهش تکرار کد | منطق مشترک بین Controller، Job، Command و API در یک کلاس قرار میگیرد |
| خوانایی بهتر | هر Service مسئول یک بخش مشخص از منطق برنامه است |
| نگهداری سادهتر | تغییرات تجاری در یک نقطه متمرکز میشود |
| جداسازی مسئولیتها | Controller، Model و Service هرکدام نقش مشخصتری دارند |
| مناسب توسعه تیمی | اعضای تیم راحتتر میتوانند ساختار پروژه را درک کنند |
| پشتیبانی بهتر از Dependency Injection | وابستگیها بهصورت تمیز از طریق Constructor دریافت میشوند |
فرض کنید در یک پروژه هم از پنل مدیریت، هم API موبایل و هم یک Job زمانبندیشده نیاز دارید محصولی را فعال کنید. اگر منطق فعالسازی محصول داخل Controller باشد، در بخشهای دیگر باید همان منطق را تکرار کنید. اما اگر این منطق داخل ProductService باشد، همه بخشها میتوانند از همان Service استفاده کنند.
Service Layer چیست؟
Service Layer یک لایه معماری بین Controller و بخشهای پایینتر برنامه مثل Model، Repository، سیستمهای خارجی یا APIهای دیگر است. این لایه معمولاً منطق تجاری برنامه را نگهداری میکند.
در یک معماری ساده لاراولی، جریان درخواست میتواند اینگونه باشد:
Route → Controller → Service → Model/Repository/External API → Response
برای مثال:
ثبت سفارش:
Route → OrderController → OrderService → Order / PaymentGateway / SmsService
در این ساختار، Controller مستقیماً همه کارها را انجام نمیدهد. بلکه درخواست را دریافت کرده و اجرای فرآیند را به Service میسپارد.
تفاوت Service با Controller
Controller وظیفه مدیریت HTTP Request و Response را دارد. یعنی Controller باید با Request، Redirect، View، JSON Response و Route Model Binding سروکار داشته باشد. اما Service نباید وابسته به لایه HTTP باشد، مگر در موارد خاص.
| مورد | Controller | Service |
|---|---|---|
| مسئولیت اصلی | دریافت Request و بازگرداندن Response | اجرای منطق تجاری |
| وابستگی به HTTP | زیاد | کم یا صفر |
| محل استفاده | Routeهای Web و API | Controller، Job، Command، Listener، تست |
| مناسب برای | هماهنگسازی درخواست | پیادهسازی فرآیندهای برنامه |
| تستپذیری | معمولاً با Feature Test | معمولاً با Unit Test یا Integration Test |
| خروجی | View، Redirect، JSON | Object، Array، DTO، Result |
نمونه Controller نامناسب:
public function store(Request $request)
{
$data = $request->validate([
'customer_id' => ['required', 'exists:customers,id'],
'items' => ['required', 'array'],
]);
$order = Order::create([
'customer_id' => $data['customer_id'],
'status' => 'pending',
]);
foreach ($data['items'] as $item) {
$product = Product::findOrFail($item['product_id']);
if ($product->stock < $item['quantity']) {
throw new \Exception('Insufficient stock');
}
$order->items()->create([
'product_id' => $product->id,
'quantity' => $item['quantity'],
'price' => $product->price,
]);
$product->decrement('stock', $item['quantity']);
}
Mail::to($order->customer->email)->send(new OrderCreatedMail($order));
return redirect()->route('orders.show', $order);
}
این کنترلر بیش از حد کار انجام میدهد. نسخه بهتر:
public function store(StoreOrderRequest $request)
{
$order = $this->orderService->createOrder(
$request->validated()
);
return redirect()->route('orders.show', $order);
}
در این نسخه، کنترلر ساده و قابل فهم است. منطق اصلی داخل OrderService قرار دارد.
تفاوت Service با Model
Model در لاراول نماینده داده و ارتباط با جدول دیتابیس است. مدلها رابطهها، Scopeها، Castها، Accessorها، Mutatorها و رفتارهای نزدیک به داده را مدیریت میکنند. اما Service معمولاً فرآیندهای سطح بالاتر کسبوکار را انجام میدهد.
| مورد | Model | Service |
|---|---|---|
| مسئولیت | نمایش Entity و ارتباط با دیتابیس | اجرای فرآیند تجاری |
| مثال | Product, Order, User | OrderService, PaymentService |
| شامل رابطهها | بله | معمولاً خیر |
| شامل منطق چندمرحلهای | محدود | بله |
| ارتباط با API خارجی | بهتر است مستقیم نباشد | مناسبتر است |
| محل قرارگیری | app/Models | app/Services |
مثلاً متد زیر میتواند در Model منطقی باشد:
public function isAvailable(): bool
{
return $this->stock > 0 && $this->status === 'active';
}
اما فرآیند زیر بهتر است داخل Service باشد:
public function createOrder(array $data): Order
{
// Validate business rules
// Create order
// Create order items
// Update stock
// Create payment
// Send notifications
}
Model باید رفتارهای نزدیک به خود Entity را نگه دارد، نه کل فرآیندهای پیچیده کسبوکار را.
چه زمانی باید از Service در Laravel استفاده کنیم؟
همیشه لازم نیست برای هر عملیات ساده یک Service بسازید. اگر فقط یک رکورد ساده ذخیره میکنید، شاید نوشتن مستقیم آن در Controller قابل قبول باشد. اما در شرایط زیر استفاده از Service بسیار توصیه میشود:
1. وقتی منطق تجاری پیچیده است
مثلاً ثبت سفارش، محاسبه پورسانت، تمدید اشتراک، مدیریت پرداخت یا صدور فاکتور.
2. وقتی یک منطق در چند جای پروژه استفاده میشود
مثلاً هم Controller و هم Job و هم Command باید از یک فرآیند مشترک استفاده کنند.
3. وقتی با سیستم خارجی ارتباط دارید
مثل درگاه پرداخت، سرویس پیامک، API حسابداری، CRM خارجی یا سرویس ارسال ایمیل.
4. وقتی نیاز به تست مستقل دارید
Serviceها را میتوان بدون وابستگی مستقیم به HTTP تست کرد.
5. وقتی Controller بیش از حد بزرگ شده است
اگر کنترلر شما طولانی، شرطی و پیچیده شده، احتمالاً زمان ساخت Service فرا رسیده است.
6. وقتی عملیات چند مدل را درگیر میکند
مثلاً ثبت سفارش هم با Order، هم OrderItem، هم Product، هم Payment و هم Customer سروکار دارد.
ساخت Service در لاراول
لاراول دستور Artisan پیشفرضی مثل make:service ندارد. بنابراین معمولاً فایل Service را دستی میسازیم.
مسیر پیشنهادی:
app/Services/ProductService.php
نمونه:
<?php
namespace App\Services;
use App\Models\Product;
class ProductService
{
public function create(array $data): Product
{
return Product::create($data);
}
}
برای ساختارهای بزرگتر میتوان Serviceها را بر اساس دامنه تقسیم کرد:
app/Services/
ProductService.php
OrderService.php
PaymentService.php
SmsService.php
ReportService.php
یا ساختار دامنهمحور:
app/Domain/
Order/
Services/
CreateOrderService.php
CancelOrderService.php
Payment/
Services/
PaymentGatewayService.php
در پروژههای شرکتی، انتخاب ساختار باید بر اساس اندازه پروژه، تعداد تیم، پیچیدگی دامنه و آینده محصول انجام شود.
مثال ساده Service در لاراول
فرض کنید میخواهیم منطق ایجاد محصول را از Controller جدا کنیم.
ProductService
<?php
namespace App\Services;
use App\Models\Product;
use Illuminate\Support\Str;
class ProductService
{
public function create(array $data): Product
{
$data['slug'] = Str::slug($data['title']);
return Product::create($data);
}
public function update(Product $product, array $data): Product
{
if (isset($data['title'])) {
$data['slug'] = Str::slug($data['title']);
}
$product->update($data);
return $product;
}
}
ProductController
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Product\StoreProductRequest;
use App\Http\Requests\Product\UpdateProductRequest;
use App\Models\Product;
use App\Services\ProductService;
class ProductController extends Controller
{
public function __construct(
protected ProductService $productService
) {}
public function store(StoreProductRequest $request)
{
$product = $this->productService->create(
$request->validated()
);
return redirect()
->route('admin.products.show', $product)
->with('success', 'محصول با موفقیت ایجاد شد.');
}
public function update(UpdateProductRequest $request, Product $product)
{
$this->productService->update(
$product,
$request->validated()
);
return redirect()
->route('admin.products.show', $product)
->with('success', 'محصول با موفقیت ویرایش شد.');
}
}
در این طراحی، Controller فقط عملیات HTTP را مدیریت میکند و منطق ساخت slug داخل Service قرار گرفته است.
مثال پیشرفته: OrderService برای ثبت سفارش
ثبت سفارش نمونهای عالی برای استفاده از Service است؛ چون معمولاً چندین مرحله دارد.
<?php
namespace App\Services;
use App\Models\Order;
use App\Models\Product;
use Illuminate\Support\Facades\DB;
use RuntimeException;
class OrderService
{
public function createOrder(array $data): Order
{
return DB::transaction(function () use ($data) {
$order = Order::create([
'customer_id' => $data['customer_id'],
'status' => 'pending',
'total_price' => 0,
]);
$totalPrice = 0;
foreach ($data['items'] as $item) {
$product = Product::lockForUpdate()
->findOrFail($item['product_id']);
if ($product->stock < $item['quantity']) {
throw new RuntimeException('موجودی محصول کافی نیست.');
}
$lineTotal = $product->price * $item['quantity'];
$order->items()->create([
'product_id' => $product->id,
'quantity' => $item['quantity'],
'unit_price' => $product->price,
'total_price' => $lineTotal,
]);
$product->decrement('stock', $item['quantity']);
$totalPrice += $lineTotal;
}
$order->update([
'total_price' => $totalPrice,
]);
return $order;
});
}
}
در این مثال، OrderService چند کار مهم انجام میدهد:
- ایجاد سفارش
- بررسی موجودی محصول
- قفل کردن رکورد برای جلوگیری از Race Condition
- ایجاد آیتمهای سفارش
- کاهش موجودی
- محاسبه مبلغ کل
- اجرای همه مراحل داخل Transaction
چنین منطقی اگر داخل Controller قرار بگیرد، کنترلر بسیار شلوغ خواهد شد. اما در Service، ساختار برنامه تمیزتر و تستپذیرتر میشود.
Service و Dependency Injection در Laravel
یکی از دلایل مهمی که Serviceها در لاراول خوب کار میکنند، پشتیبانی Laravel از Dependency Injection است. طبق مستندات رسمی Laravel، Service Container برای مدیریت وابستگی کلاسها و تزریق آنها استفاده میشود و بسیاری از کلاسها مانند Controllerها بهصورت خودکار وابستگیهای خود را از Container دریافت میکنند.
یعنی شما میتوانید Service را در Constructor کنترلر Type-Hint کنید:
public function __construct(
protected OrderService $orderService
) {}
لاراول بهصورت خودکار OrderService را میسازد و به کنترلر تزریق میکند، البته تا زمانی که وابستگیهای آن قابل Resolve شدن باشند.
نمونه Service با وابستگی:
<?php
namespace App\Services;
use App\Contracts\PaymentGatewayInterface;
class PaymentService
{
public function __construct(
protected PaymentGatewayInterface $gateway
) {}
public function pay(int $amount): array
{
return $this->gateway->charge($amount);
}
}
در این حالت، اگر از Interface استفاده کنیم، باید به Laravel بگوییم کدام کلاس باید برای این Interface ساخته شود. این کار با Service Container و Service Provider انجام میشود.
Service Container و نقش آن در Serviceها
Service Container قلب مدیریت وابستگیها در Laravel است. وقتی یک کلاس در Constructor خود کلاس دیگری را Type-Hint میکند، Laravel تلاش میکند وابستگی را Resolve کند. اگر وابستگی یک کلاس ساده باشد، معمولاً Laravel خودش آن را میسازد. اما اگر وابستگی یک Interface باشد، باید Binding تعریف شود.
نمونه Interface:
<?php
namespace App\Contracts;
interface SmsGatewayInterface
{
public function send(string $mobile, string $message): bool;
}
پیادهسازی:
<?php
namespace App\Services\Sms;
use App\Contracts\SmsGatewayInterface;
class KavenegarSmsGateway implements SmsGatewayInterface
{
public function send(string $mobile, string $message): bool
{
// Send SMS via provider API
return true;
}
}
Service:
<?php
namespace App\Services;
use App\Contracts\SmsGatewayInterface;
class NotificationService
{
public function __construct(
protected SmsGatewayInterface $sms
) {}
public function sendOrderCreatedMessage(string $mobile): bool
{
return $this->sms->send(
$mobile,
'سفارش شما با موفقیت ثبت شد.'
);
}
}
حالا باید Binding تعریف شود:
use App\Contracts\SmsGatewayInterface;
use App\Services\Sms\KavenegarSmsGateway;
$this->app->bind(SmsGatewayInterface::class, KavenegarSmsGateway::class);
این Binding معمولاً داخل یک Service Provider انجام میشود.
Service Provider و ارتباط آن با Serviceها
Service Provider در Laravel محل ثبت Bindingها، تنظیمات و Bootstrap شدن بخشهای مختلف برنامه است. طبق مستندات رسمی Laravel، همه Service Providerها از کلاس Illuminate\Support\ServiceProvider ارثبری میکنند و بیشتر آنها دو متد register و boot دارند؛ در متد register باید فقط Bindingها در Service Container ثبت شوند.
ساخت Service Provider:
php artisan make:provider AppServiceProvider
یا ساخت Provider اختصاصی:
php artisan make:provider PaymentServiceProvider
نمونه:
<?php
namespace App\Providers;
use App\Contracts\PaymentGatewayInterface;
use App\Services\Payment\ZarinpalGateway;
use Illuminate\Support\ServiceProvider;
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
PaymentGatewayInterface::class,
ZarinpalGateway::class
);
}
public function boot(): void
{
//
}
}
در Laravel 12، Service Providerهای تعریفشده توسط کاربر در فایل bootstrap/providers.php ثبت میشوند. مستندات رسمی Laravel نیز بخش Service Providers را به نحوه نوشتن و ثبت Providerهای سفارشی اختصاص داده است.
bind و singleton در Service Container
هنگام ثبت Service در Container، معمولاً با دو مفهوم مهم مواجه میشویم: bind و singleton.
bind
هر بار که کلاس Resolve شود، یک نمونه جدید ساخته میشود:
$this->app->bind(
PaymentGatewayInterface::class,
ZarinpalGateway::class
);
singleton
فقط یک نمونه ساخته میشود و در درخواستهای بعدی همان نمونه برگردانده میشود:
$this->app->singleton(
CurrencyRateService::class,
fn () => new CurrencyRateService()
);
استفاده از singleton برای سرویسهایی مناسب است که نگهداری یک نمونه مشترک از آنها منطقی است. اما برای سرویسهایی که State متغیر و حساس دارند، باید با احتیاط از Singleton استفاده کرد.
ساختار پیشنهادی Serviceها در پروژه شرکتی
در یک پروژه شرکتی متوسط، میتوان از ساختار زیر استفاده کرد:
app/
Http/
Controllers/
Requests/
Resources/
Models/
Services/
ProductService.php
OrderService.php
PaymentService.php
SmsService.php
ReportService.php
Contracts/
PaymentGatewayInterface.php
SmsGatewayInterface.php
Providers/
PaymentServiceProvider.php
برای پروژههای بزرگتر، ساختار دامنهمحور بهتر است:
app/
Domain/
Order/
Services/
CreateOrderService.php
CancelOrderService.php
Data/
OrderData.php
Events/
OrderCreated.php
Payment/
Services/
PaymentService.php
Contracts/
PaymentGatewayInterface.php
Gateways/
ZarinpalGateway.php
ساختار دوم برای تیمهای بزرگ و پروژههای Enterprise مناسبتر است، اما برای پروژههای کوچک ممکن است بیش از حد پیچیده باشد.
Service، Action و Repository چه تفاوتی دارند؟
در معماری Laravel گاهی مفاهیم Service، Action و Repository با هم اشتباه گرفته میشوند.
| مفهوم | هدف اصلی | مثال |
|---|---|---|
| Service | مدیریت منطق تجاری یا فرآیندهای برنامه | OrderService, PaymentService |
| Action | انجام یک کار مشخص و محدود | CreateOrderAction, SendInvoiceAction |
| Repository | جداسازی منطق دسترسی به داده | ProductRepository |
| Model | نمایش Entity و تعامل با دیتابیس | Product, Order |
Service میتواند شامل چند متد مرتبط باشد:
$orderService->createOrder();
$orderService->cancelOrder();
$orderService->refundOrder();
اما Action معمولاً فقط یک کار انجام میدهد:
$createOrderAction->execute($data);
Repository بیشتر برای Queryها و دسترسی به داده استفاده میشود، نه اجرای منطق تجاری.
در بسیاری از پروژههای Laravel، استفاده از Service کافی است و نیازی نیست Repository یا Action را بیدلیل اضافه کنیم. معماری خوب یعنی حل مشکل واقعی، نه زیاد کردن تعداد کلاسها.
Service برای ارتباط با APIهای خارجی
یکی از بهترین کاربردهای Serviceها، مدیریت ارتباط با سرویسهای خارجی است. مثلاً درگاه پرداخت، پیامک، سرویس ایمیل، API حسابداری، سرویس حملونقل یا CRM خارجی.
نمونه PaymentGatewayService:
<?php
namespace App\Services\Payment;
use Illuminate\Support\Facades\Http;
class ZarinpalGateway
{
public function requestPayment(int $amount, string $callbackUrl): array
{
$response = Http::post('https://example-payment-provider.test/request', [
'amount' => $amount,
'callback_url' => $callbackUrl,
]);
return $response->json();
}
public function verifyPayment(string $authority, int $amount): array
{
$response = Http::post('https://example-payment-provider.test/verify', [
'authority' => $authority,
'amount' => $amount,
]);
return $response->json();
}
}
Controller نباید مستقیماً با API خارجی کار کند. اگر روزی درگاه پرداخت تغییر کند، فقط Service مربوطه تغییر میکند و Controllerها دستنخورده باقی میمانند.
Service و Queue در لاراول
گاهی یک Service عملیاتی انجام میدهد که زمانبر است؛ مثل ارسال ایمیل انبوه، پردازش فایل، تولید گزارش، اتصال به API خارجی یا ارسال چندین پیامک. در این موارد بهتر است عملیات زمانبر را به Job و Queue منتقل کنیم.
Laravel Queue برای اجرای کارهای زمانبر در پسزمینه استفاده میشود و طبق مستندات رسمی Laravel، Queueها از Connectionهای مختلفی مانند Redis، Amazon SQS، Beanstalk و سایر Backendها پشتیبانی میکنند.
ساخت Job:
php artisan make:job SendOrderCreatedNotification
نمونه استفاده از Service در Job:
<?php
namespace App\Jobs;
use App\Models\Order;
use App\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class SendOrderCreatedNotification implements ShouldQueue
{
use Queueable;
public function __construct(
public Order $order
) {}
public function handle(NotificationService $notificationService): void
{
$notificationService->sendOrderCreated($this->order);
}
}
در اینجا Job مسئول اجرای پسزمینه است و Service مسئول منطق ارسال اعلان. این تفکیک باعث میشود کد تمیزتر و قابل تستتر باشد.
Service و Transaction دیتابیس
اگر Service شامل چند عملیات دیتابیس است که باید همه با هم موفق یا ناموفق شوند، باید از Transaction استفاده کرد.
نمونه:
use Illuminate\Support\Facades\DB;
class SubscriptionService
{
public function renew(array $data): Subscription
{
return DB::transaction(function () use ($data) {
$subscription = Subscription::findOrFail($data['subscription_id']);
$subscription->update([
'expires_at' => now()->addMonth(),
]);
Payment::create([
'subscription_id' => $subscription->id,
'amount' => $data['amount'],
'status' => 'paid',
]);
return $subscription;
});
}
}
اگر یکی از عملیاتها خطا بدهد، Transaction باعث میشود تغییرات قبلی Rollback شوند. این موضوع در فرآیندهای مالی، سفارش، انبار و اشتراک اهمیت بسیار زیادی دارد.
Service و تستنویسی در لاراول
یکی از بزرگترین مزایای Serviceها، تستپذیری بهتر است. وقتی منطق تجاری داخل Controller باشد، برای تست آن معمولاً باید Request HTTP شبیهسازی شود. اما وقتی منطق داخل Service باشد، میتوان آن را مستقلتر تست کرد.
نمونه تست ساده:
<?php
namespace Tests\Feature;
use App\Models\Product;
use App\Services\ProductService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProductServiceTest extends TestCase
{
use RefreshDatabase;
public function test_it_can_create_product(): void
{
$service = app(ProductService::class);
$product = $service->create([
'title' => 'CRM Software',
'price' => 5000000,
]);
$this->assertInstanceOf(Product::class, $product);
$this->assertDatabaseHas('products', [
'title' => 'CRM Software',
]);
}
}
اگر Service وابسته به API خارجی باشد، میتوان آن وابستگی را Mock کرد. این کار باعث میشود تستها سریعتر، پایدارتر و مستقلتر باشند.
خروجی Service باید چه باشد؟
یک Service میتواند بسته به نوع عملیات خروجیهای مختلفی داشته باشد:
| نوع خروجی | کاربرد |
|---|---|
| Model | وقتی عملیات یک Entity ایجاد یا ویرایش میکند |
| Collection | وقتی لیستی از دادهها برمیگردد |
| Array | برای دادههای ساده یا پاسخ API خارجی |
| DTO | برای ساختارهای حرفهایتر و Type-Safe |
| Boolean | برای عملیات موفق/ناموفق ساده |
| Result Object | برای فرآیندهای پیچیده با پیام، وضعیت و داده |
مثلاً:
public function create(array $data): Product
{
return Product::create($data);
}
یا:
public function send(string $mobile, string $message): bool
{
return $this->gateway->send($mobile, $message);
}
در پروژههای بزرگ، استفاده از DTO یا Result Object میتواند خوانایی و کنترل خطا را بهتر کند.
مدیریت خطا در Serviceها
Serviceها باید خطاها را شفاف و قابل کنترل مدیریت کنند. مثلاً اگر موجودی محصول کافی نیست، بهتر است یک Exception مشخص ایجاد شود.
namespace App\Exceptions;
use Exception;
class InsufficientStockException extends Exception
{
//
}
استفاده در Service:
if ($product->stock < $quantity) {
throw new InsufficientStockException('موجودی محصول کافی نیست.');
}
سپس Controller یا Handler میتواند این خطا را به پاسخ مناسب تبدیل کند.
try {
$order = $this->orderService->createOrder($data);
} catch (InsufficientStockException $exception) {
return back()->withErrors([
'stock' => $exception->getMessage(),
]);
}
در APIها نیز میتوان خطا را به JSON Response مناسب تبدیل کرد.
Serviceها و قوانین SOLID
Serviceها اگر درست طراحی شوند، به رعایت اصول SOLID کمک میکنند.
Single Responsibility Principle
هر Service باید مسئول یک بخش مشخص باشد. مثلاً PaymentService نباید همزمان مسئول پرداخت، ارسال پیامک، تولید گزارش و مدیریت کاربر باشد.
Open/Closed Principle
با استفاده از Interfaceها میتوان رفتارها را قابل توسعه کرد، بدون اینکه کدهای قبلی زیاد تغییر کنند.
Dependency Inversion Principle
Service بهتر است به Interface وابسته باشد، نه پیادهسازی مستقیم.
مثال:
public function __construct(
protected PaymentGatewayInterface $gateway
) {}
این کار باعث میشود تغییر درگاه پرداخت سادهتر شود.
اشتباهات رایج در طراحی Serviceها
1. ساخت Service برای هر چیز کوچک
اگر برای هر متد ساده یک Service بسازید، پروژه بیدلیل پیچیده میشود. Service باید مشکل واقعی حل کند.
2. تبدیل Service به God Class
اگر OrderService شامل دهها متد نامرتبط و هزاران خط کد باشد، خودش به مشکل تبدیل میشود. در این حالت باید آن را به Serviceها یا Actionهای کوچکتر تقسیم کرد.
3. وابسته کردن Service به Request
Service بهتر است آرایه، DTO یا پارامترهای مشخص دریافت کند، نه خود Request.
نامناسب:
public function create(Request $request)
{
//
}
بهتر:
public function create(array $data)
{
//
}
4. بازگرداندن Response از Service
Service نباید redirect() یا response()->json() برگرداند. این کار مربوط به Controller است.
نامناسب:
return redirect()->route('orders.index');
بهتر:
return $order;
5. قرار دادن Queryهای گزارشگیری پیچیده در Service عمومی
اگر Queryهای گزارشگیری خیلی پیچیده هستند، بهتر است از ReportService، Query Object یا کلاس اختصاصی استفاده شود.
6. استفاده نکردن از Interface برای سرویسهای قابل تعویض
برای سرویسهایی مثل پیامک، پرداخت و API خارجی، Interface کمک میکند پیادهسازیها راحتتر تغییر کنند.
بهترین روشهای طراحی Service در Laravel
1. نام Service را واضح انتخاب کنید
نامهایی مثل OrderService، PaymentService و InvoiceService خوب هستند، اما اگر عملیات خیلی مشخص است، نامهایی مثل CreateOrderService یا CancelSubscriptionService بهترند.
2. Service را مستقل از HTTP نگه دارید
Service نباید به Request، RedirectResponse یا View وابسته باشد. ورودی تمیز بگیرد و خروجی قابل استفاده برگرداند.
3. از Dependency Injection استفاده کنید
وابستگیها را با new داخل متد نسازید. آنها را از Constructor دریافت کنید.
4. منطق اعتبارسنجی ورودی را در Form Request نگه دارید
Service بهتر است با داده معتبر کار کند. اعتبارسنجی HTTP معمولاً در Form Request انجام میشود.
5. برای عملیات مالی و چندمرحلهای از Transaction استفاده کنید
ثبت سفارش، پرداخت، اشتراک و عملیات انبار باید با دقت Transactional طراحی شوند.
6. Serviceهای خارجی را پشت Interface پنهان کنید
برای پیامک، پرداخت، ایمیل، حسابداری و سرویسهای خارجی از Interface استفاده کنید.
7. عملیات زمانبر را به Queue بسپارید
Service میتواند Job Dispatch کند یا Job میتواند از Service استفاده کند. انتخاب دقیق به معماری پروژه بستگی دارد.
8. Service را قابل تست طراحی کنید
ورودی و خروجی مشخص، وابستگیهای تزریقشده و نبود وابستگی مستقیم به HTTP باعث تستپذیری بهتر میشود.
9. از ساختار ثابت در تیم استفاده کنید
تیم باید توافق کند Serviceها کجا قرار میگیرند، چطور نامگذاری میشوند و چه مسئولیتی دارند.
10. مستندسازی داخلی داشته باشید
برای Serviceهای حساس مثل پرداخت، سفارش، حسابداری و ارسال پیامک، توضیح کوتاه و تست مناسب بسیار ضروری است.
جدول مقایسه Service با اجزای مهم Laravel
| بخش | نقش اصلی | آیا مناسب منطق تجاری است؟ | مثال کاربرد |
|---|---|---|---|
| Controller | مدیریت Request و Response | محدود | دریافت فرم و نمایش نتیجه |
| Model | نمایش Entity و ارتباط با دیتابیس | فقط منطق نزدیک به داده | بررسی فعال بودن محصول |
| Service | اجرای منطق تجاری | بله | ثبت سفارش، پرداخت، صدور فاکتور |
| Form Request | اعتبارسنجی ورودی | خیر | قوانین فرم ایجاد محصول |
| Job | اجرای کار پسزمینه | بخشی از فرآیند | ارسال ایمیل بعد از ثبت سفارش |
| Event/Listener | واکنش به رخداد | بله، اما غیرمستقیم | ارسال اعلان بعد از پرداخت |
| Policy | کنترل مجوز | خیر | بررسی اجازه ویرایش محصول |
| Repository | دسترسی به داده | نه لزوماً | Queryهای سفارشی محصول |
نمونه کامل Service در یک پروژه شرکتی
فرض کنیم یک شرکت نرمافزاری سیستم اشتراک کاربران دارد. کاربر میتواند اشتراک خریداری کند و بعد از پرداخت موفق، اشتراک او فعال شود.
SubscriptionService
<?php
namespace App\Services;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class SubscriptionService
{
public function subscribe(User $user, Plan $plan): Subscription
{
return DB::transaction(function () use ($user, $plan) {
$subscription = Subscription::create([
'user_id' => $user->id,
'plan_id' => $plan->id,
'starts_at' => now(),
'ends_at' => now()->addDays($plan->duration_days),
'status' => 'active',
]);
$user->update([
'current_plan_id' => $plan->id,
]);
return $subscription;
});
}
public function cancel(Subscription $subscription): Subscription
{
$subscription->update([
'status' => 'cancelled',
'cancelled_at' => now(),
]);
return $subscription;
}
}
Controller
<?php
namespace App\Http\Controllers;
use App\Models\Plan;
use App\Services\SubscriptionService;
use Illuminate\Http\Request;
class SubscriptionController extends Controller
{
public function __construct(
protected SubscriptionService $subscriptionService
) {}
public function store(Request $request, Plan $plan)
{
$subscription = $this->subscriptionService->subscribe(
$request->user(),
$plan
);
return redirect()
->route('subscriptions.show', $subscription)
->with('success', 'اشتراک با موفقیت فعال شد.');
}
}
در این ساختار، کنترلر فقط درخواست را مدیریت میکند و Service منطق فعالسازی اشتراک را انجام میدهد.
Serviceها در پروژههای API محور
در پروژههایی که Laravel بهعنوان Backend برای اپلیکیشن موبایل یا SPA استفاده میشود، Serviceها اهمیت بیشتری پیدا میکنند. چون ممکن است یک منطق تجاری از چند مسیر مختلف فراخوانی شود:
- API موبایل
- پنل ادمین
- Job زمانبندیشده
- Webhook درگاه پرداخت
- Command داخلی
اگر منطق داخل Controller باشد، مجبور میشوید آن را تکرار کنید. اما اگر داخل Service باشد، همه ورودیها میتوانند از یک منطق مشترک استفاده کنند.
نمونه:
class PaymentCallbackController extends Controller
{
public function __invoke(Request $request, PaymentService $paymentService)
{
$payment = $paymentService->verifyCallback(
$request->all()
);
return response()->json([
'status' => $payment->status,
]);
}
}
در این مثال، Controller فقط Callback را دریافت میکند و پردازش اصلی پرداخت در PaymentService انجام میشود.
Serviceها و سئو در پروژههای لاراولی
شاید در نگاه اول Serviceها ارتباط مستقیمی با SEO نداشته باشند، اما در سایتهای شرکتی، فروشگاهی و محتوایی، Service Layer میتواند به کیفیت سئو کمک کند.
برای مثال، در یک سایت محتوایی میتوان PostPublishingService داشت که هنگام انتشار مقاله این کارها را انجام دهد:
- بررسی کامل بودن عنوان سئو
- بررسی وجود Meta Description
- ساخت Slug استاندارد
- ایجاد Canonical URL
- ثبت تاریخ انتشار
- پاکسازی Cache صفحات
- ارسال Ping به سرویسهای داخلی
- تولید Sitemap جدید
نمونه ساده:
class PostPublishingService
{
public function publish(Post $post): Post
{
if (! $post->meta_title || ! $post->meta_description) {
throw new RuntimeException('اطلاعات سئو کامل نیست.');
}
$post->update([
'status' => 'published',
'published_at' => now(),
]);
cache()->forget('latest_posts');
return $post;
}
}
در این حالت، منطق انتشار مقاله از Controller جدا شده و اگر انتشار از پنل مدیریت، Job زمانبندیشده یا API انجام شود، همه از یک Service مشترک استفاده میکنند.
FAQ؛ سوالات متداول درباره Serviceها در لاراول
1. Service در لاراول چیست؟
Service یک کلاس مستقل برای مدیریت منطق تجاری یا فرآیندهای کاربردی برنامه است. Serviceها معمولاً برای جدا کردن منطق پیچیده از Controller و Model استفاده میشوند.
2. آیا Laravel دستور make:service دارد؟
بهصورت پیشفرض، Laravel دستور رسمی make:service ندارد. معمولاً پوشه app/Services و فایلهای Service بهصورت دستی ساخته میشوند.
3. Serviceها در کدام مسیر قرار میگیرند؟
مسیر رایج app/Services است. اما در پروژههای بزرگ میتوان از ساختارهایی مثل app/Domain/*/Services استفاده کرد.
4. تفاوت Service با Controller چیست؟
Controller درخواست HTTP را دریافت و پاسخ مناسب برمیگرداند، اما Service منطق تجاری را اجرا میکند. Service نباید مستقیماً مسئول Redirect، View یا JSON Response باشد.
5. تفاوت Service با Model چیست؟
Model نماینده داده و جدول دیتابیس است، اما Service فرآیندهای تجاری و چندمرحلهای را مدیریت میکند. Model برای رابطهها، Scopeها و رفتارهای نزدیک به داده مناسب است.
6. آیا همیشه باید از Service استفاده کنیم؟
خیر. برای عملیات ساده، استفاده مستقیم از Model در Controller قابل قبول است. اما برای منطق پیچیده، تکراری، چندمرحلهای یا قابل استفاده مجدد، Service انتخاب مناسبی است.
7. آیا Service باید Interface داشته باشد؟
همیشه نه. اما برای سرویسهایی که ممکن است چند پیادهسازی داشته باشند، مثل درگاه پرداخت، پیامک یا API خارجی، استفاده از Interface بسیار مفید است.
8. آیا Service میتواند از Model استفاده کند؟
بله. Service معمولاً با Modelها، Repositoryها، APIهای خارجی، Jobها و سایر سرویسها تعامل دارد.
9. آیا Service باید خروجی JSON برگرداند؟
معمولاً خیر. خروجی JSON مربوط به Controller یا Resource است. Service بهتر است Model، Array، DTO، Boolean یا Result Object برگرداند.
10. چطور Service را تست کنیم؟
میتوان Service را از Container دریافت کرد و متدهای آن را با دادههای تستی اجرا کرد. اگر Service وابسته به API خارجی باشد، بهتر است وابستگیها Mock شوند.
جمعبندی
Serviceها در لاراول یکی از مهمترین ابزارهای معماری برای ساخت پروژههای تمیز، قابل توسعه و قابل تست هستند. اگرچه Laravel شما را مجبور به استفاده از Service Layer نمیکند، اما امکاناتی مانند Service Container، Dependency Injection و Service Provider باعث میشوند پیادهسازی Serviceها در لاراول بسیار طبیعی و قدرتمند باشد.
Serviceها کمک میکنند Controllerها سبک بمانند، Modelها بیش از حد سنگین نشوند و منطق تجاری برنامه در کلاسهایی مشخص و قابل نگهداری قرار بگیرد. این موضوع در پروژههای شرکتی، فروشگاهی، مالی، CRM، ERP، API محور و سیستمهای چندبخشی اهمیت بسیار زیادی دارد.
نکته مهم این است که Service نباید بیدلیل به پروژه اضافه شود. هدف Service سادهسازی معماری است، نه پیچیدهتر کردن آن. هرجا منطق برنامه تکراری، پیچیده، چندمرحلهای، وابسته به چند مدل یا نیازمند تست مستقل بود، Service میتواند انتخاب بسیار مناسبی باشد.
در یک پروژه حرفهای Laravel، ترکیب Controllerهای سبک، Form Requestهای دقیق، Modelهای تمیز، Serviceهای متمرکز، Policyهای مشخص، Jobهای پسزمینه و Service Providerهای درست، پایهای محکم برای توسعه بلندمدت محصول ایجاد میکند. 💼
CTA
اگر پروژه Laravel شما در حال رشد است و Controllerها یا Modelها بیش از حد شلوغ شدهاند، زمان آن رسیده معماری نرمافزار را بازبینی کنید. تیم ما میتواند در طراحی Service Layer، بازطراحی Controllerهای سنگین، پیادهسازی معماری تمیز، بهینهسازی منطق تجاری، تستنویسی و استانداردسازی پروژههای Laravel به شما کمک کند. برای بررسی فنی پروژه لاراول خود، با ما تماس بگیرید و مسیر توسعه نرمافزار را حرفهایتر، پایدارتر و مقیاسپذیرتر ادامه دهید. 🚀
منابع رسمی
- مستندات رسمی Service Container در Laravel 12؛ برای آشنایی با مدیریت وابستگیها و Dependency Injection در لاراول.
مطالعه مستندات رسمی Laravel درباره Service Container - مستندات رسمی Service Providers در Laravel 12؛ برای یادگیری ثبت Bindingها و ساخت Providerهای سفارشی.
مطالعه مستندات رسمی Laravel درباره Service Providers - مستندات رسمی Queue در Laravel 12؛ برای اجرای عملیات زمانبر در پسزمینه و استفاده از Jobها.
مطالعه مستندات رسمی Laravel درباره Queues