سرویس کانتینر و سرویس پرووایدر در لاراول

این ویرگول شما را در درک مفاهیمی مثل سرویس کانتینر و سرویس پرووایدر در لاراول راهنمایی می کند. با مفاهیم اساسی شروع می کنیم و در پایان شما ایده ی چگونگی ترکیب این مفاهیم در کنار هم را فرا خواهید گرفت.

بخش اول: Dependency Injection

تزریق وابستگی در ساده ترین تعریف به فرآیند انتقال وابستگی کلاس بصورت آرگومان به یکی از متودهای آن کلاس می گویند(معمولاً سازنده).

به کد زیر که در آن تزریق وابستگی به کار گرفته نشده است دقت کنید

<?php

namespace App;

use App\Models\Post;
use App\Services\TwitterService;

class Publication {

    public function __construct()
    {
        // dependency is instantiated inside the class
        $this->twitterService = new TwitterService();
    }

    public function publish(Post $post)
    {
        $post->publish();

        $this->socialize($post);
    }

    protected function socialize($post)
    {
        $this->twitterService->share($post);
    }

}

فرض کنید این کلاس بخشی از یک سرویس وبلاگ نویسی است که مسئول انتشار یک پست و به اشتراک گذاری آن در شبکه های اجتماعی است. متود socialize از نمونه ای از کلاس TwitterService استفاده می کند که شامل یک متود عمومی به نام share است.

<?php

namespace App\Services;

use App\Models\Post;

class TwitterService {
    public function share(Post $post)
    {
        dd('shared on Twitter!');
    }
}

همانطور که مشاهده می کنید ، در سازنده نمونه جدیدی از کلاس TwitterService ایجاد شده است. به جای ساختن نمونه در داخل کلاس ، می توانیم بصورت زیر یک نمونه را به عنوان آرگومان از خارج به سازنده تزریق کنیم.

<?php

namespace App;

use App\Models\Post;
use App\Services\TwitterService;

class Publication {

    public function __construct(TwitterService $twitterService)
    {
        $this->twitterService = $twitterService;
    }

    public function publish(Post $post)
    {
        $post->publish();

        $this->socialize($post);
    }

    protected function socialize($post)
    {
        $this->twitterService->share($post);
    }

}

برای نمایش این موضوع به قطعه کد زیر نگاهی بیندازید.

<?php

// routes/web.php

use App\Publication;
use App\Models\Post;
use App\Services\TwitterService;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $post = new Post();

    // dependency injection
    $publication = new Publication(new TwitterService());

    dd($publication->publish($post));

    // shared on Twitter!
});

این پایه ای ترین نوع تزریق وابستگی بود. اعمال تزریق وابستگی به یک کلاس باعث وارونگی کنترل(inversion of control) می شود. ابتدا کلاس وابسته یعنی Publication کنترل ایجاد وابستگی کلاس یعنی TwitterService را در اختیار داشت در حالی که بعداً ، این کنترل از حوزه ی اختیاراتش خارج و به کس دیگری سپرده شد.

بخش دوم: IoC Container

در بخش قبلی ، با ایده تزریق وابستگی آشنا شدید و به شما نشان داده شد که چگونه کنترل ایجاد وابستگی ها از کلاس گرفته می شود. IoC کانتینر می تواند روند تزریق وابستگی را کارآمدتر کند. IoC کانتینر که به DI کانتینر نیز معروف است یک کلاس ساده است که می تواند در هنگام درخواست داده هایی را که قبلا در خود ذخیره کرده است را ارائه کند. یک IoC کانتینر ساده را می توان به شکل زیر نوشت:

<?php

namespace App;

class Container {

    // array for keeping the container bindings
    protected $bindings = [];

    // binds new data to the container
    public function bind($key, $value)
    {
        // bind the given value with the given key
        $this->bindings[$key] = $value;
    }

    // returns bound data from the container
    public function make($key)
    {
        if (isset($this->bindings[$key])) {
            // check if the bound data is a callback
            if (is_callable($this->bindings[$key])) {
                // if yes, call the callback and return the value
                return call_user_func($this->bindings[$key]);
            } else {
                // if not, return the value as it is
                return $this->bindings[$key];
            }
        }
    }

}

با استفاده از متود bind می توانید هر داده ای را در این کانتینر نگهداری کنید. در مثال زیر کانتینر یک رشته را نگهداری و سپس برگردانده است.

<?php

// routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $container = new Container();

    $container->bind('name', 'Farhan Hasin Chowdhury');

    dd($container->make('name'));

    // Farhan Hasin Chowdhury
});

با ارسال callback بعنوان آرگومان دوم که یک نمونه از کلاس را می سازد می توانید برای نگهداری کلاس ها از آن استفاده کنید.

<?php

// routes/web.php

use App\Container;
use App\Service\TwitterService;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $container = new Container;

    $container->bind(TwitterService::class, function(){
        return new App\Services\TwitterService;
    });

    ddd($container->make(TwitterService::class));

    // App\Services\TwitterService {#269}
});


فرض کنید کلاس TwitterService به کلیدی برای اعتبارسنجی احتیاج دارد. در این مورد می توانیم به شکل زیر عمل کنیم.


<?php

// routes/web.php

use App\Container;
use App\Service\TwitterService;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $container = new Container;

    $container->bind('ApiKey', 'very-secret-api-key');

    $container->bind(TwitterService::class, function() use ($container){
        return new App\Services\TwitterService($container->make('ApiKey'));
    });

    ddd($container->make(TwitterService::class));

    // App\Services\TwitterService {#269 ▼
    //     #apiKey: &quotvery-secret-api-key&quot
    // }
});

زمانیکه یک داده ای را به کانتینر بایند می کنید هر زمان که نیاز داشته باشید فقط کافیست آن را درخواست کنید. با این روش تنها یکبار از new استفاده می کنید. هدف این نیست که بگوییم استفاده از new بد است اما هر بار که از new استفاده می کنید مجبور هستید که مراقب ارسال صحیح وابستگی ها به کلاس ها باشید. با Ioc کانتینر دیگر شما نگرانی از بابت تزریق وابستگی نخواهید داشت. IoC کانتینر حتی کد شما را منعطف تر هم خواهد کرد. وضعیتی را در نظر داشته باشید که در آن می خواهید به جای استفاده از کلاس TwitterService از کلاس دیگری مثلا کلاس LinkedInService استفاده کنید.

کد پیاده سازی شده ی فعلی خیلی مناسب این کار نیست چرا که برای تعویض کلاس TwitterService شما باید یک کلاس جدید به کانتینر بایند کنید و تمامی رفرنس ها به کلاس قبلی را تغییر دهید.

شما می توانید این کار را با استفاده از Interfaceها خیلی راحت تر انجام دهید. برای شروع اینترفیسی با نام SocialMediaServiceInterface می سازیم:

<?php

namespace App\Interfaces;

use App\Models\Post;

interface SocialMediaServiceInterface {
    public function share(Post $post);
}

و در ادامه کلاس TwitterService باید اینترفیس را پیاده کند:

<?php

namespace App\Services;

use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;

class TwitterService implements SocialMediaServiceInterface {
    protected $apiKey;

    public function __construct($apiKey)
    {
        $this->apiKey = $apiKey;
    }

    public function share(Post $post)
    {
        dd('shared on Twitter!');
    }
}

بجای اینکه کلاس concrete را به کانتینر بایند کنیم اینترفیس را بایند می کنیم. و در callback یک نمونه از کلاس TwitterService را مثل قبل می سازیم.

<?php

// routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;
use App\Interfaces\SocialMediaServiceInterface;

Route::get('/', function () {
    $container = new Container;

    $container->bind('ApiKey', 'very-secret-api-key');

    $container->bind(SocialMediaServiceInterface::class, function() use ($container){
        return new App\Services\TwitterService($container->make('ApiKey'));
    });

    ddd($container->make(SocialMediaServiceInterface::class));

    // App\Services\TwitterService {#269 ▼
    //     #apiKey: &quotvery-secret-api-key&quot
    // }
});

کد مثل سابق کار می کند. ولی زمانی که قصد استفاده از لینکدین بجای توئیتر را داشته باشید با اینترفیسی که ساخته ایم این کار به شکلی ساده قابل انجام است. تنها با دو قدم به آن دست پیدا می کنید.

کلاس LinkedInService را می سازیم که باید اینترفیس SocialMediaServiceInterface را پیاده کند:

<?php

namespace App\Services;

use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;

class LinkedInService implements SocialMediaServiceInterface {
    public function share(Post $post)
    {
        dd('shared on LinkedIn!');
    }
}

این بار در callback به جای برگشت نمونه ای از کلاس TwitterService کافیست LinkedInService را برگردانید.

<?php

// routes/web.php

use App\Container;
use App\Interfaces\SocialMediaServiceInterface;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $container = new Container;

    $container->bind(SocialMediaServiceInterface::class, function() {
        return new App\Services\LinkedInService();
    });

    ddd($container->make(SocialMediaServiceInterface::class));

    // App\Services\LinkedInService {#269}
});

این رویکرد باعث می شود قسمت های دیگر کد بدون تغییر بماند و تنها متود bind را آپدیت نماییم. تا زمانیکه یک کلاس اینترفیس SocialMediaServiceInterface را پیاده کند به راحتی می تواند به کانتینر بایند شود و از آن بعنوان سرویس جدید استفاده کنیم.

بخش سوم: Service Container and Service Providers

لاراول یک Ioc کانتینر قدرتمند به نام service container دارد که در این بخش می خواهیم مثال های قبل را با کمک آن بازنویسی کنیم.

<?php

// routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    app()->bind('App\Interfaces\SocialMediaService', function() {
        return new App\Services\LinkedInService();
    });

    ddd(app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}
});

در هر برنامه ی لاراولی هلپر ()app یک نمونه از کانتینر را برمی گرداند. درست مشابه کانتینر سفارشی که ساخته بودیم سرویس کانتینر لاراول نیز متود های make , bind را برای بایند کردن و بازیابی سرویس ها استفاده می کند. سرویس کانتینر لاراول متود دیگری با نام singleton هم دارد که برای بایند کردن کلاس هایی که نیاز داریم singleton (تنها یک نمونه از کلاس در طول اجرای برنامه وجود داشته باشد) باشند مورد استفاده قرار می گیرد. این مورد را با مثال زیر بهتر متوجه می شوید.

<?php

// routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    app()->bind('App\Interfaces\SocialMediaService', function() {
        return new App\Services\LinkedInService();
    });

    ddd(app()->make('App\Interfaces\SocialMediaService'), app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}
    // App\Services\LinkedInService {#269}
});

همانطور که در انتها می بینید دو نمونه ی بازیابی شده [ 262# , 269# ] متفاوت هستند. اگه شما کلاس را به شکل singleton بایند کنید آنگاه هر چند بار هم که سرویس را بازیابی کنید نمونه های یکسانی را دریافت خواهید کرد.

<?php

// routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    app()->singleton('App\Interfaces\SocialMediaService', function() {
        return new App\Services\LinkedInService();
    });

    ddd(app()->make('App\Interfaces\SocialMediaService'), app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}
    // App\Services\LinkedInService {#262}
});

الان که متودهای bind, make, singleton را شناختید زمان آن فرا رسیده که مکان مناسب قرارگیری این متود ها را یاد بگیرید. به وضوح مشخص است که نباید در فایل مسیر یا کنترلر ها باشند. مکان مناسب سرویس پرووایدر ها هستند. سرویس پرووایدرها کلاس هایی هستند که در مسیر app/Providers قرار دارند. سرویس پرووایدر هامسئول راه اندازی اکثر سرویس های فریمورک می باشند.

هر پروژه ی لاراولی بصورت پیشفرض 5 سرویس پرووایدر دارد. که در میان آن ها AppServiceProvider با دو متود خالی [ boot, register ] هم وجود دارد. متود register برای ثبت سرویس های جدید مورد استفاده قرار می گیرد یعنی همان جایی که باید bind یا singleton قرار داده شوند.

<?php

namespace App\Providers;

use App\Interfaces\SocialMediaService;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(SocialMediaService::class, function() {
            return new \App\Services\LinkedInService;
        });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}


در داخل پرووایدرها به کانتینر به سادگی از طریق this->app$ بجای هلپر ()app دسترسی دارید.

در متود boot هر آن چیزی که برای راه اندازی سرویس های رجیستر شده لازم است قرار می گیرد. ?

بعد از اینکه تمامی پرووایدرها رجیستر شدند نوبت به بوت شدن آنها می رسد که این عمل منجر به فراخوانی متود boot در تمامی پرووایدرها خواهد شد. زمانی که از سرویس پرووایدرها استفاده می کنیم اشتباه متداولی که رخ می دهد این است که در یک سرویس پرووایدر از سرویسی که در یک پرووایدر دیگر رجیستر شده است استفاده می کنیم و از آنجائیکه تضمینی در بارگذاری تمامی پرووایدرهای دیگر نیست ممکن است سرویسی که مورد نیاز است هنوز در دسترس نباشد. بنابراین کدهایی که در سرویس پرووایدری نیاز به دیگر سرویس ها دارند باید در متود boot قرار داده شوند. بطور خلاصه متود register تنها برای رجیستر کردن سرویس ها داخل کانتینر استفاده می شود و در داخل متود boot شما هر کدی را که تصور می کنید می توانید قرار دهید از رجیستر کردن event listeners ها گرفته تا include کردن routeها.

یک مثال خوب برای این مورد BroadcastingServiceProvider است.

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;

class BroadcastServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Broadcast::routes();

        require base_path('routes/channels.php');
    }
}

همانطور که می بینید ابتدا متود routes را فراخوانی و سپس فایل channels.php را لود کرده تا در ادامه مسیرهای broadcasting فعال و در دسترس باشند.

برای بایند کردن یک یا دو مورد ساده شما می توانید از همان AppServiceProvider استفاده نمایید ولی در مواردی که تعداد بایند ها بیشتر می شود یا به یک لاجیک پیچیده برای اجرا شدن نیاز دارید می توانید یک پرووایدر جدید با دستور زیر بسازید.

php artisan make:provider <provider name> 

بخش چهارم: The Full Picture

حال که با مفاهیم مختلفی مثل dependency injection, inversion of control, service container, service providers آشنا شده اید وقت آن رسیده که که نشان دهیم چگونه در کنار هم کار میکنند.

به کلاس Publication که در بخش های قبل با آن کار کردیم برمی گردیم. همانطور که به یاد دارید کلاس Publication به کلاس TwitterService وابستگی داشت که ما اینترفیسی را تعریف کردیم و حالا می خواهیم کلاس Publication از آن بهره بگیرد.

<?php

namespace App;

use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;

class Publication {
    protected $socialMediaService;

    public function __construct(SocialMediaServiceInterface $socialMediaService)
    {
        $this->socialMediaService = $socialMediaService;
    }

    public function publish(Post $post)
    {
        $post->publish();

        $this->socialize($post);
    }

    protected function socialize($post)
    {
        $this->socialMediaService->share($post);
    }

}

به جای وابستگی به یک کلاس خاص Publication هر کلاسی را که SocialMediaServiceInterface را پیاده کرده باشد را می پذیرد. در اینجا بدلیل اینکه SocialMediaServiceInterface را به LinkedInService بایند کردیم app()->make(SocialMediaServiceInterface::class) یک نمونه از LinkedInService را بر می گرداند.

با توجه به اینکه کلاس Publication را در کانتینر بایند نکردیم. به نظر شما اگر app()->make(Publication::class) را اجرا کنیم چه اتفاقی می افتد.

<?php

// routes/web.php

use App\Publication;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $publication = app()->make(Publication::class);

    ddd($publication);

    // App\Publication {#273 ▼
    //     #socialMediaService: App\Services\LinkedInService {#272}
    // }      
});


همانطور که میبینید لاراول با موفقیت بدون اینکه قبلا کلاس Publication را بایند کرده باشیم یک نمونه از آن را برای ما بر می گرداند. در نگاه اول به نظر می رسد یک معجزه اتفاق افتاده است اما واقیت آن است که زمانیکه لاراول به خط app()->make(Publication::class) می رسد در کانتینر به دنبال آن می گردد. زمانیکه برای آن ورودی پیدا نمی کند به سازنده ی کلاس نگاه می کند و متوجه می شود که به کلاسی که اینترفیس SocialMediaServiceInterface را پیاده کرده باشد نیاز دارد و زمانیکه ورودی مورد نیاز را در کانتینر پیدا کرد یک نمونه از آنرا ساخته و بر می گرداند و با موفقیت یک نمونه از کلاس Publication را می سازد.

مادامی که اینترفیسی ها به درستی بایند شده باشند و وابستگی هاtype-hint شده باشند لاراول به راحتی نمونه را برای شما می سازد.

<?php

// routes/web.php

use App\Publication;
use Illuminate\Support\Facades\Route;

Route::get('/', function (Publication $publication) {
    ddd($publication);

    // App\Publication {#276 ▼
    //     #socialMediaService: App\Services\LinkedInService {#275}
    // }
});


بخش پنجم: تست با کمک Service Container

استفاده از سرویس کانتینر این مزیت را به ما می دهد که به راحتی بتوانیم یک سرویس ماک شده بنویسیم و در تست ها از آن استفاده کنیم. در مثال بالا که از سرویسی مثل توییتر یا لینکدین برای اشتراک گذاری محتوا استفاده می کردیم چگونه می توانستیم فانکشنالیتی های کلاس Publication را تست کنیم در صورتیکه به سرویس بیرونی وابستگی داشت. با استفاده از سرویس کانتینر به راحتی می توانیم در تست هایمان سرویس ماک شده ی خودمان را بایند کرده و در ادامه کلاس Publication از سرویس ماک شده ای که ما تعریف کردیم استفاده خواهد کرد و تست ها بدون وابستگی به سرویس بیرونی کار خود را ادامه می دهند.

سخن پایانی

امیدوارم با خواندن مطلب بالا سرویس کانتینر و مفاهیم مرتبط با آن کمی برای شما روشن شده باشد و از این به بعد با اعتماد به نفس بیشتری وابستگی های type-hint شده را در سازنده ی کلاس استفاده می کنید.


پیروز باشید...

منبع اصلی