حاوية الخدمات وحقن التبعيات
حاوية الخدمات وحقن التبعيات
حاوية الخدمات في Laravel هي واحدة من أقوى ميزاته، حيث تدير تبعيات الفئات وتنفذ حقن التبعيات في جميع أنحاء الإطار. فهم الحاوية هو مفتاح كتابة كود قابل للصيانة والاختبار ومفصول بشكل فضفاض.
ما هي حاوية الخدمات؟
حاوية الخدمات (تسمى أيضاً IoC Container - Inversion of Control) هي أداة قوية لإدارة تبعيات الفئات. بدلاً من إنشاء الكائنات وتبعياتها يدوياً، تتولى الحاوية هذا تلقائياً.
// UserController.php - النهج اليدوي
class UserController extends Controller
{
public function index()
{
// إنشاء التبعيات يدوياً
$config = new Configuration();
$logger = new Logger($config);
$database = new Database($config);
$repository = new UserRepository($database, $logger);
$users = $repository->all();
return view('users.index', compact('users'));
}
}
// المشاكل:
// 1. مقترن بإحكام مع التطبيقات الملموسة
// 2. صعب الاختبار (لا يمكن محاكاة التبعيات بسهولة)
// 3. كود متكرر
// 4. صعب الصيانة عند تغيير التبعيات
// UserController.php - استخدام حقن التبعيات
class UserController extends Controller
{
protected $users;
// الحاوية تحل UserRepository وتبعياته تلقائياً
public function __construct(UserRepository $users)
{
$this->users = $users;
}
public function index()
{
$users = $this->users->all();
return view('users.index', compact('users'));
}
}
// الفوائد:
// 1. مفصول بشكل فضفاض - يعتمد على الواجهة، وليس التطبيق
// 2. سهل الاختبار - يمكن حقن كائنات وهمية
// 3. كود نظيف - لا يوجد إنشاء يدوي
// 4. مرن - تبديل التطبيقات بسهولة
الربط بالحاوية
الربط يخبر الحاوية كيفية حل فئة أو واجهة معينة. يتم هذا عادة في موفري الخدمات:
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\PaymentGateway;
use App\Services\StripePaymentGateway;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// ربط بسيط - إنشاء instance جديد في كل مرة
$this->app->bind(PaymentGateway::class, function ($app) {
return new StripePaymentGateway(
config('services.stripe.key')
);
});
// Singleton - إنشاء مرة واحدة، إعادة استخدام في كل مكان
$this->app->singleton(DatabaseConnection::class, function ($app) {
return new DatabaseConnection(
config('database.default')
);
});
// Scoped - singleton داخل طلب HTTP
$this->app->scoped(ShoppingCart::class, function ($app) {
return new ShoppingCart(session()->getId());
});
// ربط Instance - ربط instance موجود
$apiClient = new ApiClient('https://api.example.com');
$this->app->instance(ApiClient::class, $apiClient);
}
}
- bind(): instance جديد في كل مرة (افتراضي لمعظم الخدمات)
- singleton(): instance واحد للتطبيق بأكمله (قاعدة البيانات، الذاكرة المؤقتة، المسجل)
- scoped(): instance واحد لكل طلب (عربة التسوق، بيانات جلسة المستخدم)
- instance(): ربط كائن موجود مسبقاً
الحل التلقائي والتلميح إلى النوع
يمكن لحاوية Laravel حل الفئات تلقائياً دون ربط صريح إذا لم يكن لديها تبعيات في المنشئ أو لديها فقط تبعيات قابلة للحل التلقائي أيضاً:
// هذه الفئات قابلة للحل التلقائي
// لا توجد تبعيات
class SimpleService
{
public function execute()
{
return 'Executed';
}
}
// تبعيات قابلة للحل (فئات ملموسة)
class UserService
{
public function __construct(
protected Logger $logger,
protected UserRepository $repository
) {}
}
// الاستخدام في المتحكم - لا حاجة للربط
class UserController extends Controller
{
public function __construct(
protected UserService $userService,
protected SimpleService $simpleService
) {}
public function index()
{
$this->simpleService->execute();
return $this->userService->getUsers();
}
}
// الحاوية تلقائياً:
// 1. تحل SimpleService (لا توجد تبعيات)
// 2. تحل Logger و UserRepository لـ UserService
// 3. تنشئ UserService مع تبعياته
// 4. تحقن كل شيء في UserController
ربط الواجهة بالتطبيق
ربط الواجهات بالتطبيقات هو نمط أساسي للتطبيقات القابلة للصيانة:
// تعريف الواجهة
namespace App\Contracts;
interface PaymentGatewayInterface
{
public function charge(int $amount): bool;
public function refund(string $transactionId): bool;
}
// التطبيق
namespace App\Services;
use App\Contracts\PaymentGatewayInterface;
class StripePaymentGateway implements PaymentGatewayInterface
{
public function charge(int $amount): bool
{
// تطبيق خاص بـ Stripe
return true;
}
public function refund(string $transactionId): bool
{
// تطبيق خاص بـ Stripe
return true;
}
}
// ربط الواجهة بالتطبيق في موفر الخدمات
public function register(): void
{
$this->app->bind(
PaymentGatewayInterface::class,
StripePaymentGateway::class
);
}
// الاستخدام في المتحكم - يعتمد على الواجهة، وليس التطبيق
class PaymentController extends Controller
{
public function __construct(
protected PaymentGatewayInterface $gateway
) {}
public function charge(Request $request)
{
$success = $this->gateway->charge($request->amount);
return response()->json(['success' => $success]);
}
}
// الفوائد:
// 1. يمكن تبديل StripePaymentGateway بـ PayPalPaymentGateway
// 2. سهل المحاكاة في الاختبارات
// 3. الكود يعتمد على التجريد، وليس الفئة الملموسة
الحل اليدوي من الحاوية
أحياناً تحتاج إلى حل instances يدوياً من الحاوية:
// استخدام مساعد app()
$userService = app(UserService::class);
// استخدام App facade
use Illuminate\Support\Facades\App;
$userService = App::make(UserService::class);
// استخدام مساعد resolve()
$userService = resolve(UserService::class);
// الحل مع المعاملات
$report = app(ReportGenerator::class, [
'startDate' => now()->subMonth(),
'endDate' => now()
]);
// استدعاء الطرق مع حقن التبعيات
$result = app()->call(function (UserService $service) {
return $service->getActiveUsers();
});
// استدعاء طرق الفئة
$result = app()->call([UserService::class, 'getActiveUsers']);
// حقن الطريقة في المتحكمات
class ReportController extends Controller
{
// التبعيات محقونة في الطريقة، وليس المنشئ
public function generate(
Request $request,
ReportGenerator $generator,
PdfExporter $exporter
) {
$report = $generator->create($request->all());
return $exporter->export($report);
}
}
الربط السياقي
أحياناً تحتاج إلى تطبيقات مختلفة لواجهة اعتماداً على الفئة التي تحتاجها:
// فئتان تحتاجان إلى تطبيقات ذاكرة مؤقتة مختلفة
namespace App\Services;
class UserService
{
public function __construct(CacheInterface $cache) {}
}
class ReportService
{
public function __construct(CacheInterface $cache) {}
}
// في موفر الخدمات - ربط تطبيقات مختلفة سياقياً
public function register(): void
{
// UserService يحصل على RedisCache
$this->app->when(UserService::class)
->needs(CacheInterface::class)
->give(function () {
return new RedisCache();
});
// ReportService يحصل على FileCache
$this->app->when(ReportService::class)
->needs(CacheInterface::class)
->give(function () {
return new FileCache();
});
// فئات متعددة يمكن أن تشارك نفس التطبيق
$this->app->when([
OrderService::class,
InvoiceService::class,
ShippingService::class
])
->needs(LoggerInterface::class)
->give(DatabaseLogger::class);
}
// الربط السياقي مع القيم الأولية
$this->app->when(DatabaseConnection::class)
->needs('$host')
->give(config('database.host'));
$this->app->when(ApiClient::class)
->needs('$timeout')
->giveConfig('services.api.timeout'); // اختصار لقيم config
Facades مقابل حقن التبعيات
يوفر Laravel كلاً من facades وحقن التبعيات. فهم متى تستخدم كل منها:
// استخدام Facade - واجهة شبيهة بالثابت
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
public function index()
{
$users = Cache::remember('users', 3600, function () {
return User::all();
});
return view('users.index', compact('users'));
}
}
// استخدام حقن التبعيات - حقن العقد
use Illuminate\Contracts\Cache\Repository as CacheRepository;
class UserController extends Controller
{
public function __construct(
protected CacheRepository $cache
) {}
public function index()
{
$users = $this->cache->remember('users', 3600, function () {
return User::all();
});
return view('users.index', compact('users'));
}
}
// متى تستخدم كل منها:
// Facades - جيد لـ:
// - النماذج الأولية السريعة
// - النصوص والأوامر البسيطة
// - الاستخدام لمرة واحدة في طريقة
// - عندما لا تكون قابلية الاختبار همّاً رئيسياً
// حقن التبعيات - جيد لـ:
// - الفئات التي سيتم اختبارها كوحدات
// - عندما تحتاج إلى تبديل التطبيقات
// - الخدمات المعقدة مع العديد من التبعيات
// - اتباع مبادئ SOLID بدقة
أحداث الحاوية وcallbacks الحل
تطلق الحاوية أحداثاً عند حل الفئات، مما يسمح لك بتكوين الكائنات بعد الإنشاء:
// في موفر الخدمات
// تشغيل callback بعد حل فئة
$this->app->resolving(PaymentService::class, function ($service, $app) {
// تكوين الخدمة بعد إنشائها
$service->setApiKey(config('services.payment.key'));
$service->setEnvironment(app()->environment());
});
// تشغيل callback لجميع الحلول
$this->app->resolving(function ($object, $app) {
// يتم استدعاؤه لكل كائن محلول من الحاوية
if (method_exists($object, 'setLogger')) {
$object->setLogger($app->make(LoggerInterface::class));
}
});
// تمديد ربط موجود
$this->app->extend(ApiClient::class, function ($service, $app) {
// تعديل الخدمة بعد إنشائها
$service->addMiddleware(new RateLimitMiddleware());
return $service;
});
// AfterResolving - مشابه لـ resolving ولكن يعمل بعد جميع callbacks الحل
$this->app->afterResolving(UserService::class, function ($service, $app) {
$service->boot();
});
الخلاصة
حاوية الخدمات هي قلب بنية Laravel:
- حقن التبعيات: حل وحقن تبعيات الفئات تلقائياً
- الربط: إخبار الحاوية كيفية إنشاء الكائنات (bind، singleton، scoped)
- ربط الواجهة: البرمجة على الواجهات للحصول على كود مرن وقابل للاختبار
- الربط السياقي: تطبيقات مختلفة لسياقات مختلفة
- الحل التلقائي: لا حاجة للربط للفئات البسيطة
- Facades مقابل DI: كلاهما صالح؛ اختر بناءً على حالة الاستخدام
في الدرس التالي، سنستكشف موفري الخدمات، وهي المكان المركزي لربط الحاوية وبدء تشغيل التطبيق.