حاوي الخدمات (Service Container) في Laravel

من موسوعة حسوب

مقدمة

حاوي خدمات Laravel (أي Laravel service container) هو أداةٌ قوية لإدارة اعتِماديَّات (dependencies) الصِّنف والقيام بإضافة اعتِماديَّات (dependency injection)." إضافة الاعتِماديَّات" هو مصطلح تقني يعني في مُجمله "إضافة" اعتماديات الصنف باستخدام التابع الباني (constructor) أو في بعض الحالات توابع ضبط القيم (setter).

لنلقِ نظرة على هذا المثال البسيط:

<?php

namespace App\Http\Controllers;

use App\User;
use App\Repositories\UserRepository;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
    /**
     * مستودع المُستخدِم تطبيق
     *
     * @var UserRepository
     */
    protected $users;

    /**
     * أنشِئ نسخة وحدة تحكم جديدة.
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * عرض الملف الشخصي للمُستخدِم المُعطى
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $user = $this->users->find($id);
        return view('user.profile', ['user' => $user]);
    }
}

في هذا المثال يحتاج UserController إلى جلب المُستخدمين من مصدر بيانات. لهذا السبب سنضيف (inject) خدمة (service) تقدر على جلب المُستخدمين. يَستخدم UserRepository، في هذا السياق، Eloquent لجلب معطيات المُستخدم من قاعدة البيانات. ولكن لمّا كان المستودع (repository) مُضافًا (injected)، فنستطيع تبديله إلى تعريف استخدام آخر (implementation) بسهولة. نستطيع أيضا أن "نُقلِّد" أو نصنع تعريف استخدام مزيّف من UserRepository عند اختبار تطبيقنا.

فهم حاوي خدمات Laravel فهمًا عميقًا هو مسألةٌ جوهريةٌ في بناء تطبيقات ضخمة قوية، وفي المساهمة لأساس إطار Laravel ذاته.

الارتباط (Binding)

أساسيات الارتباط

كل ارتباطات حاوي خدماتك (service container bindings) تقريبًا ستُسَجَّل داخل مقدمي الخدمات، لذا ستوضح كل الأمثلة التالية طريقة استخدام الحاوي في ذاك السياق.

ملاحظة: لا حاجة لربط (bind) الأصناف بالحاوي ان لم تعتمد (depend) على أي واجهة (interface). لا يحتاج الحاوي أن يعطى تعليمات حول كيفية بناء هذه الكائنات (objects) لأنه قادر على استبيانها (resolve) باستخدام الانعكاس (reflection)

الارتباط البسيط

يمكنك دائما الولوج لحاوي الخدمات داخل مقدم خدمات عبر الخاصية ‎$this->app. يمكننا تسجيل الارتباط باستخدام الدالَّة bind، مع تمرير اسم الصنف أو الواجهة الذي نريد تسجيله مع Closure والذي يعيد نسخة من الصنف:

$this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

لاحظ أننا نحصل على الحاوي ذاته كمتغيّر وسيط للمستبين (resolver). يمكننا بعدها استخدام الحاوي لاستبيان الاعتماديات الثانوية (sub-dependencies) للكائن الذي نبنيه.

ربط Singleton

يربط التابع singleton صنفًا أو واجهة بالحاوي الذي يجب أن يُستبين مرة واحدة. تُعاد نسخة نفس الكائن (object) في الاستدعاءات التالية للحاوي لحظة استبيان ارتباط singleton:

$this->app->singleton('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

ربط النُسخ (Binding Instances)

تستطيع أيضًا ربط نسخة كائن موجودة بالحاوي باستخدام التابع instance. ستُعاد النسخة المٌعطاة في الاستدعاءات التالية للحاوي:

$api = new HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\API', $api);

ربط الأنواع الأساسية (Binding Primitives)

قد تحتاج أحيانًا إلى صنفٍ يتلقى بعض الأصناف المُضافة (injected classes)، ولكن قد تحتاج أيضًا لقيمة مُضافة أوليّة (primitive) مثل الأعداد الصحيحة (integers). يمكنك استخدام أي ارتباط سياقي (contextual binding) لإضافة أي قيمة يحتاج إليها صنفك:

$this->app->when('App\Http\Controllers\UserController')
          ->needs('$variableName')
          ->give($value);

ربط الواجهات بتعاريف استخدام (Binding Interfaces To Implementations)

من أقوى خاصيات حاوي الخدمات هي قدرته على ربط واجهات بتعريف استخدام معين. فلنفترض مثلًا أن لدينا واجهة EventPusher وتعريف استخدام RedisEventPusher. عندما ننتهي من برمجة تعريف استخدامنا RedisEventPusher لهذه الواجهة نستطيع تسجيله بحاوي الخدمات بهذه الطريقة:

$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'
);

يخبر هذا التصريح الحاوي بوجوب إضافة RedisEventPusher عند احتياج الصنف لتعريف استخدام EventPusher. يمكننا الآن التلميح على نوع (type-hint) الواجهة EventPusher في التابع الباني أو أي موضع آخر تضاف فيه الاعتماديات من طرف حاوي الخدمات:

use App\Contracts\EventPusher;

/**
 *  أنشِئ نسخة صنف جديد
 *
 * @param  EventPusher  $pusher
 * @return void
 */
public function __construct(EventPusher $pusher)
{
    $this->pusher = $pusher;
}

الربط السياقي

قد يكون لديك في بعض الأحيان صنفان يستخدمان نفس الواجهة لكنك ترغب بإضافة تعاريف استخدام مختلفة لكل صنف. مثلًا، قد تعتمد وحدتا تحكم على تعاريف استخدام مختلفة من عقد (contract) ‏Illuminate\Contracts\Filesystem\Filesystem.

يوفر Laravel واجهة بسيطة وسلسة لتعريف هذا السلوك:

use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

الوسم

قد تحتاج بين الحين والآخر أن تستبين كامل "تصنيف" معين من الارتباطات. فمثلا ربما تبني مُجَمِّع تقارير يتلقَّى مصفوفة من عدة تعاريف استخدام للواجهة Report. يمكنك وضع وسم عليها بعد تسجيل تعريف الاستخدام Report باستخدام التابع tag:

$this->app->bind('SpeedReport', function () {
   //
});

$this->app->bind('MemoryReport', function () {
   //
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

يمكنك استبيان كل الخدمات معًا بسهولة بعد وسمهن عبر التابع tagged:

$this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
});

توسيع الارتباط

يسمح التابع extend بتعديل الخدمات المُسْتَبْيَنَة. على سبيل المثال تستطيع إضافة تعليمات برمجية أخرى للتصريح أو ضبط الخدمة بعد استبيانها. تقبل الدالة extend تعبيرًا مغلقًا (closure) يُعيد الخدمة المُعَدلَّة كمُتغيِّره الوسيط الوحيد:

$this->app->extend(Service::class, function($service) {
    return new DecoratedService($service);
});

الاستبيان

الدالة make

تستطيع استخدام الدالة make لاستبيان صنف خارج الحاوي. تقبل الدالة make اسم الصنف أو الواجهة التي تريد استبيانها:

$api = $this->app->make('HelpSpot\API');

تستطيع استخدام مُساعد الاستبيان العام (global resolve helper) إن كنت بموضع من تعليماتك البرمجية لا يسمح لك بالوصول للمتغير ‎$app:

$api = resolve('HelpSpot\API');

يمكنك إضافة اعتماديات صنفك، إن لم تكن قابلةً للاستبيان عبر الحاوي، بتمريرها كمصفوفة ترابطية إلى التابع makeWith:

$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);

الإضافة التلقائية

يجدر بالذكر أنَّ باستطاعتك أن "تلمح على نوع" (type-hint) الاعتماديات في الدالة البانية constructor التي يستبينها الحاوي بما فيها وحدات التحكم (controllers)، ومنصتات الأحداث (event listeners)، والأعمال المصفوفة في الطوابير (queue jobs) والبرمجيات الوسيطة (middleware) والمزيد. هكذا يجب استبيان معظم كائناتك من قبل الحاوي في التطبيق.

على سبيل المثال، تستطيع التلميح على نوع المستودع الذي يُعَرِّفُه تطبيقك في الدالة البانية لوحدة التحكّم. سيُستَبان المستودع تلقائيًا ويضاف للصنف:

<?php

namespace App\Http\Controllers;

use App\Users\Repository as UserRepository;

class UserController extends Controller
{
    /**
     * نسخة مستودع المستخدم
     */
    protected $users;

    /**
     * أنشِئ نسخة وحدة تحكم جديدة
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     *  الموافق ID اعرِض المستخدم ذا المعرّف    
     *
     * @param  int  $id
  * @return Response
     */
    public function show($id)
    {
       //
    }
}

أحداث الحاوي (Container Events)

يطلق حاوي الخدمات الأحداث في كل مرة يستبين فيها كائنًا. تستطيع الإنصات لهذا الحدث باستخدام التابع resolving:

$this->app->resolving(function ($object, $app) {
   // يُستدعى عند استبيان الحاوي لكائن من أي نوع
});

$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
   // "HelpSpot\API" يُستدعى عند استبيان الحاوي لأي كائن من النوع  
});

كما ترى في المثال، سيمرر الكائن المستَبْيَن لرد النداء (callback) مما يمكنك من ضبط أي خاصيات اضافيّة في الكائن قبل إعطائه للمُستهلك.

الواجهة PSR-11

يستخدم حاوي خدمات Laravel الواجهة PSR-11. لذلك تستطيع التلميح على نوع واجهة الحاوي PSR-11 للحصول على نسخة من حاوي Laravel:

use Psr\Container\ContainerInterface;

Route::get('/', function (ContainerInterface $container) {
    $service = $container->get('Service');

    //
});

ملاحظة: سيسبب استدعاء الدالة get رمي استثناء (exception) ان لم يُربط المُعرف بشكل واضح بالحاوي.

مصادر