یکی از مهمترین موضوعات در برنامهنویسی حرفهای Laravel، نحوه مدیریت وابستگیهای بین کلاسهاست. وقتی یک پروژه کوچک را شروع میکنید، ممکن است ساختن اشیا با دستور new تمام نیازهای شما را برطرف کند؛ اما با بزرگتر شدن پروژه، تعداد کلاسها، سرویسها و ارتباطات بین آنها افزایش پیدا میکند. در چنین شرایطی، تغییر یک بخش از برنامه ممکن است به اصلاح چندین کلاس دیگر نیاز داشته باشد.
Dependency Injection یا تزریق وابستگی یکی از روشهایی است که به شما کمک میکند وابستگیهای کلاسها را به شکلی قابل مدیریت، انعطافپذیر و قابل تست طراحی کنید.
Laravel امکانات قدرتمندی برای پیادهسازی این مفهوم در اختیار برنامهنویسان قرار میدهد. مهمترین آنها Service Container است؛ سازوکاری که میتواند وابستگیهای مورد نیاز کلاسها را تشخیص دهد و در اختیار آنها قرار دهد.
در این مقاله یاد میگیریم Dependency Injection چیست، چه تفاوتی با Service Container دارد، چگونه در Controller و Service از آن استفاده کنیم و چطور یک Interface را به پیادهسازی مشخصی متصل کنیم.
Dependency Injection چیست؟
Dependency Injection به معنای تزریق وابستگی است. وابستگی زمانی ایجاد میشود که یک کلاس برای انجام وظایف خود به کلاس یا سرویس دیگری نیاز داشته باشد.
برای مثال، فرض کنید در یک فروشگاه اینترنتی، کلاس ثبت سفارش برای ارسال پیامک تأیید سفارش به یک سرویس پیامکی نیاز دارد.
در این حالت، سرویس پیامکی یک وابستگی برای کلاس ثبت سفارش محسوب میشود.
یک روش ساده برای پیادهسازی این ارتباط، ساختن مستقیم شیء مورد نیاز درون کلاس است:
<?php
namespace App\Services;
class OrderService
{
public function createOrder(): void
{
$smsService = new SmsService();
// ثبت سفارش
$smsService->send(
'09120000000',
'سفارش شما ثبت شد.'
);
}
}
این کد ممکن است در نگاه اول قابل قبول باشد؛ اما کلاس OrderService مستقیماً به کلاس SmsService وابسته شده است.
اگر بعداً بخواهیم سرویس پیامکی را تغییر دهیم، از یک سرویس پیامکی دیگر استفاده کنیم یا هنگام تست، ارسال واقعی پیامک را غیرفعال کنیم، مدیریت این وابستگی دشوارتر میشود.
در Dependency Injection، بهجای آنکه کلاس مسئول ساختن وابستگی خودش باشد، وابستگی از بیرون به آن داده میشود.
برای مثال:
<?php
namespace App\Services;
class OrderService
{
public function __construct(
protected SmsService $smsService
) {}
public function createOrder(): void
{
// ثبت سفارش
$this->smsService->send(
'09120000000',
'سفارش شما ثبت شد.'
);
}
}
در این مثال، شیء SmsService از طریق سازنده کلاس یا همان __construct() به OrderService تزریق میشود.
این روش را Constructor Injection مینامیم.
نکته مهم این است که Dependency Injection بهتنهایی وابستگیهای کلاس را حذف نمیکند؛ بلکه نحوه تأمین و مدیریت آنها را تغییر میدهد.
چرا باید از Dependency Injection در Laravel استفاده کنیم؟
استفاده صحیح از تزریق وابستگی مزایای مختلفی دارد.
۱. کاهش وابستگی مستقیم بین کلاسها
وقتی یک کلاس خودش وابستگیهایش را ایجاد میکند، به جزئیات پیادهسازی آنها وابسته میشود. با تزریق وابستگی، میتوان مسئولیت ساختن اشیا را از مسئولیت اجرای منطق برنامه جدا کرد.
۲. نگهداری آسانتر کد
فرض کنید در چند بخش از پروژه به سرویس ارسال پیامک نیاز دارید. اگر نحوه ساخت این سرویس در همه کلاسها تکرار شده باشد، تغییر پیادهسازی آن دشوارتر خواهد بود.
با طراحی مناسب وابستگیها، تغییرات سادهتر و قابل کنترلتر میشوند.
۳. تستنویسی آسانتر
در زمان تست یک سرویس، معمولاً نمیخواهیم به درگاه پیامک واقعی، سرویس پرداخت یا API خارجی متصل شویم.
با استفاده از Dependency Injection میتوانیم یک پیادهسازی آزمایشی یا Mock را به کلاس تزریق کنیم و رفتار آن را بدون اجرای سرویس واقعی بررسی کنیم.
۴. امکان تعویض پیادهسازی
اگر کلاسها به یک قرارداد یا Interface وابسته باشند، میتوان پیادهسازی آن قرارداد را بدون تغییر منطق اصلی کلاس جایگزین کرد.
البته رسیدن به این مزیت به طراحی درست وابستگیها نیاز دارد؛ استفاده از Dependency Injection بهتنهایی تضمین نمیکند که کد کاملاً مستقل باشد.
تفاوت Dependency Injection و Service Container در Laravel
این دو مفهوم با یکدیگر ارتباط دارند، اما یکسان نیستند.
Dependency Injection یک الگوی طراحی است که مشخص میکند وابستگیهای یک کلاس چگونه از بیرون به آن داده شوند.
در مقابل، Service Container ابزار Laravel برای مدیریت و تأمین این وابستگیهاست.
برای مثال، اگر کلاس زیر را داشته باشیم:
<?php
namespace App\Services;
class InvoiceService
{
public function calculate(): int
{
return 150000;
}
}
و آن را به یک Controller تزریق کنیم:
<?php
namespace App\Http\Controllers;
use App\Services\InvoiceService;
class InvoiceController extends Controller
{
public function __construct(
protected InvoiceService $invoiceService
) {}
public function show()
{
$total = $this->invoiceService->calculate();
return response()->json([
'total' => $total,
]);
}
}
Laravel میتواند کلاس InvoiceService را بهصورت خودکار ایجاد کرده و به سازنده InvoiceController تحویل دهد.
این کار به کمک Service Container انجام میشود.
در بسیاری از موارد، برای تزریق کلاسهای معمولی نیازی به ثبت دستی آنها در Container ندارید؛ Laravel میتواند کلاسهایی را که وابستگیهای قابل تشخیص دارند، بهصورت خودکار ایجاد کند.
آموزش Constructor Injection در Laravel
Constructor Injection یکی از رایجترین روشهای استفاده از Dependency Injection در Laravel است.
در این روش، وابستگی مورد نیاز کلاس را بهعنوان پارامتر سازنده تعریف میکنیم.
برای مثال، ابتدا یک سرویس برای مدیریت محصولات ایجاد کنید.
مسیر فایل:
app/Services/ProductService.php
<?php
namespace App\Services;
class ProductService
{
public function getProducts(): array
{
return [
[
'id' => 1,
'name' => 'Laptop',
'price' => 35000000,
],
[
'id' => 2,
'name' => 'Keyboard',
'price' => 1200000,
],
];
}
}
حالا یک Controller ایجاد کنید:
php artisan make:controller ProductController
فایل app/Http/Controllers/ProductController.php را به شکل زیر بنویسید:
<?php
namespace App\Http\Controllers;
use App\Services\ProductService;
class ProductController extends Controller
{
public function __construct(
protected ProductService $productService
) {}
public function index()
{
return response()->json(
$this->productService->getProducts()
);
}
}
در این مثال، ProductController برای دریافت محصولات به ProductService نیاز دارد.
با تعریف نوع پارامتر سازنده به شکل ProductService، Laravel میتواند وابستگی را تشخیص دهد و آن را تزریق کند.
برای آزمایش، مسیر زیر را در فایل routes/web.php اضافه کنید:
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;
Route::get('/products', [
ProductController::class,
'index',
]);
حالا برنامه را اجرا کنید:
php artisan serve
و آدرس زیر را باز کنید:
http://127.0.0.1:8000/products
باید اطلاعات محصولات بهصورت JSON نمایش داده شود.
نکته: برای این مثال ساده، استفاده از سرویس مستقل الزامی نیست؛ هدف، یادگیری نحوه تزریق وابستگی است. در پروژه واقعی، زمانی این تفکیک ارزش بیشتری پیدا میکند که منطق محصولات پیچیدهتر شود یا در چند بخش برنامه مورد استفاده قرار بگیرد.
Method Injection در Laravel چیست؟
در کنار Constructor Injection، میتوان وابستگیها را مستقیماً به متد مورد نظر تزریق کرد. این روش را Method Injection مینامند.
برای مثال:
<?php
namespace App\Http\Controllers;
use App\Services\ProductService;
class ProductController extends Controller
{
public function index(ProductService $productService)
{
return response()->json(
$productService->getProducts()
);
}
}
در این مثال، وابستگی بهجای سازنده، به متد index() داده شده است.
Laravel میتواند وابستگیهای قابل حل را برای متدهای Controller تأمین کند.
چه زمانی از Constructor Injection استفاده کنیم؟
وقتی یک کلاس برای انجام وظایف اصلی خود به یک سرویس نیاز دارد و چندین متد آن از همان سرویس استفاده میکنند، Constructor Injection انتخاب مناسبی است.
چه زمانی از Method Injection استفاده کنیم؟
وقتی فقط یک متد مشخص به یک وابستگی نیاز دارد، Method Injection میتواند مناسبتر باشد؛ زیرا وابستگی فقط در همان متد اعلام میشود.
در Laravel باید به این نکته توجه کنید که Method Injection در Controller با مکانیزم اجرای متدها توسط Container کار میکند. این موضوع با فراخوانی عادی یک متد PHP از داخل کد متفاوت است؛ در فراخوانی عادی، خودتان باید آرگومان مورد نیاز را ارسال کنید.
تزریق وابستگی به کمک Interface در Laravel
تا اینجا یک کلاس مشخص را به کلاس دیگر تزریق کردیم. اما در پروژههای بزرگتر، ممکن است بخواهیم پیادهسازی یک سرویس را در آینده تغییر دهیم.
برای مثال، فرض کنید یک فروشگاه اینترنتی داریم که در حال حاضر از سرویس پیامکی A استفاده میکند، اما احتمال دارد در آینده به سرویس پیامکی B مهاجرت کنیم.
اگر کلاس سفارش مستقیماً به SmsServiceA وابسته باشد، تعویض سرویس به تغییر کدهای وابسته نیاز پیدا میکند.
راهکار مناسب، تعریف یک Interface و وابسته کردن کلاس سفارش به آن قرارداد است.
مرحله اول: ایجاد Interface
فایل زیر را ایجاد کنید:
app/Contracts/SmsSender.php
<?php
namespace App\Contracts;
interface SmsSender
{
public function send(
string $phone,
string $message
): bool;
}
این Interface مشخص میکند هر کلاسی که قرارداد SmsSender را پیادهسازی میکند، باید متد send() را داشته باشد.
مرحله دوم: ایجاد پیادهسازی Interface
فایل زیر را ایجاد کنید:
app/Services/LogSmsSender.php
<?php
namespace App\Services;
use App\Contracts\SmsSender;
use Illuminate\Support\Facades\Log;
class LogSmsSender implements SmsSender
{
public function send(
string $phone,
string $message
): bool {
Log::info('SMS simulation', [
'phone' => $phone,
'message' => $message,
]);
return true;
}
}
این کلاس برای نمونه، پیام را در لاگ برنامه ثبت میکند و پیامک واقعی نمیفرستد.
مرحله سوم: تزریق Interface به سرویس سفارش
فایل app/Services/OrderService.php:
<?php
namespace App\Services;
use App\Contracts\SmsSender;
class OrderService
{
public function __construct(
protected SmsSender $smsSender
) {}
public function notifyCustomer(
string $phone
): bool {
return $this->smsSender->send(
$phone,
'سفارش شما ثبت شد.'
);
}
}
دقت کنید که OrderService به LogSmsSender وابسته نیست؛ بلکه فقط قرارداد SmsSender را میشناسد.
اما Laravel از کجا باید بفهمد که برای SmsSender کدام کلاس را ایجاد کند؟
پاسخ، Interface Binding در Service Container است.
مرحله چهارم: اتصال Interface به پیادهسازی
در Laravel، اتصال Interface به پیادهسازی معمولاً در یک Service Provider انجام میشود.
در پروژههای جدید Laravel، میتوانید از فایل app/Providers/AppServiceProvider.php استفاده کنید.
متد register() را به شکل زیر تکمیل کنید:
<?php
namespace App\Providers;
use App\Contracts\SmsSender;
use App\Services\LogSmsSender;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
SmsSender::class,
LogSmsSender::class
);
}
public function boot(): void
{
//
}
}
این دستور به Laravel میگوید هر زمان کلاسی به SmsSender نیاز داشت، یک نمونه از LogSmsSender در اختیار آن قرار دهد.
اکنون Container میتواند زنجیره وابستگیها را حل کند:
- یک کلاس به
OrderServiceنیاز دارد. OrderServiceبهSmsSenderنیاز دارد.- Container میداند
SmsSenderبهLogSmsSenderمتصل است. - Laravel پیادهسازی مناسب را ایجاد و تزریق میکند.
اگر در آینده یک کلاس مانند ApiSmsSender بسازید که همان Interface را پیادهسازی کند، میتوانید Binding را تغییر دهید:
$this->app->bind(
SmsSender::class,
ApiSmsSender::class
);
البته کلاس جدید باید قرارداد SmsSender را بهدرستی پیادهسازی کند.
تفاوت bind و singleton در Laravel
Laravel چند روش برای ثبت وابستگیها در Service Container دارد. دو مورد پرکاربرد، bind() و singleton() هستند.
متد bind()
با استفاده از bind()، Container در هر بار حل وابستگی، معمولاً نمونه جدیدی از کلاس ایجاد میکند.
$this->app->bind(
SmsSender::class,
LogSmsSender::class
);
این روش برای سرویسهایی مناسب است که به نمونه مشترک و ماندگار نیاز ندارند.
متد singleton()
با استفاده از singleton()، Container همان نمونه ساختهشده را برای درخواستهای بعدی در همان چرخه عمر Application دوباره استفاده میکند.
$this->app->singleton(
SmsSender::class,
LogSmsSender::class
);
تفاوت مهم این است که singleton() صرفاً یک نام دیگر برای bind() نیست؛ بلکه رفتار مربوط به عمر نمونه را تغییر میدهد.
نباید هر سرویسی را بدون بررسی بهصورت Singleton ثبت کرد. اگر یک شیء وضعیت متغیر و مخصوص هر درخواست را در خود نگه دارد، استفاده نامناسب از Singleton، بهویژه در محیطهایی مانند Laravel Octane، میتواند باعث باقی ماندن وضعیت ناخواسته بین درخواستها شود.
برای انتخاب بین این دو، ابتدا باید مشخص کنید که آیا واقعاً به یک نمونه مشترک نیاز دارید یا خیر.
آیا همه کلاسها باید در Service Container ثبت شوند؟
خیر. یکی از مزیتهای Laravel این است که بسیاری از کلاسهای معمولی را بدون ثبت دستی میتواند ایجاد کند.
برای مثال، اگر یک کلاس فقط به کلاسهای مشخص و قابل ساخت وابسته باشد، اغلب نیازی به ثبت آن در Service Provider ندارید:
<?php
namespace App\Services;
class PriceCalculator
{
public function calculate(
int $price,
int $discount
): int {
return $price - $discount;
}
}
اما اگر کلاسی به یک Interface وابسته باشد، Container بهطور معمول نمیتواند فقط از روی نام Interface تشخیص دهد که کدام پیادهسازی را باید انتخاب کند.
در این شرایط، باید Binding مناسب تعریف شود یا از سازوکار پیکربندی دیگری که پیادهسازی را مشخص میکند استفاده کنید.
همچنین وقتی یک کلاس به مقادیر سادهای مانند عدد، رشته یا تنظیمات خاص نیاز دارد، ممکن است لازم باشد این مقادیر را به شکل مناسب به Container معرفی کنید.
استفاده از Dependency Injection در تستنویسی Laravel
یکی از کاربردهای مهم Dependency Injection، سادهتر کردن تستهاست.
فرض کنید میخواهیم بررسی کنیم که OrderService پیام تأیید سفارش را از طریق SmsSender ارسال میکند. لازم نیست در تست، پیامک واقعی ارسال شود؛ میتوانیم از Mock استفاده کنیم.
نمونه تست با PHPUnit و Mockery:
<?php
namespace Tests\Unit;
use App\Contracts\SmsSender;
use App\Services\OrderService;
use Mockery;
use Tests\TestCase;
class OrderServiceTest extends TestCase
{
public function test_it_notifies_customer(): void
{
$smsSender = Mockery::mock(SmsSender::class);
$smsSender
->shouldReceive('send')
->once()
->with(
'09120000000',
'سفارش شما ثبت شد.'
)
->andReturn(true);
$orderService = new OrderService($smsSender);
$result = $orderService->notifyCustomer(
'09120000000'
);
$this->assertTrue($result);
}
}
در این تست، یک Mock از SmsSender ایجاد شده است. سپس انتظار داریم متد send() دقیقاً یک بار و با آرگومانهای مشخص فراخوانی شود.
به این ترتیب، بدون اتصال به سرویس پیامکی واقعی میتوانیم رفتار OrderService را بررسی کنیم.
در پروژههای Laravel، بسته به نوع تست، میتوان از امکانات Container و Mock کردن وابستگیها نیز استفاده کرد. انتخاب روش مناسب به این بستگی دارد که قصد دارید خود کلاس را جداگانه آزمایش کنید یا رفتار چند بخش از برنامه را در کنار یکدیگر بررسی کنید.
خطاهای رایج هنگام استفاده از Dependency Injection
۱. وابسته شدن به پیادهسازی بهجای قرارداد
اگر کلاسهای اصلی برنامه مستقیماً به پیادهسازیهای جزئی وابسته باشند، تعویض سرویسها دشوارتر میشود.
راهکار: در بخشهایی که احتمال تعویض پیادهسازی وجود دارد، استفاده از Interface را بررسی کنید. البته برای هر کلاس سادهای نیازی به تعریف Interface جداگانه نیست.
۲. ثبت غیرضروری همه کلاسها در Container
ثبت تمام کلاسها در Service Provider باعث پیچیدهتر شدن پیکربندی میشود، در حالی که بسیاری از کلاسها به ثبت دستی احتیاج ندارند.
راهکار: ابتدا از قابلیت Automatic Resolution در Laravel استفاده کنید و فقط در صورت نیاز Binding تعریف کنید.
۳. استفاده بیدلیل از Singleton
اگر یک سرویس وضعیت متغیر و مخصوص یک عملیات یا درخواست را نگه میدارد، اشتراکگذاری همان نمونه ممکن است رفتار نامطلوب ایجاد کند.
راهکار: عمر مورد نیاز هر وابستگی را پیش از انتخاب singleton() مشخص کنید.
۴. تزریق تعداد زیادی وابستگی به یک کلاس
اگر سازنده یک کلاس به تعداد زیادی سرویس نیاز داشته باشد، ممکن است کلاس مسئولیتهای بیش از حدی داشته باشد یا طراحی آن به بازنگری نیاز داشته باشد.
راهکار: وظایف کلاس را بررسی کنید و در صورت لزوم مسئولیتها را به سرویسهای کوچکتر و مشخصتر تقسیم کنید. صرفاً برای کاهش تعداد پارامترها، کلاسهای اضافی ایجاد نکنید.
۵. اشتباه گرفتن Dependency Injection با Facade
Facadeها نیز راهی برای دسترسی به امکانات Laravel هستند، اما استفاده از آنها دقیقاً معادل تزریق صریح وابستگی به سازنده نیست.
راهکار: زمانی که میخواهید وابستگیهای یک کلاس روشن، قابل تعویض و قابل آزمایش باشند، Constructor Injection انتخاب مناسبی است. Facadeها نیز در جای مناسب میتوانند مفید باشند.
چه زمانی استفاده از Dependency Injection ضروری است؟
Dependency Injection در تمام بخشهای برنامه به یک اندازه اهمیت ندارد.
در یک تابع کوچک و مستقل که هیچ وابستگی خارجی ندارد، ممکن است نیازی به آن نباشد. اما وقتی کلاس شما به سرویسهای دیگری مانند درگاه پرداخت، سرویس پیامکی، API خارجی، Repository یا سیستم ذخیرهسازی فایل وابسته است، تزریق وابستگی میتواند طراحی را بسیار بهتر کند.
در پروژههای متوسط و بزرگ Laravel، استفاده از Dependency Injection در کنار Service Layer، Repository Pattern و اصول SOLID به ساختارمندتر شدن کد کمک میکند.
نکته کلیدی این است که هدف، پیچیده کردن معماری نیست؛ بلکه باید مدیریت تغییرات، نگهداری و تست کد را سادهتر کنیم.
جمعبندی
Dependency Injection یکی از مفاهیم مهم در توسعه برنامههای حرفهای با Laravel است. این روش اجازه میدهد وابستگیهای یک کلاس از بیرون تأمین شوند و کلاسها مجبور نباشند تمام اشیای مورد نیاز خود را مستقیماً ایجاد کنند.
در Laravel، Service Container این فرآیند را تا حد زیادی خودکار میکند. برای کلاسهای معمولی اغلب به تنظیمات خاصی نیاز نداریم؛ اما زمانی که از Interface استفاده میکنیم، باید پیادهسازی مورد نظر را به Container معرفی کنیم.
برای استفاده صحیح از Dependency Injection، این نکات را به خاطر بسپارید:
- برای وابستگیهای اصلی کلاس، Constructor Injection را در نظر بگیرید.
- برای وابستگیهای مورد نیاز یک متد خاص، Method Injection میتواند مناسب باشد.
- در صورت نیاز به تعویض پیادهسازی، به Interface و Binding توجه کنید.
- بین
bind()وsingleton()بر اساس عمر مورد نیاز نمونه انتخاب کنید. - از Dependency Injection برای سادهتر شدن تستها استفاده کنید.
- از ایجاد Interface و Service Provider غیرضروری خودداری کنید.
با تسلط بر این مفهوم، درک معماری Service Layer، Repository Pattern و اصول SOLID نیز سادهتر خواهد شد.
سوالات متداول
Dependency Injection در Laravel چیست؟
Dependency Injection روشی برای تأمین وابستگیهای یک کلاس از بیرون آن است. Laravel با استفاده از Service Container میتواند بسیاری از این وابستگیها را بهصورت خودکار ایجاد و تزریق کند.
تفاوت Dependency Injection و Service Container چیست؟
Dependency Injection یک الگوی طراحی است؛ Service Container ابزار Laravel برای مدیریت و حل وابستگیهاست.
آیا برای استفاده از Dependency Injection باید همه کلاسها را ثبت کنیم؟
خیر. Laravel میتواند بسیاری از کلاسهای معمولی را بهصورت خودکار ایجاد کند. ثبت دستی معمولاً زمانی لازم میشود که پیادهسازی یک Interface یا وابستگی خاص باید مشخص شود.
Constructor Injection بهتر است یا Method Injection؟
هیچکدام در تمام شرایط بهترین نیستند. اگر وابستگی برای وظایف اصلی کلاس لازم است، Constructor Injection معمولاً انتخاب مناسبی است. اگر فقط یک متد به آن نیاز دارد، Method Injection میتواند خواناتر باشد.
آیا Dependency Injection سرعت Laravel را کاهش میدهد؟
استفاده معمول از Dependency Injection بخشی از سازوکار عادی Laravel است و بهخودیخود به معنای کند شدن محسوس برنامه نیست. عملکرد واقعی به ساختار برنامه، تعداد عملیات، نحوه حل وابستگیها و سایر عوامل بستگی دارد.
آیا استفاده از Interface در Dependency Injection اجباری است؟
خیر. میتوان یک کلاس مشخص را مستقیماً تزریق کرد. Interface زمانی ارزش بیشتری دارد که بخواهید قرارداد را از پیادهسازی جدا کنید یا پیادهسازیهای متفاوتی داشته باشید.





