Laravel的服务提供者的作用是什么?

1). 当前使用的 Laravel 版本?

Laravel8.83.9

根据网上的教程,我跟着写了一遍服务提供者相关代码,我先给大家看看吧,模拟发短信功能的。
第一步:新建一个接口:

namespace App\Services;

interface SmsService
{
    public function send($phone, $content);
}

第二步:新建短信具体功能类,实现SmsService接口:

namespace App\Services;

class AliSms implements SmsService
{
    public function send($phone, $content)
    {
        return "阿里发短信平台手机号:" . $phone . " 内容:" . $content;
    }
}

这里假设是阿里的发短信平台。

第三步:新建服务提供者:

php artisan make:provider SmsServiceProvider

会在app\Providers 下面生成SmsServiceProvider:

namespace App\Providers;

use App\Services\AliSms;
use Illuminate\Support\ServiceProvider;

class SmsServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('sms', function () {
            return new AliSms();
        });
    }
}

第四步:在config\app.php providers数组中加入 App\Providers\SmsServiceProvider::class,

第五步:新建控制器:

namespace App\Http\Controllers;

use Illuminate\Support\Facades\App;
use App\Services\SmsService;

class BlackController extends Controller
{
    public function index()
    {
        echo app('sms')->send("18700000000","你好呀");
    }
}

运行之后成功打印:阿里发短信平台手机号:18700000000 内容:你好呀

代码是成功运行的,可就是不明白为什么需要这么复杂的过程,有些人会说以后如果换一个短信平台,只要改服务提供者就可以了,业务代码不需要改。但是,我觉得如果真是这样,我也可以写一个单独的短信发送公共类,在刚刚的控制器里面调:SendClass::send(“18700000000”,”你好呀”);以后要改其他平台发送短信,我也就改send方法而已,调用方也无需改。

所以不清楚服务提供者真正的用途是什么?有没有知道的举个通俗一点的场景说一说。

《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 42

USB知道吗?可以插鼠标,可以插手机,可以插键盘

2年前 评论
大张 1年前
bluememory (楼主) 2年前

优。。。雅。。。 :joy:

2年前 评论

翻译:Laravel 中的服务提供者:Service Providers 是什么以及如何使用

今天怎么都是来问 ServiceProvider 的。

ServiceProvider 是在请求周期最早的时候进行加载的,其目的就是注册可以全局调用的服务(功能),在代码运行时可以直接对其调用,不必去临时加载。

举个例子:

前提:

夏天来了,室外温度 38°,每个人走进 7-11,几乎都需要买一杯可乐,或者冰淇淋,或者冰咖啡。

当你走进 7-11 的时候直接走过去拿就行了,你也不用管它是怎么做出来的,这些在你进入 7-11 的时候就已经准备好了。

那为什么 7-11 会准备 咖啡机、饮料机、冰淇淋机 呢?它怎么不准备 自动洗鞋机 呢?

因为 咖啡机、饮料机、冰淇淋机 是顾客最频繁使用的功能,每天将耗费店员很多时间来做这些事情。所以干脆把它们以「服务」的形式来提供,把职责从店员中抽离了出来,可以省掉他们很多时间。

  1. 当你走进 7-11 (当一个请求进入应用程序)
  2. 咖啡机、饮料机、冰淇淋机就已经为你准备好了 (应用程序就已经加载了程序运行必备的服务提供者,随时可以调用)
  3. 几乎每个人都要买一杯可乐,冰淇淋、冰咖啡 (不必等待,拿来就用,你也不用管冰淇淋是怎么拉的花,因为服务提供者已经将这些功能封装好了)
  4. 想象一下如果没有 咖啡机、饮料机、冰淇淋机,需要消耗多少店员的时间,顾客需要浪费多少时间?(假如一个用户授权 Provider,在应用程序中有 100 多处都在调用它,改成每次调用都实例化一次需要浪费多少时间?)

但还有一种情况:

假设你就非要在 7-11 洗鞋,但是购买这个服务的人比较少,而且比较麻烦,店员不能亲自去做。所以干脆配置了一台自动洗鞋机,但由于平时用的人比较少,都是处于关闭状态,当有顾客需要洗鞋子时,店员需要打开自动洗鞋机把鞋子放进去。

这就是 Defer 延迟加载,因为「洗鞋」功能并不常用,但它又比较麻烦,所以也为它配置了一个「服务」,但是它不是必须和请求进入应用时和其他 Provider 加载,它是按需加载的。

<?php

namespace App\Providers;

use App\Services\SevenEleven\AutoMachine;
use Illuminate\Contracts\Support\DeferrableProvider; // Defer 加载
use Illuminate\Support\ServiceProvider;

class WashShoesServiceProvider extends ServiceProvider implements DeferrableProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(WashShoes::class, function ($app) {
            return new WashShoes();
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return [WashShoes::class];
    }
}

明白了没?

2年前 评论
MArtian (作者) 2年前
bluememory (楼主) 2年前
bluememory (楼主) 2年前
mohy 1年前
lock22 1年前

官方文档其实都有写啦,服务容器 IoC,DI 可以也看看

2年前 评论
bluememory (楼主) 2年前

服务提供者并不仅限于容器绑定,它还可以做很多事情,详见文档中 Package Development (扩展包开发)章节,这里面提到的所有的都是要在服务提供者中完成的。

正如 @MArtian 前面提到的那样,Service Provider 的执行时机较早,所以你可以在这里做一些框架启动阶段所要做的事情,比如注册事件监听、注册/替换配置、容器绑定、Babel 指令等。

容器绑定仅仅是可以在 Service Provider 中可以做的一部分,也是被常用于举例的。

现在再来细说容器绑定。

在刚刚的控制器里面调:SendClass::send (“18700000000”,” 你好呀”); 以后要改其他平台发送短信,我也就改 send 方法而已,调用方也无需改。

静态方法调用最大的好处就是不用 new ,直接就可以用,但是这样带来了最大的问题,就是不可测性,很难对静态方法进行替换测试,如果有配置有 CI/CD ,你总不希望,每次测试的时候都真实发送一条短信吧。

比如现在我们要用 AliSMS 这个类来发送短信,如果你不想每次都 new AliSMS($token) 然后再调用 send 的话,你肯定需要再封装一个 factory 方法,在这个方法里面去 new AliSMS($token)。使用的时候直接 AliSMS::factory()(耦合)。这样就能拿到一个新实例,那假如,现在这个 AliSMS 是一个第三包里面的, 你不能修改它,那你就需要单独创建一个 Sender 类或者函数来做这件事情了。

也就是说为了实现发短信的基本功能,你需要修改原类,或者再创建一个 Sender 类,来愉快的使用,从而避免每次都 new AliSMS($token)

如果使用服务提供者的容器绑定,你只需要像下面这样。

public function register(){
  $this->app->bind(AliSMS::class, function(){
     return new AliSMS('<TOKEN>');
  });

  // 还可以绑定成其他 alias,方便使用,比如 sms,
}

现在,你就可以在 Laravel 所有支持类型提示注入的时候声明一个 AliSMS 的类型即可,就像控制器方法常用的 Request 一样,从某种意义上来说,你还应该定义一个契约(接口)这才是好的实践。

当然,前面的需求,除了 bind ,还可以用 extend 和 resolving 都可以做这个事情。

其次就是相较于静态方法,这种方式的可被测试性也更高。

2年前 评论
bluememory (楼主) 2年前
bluememory (楼主) 2年前
Rache1 (作者) 2年前

:smile: 结合容器即可解锁服务提供能者新姿势,先引用上面部分内容和评论看下。

我知道,你的角度是从加载时间点这个维度说明服务提供者的优势的

我可以新建一个公共类SendClass,他里面有个send方法来发短信,send方法里面就是具体调用阿里平台接口什么的逻辑发短信。然后其他控制器就可以调SendClass::send()就可以了。如果以后不使用阿里的改使用微信的了,那我只要改send里面就行了,改为调微信平台逻辑就好了。

在服务提供者中为容器绑定单例都是基操了,前面评论也有提到。

那假如部分业务使用阿里短信,部分业务腾讯短信,或者部分业务需要本地存储,部分业务需要云存储,

实际情况花里胡哨的可能太多了

咱们看下文档中的上下文绑定, 控制器中完全一样的用法就行

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

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

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

另外,服务提供者为第三方服务带来了很大便利,比如何时注册路由,何时刷新缓存中的数据。

来看下 RouteServiceProvider

    public function register()
    {
        $this->booted(function () {
            $this->setRootControllerNamespace();

            if ($this->routesAreCached()) {
                $this->loadCachedRoutes();
            } else {
                $this->loadRoutes();

                $this->app->booted(function () {
                    $this->app['router']->getRoutes()->refreshNameLookups();
                    $this->app['router']->getRoutes()->refreshActionLookups();
                });
            }
        });
    }

千言万语难汇于一句话,这样的帖子子评论太难了,但是看到大家写这么多,我也忍不住。。

2年前 评论
yyy123456 1年前

这实际就是个数组缓存吧。当调用字符串 key ‘sms’的时候,会调用闭包返回一个sms的实例。容器是个单例,这么做的原因我想是因为让程序不用每次请求的时候都生成一个sms实例。从而直接去容器里取这个服务就行了

1年前 评论

:joy:学习了很多, 但是我一直找不到实战中的一些使用 除了简单的单例绑定以外, 我觉得如果单纯写业务代码用的真的很少, 但是如果是要搭项目基本架构的话,比如上传服务,或许可以用到容器,因为会有很多不同的第三方上传

1年前 评论
浪里小白龙 1年前

我觉得这么理解可能更好理解一点,如果你定义一个生成订单的服务,比如:createOrder,恰好生成支付宝和微信的订单规则是不一样的,对于调用的人只需要调用createOrder这个服务就可以了,你只需要在createOrder中把两种可能处理好就可以

1年前 评论
bluememory (楼主) 1年前
Diudiuuuu (作者) 1年前
bluememory (楼主) 1年前
Diudiuuuu (作者) 1年前

个人拙见:有一个作用可能是方便单测

拿楼主 BlackController 的 index 方法举例,如果你用个公共类 SendClass ,导致 index 方法的测试要依赖 SendClass,而如果通过容器的方式注入,可以在单测时模拟一个模拟实例,只是简单的模拟输出,而不会真正的去发送短信。

当然,你说我单测也可以更改 SendClass 的行为,但需要更改你的业务逻辑代码。而容器注册也不一定非得用 ServiceProvider 的方式。只是 Laravel 提供给你的一种组织代码的方式而已,用不用在于自己。

1年前 评论

file

基本只用服务容器, 根本原因:

  1. 懒着 new
  2. 懒着自己实现单例

回到 服务提供者,假设没有这个功能, 扩展包还怎么注册。
核心方法就是
register: 绑定 服务容器
boot: 给服务初始化用的

因为用得多,也的确是核心,就产生个名词呗。 :sweat_smile:


至于上面的代码创建个服务提供者,应该是太闲了。

1年前 评论
陈先生

有没有一种可能,我是说可能啊
你发送短信需要依赖一个用户服务,用户服务需要依赖一个隐私服务,而你的短信需要依赖一个网关服务,而你的网关服务有需要依赖一个调度服务,然而 这些服务都是一一唯一对应,如果你要换一个短信,就要同时换掉,用户服务,隐私服务,网关服务,调度服务,假如你用 ServiceProvider 你只需要更换一下各个服务真实使用的类即可。而不是 你一个一个的去换,递归一样的去更换 new 的对象。

多用用才能明白他是做什么的。问出来的都是别人的结论,不一定百分百适合你。你能用20次 ServiceProvider 你也会有你的理解的。

1年前 评论
bluememory (楼主) 1年前
bluememory (楼主) 1年前
陈先生 (作者) 1年前
大张 1年前

用过三方组件包嘛,以 intervention/image (压缩图片) 为例,它会在文档里说
config/app.php$providers 数组中添加 Intervention\Image\ImageServiceProvider::class
$aliases 数组中添加 'Image' => Intervention\Image\Facades\Image::class
然后 use Image; 就可以使用了。
再看 ImageServiceProvider::classregister()

$app->singleton('image', function ($app) {
    return new ImageManager($this->getImageConfig($app));
});
$app->alias('image', 'Intervention\Image\ImageManager');

不使用服务提供者也行,use Intervention\Image\ImageManager; 也可以使用,但是不能使用门面 facade
实质就是容器初始化时向其中提供服务,至于为什么要这样做?那就需要了解为什么要使用容器了。

1年前 评论
bluememory (楼主) 1年前
php_yt (作者) 1年前
秦晓武

题主是觉得例举的5步太麻烦了?直接一个静态类发完短信就行了,扩展性不差,代码量也少。
我的理解是这样的:

  1. 面向功能实现,那第5步就行了,甚至一个send函数就OK。
  2. 考虑到别的地方也要发,就需要第3步,创建一个静态类。
  3. 考虑到可能有多种发送途径,第1,2步的接口设计还是不错的。

对比下,比服务模式也就多了个配置(第4步),和一种不太习惯的app()调用方式

刚开始看这东西,不理解,自己模仿了下也觉得不得劲。写多了后发现:

  1. 功能是为了解决用户需求,而写法,是为了解决程序员需求
  2. 没必要生搬硬套,看得懂人家写的,自己需要的时候会用,足矣
  3. 这种可以算作是面向服务开发,解决的是更庞大的功能治理

自己写麻烦,但系统自带的authroute挺好用的

例子:

  1. 验证输入合法性,写一句正则判断就搞定一了
  2. 多个表单需要同个正则,就封装一个函数
  3. 多种正则集合管理,封装一个类
  4. 不同国家的电话格式不同,增加接口和扩展
  5. 多个项目需要类似功能,打包成基础类库
  6. 需要一个完善的开箱即用表单,requestService + validateService + databaseService + resoponseService + otherService = formService

我觉得,服务及服务提供者,让协作更容易了

希望能解答你的疑惑

1年前 评论

而且你文中的写法其实也不推荐,建议这样:

class SmsServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(SmsService::class, function () {
            return new AliSms();
        });
    }
}

然后在调用的时候,直接通过容器注入,而不是手动去容器中取:

class BlackController extends Controller
{
    public function index(SmsService $sms)
    {
        echo $sms->send("18700000000","你好呀");
    }
}

现在,如果要接入新的短信服务,那么实现 SmsService 接口即可,甚至可以直接把 Provider 中改成配置文件,就完全符合开闭原则了;而你如果用工具类来做,是不是还要改代码?改代码是不是就意味着有可能出bug?

1年前 评论
bluememory (楼主) 1年前
大张 (作者) 1年前
bluememory (楼主) 1年前
大张 (作者) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!