Laravel 的服务容器是框架中最重要的部分之一,但它却很少受到许多开发人员的关注。在面试了大量的候选人后,我意识到这种无知背后有两个主要原因。

  • 他们发现依赖注入的想法很难理解。更不用说 IoC 和 IoC 容器的想法了。
  • 他们不知道他们是否会使用容器。

依赖注入和 IoC

依赖注入的过度简化定义是将类依赖作为参数传递给其方法之一(通常是构造函数)的过程。

看看下面这段没有依赖注入的代码:

<?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);
    }

}

对于这个简单的演示,您可以使用文件/中的路由回调routes/web.php。

<?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!
});

这是基本级别的依赖注入。将依赖注入应用到类会导致控制反转。以前,依赖类iePublication控制着依赖类ie 的实例化,TwitterService而后来,控制权已移交给框架。

IoC 容器

IoC 容器可以使依赖注入的过程更加高效。它是一个简单的类,能够在需要时保存和提供数据片段。一个简化的 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
});

您可以通过传递一个回调函数将类绑定到此容器,该回调函数将类的实例作为第二个参数返回。


<?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班级需要一个 API 密钥来进行身份验证。在这种情况下,您可以执行以下操作:


<?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: "very-secret-api-key"
    // }
});

将一条数据绑定到容器后,您可以在必要时请求它。这样,您只需使用new关键字一次。

我并不是说使用new不好。但是每次使用 时new,都必须小心将正确的依赖项传递给类。然而,对于 IoC 容器,容器负责注入依赖项。

IoC 容器可以使您的代码更加灵活。考虑一种情况,您希望将TwitterService类与其他类(如LinkedInService类)交换。

该系统的当前实现不是很适合。要替换TwitterService该类,您必须创建一个新类,将其绑定到容器,并替换对前一个类的所有引用。

它不必是那样的。您可以通过使用接口来简化此过程。首先创建一个新的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!');
    }
}

不是将具体类绑定到容器,而是绑定接口。在回调中,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: "very-secret-api-key"
    // }
});

到目前为止,代码和以前一样工作。当您想使用 LinkedIn 而不是 Twitter 时,乐趣就开始了。界面到位后,您可以通过两个简单的步骤完成此操作。

创建一个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!');
    }
}

更新对bind()方法的调用以改为返回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}
});

现在你得到了这个LinkedInService类的一个实例。这种方法的美妙之处在于其他地方的所有代码都保持不变。您只需更新bind()方法调用。只要一个类实现了它,SocialMediaServiceInterface它就可以作为一个有效的社交媒体服务绑定到容器。

服务容器和服务提供者

Laravel 自带了一个更强大的 IoC 容器,称为服务容器。您可以使用服务容器重写上一节中的示例,如下所示:

<?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}
});

在每个 Laravel 应用程序中,app实例都是容器。助手返回容器的app()一个实例。

就像你的自定义容器一样,Laravel 服务容器有一个bind()和一个make()用于绑定服务和检索服务的方法。

还有一种方法叫做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)表示,这两个实例彼此不同。如果你将这个类绑定为一个单例,你会看到一些不同的东西。

<?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()、singleton()和make()方法,接下来您必须了解的是在哪里放置这些方法调用。您当然不能将它们放入您的控制器或模型中。

放置绑定的正确位置是服务提供商。服务提供者是驻留在app/Providers目录中的类。这些是框架的基石,负责引导大多数框架服务。

默认情况下,每个新的 Laravel 项目都带有五个服务提供者类。其中,AppServiceProvider类默认为空,有两种方法。它们是register()和boot()。

该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()方法用于引导注册服务所需的逻辑。一个很好的例子是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');
    }
}

如您所见,它调用该Broadcast::routes()方法并需要该routes/channels.php文件,从而使广播路由在该过程中处于活动状态。

对于本例中的一两个简单绑定,您可以AppServiceProvider使用php artisan make:provider

点赞(0)

评论列表 共有 0 评论

暂无评论

微信服务号

微信客服

淘宝店铺

support@elephdev.com

发表
评论
Go
顶部