Laravel核心概念梳理

目录
  1. 1. 依赖注入篇
    1. 1.1. 问题引出
    2. 1.2. 建立约定
    3. 1.3. 更进一步
    4. 1.4. 太像 Java?
  2. 2. 服务容器篇
  3. 3. 接口即契约篇
    1. 3.1. 强类型与鸭子类型
    2. 3.2. 一个契约示例
    3. 3.3. 接口&团队开发
  4. 4. 服务提供者篇
    1. 4.1. 作为引导者
      1. 4.1.1. 延迟加载的服务提供者
    2. 4.2. 作为管理者
    3. 4.3. 启动提供者
    4. 4.4. 框架核心

依赖注入篇

问题引出

整个 Laravel 框架的基石是一个功能强大的 IoC 容器(控制反转容器),如果你想真正从底层理解 Laravel 框架,就必须好好掌握它。不过,也不要被这个名头吓住,要知道 IoC 容器只不过是一种用于方便我们实现「依赖注入」这种软件设计模式的工具。而且要实现依赖注入并不一定非要通过 IoC 容器,只是使用 IoC 容器会更容易一点儿。

首先,来看看我们为何要使用依赖注入,或者说它能为我们的软件开发带来什么好处。考虑下列代码中的类和方法:

1
2
3
4
5
6
7
8
class UserController extends BaseController
{
public function getIndex()
{
$users = User::all();
return View::make('users.index', compact('users'));
}
}

这段代码看起来很简洁,但是不与数据库打交道的话,我们将无法测试这段代码。也就是说,Eloquent ORM 和该控制器有着紧耦合关系。如果不使用 Eloquent ORM,不连接到实际数据库,我们就没办法运行或者测试这段代码。同时,这段代码也违背了「关注点分离」这个软件设计原则。简单来讲:控制器知道的太多了。控制器不需要去了解数据是从哪儿来的,只要知道如何访问就行。控制器也不需要知道数据在 MySQL 中是否有效,只需要知道它目前是可用的。

关注点分离:每一个类都应该是单一职责的,并且这个职责应该完全被这个类封装。

所以,如果可以完全解耦 Web 控制器层和数据访问层解耦,将会给我们带来诸多便利:这会使得迁移数据存储实现更容易;也会使得代码测试更容易。「Web控制器」的职责就是真实应用的传输层:仅负责收集用户请求数据,然后将其传递给处理方。

假设你有一个类似于监控器的应用程序,该应用有很多线缆接口,你可以通过这些接口来访问监控器的功能,接口包括 HDMI,VGA,DVI 等。把互联网想象成另一个插进应用的线缆接口,显示器的大部分功能都是与线缆接口无关的、互相独立的。线缆接口只是一种传输机制,就像 HTTP 只是你程序的一种传输机制一样。所以,我们不想把传输机制(控制器)和业务逻辑混在一起。这样做的好处是很多其他的传输层比如 API 接口、移动 App 等都可以访问我们的业务逻辑。

因此,以后开发代码就别再将控制器和 Eloquent ORM 耦合在一起了,咱们来注入一个仓库类吧。

建立约定

首先,我们来定义一个接口,然后实现该接口。

1
2
3
4
5
6
7
8
9
10
11
12
interface UserRepositoryInterface
{
public function all(): array;
}

class DbUserRepository implements UserRepositoryInterface
{
public function all(): array
{
return User::all()->toArray();
}
}

然后,我们将该接口的实现注入到我们的控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
class UserController extends BaseController
{
public function __construct(UserRepositoryInterface $users)
{
$this->users = $users;
}

public function getIndex()
{
$users=$this->users->all();
return View::make('users.index', compact('users'));
}
}

现在,我们的控制器就完全不知道数据存储在哪了。在这里,无知是福!我们的数据可能来自 MySQL、MongoDB 或者 Redis,我们的控制器不知道也不需要知道到底用的是什么数据库,以及它们是如何存储数据的,在具体实现上有什么区别。仅仅做出了这么小小的改变,我们就可以独立于数据层来测试 Web 层了,将来如果需要的话,切换存储实现也会很容易,两者相互独立,只要调用方法名不改,我们的控制器代码不用做任何改动。

严守边界:始终牢记保持明确的责任边界,控制器和路由是作为 HTTP 和应用程序之间的中介者来提供服务的(用户浏览应用的时候,路由/控制器作为中介将其引导到对应的服务)。当编写大型应用程序时,不要将你的领域逻辑混杂在控制器或路由中。

为了巩固你对这一理念的理解,我们来写一个测试案例。首先,我们要通过 Mockery 动态模拟一个仓库类实例,并将其绑定到应用的 IoC 容器里。然后,发起一个请求,通过断言判定控制器是否正确地调用了这个仓库类:

1
2
3
4
5
6
7
8
9
10
public function testUserTest()
{
$repository = \Mockery::mock(UserRepositoryInterface::class);
$repository->shouldReceive('all')->once()->andReturn(['学院君']);
$this->instance(UserRepositoryInterface::class, $repository);
$response = $this->get('/users');

$response->assertStatus(200);
$response->assertViewHas('users', ['学院君']);
}

更进一步

让我们考虑另一个例子来巩固理解。当付费会员订阅的某项服务周期快结束了,可能需要去提醒用户该续费了。我们会定义两个接口,或者叫契约(这些契约使我们在更改实际实现时更加灵活),一个是支付接口,一个是通知接口:

1
2
3
4
5
6
7
8
9
interface BillerInterface 
{
public function bill(array $user, $amount);
}

interface BillingNotifierInterface
{
public function notify(array $user, $amount);
}

接下来我们要写一个 BillerInterface 接口的实现:

1
2
3
4
5
6
7
8
9
10
11
12
class StripeBiller implements BillerInterface
{
public function __construct(BillingNotifierInterface $notifier)
{
$this->notifier = $notifier;
}
public function bill(array $user, $amount)
{
// Bill the user via Stripe...
$this->notifier->notify($user, $amount);
}
}

通过将责任划分到不同类中,我们现在可以很容易将不同的通知实现类注入到账单类里面。比如,我们可以注入一个 SmsNotifier 或者 EmailNotifier。账单类只需遵守了自己的契约即可(实现了账单接口方法),不需要考虑如何实现通知功能。只要是遵守账单通知契约(接口)的类,账单类都可以用。这不仅让我们的开发维护更加灵活,而且还可以通过模拟BillingNotifierInterface 实现类来进行账单类的隔离测试,就像我们在上一个测试用例里做的那样。

面向接口开发:编写接口看上去好像要多写一些代码,但是磨刀不误砍柴工,对于大型项目而言实际上反而能提升你的开发效率,这就是软件设计领域经常说的面向接口开发,而不是面向对象开发。从测试角度来说,你不用实现任何接口,就能通过 Mockery 库模拟接口实现实例,进而测试整个后端逻辑!

前面说了这么多,回到我们的主题,我们要如何做依赖注入呢?很简单:

1
$biller = new StripeBiller(new SmsNotifier);

这就是一个依赖注入。账单类 StripeBiller 不用考虑如何通知用户,我们直接传递给它一个通知实现类 SmsNotifier 的实例。从代码角度来说,这可能只是个微小的变动,但这种设计模式的引入,绝对会使你的整个应用架构焕然一新:因为明确指定了类的职责边界,实现了不同层和服务之间的解耦,你的代码变得更加容易维护;此外,从面向接口编程的角度来看,代码变得更加容易测试,你只需通过模拟注入依赖即可,不同类之间的测试完全可以隔离开来。

那么 IoC 容器呢?难道依赖注入不需要 IoC 容器了么?当然不需要!在接下来的章节里面你会了解到,IoC 容器使得依赖注入更易于管理,但是容器本身不是依赖注入所必须的。只要遵循本章提出的原则,你可以在任何项目里面实现依赖注入,而不必管该项目是否提供了容器。

太像 Java?

在 PHP 中使用接口的一个常见批评就是代码看上去太像 Java —— 意思是让代码显得太冗长,你必须定义接口然后实现它,要多按好多次键盘。

对于小而简单的应用来说,以上说法也对,在这种规模的应用中,接口通常是不必要的。将代码耦合到那些你认为不会改变的地方也是可以的,比如都放在控制器方法中。在你确定以后不会发生改变的地方就没有必要使用接口了,比如一次性的任务,或者一些原型或演示项目,毕竟这种灵活性会带来更多的代码量。

架构师可能会说「不会改变的地方是不存在的」。不过话说回来,有时候的确不会改。而且小型的应用也不需要架构师,架构师们都是为大型应用服务的。

在大型应用中,接口是很有帮助的。和提升的代码灵活性、可测试性相比,多敲几下键盘花费的时间就显得微不足道了。当你在不同的接口实现类之间切换如飞的时候,你的经理一定会被你的神速惊到。此外,你也能够写出更能适应变化的代码。

总而言之,记住本书提倡的「简单」架构。如果你在写小型应用的时候不想遵守接口原则,回退到原始模式,别觉得不好意思,那没什么不对。不管如何,我都希望你们牢记「Code Happy」,快乐撸码,这应该是我们的初心。如果你真的不喜欢写接口,那就怎么舒服怎么来吧,做人嘛,开心最重要,不过还是希望你闲暇的时候可以好好评估下这件事。

服务容器篇

声明:原书中本章叫做 IoC 容器,在 Laravel 5 中,IoC 容器改名为服务容器,所以,在后续章节,IoC 容器和服务容器指代同一个东西。

我们已经了解了依赖注入及其使用,接下来咱们一起来探索控制反转容器(IoC)。我们前面已经说过,通过 IoC 容器可以帮助我们更方便地管理类依赖,而且 Laravel 提供了一个功能强大的 IoC 容器。这个 IoC 容器在 Laravel 中被称作服务容器,是整个 Laravel 框架最核心的部分,在它的调度下,框架各个组件可以很好的组合在一起工作。实际上,Laravel 的Application 类就是一个继承自 Container 的容器类,它就是整个 Laravel 应用的服务容器。

IoC 容器:控制反转容器让依赖注入更方便,它负责在整个应用生命周期内解析和注入那些定义在容器中的类和接口。
学院君注:在 Laravel 中经常提及的服务容器就是这里的 IoC 容器,你可以把服务容器看作 IoC 容器在 Laravel 框架中的方言别名,两者等价。

在 Laravel 应用中,可以通过 App 门面来访问服务容器,还可以通过辅助函数 app() 来访问,如果是在服务提供者(可以理解为一个专门用于绑定接口与实现到服务容器的地方)中,则一般通过 $this->app 来访问容器。服务容器提供了很多方法,不过我们会从最基础的开始。下面我们继续使用上一章创建的 BillerInterfaceBillingNotifierInterface 为例,并且假设在应用中使用 Stripe 进行支付操作。我们可以将支付接口的 Stripe 实现类绑定到容器里,这项工作可以在服务提供者的 register() 方法中完成(在本系列文档中,不特别说明,我们使用的都是 AppServiceProvider),就像这样:

1
2
3
4
5
6
public function register()
{
$this->app->bind(BillerInterface::class, function ($app) {
return new StripeBiller($app->make(BillingNotifierInterface::class));
});
}

注意在我们在初始化 BillingInterface 实现类时,额外需要一个BillingNotifierInterface 的实现,为此,我们需要编写一个针对该接口的实现类 EmailBillingNotifier,具体实现先留空:

1
2
3
4
5
6
7
8
9
10
namespace App\Services;
use App\Contracts\BillingNotifierInterface;

class EmailBillingNotifier implements BillingNotifierInterface
{
public function notify(array $user, $amount)
{
// TODO: Implement notify() method.
}
}

然后在服务提供者中将其绑定到所实现的接口:

1
2
3
$this->app->bind(BillingNotifierInterface::class, function ($app) {
return new EmailBillingNotifier();
});

注:注意到我们在定义绑定关系的时候使用的是匿名函数,这样做的好处是用到该依赖时才会实例化,从而提升了应用的性能。

如你所见,这个服务容器就是个用来注册各种接口与实现绑定的地方。一旦一个类在容器里注册了以后,就可以很容易地在应用的任何位置解析并调用它。我们甚至还可以在一个绑定函数内解析其它的绑定关系,就像我们上面做的那样。

一旦我们使用了服务容器,切换接口的实现就是一行代码的事儿。举个例子,考虑以下代码:

1
2
3
4
5
6
class UserController extends BaseController{
public function __construct(BillerInterface $biller)
{
$this->biller = $biller;
}
}

当这个控制器被服务容器实例化的时候,引用 EmailBillingNotifierStripeBiller 会被注入到这个控制器中。现在,如果我们想要换一种通知的实现方式,比如通过短信发送通知(仿照 EmailBillingNotifier 新建一个 SmsBillingNotifier 类),只需在服务提供者中修改绑定到通知接口的实现类即可,其它任何地方都不用修改:

1
2
3
$this->app->bind(BillingNotifierInterface::class, function ($app) {
return new SmsBillingNotifier();
});

这样,不管在应用的哪个地方注入/解析账单通知接口,都会得到 SmsBillingNotifier 类的实例。利用这种架构设计,我们的应用可以在各种服务的不同实现方式之间快速切换。

只改一行代码就能切换接口实现,真的是很强大。例如,如果我们想把短信服务的提供商从原来的联通替换为移动,可以开发一个新的基于移动接口实现的短信服务类,然后切换绑定语句。如果移动服务挂了,只需修改一行代码就可以快速切换回原来的短信提供商,这正是服务容器的强大之处。

有时候,你可能想在整个应用生命周期中只实例化某类一次,类似单例模式,可以通过 singleton 方法来注册接口与实现类:

1
2
3
$this->app->singleton(BillingNotifierInterface::class, function ($app) {
return new SmsBillingNotifier();
});

现在,只要服务容器解析过这个账单通知对象实例一次,在剩余的请求生命周期中都会使用同一个实例。

服务容器还提供了和 singleton 方法很类似的 instance 方法,区别是 instance 方法可以绑定一个已经存在的对象实例。然后容器每次解析的时候都会返回这个对象实例。

1
2
$notifier = new SmsBillingNotifier;
$this->app->instance(BillingNotifierInterface::class, $notifier);

现在我们已经熟悉了服务容器的基本使用,接下来,让我们深入挖掘它更加强大的功能:依靠反射来动态解析类。

单独使用容器:即使你的项目不是基于 Laravel 框架的,依然可以使用Laravel 的服务容器,只要通过 Composer 安装 illuminate/container 就好了。

接口即契约篇

强类型与鸭子类型

在之前的章节里,我们讨论了依赖注入的基础知识:什么是依赖注入;如何实现依赖注入;依赖注入有什么好处。之前的例子中也模拟了将接口注入到类里面的过程。在我们继续学习后续内容之前,有必要深入讨论一下接口,而这正是很多 PHP 开发者所不熟悉的。

在我成为 PHP 程序员之前,我是写 .NET 的。你觉得我是喜欢原生代码还是什么?在 .NET 里到处都是接口,而且很多接口都定义在 .NET 框架核心中了,对此有充分理由:很多 .NET 语言比如 C# 和 VB.NET 都是强类型的。在强类型语言中,当你给一个函数传参时,必须指定变量类型。例如,在 C# 中我们会这么做:

1
2
3
4
public int BillUser(User user)
{
this.biller.bill(user.GetId(), this.amount)
}

注意,在这里,我们不仅要定义传进去的参数是什么类型的,还要定义这个方法的返回值是什么类型的。C# 鼓励类型安全。除了指定的 User 对象之外,它不允许我们传递其他类型的对象到 BillUser 方法中。

然而 PHP 是一种鸭子类型语言。所谓鸭子类型语言,说的是一个对象的可用方法取决于其使用方式,而非这个对象继承自谁,或者实现了什么接口。我们先来看个例子:

1
2
3
4
public function billUser($user)
{
$this->biller->bill($user->getId(), $this->amount);
}

在 PHP 中,我们不必显式告诉一个方法需要什么类型的参数。实际上,我们可以传递任何类型的对象到 billUser 方法,只要这个对象提供了 getId 方法。这里有个关于鸭子类型的解释:如果一个东西看起来像鸭子,叫起来也像鸭子,那它就是鸭子。换言之,在本例中,如果一个对象看上去像 User,方法响应也像 User,那它就是个 User 对象。

学院君注:套用《JavaScript权威指南》对鸭子类型的解释,在 PHP 中,如果一个对象可以像鸭子一样走路、游泳并且嘎嘎叫,就认为这个对象是鸭子对象,哪怕它不是从鸭子类继承而来。换句话说,PHP 是弱类型语言,对象类型在运行时动态判断。

不过,PHP 到底有没有任何强类型功能呢?当然有!PHP 混合了强类型和鸭子类型(弱类型)结构。为了说明这点,我们来重写一下 billUser 方法:

1
2
3
4
public function billUser(User $user)
{
$this->biller->bill($user->getId(), $amount);
}

给方法签名加上了 User 类型约束后,我们现在可以确保所有传入billUser 方法的对象,要么是 User 类的实例,要么是一个继承自 User 类的对象实例。

强类型和弱类型各有优劣。在强类型语言中,编译器通常能提供编译时错误检查的功能,这个功能在提高代码质量方面非常有用,可以避免开发人员将危险代码交付到线上,此外,方法的输入和输出也更加明确。

与此同时,强类型的特性也使得程序僵化。举个例子,在 Eloquent ORM 中,类似 whereEmailOrName 这样的动态方法就不可能在 C# 之类的强类型语言里实现。我们这里不讨论强类型和弱类型哪种编程范式更好,而是要记住它们各自的优劣之处。在 PHP 里面,不管使用强类型还是弱类型,都没问题,没犯什么错误。错误的是不假思索,不区分具体适用场景和问题,为了使用某种类型而使用。

一个契约示例

接口如同契约。接口并不包含任何代码实现,只是定义了一个实现该接口的对象必须实现的一系列方法。如果一个对象实现了一个接口,那么我们就能保证这个接口所定义的一系列方法都能在这个对象上调用。由于有接口契约保证特定方法的实现,通过多态也能使类型安全的语言变得更灵活。

关于多态:多态含义很广,从本质上说,是一个实体拥有多种形式。在本书中,我们讲多态说的是一个接口有多钟实现方式。例如,UserRepositoryInterface 可以有 MySQL 和 Redis 两种实现,并且每一种实现都是 UserRepositoryInterface 的一个实例。

为了说明接口在强类型语言中的灵活性,我们们来写一个简单的酒店客房预订代码。考虑以下接口:

1
2
3
4
5
interface ProviderInterface
{
public function getLowestPrice($location);
public function book($location);
}

当用户预订房间时,我们需要将此事记录在系统里。所以在 User 类里添加如下方法:

1
2
3
4
5
6
7
8
class User
{
public function bookLocation(ProviderInterface $provider, $location)
{
$amountCharged = $provider->book($location);
$this->logBookedLocation($location, $amountCharged);
}
}

由于我们对 $provider 做了类型约束,在 User 类的 bookLocation 方法中,就可以放心大胆的认为 $provider 实例上的 book 方法是可以调用的。这给我们复用 bookLocation 方法带来了灵活性,完全不必关心用户倾向哪家酒店提供商。最后,我们编写一些代码来体验下这种灵活性:

1
2
3
4
5
6
7
8
$location = '希尔顿, 达拉斯';

$cheapestProvider = $this->findCheapest($location, array(
new PricelineProvider,
new OrbitzProvider,
));

$user->bookLocation($cheapestProvider, $location);

太棒了!不管哪家酒店是最便宜的,我们都能够将它传入 User 对象来预订房间了。由于 User 对象只需要有一个遵从 ProviderInterface 契约的对象实例就可以了,所以未来如果有新的酒店供应商,我们的代码也可以很好的工作。

忘掉细节:记住,接口实际上并不做任何事情。它只是简单的定义了实现类必须拥有的一系列方法。

接口&团队开发

当你的团队在构建大型应用时,不同的功能模块往往有着不同的开发进度。例如,一个开发人员在开发数据层,另一个开发人员在做前端和控制器层。前端开发者想要测试他的控制器,但是后端开发进度比较慢,无法联调。如果这两个开发者能以接口或契约的方式达成协议,然后后端开发的所有类都遵循这种协议,就像下面这段代码:

1
2
3
4
interface OrderRepositoryInterface 
{
public function getMostRecent(User $user);
}

一旦建立了契约,就算契约还没有真正实现,前端开发者也可以测试他的控制器了!这样一来,应用中的不同组件就可以按不同的速度开发,同时仍然允许编写适当的单元测试。此外,这种方式还可以使组件内部的改动不会影响到其它不相关的组件。要始终牢记「无知是福」。我们不想让类知道依赖是如何工作的,只需要知道它们能做什么。所以,先定义好契约,再来写控制器:

1
2
3
4
5
6
7
8
9
10
11
class OrderController {
public function __construct(OrderRepositoryInterface $orders)
{
$this->orders = $orders;
}
public function getRecent()
{
$recent = $this->orders->getMostRecent(Auth::user());
return View::make('orders.recent', compact('recent'));
}
}

前端开发者甚至可以为这接口写个「假」实现,然后这个应用的视图就可以用假数据渲染了:

1
2
3
4
5
6
7
class DummyOrderRepository implements OrderRepositoryInterface 
{
public function getMostRecent(User $user)
{
return array('Order 1', 'Order 2', 'Order 3');
}
}

编写好假实现之后,就可以在服务容器里将其绑定到契约上,然后在整个应用中都可以调用它了:

1
2
3
$this->app->bind(OrderRepositoryInterface::class, function ($app) {
return new DummyOrderRepository();
});

接下来,如果后台开发者写完了真正的实现代码,如RedisOrderRepository。服务容器中的绑定可以轻松切换到新的实现,整个应用将会使用开始从 Redis 读取出来的订单数据。

接口即纲领:接口有助于开发应用所提供的、已定义好的功能「框架」。 在组件的设计阶段,团队里使用接口进行讨论是很方便的,例如,定义一个 BillingNotifierInterface 接口,然后讨论它提供哪些方法。在编写任何实现代码前,最好先通过接口讨论达成一致,这是构建一套好 API 的必要前提!

服务提供者篇

作为引导者

Laravel 服务提供者主要用来进行注册服务容器绑定(即注册接口及其实现类的绑定)。事实上,Laravel 有好几十个服务提供者,用于管理框架核心组件的容器绑定。几乎框架里每一个组件的容器绑定都是靠服务提供者来完成的。你可以在 config/app.php 这个配置文件里查看项目目前有哪些服务提供者(从 Laravel 5.5 开始,Laravel 提供了包自动发现功能,所以这里也不一定全了)。

一个服务提供者必须至少有一个 register 方法。你可以在这个方法里将类绑定到容器,就像我们前面做的那样。当一个请求进入应用,框架启动时,所有罗列在配置文件里的服务提供者的 register 方法就会被调用。这在应用请求生命周期很早的阶段就会发生,所以在我们编写业务逻辑代码时,所有的服务都已经准备好了。

register Vs. boot 方法:永远不要在 register 方法里面使用任何服务。该方法只是用来将对象绑定到服务容器的地方。所有关于绑定类的解析、交互都要在 boot 方法(服务提供者的另一个方法)里进行。

一些通过 Composer 安装的第三方扩展包也会有服务提供者。这些第三方扩展包的安装说明里一般都会告诉你要在配置文件 config/app.phpproviders 数组里注册其提供的服务提供者(如果支持包自动发现,则不必这么做)。只有注册了对应的服务提供者,才能使用扩展包提供的服务。

包提供者:不是所有的第三方扩展包都需要服务提供者。实际上,任何扩展包都不需要服务提供者,因为服务提供者只是启动服务组件并让它们可以立即使用,它们只是一个用来组织启动代码和容器绑定的地方。

延迟加载的服务提供者

不是每一个罗列在配置文件 config/app.phpproviders 数组里的服务提供者在每次请求时都需要被实例化。这会对性能有影响,尤其是服务提供者注册的服务在这个请求中根本用不到的情况下。例如,QueueServiceProvider 注册的服务就不是每次请求都用得到,只有在请求用到队列时才会用到。

在 Laravel 4 中,延迟服务提供者加载是通过存放在 app/storage/meta 目录下的「服务清单」来实现的,清单中罗列了应用的所有服务提供者及其注册到容器中的名称。当需要获取 queue 容器绑定时,就会实例化并运行 QueueServiceProvider

在 Laravel 5 中,我们通过一种新的方式来实现延迟加载服务提供者,在需要延迟加载的服务提供者中将属性 $defer 设置为 true,并重写 providers 方法即可,在这个方法中,我们会以数组方式返回该服务提供者注册的服务容器绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

namespace App\Providers;

use Riak\Connection;
use Illuminate\Support\ServiceProvider;

class RiakServiceProvider extends ServiceProvider{
/**
* 服务提供者加是否延迟加载.
*
* @var bool
*/
protected $defer = true;

/**
* 注册服务提供者
*
* @return void
*/
public function register()
{
$this->app->singleton(Connection::class, function ($app) {
return new Connection($app['config']['riak']);
});
}

/**
* 获取由提供者提供的服务.
*
* @return array
*/
public function provides()
{
return [Connection::class];
}

}

作为管理者

构建一个架构良好的 Laravel 应用的关键就是学习使用服务提供者作为管理工具。

我们先来看个例子吧。也许我们的应用正在使用 Pusher 通过 WebSocket 推送消息给客户端。为了将我们的应用和 Pusher 解耦,最好创建一个新的 EventPusherInterface 接口和对应的实现类 PusherEventPusher,这样随着需求变化或应用增长,我们就可以随时轻松切换 WebSocket 提供商:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface EventPusherInterface
{
public function push($message, array $data = array());
}

<?php
namespace App\Services;

use App\Contracts\EventPusherInterface;
use App\Contracts\PusherSdkInterface;

class PusherEventPusher implements EventPusherInterface
{
public function __construct(PusherSdkInterface $pusher)
{
$this->pusher = $pusher;
}

public function push($message, array $data = array())
{
// 通过 Pusher SDK 推送消息
}
}

接下来,我们创建一个服务提供者 EventPusherServiceProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Providers;

use App\Contracts\EventPusherInterface;
use App\Contracts\PusherSdkInterface;
use App\Services\PusherEventPusher;
use Illuminate\Support\ServiceProvider;
use Pusher\Pusher;

class EventPusherServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(PusherSdkInterface::class, function () {
return new Pusher('app-key', 'secret-key', 'app-id');
});

$this->app->singleton(EventPusherInterface::class, PusherEventPusher::class);
}
}

很好!现在我们对事件推送进行了清晰的抽象,同时也有了一个很方便的地方注册和绑定其他相关对象到容器里。最后,只需要将 EventPusherServiceProvider 注册到 config/app.php 配置文件的providers 数组就可以了。现在我们就可以将 EventPusherInterface 注入到应用代码里的任何控制器或类中。

在绑定接口实现类时使用 bind 还是 singleton 方法可以这样来考虑:如果在一次请求生命周期中该类只需要有一个实例,就使用 singleton;否则就使用 bind

我们在容器章节里面已经提到过,继承自 Illuminate\Support\ServiceProvider 的服务提供者都有一个 $app 属性,该属性是一个继承自 Container 类的完整 Illuminate\Foundation\Application 实例。因此,我们可以通过这个 $app 属性调用服务容器中的所有方法,如果你更喜欢用 App 门面,也可以这么做实现同样的功能:

1
App::singleton(EventPusherInterface::class, PusherEventPusher::class);

当然,服务提供者的功能不仅仅局限于注册特定类型的服务。我们还可以使用它们注册云存储服务、数据库访问服务、自定义的视图引擎如 Twig 等等。服务提供者只是应用程序的引导和管理工具,没什么其他的。

所以,尽情的去创建你自己的服务提供者吧。并不是非要等到发布个什么扩展包才需要服务提供者,对 Laravel 应用而言,它们只是非常好的代码管理工具而已。你要学会善用它们去引导和管理应用中的各个组件,以便组件之间可以相互组合,协同工作。

启动提供者

在所有服务提供者都注册以后(register 方法调用完),它们就进入了「启动」状态。这将会触发每个服务提供者执行各自的 boot 方法。在使用服务提供者时,一种常见的错误就是在register 方法里面调用其他提供者注册的服务。由于在某个服务提供者的 register 方法里,不能保证所有其他服务都已经被注册,在该方法里调用别的服务有可能会出现该服务不可用。因此,调用其它服务的代码应该被定义在服务提供者的 boot 方法中。register 方法只能用于注册服务到容器。

boot 方法中,你想做什么都可以:注册事件监听器、引入路由文件、注册过滤器、或者任何其他你能想到的事。再次强调,使用服务提供者作为管理工具的时候,如果你想将几个相关的事件监听器聚合到一起,就将它们放到该服务提供者的 boot 方法里。

现在,我们已经学习了依赖注入,以及如何通过服务提供者来组织管理我们的项目。此时此刻,我们已经为构建可维护、可测试、架构良好的 Laravel 应用打下了一个很好的基础。接下来,我们将探索 Laravel 框架本身是如何使用服务提供者的,并且深究其原理!

不要被条条框框束缚。记住,服务提供者并不只是扩展包才能使用。请尽情使用它来组织管理你的应用服务。

框架核心

至此,你可能已经注意到,在 config/app.php 配置文件里面已经有了很多服务提供者,其中每一个都负责引导框架核心的一部分服务。比如MigrationServiceProvider 负责引导用于运行数据库迁移的类,包括Artisan 迁移命令。EventServiceProvide 负责引导和注册事件调度类。尽管不同的服务提供者有着不同的复杂度,有些比较大,另一些相对较小,但它们都负责引导核心的一部分功能。

提升对 Laravel 核心代码理解的最好方法是去读核心服务提供者的源码。如果你对这些服务提供者的功能以及每个服务提供者注册了什么都很熟悉,那么你将会对Laravel 底层是如何工作的有更加深刻的理解。

大部分核心服务提供者是延迟加载的,这意味着不是每次请求都会加载它们;不过,一些用于引导框架基础服务的服务提供者是每一次请求都会被加载的,比如 FilesystemServiceProvideExceptionServiceProvider。核心服务提供者和应用容器将框架的不同部分联系起来,形成一个单一的、内聚的整体。这些核心服务提供者就是框架的构建块。

正如之前提到的那样,如果你想深入理解框架是如何运行的,请阅读 Laravel 框架的核心服务提供者的源码。通读之后,你将会对框架如何把各部分功能模块组合在一起,以及每一个服务提供者为应用提供了哪些功能有更加扎实的理解。此外,有了这些更深入的理解,你也可以为更好的 Laravel 生态系统添砖加瓦!

转自:Laravel 从学徒到工匠