平时在使用DB类的时候,总是好奇为什么我可以通过调用一个DB类中不存在的方法来实现和数据库的交互.有几次因为好奇点进DB类的实现,发现DB类的父类是Facade
类,然而无论是DB类或者是Facade类,都没有我调用的函数的影子.而且一般有这个疑惑时都是在写项目,也就没有心情深究.于是我趁现在无事,探究了一番DB类的工作原理.
这也是我第一次阅读源码,这几个晚上,我都在为Laravel的编写人员对于PHP这门语言的了解程度感到叹服.更让我惊叹的是这个框架的设计模式,越往下读,就越是给人一种"还可以这么写?"的感觉.由于我看的源码过少,不太理解这种设计模式的好处,希望我之后能理解.
由于本人水平所限,文章中难免有错漏,敬请各位大神斧正.
(所使用Laravel版本: 5.5)
静态调用DB类时,都发生了什么?
DB类本身的代码非常短,带空行和注释都只有21行:
<?php
namespace Illuminate\Support\Facades;
/**
* @see \Illuminate\Database\DatabaseManager
* @see \Illuminate\Database\Connection
*/
class DB extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'db';
}
}
当然这是因为这个类是抽象类Facade
(以下称为门面类)派生的,我们应该到门面类的实现里面去寻找答案
实现DB类的第一大关键在于门面类里重载的一个魔术方法:
abstract class Facade
{
//省略之前的方法
/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
}
这个__callStatic
的作用就是,当这个类被静态调用一个类中不存在的方法时,相当于调用了这个方法,并将调用的方法名称及其参数都作为这个魔术方法的参数
仅看这段代码,就不难猜出,Facade在其中起到的作用在于把原本要由DB类进行处理的调用转交给了另一个类进行处理.这个类就是DB类头注释中提到的\Illuminate\Database\DatabaseManager
这个类.那么Facade是怎么找到这个类的呢,下面会进行讲解.
Facade类是如何转交自己的工作的?
上面这段代码里调用了一个自己的方法getFacadeRoot
,这个方法的代码也非常简单:
/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
其中getFacadeAccessor
这个方法返回的结果在DB类里面我们已经看到了,就是字符串db
.很显然这个方法的作用是给框架使用者调用不同的类时进行不同的处理.
而resolveFacadeInstance
这个方法具体实现如下:
/**
* Resolve the facade root instance from the container.
*
* @param string|object $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
return static::$resolvedInstance[$name] = static::$app[$name];
}
顾名思义,这个方法的作用是解析门面类
的实例.从上面的代码可以得知这个方法最终返回的是$app
这个静态成员的一部分.
然而在上述过程中始终没有对$app
进行任何操作,因此可以推测是框架在处理使用者编写的业务前,提前对门面类进行了一些处理.通过在门面类的定义中寻找static::app
可以发现以下方法对这个静态成员进行了定义.
/**
* Set the application instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public static function setFacadeApplication($app)
{
static::$app = $app;
}
dd一下里面的$app
,并用一个什么都不做的空接口测试一下,发现这个方法被调用了:
- 修改源码
public static function setFacadeApplication($app)
{
static::$app = $app;
dd(static::$app);
}
- 调用空接口(路由省略)
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TestController extends Controller
{
public function testDB() {
return response();
}
}
- 获得以下返回
正如setFacadeApplication
方法的头注释里所说,静态成员$app
被定义成了一个Application
类.
说点题外话,为什么可以有$app[$name]
这种写法呢?那是由PHP的一个预定义接口ArrayAccess
实现的,其作用是可以让你像调用数组一样调用一个类.而laravel的Container
继承了ArrayAccess
,然后Application
类又继承了Container
.
回到正题,通过setFacadeApplication
方法的头注释索引到Application
,发现它是一个抽象类,它的具体实现在vendor/laravel/framework/src/Illuminate/Foundation/Application.php
.
我们可以在这个类的registerCoreContainerAliases
方法里发现一个熟悉的身影:
/**
* Register the core class aliases in the container.
*
* @return void
*/
public function registerCoreContainerAliases()
{
...
'db' => [\Illuminate\Database\DatabaseManager::class],
...
}
到此DB
类和DatabaseManager
类是如何建立联系的已经很明确了.接口被调用,在进行框架使用者定义的行为前,框架进行了预处理,把在Application
类的registerCoreContainerAliases
方法里注册了的类的别名生成对应,再把这整个类的一个实例交给了Facade
类里的一个静态成员$app
.如此一来,便可通过继承于Facade
类的各个类重载getFacadeAccessor
方法,来实现不同的类与相应的类建立联系.
整个工作流程可以如下所示:
最后再对最终结果进行一次验证.由于Facade类在框架运行中不止被调用了一次,于是我加了一个判断让它在运行到我的业务时再进行处理:
- 添加一个断点
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
if ($method === 'table') {
dd($instance);
}
return $instance->$method(...$args);
}
- 修改测试接口,调用DB类
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TestController extends Controller
{
public function testDB() {
return response(DB::table('users')->where('id', 1)->first()->id);
}
}
- 调用接口后结果如下
也就是说,经过上述处理后,最终业务给到了DatabaseManager
类.下一篇我会讲讲DatabaseManager
类是如何把各种方法变成SQL语句的.
后记
好像差不多大半年没写博客了,其实这大半年学的也不少,只是觉得那些基础的东西网上博客一大把,我再写一遍也没有什么意思了,所以就一直没写.其实就是懒之后还是会不定期写博客的,不过得等我找到网上文章比较少,而且有意思的东西,再拿来和大家分享了.
评论