إطار Laravel

حاوية الخدمات وحقن التبعيات

20 دقيقة الدرس 23 من 45

حاوية الخدمات وحقن التبعيات

حاوية الخدمات في 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 وحقن التبعيات. فهم متى تستخدم كل منها:

Facades مقابل DI:
// استخدام 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 بدقة
ملاحظة الاختبار: كل من facades وحقن التبعيات قابلة للاختبار في Laravel. لدى Facades مساعدين اختبار مدمجين، ولكن DI يجعل من الأسهل حقن كائنات وهمية في اختبارات الوحدة.

أحداث الحاوية و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();
});
تمرين عملي 1: أنشئ NotificationService يعتمد على NotificationChannelInterface. أنشئ تطبيقين: EmailChannel و SmsChannel. اربط الواجهة بـ EmailChannel افتراضياً، ولكن استخدم الربط السياقي بحيث يستخدم UrgentAlertService SmsChannel بينما يستخدم RegularAlertService EmailChannel.
تمرين عملي 2: ابنِ فئة ReportGenerator تعتمد على PdfExporter و DataRepository و Logger interfaces. أنشئ تطبيقات ملموسة واربطها في موفر خدمات. ثم أنشئ متحكماً يستخدم ReportGenerator من خلال حقن التبعيات لإنشاء وتنزيل تقرير PDF.
تمرين عملي 3: أنشئ CacheManager singleton يتتبع نجاحات وإخفاقات الذاكرة المؤقتة. استخدم callback الحل للحاوية لحقن CacheManager تلقائياً في أي فئة لديها طريقة setCacheManager(). اختبره من خلال إنشاء خدمتين مختلفتين تتلقى كلاهما نفس instance من CacheManager.

الخلاصة

حاوية الخدمات هي قلب بنية Laravel:

  • حقن التبعيات: حل وحقن تبعيات الفئات تلقائياً
  • الربط: إخبار الحاوية كيفية إنشاء الكائنات (bind، singleton، scoped)
  • ربط الواجهة: البرمجة على الواجهات للحصول على كود مرن وقابل للاختبار
  • الربط السياقي: تطبيقات مختلفة لسياقات مختلفة
  • الحل التلقائي: لا حاجة للربط للفئات البسيطة
  • Facades مقابل DI: كلاهما صالح؛ اختر بناءً على حالة الاستخدام

في الدرس التالي، سنستكشف موفري الخدمات، وهي المكان المركزي لربط الحاوية وبدء تشغيل التطبيق.