小白也能看懂的 Laravel 核心概念讲解
自动依赖注入
什么是依赖注入,用大白话将通过类型提示的方式向函数传递参数。
实例 1
首先,定义一个类:
/routes/web.php
class Bar {}
假如我们在其他地方要使用到 Bar
提供的功能(服务),怎么办,直接传入参数即可:
/routes/web.php
Route::get('bar', function(Bar $bar) {
dd($bar);
});
访问 /bar
,显示 $bar
的实例:
Bar {#272}
也就是说,我们不需要先对其进行实例!如果学过 PHP 的面向对象,都知道,正常做法是这样:
class Bar {}
$bar = new Bar();
dd($bar);
实例 2
可以看一个稍微复杂的例子:
class Baz {}
class Bar
{
public $baz;
public function __construct(Baz $baz)
{
$this->baz = $baz;
}
}
$baz = new Baz();
$bar = new Bar($baz);
dd($bar);
为了在 Bar
中能够使用 Baz
的功能,我们需要实例化一个 Baz
,然后在实例化 Bar
的时候传入 Baz
实例。
在 Laravel 中,不仅仅可以自动注入 Bar
,也可以自动注入 Baz
:
/routes/web.php
class Baz {}
class Bar
{
public $baz;
public function __construct(Baz $baz)
{
$this->baz = $baz;
}
}
Route::get('bar', function(Bar $bar) {
dd($bar->baz);
});
显示结果:
Baz {#276}
小结
通过上述两个例子,可以看出,在 Laravel 中,我们要在类或者函数中使用其他类体用的服务,只需要通过类型提示的方式传递参数,而 Laravel 会自动帮我们去寻找响对应的依赖。
那么,Laravel 是如何完成这项工作的呢?答案就是通过服务容器。
服务容器
什么是服务容器
服务容器,很好理解,就是装着各种服务实例的特殊类。可以通过「去餐馆吃饭」来进行类比:
吃饭 - 使用服务,即调用该服务的地方
饭 - 服务
盘子 - 装饭的容器,即服务容器
服务员 - 服务提供者,负责装饭、上饭
这个过程在 Laravel 中如何实现呢?
饭
定义 Rice 类:
/app/Rice.php
<?php
namespace App;
class Rice
{
public function food()
{
return '香喷喷的白米饭';
}
}
把饭装盘子
在容器中定义了名为 rice
的变量(你也可以起其他名字,比如 rice_container
),绑定了 Food
的实例:
app()->bind('rice', function (){
return new \App\Rice();
});
也可以写成:
app()->bind('rice',\App\Rice::class);
现在,吃饭了,通过 make
方法提供吃饭的服务:
Route::get('eat', function() {
return app()->make('rice')->food();
// 或者 return resolve('rice')->food();
});
make
方法传入我们刚才定义的变量名即可调用该服务。
访问 /eat
,返回 香喷喷的白米饭
。
为了方便起见,我们在路由文件中直接实现了该过程,相当于自给自足。但是服务通常由服务提供者来管理的。
因此,我们可以让 AppServiceProvider
这个服务员来管理该服务:
/app/Providers/AppServiceProvider.php
namespace App\Providers;
public function register()
{
$this->app->bind('food_container',Rice::class);
}
更为常见的是,我们自己创建一个服务员:
$ php artisan make:provider RiceServiceProvider
注册:
/app/Providers/RiceServiceProvider.php
<?php
use App\Rice;
public function register()
{
$this->app->bind('rice',Rice::class);
}
这里定义了 register()
方法,但是还需要调用该方法才能真正绑定服务到容器,因此,需要将其添加到 providers
数组中:
/config/app.php
'providers' => [
App\Providers\RiceServiceProvider::class,
],
这一步有何作用呢?Laravel 在启动的时候会访问该文件,然后调用里面的所有服务提供者的 register()
方法,这样我们的服务就被绑定到容器中了。
小结
通过上述的例子,基本上可以理解服务容器和服务提供者的使用。当然了,我们更为常见的还是使用类型提示来传递参数:
use App\Rice;
Route::get('eat', function(Rice $rice) {
return $rice->food();
});
在本例中,使用自动依赖注入即可。不需要在用 bind
来手动绑定以及 make
来调用服务。那么,为什么还需要 bind
和 make
呢? make
比较好理解,我们有一些场合 Laravel 不能提供自动解析,那么这时候手动使用 make
解析就可以了,而 bind
的学问就稍微大了点,后面将会详细说明。
门面
门面是什么,我们回到刚才的「吃饭」的例子:
Route::get('eat', function(Rice $rice) {
return $rice->food();
});
在 Laravel,通常还可以这么写:
Route::get('eat', function() {
return Rice::food();
});
或者
Route::get('eat', function() {
return rice()->food();
});
那么,Laravel 是如何实现的呢?答案是通过门面。
门面方法实现
先来实现 Rice::food()
,只需要一步:
/app/RiceFacade.php
<?php
namespace App;
use Illuminate\Support\Facades\Facade;
class RiceFacade extends Facade
{
protected static function getFacadeAccessor()
{
return 'rice';
}
}
现在,RiceFacade
就代理了 Rice
类了,这就是门面的本质了。我们就可以直接使用:
Route::get('eat', function() {
dd(\App\RiceFacade::food());
});
因为 \App\RiceFacade
比较冗长,我们可以用 php 提供的 class_alias
方法起个别名吧:
/app/Providers/RiceServiceProvider.php
public function register()
{
$this->app->bind('rice',\App\Rice::class);
class_alias(\App\RiceFacade::class, 'Rice');
}
这样做的话,就实现了一开始的用法:
Route::get('eat', function() {
return Rice::food();
});
看上去就好像直接调用了 Rice
类,实际上,调用的是 RiceFacade
类来代理,因此,个人觉得Facade
翻译成假象比较合适。
最后,为了便于给代理类命名,Laravel 提供了统一命名别名的地方:
/config/app.php
'aliases' => [
'Rice' => \App\RiceFacade::class,
],
门面实现过程分析
首先:
Rice::food();
因为 Rice
是别名,所以实际上执行的是:
\App\RiceFacade::food()
但是我们的 RiceFacade
类里面并没有定义静态方法 food
啊?怎么办呢?直接抛出异常吗?不是,在 PHP 里,如果访问了不可访问的静态方法,会先调用 __callstatic
,所以执行的是:
\App\RiceFacade::__callStatic()
虽然我们在 RiceFacade
中没有定义,但是它的父类 Facade
已经定义好了:
/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php
public static function __callStatic($method, $args)
{
// 实例化 Rice {#270}
$instance = static::getFacadeRoot();
// 实例化失败,抛出异常
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
// 调用该实例的方法
return $instance->$method(...$args);
}
主要工作就是第一步实例化:
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
// 本例中:static::resolveFacadeInstance('rice')
}
进一步查看 resolveFacadeInstance()
方法:
protected static function resolveFacadeInstance($name)
{
// rice 是字符串,因此跳过该步骤
if (is_object($name)) {
return $name;
}
// 是否设置了 `rice` 实例
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
return static::$resolvedInstance[$name] = static::$app[$name];
}
第一步比较好理解,如果我们之前在 RiceFacade
这样写:
protected static function getFacadeAccessor()
{
return new \App\Rice;
}
那么就直接返回 Rice
实例了,这也是一种实现方式。
主要难点在于最后这行:
return static::$resolvedInstance[$name] = static::$app[$name];
看上去像是在访问 $app
数组,实际上是使用 数组方式来访问对象,PHP 提供了这种访问方式接口,而 Laravel 实现了该接口。
也就是说,$app
属性其实就是对 Laravel 容器的引用,因此这里实际上就是访问容器上名为 rice
的对象。而我们之前学习容器的时候,已经将 rice
绑定了 Rice
类:
public function register()
{
$this->app->bind('rice',\App\Rice::class);
// class_alias(\App\RiceFacade::class, 'Rice');
}
所以,其实就是返回该类的实例了。懂得了服务容器和服务提供者,理解门面也就不难了。
辅助方法实现
辅助方法的实现,更简单了。不就是把 app->make('rice')
封装起来嘛:
/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
if (! function_exists('rice')) {
function rice()
{
return app()->make('rice');
// 等价于 return app('rice');
// 等价于 return app()['rice'];
}
}
然后我们就可以使用了:
Route::get('eat', function() {
dd(rice()->food());
});
小结
Laravel 提供的三种访问类的方式:
依赖注入:通过类型提示的方式实现自动依赖注入
门面:通过代理来访问类
辅助方法:通过方法的方式来访问类
本质上,这三种方式都是借助于服务容器和服务提供者来实现。那么,服务容器本身有什么好处呢?我们接下来着重介绍下。
IOC
不好的实现
我们来看另外一个例子(为了方便测试,该例子都写在路由文件中),假设有三种类型的插座:USB、双孔、三孔插座,分别提供插入充电的服务:
class UsbsocketService
{
public function insert($deviceName){
return $deviceName." 正在插入 USB 充电";
}
}
class DoubleSocketService
{
public function insert($deviceName){
return $deviceName." 正在插入双孔插座充电";
}
}
class ThreeSocketService
{
public function insert($deviceName){
return $deviceName." 正在插入三孔插座充电";
}
}
设备要使用插座的服务来充电:
class Device {
protected $socketType; // 插座类型
public function __construct()
{
$this->socketType = new UsbSocketService();
}
public function power($deviceName)
{
return $this->socketType->insert($deviceName);
}
}
现在有一台手机要进行充电:
Route::get('/charge',function(){
$device = new Device();
return $device->power("手机");
});
因为 Laravel 提供了自动依赖注入功能,因此可以写成:
Route::get('/charge/{device}',function(Device $device){
return $device->power("手机");
});
访问 /charge/phone
,页面显示 phone 正在插入 USB 充电
。
假如,现在有一台电脑要充电,用的是三孔插座,那么我们就需要去修改 Device
类:
$this->socketType = new ThreeSocketService();
这真是糟糕的设计,设备类对插座服务类产生了依赖。更换设备类型时,经常就要去修改类的内部结构。
好的实现
为了解决上面的问题,可以参考「IOC」思路:即将依赖转移到外部。来看看具体怎么做。
首先定义插座类型接口:
interface SocketType {
public function insert($deviceName);
}
让每一种插座都实现该接口:
class UsbsocketService implements SocketType
{
public function insert($deviceName){
return $deviceName." 正在插入 USB 充电";
}
}
class DoubleSocketService implements SocketType
{
public function insert($deviceName){
return $deviceName." 正在插入双孔插座充电";
}
}
class ThreeSocketService implements SocketType
{
public function insert($deviceName){
return $deviceName." 正在插入三孔插座充电";
}
}
最后,设备中传入接口类型而非具体的类:
class Device {
protected $socketType; // 插座类型
public function __construct(SocketType $socketType) // 传入接口
{
$this->socketType = $socketType;
}
public function power($deviceName)
{
return $this->socketType->insert($deviceName);
}
}
实例化的时候再决定使用哪种插座类型,这样依赖就转移到了外部:
Route::get('/charge',function(){
$socketType = new ThreeSocketService();
$device = new Device($socketType);
echo $device->power("电脑");
});
我们现在可以再不修改类结构的情况下,方便的更换插座来满足不同设备的充电需求:
Route::get('/charge',function(){
$socketType = new DoubleSocketService();
$device = new Device($socketType);
echo $device->power("台灯");
});
自动依赖注入的失效
上面举的例子,我们通过 Laravel 的自动依赖注入可以进一步简化:
Route::get('/charge',function(Device $device){
echo $device->power("电脑");
});
这里的类型提示有两个,一个是 Device $device
,一个是 Device 类内部构造函数传入的 SocketType $sockType
。第一个没有问题,之前也试过。但是第二个 SocketType
是接口,而 Laravel 会将其当成类试图去匹配 SocketType
的类并将其实例化,因此访问 /charge
时候就会报错:
Target [SocketType] is not instantiable while building [Device].
错误原因很明显,Laravel 没法自动绑定接口。因此,我们就需要之前的 bind
方法来手动绑定接口啦:
app()->bind('SocketType',ThreeSocketService::class);
Route::get('/charge',function(Device $device){
echo $device->power("电脑");
});
现在,如果要更换设备,我们只需要改变绑定的值就可以了:
app()->bind('SocketType',DoubleSocketService::class);
Route::get('/charge',function(Device $device){
echo $device->power("台灯");
});
也就是说,我们将依赖转移到了外部之后,进一步由第三方容器来管理,这就是 IOC。
契约
契约,不是什么新奇的概念。其实就是上一个例子中,我们定义的接口:
interface SocketType {
public function insert($deviceName);
}
通过契约,我们就可以保持松耦合了:
public function __construct(SocketType $socketType) // 传入接口而非具体的插座类型
{
$this->socketType = $socketType;
}
然后服务容器再根据需要去绑定哪种服务即可:
app()->bind('SocketType',UsbSocketService::class);
app()->bind('SocketType',DoubleSocketService::class);
app()->bind('SocketType',ThreeSocketService::class);
转载:https://segmentfault.com/a/1190000009171779#articleHeader0
小白也能看懂的 Laravel 核心概念讲解的更多相关文章
- 小白也能看懂的Redis教学基础篇——朋友面试被Skiplist跳跃表拦住了
各位看官大大们,双节快乐 !!! 这是本系列博客的第二篇,主要讲的是Redis基础数据结构中ZSet(有序集合)底层实现之一的Skiplist跳跃表. 不知道那些是Redis基础数据结构的看官们,可以 ...
- 小白也能看懂的Redis教学基础篇——做一个时间窗限流就是这么简单
不知道ZSet(有序集合)的看官们,可以翻阅我的上一篇文章: 小白也能看懂的REDIS教学基础篇--朋友面试被SKIPLIST跳跃表拦住了 书接上回,话说我朋友小A童鞋,终于面世通过加入了一家公司.这 ...
- 小白也能看懂的插件化DroidPlugin原理(二)-- 反射机制和Hook入门
前言:在上一篇博文<小白也能看懂的插件化DroidPlugin原理(一)-- 动态代理>中详细介绍了 DroidPlugin 原理中涉及到的动态代理模式,看完上篇博文后你就会发现原来动态代 ...
- 小白也能看懂的插件化DroidPlugin原理(三)-- 如何拦截startActivity方法
前言:在前两篇文章中分别介绍了动态代理.反射机制和Hook机制,如果对这些还不太了解的童鞋建议先去参考一下前两篇文章.经过了前面两篇文章的铺垫,终于可以玩点真刀实弹的了,本篇将会通过 Hook 掉 s ...
- 【vscode高级玩家】Visual Studio Code❤️安装教程(最新版🎉教程小白也能看懂!)
目录 如果您在浏览过程中发现文章内容有误,请点此链接查看该文章的完整纯净版 下载 Linux Mac OS 安装 运行安装程序 同意使用协议 选择附加任务 准备安装 开始安装 安装完成 如果您在浏览过 ...
- 搭建分布式事务组件 seata 的Server 端和Client 端详解(小白都能看懂)
一,server 端的存储模式为:Server 端 存 储 模 式 (store-mode) 支 持 三 种 : file: ( 默 认 ) 单 机 模 式 , 全 局 事 务 会 话 信 息 内 存 ...
- 小白也能看懂插件化DroidPlugin原理(一)-- 动态代理
前言:插件化在Android开发中的优点不言而喻,也有很多文章介绍插件化的优势,所以在此不再赘述.前一阵子在项目中用到 DroidPlugin 插件框架 ,近期准备投入生产环境时出现了一些小问题,所以 ...
- 小白也能看懂的插件化DroidPlugin原理(一)-- 动态代理
前言:插件化在Android开发中的优点不言而喻,也有很多文章介绍插件化的优势,所以在此不再赘述.前一阵子在项目中用到 DroidPlugin 插件框架 ,近期准备投入生产环境时出现了一些小问题,所以 ...
- 小白都能看懂的tcp三次握手
众所周知,TCP在建立连接时需要经过三次握手.许多初学者经常对这个过程感到混乱:SYN是干什么的,怎么一会儿是1一会儿是0?怎么既有大写的ACK又有小写的ack?为什么ACK在第二次握手才开始出现?初 ...
随机推荐
- 在WPF设计工具Blend2中制作立方体图片效果
原文:在WPF设计工具Blend2中制作立方体图片效果 ------------------------------------------------------------------------ ...
- CentOSserverMysql主从复制集群结构
在配置Mysql数据库主从复制集群时间,以确保: 1.主从server操作系统版本号和位数一致. 2.Mysql版本号一致. 为了保证稳定性,最好server操作系统和Mysql数据库环境一致. Ce ...
- AutoEncoder一些实验结果,并考虑
看之前Autoencoder什么时候,我做了一些练习这里:http://ufldl.stanford.edu/wiki/index.php/Exercise:Sparse_Autoencoder .其 ...
- 罚函数(penalty function)的设计
1. encourage sparsity ℓ0 范数: non-differentiable and difficult to optimize in general ℓ1 范数: 对数约束,log ...
- Android blueZ HCI(一个):hciconfig实施和经常使用
关键词:hciconfighcitool hcidump笔者:xubin341719(欢迎转载,请明确说明,请尊重版权,谢谢.)欢迎指正错误,共同学习.共同进步! . Android blueZ H ...
- Java数据结构和算法的数组
阵列的功能: 1.固定大小 2.相同的数据类型 3. 4.数据项可反复 Java数据类型:基本类型(int和double)和对象类型.在很多编程语言中.数组也是基本类型.但在Java中把它们当作对象来 ...
- gtest写了第一个测试用例错误和结算过程
安装好gtest后,编写第一个測试案例test_main.cpp #include <iostream> #include <gtest/gtest.h> using name ...
- debian安装node.js
1,先下载nodejs: # wget http://nodejs.org/dist/v0.8.7/node-v0.8.7.tar.gz 2,解压文件 # tar xvf node-v0.8.7.ta ...
- WPF DispatcherTimer(定时器应用) 无人触摸60s自动关闭窗口
原文:WPF DispatcherTimer(定时器应用) 无人触摸60s自动关闭窗口 如果无人触摸:60s自动关闭窗口 xmal:部分 <s:SurfaceWindow x:Class=&qu ...
- css3 位置选择器 类似jq的:eq(0)
JQ使用 :eq(位置),可以选择第几个元素 CSS3里面新增了一个用法,:nth-child(位置) 可实现和JQ同样的功能 需要注意的是jq第一个是从0开始,CSS的第一个是从1开始