共计 14410 个字符,预计需要花费 37 分钟才能阅读完成。
源码浏览剖析 -PHP-laravel
如何浏览源码?浏览源码有什么用?
这个问题对于工作两年左右的程序来说就会开始去接触并且会无意关注和去理解;大都数的认为源码的浏览是为了更好的去应答面试找更高薪的工作;其实除了这样的成果以外还有的就是,能够更好地了解框架及程序的设计原理和设计思路,设计模式等;对于开发来说还能够通过对源码的浏览从中汲取良好的代码编写布局进步本人代码品质,以及对 bug 的问题剖析和修复能力以及性能扩大的能力等等。
因而就开始尝试对源码的浏览。
01. 遇到的问题;懵,一脸懵;
在对源码的浏览中,老手在没有把握技巧的时候往往是很懵,又很晕感觉绕来绕去(叮当猫都晕了)
老手个别看源码的形式是这样的;如下为一段代码【ioc 注册与解析并通过 app 加载启动利用 db 获取数据的简化过程】
<?php
class Config
{public function get($key)
{return "获取配置信息".$key;}
}
class Db
{
protected $app
function __construct(App $app)
{$this->app = $app;}
public function connection()
{$this->configuration()
}
public function configuration()
{$connections = $this->app->make("config").get("database.connections")
// ...
}
public function select()
{$this->connection()
return "查问数据";
}
}
class Ioc
{protected $bindings = [];
public function make($key)
{return $this->bindings[$key];
}
public function bind($key, $object)
{$this->bindings[$key] = $object;
}
}
class App extends Ioc
{public function __construct()
{$this->registerCoreIocBinding();
}
public function run()
{
// .. 跳过对 Controller/ 闭包的解析过程
$db = $this->make(db).select();}
protected function registerCoreIocBinding()
{
foreach ([
"config" => Config::class,
"db" => Db::class,
] as $key => $value) {$this->bind($key, $value);
}
}
}
// 调用
$app = new App();
$app->run()
?>
下面的代码中定义一个 ioc 的对象提供对容器注册与解析的外围办法,框架中的 app 或 application 继承 ioc 对象,并会对框架中的外围容器对象进行注册(利用 bind 办法);
在示例代码中提供了两个容器别离是 Config 与 Db 两个对象;在 Db 中初始化的时候要求传递 app,并在 connection 中获取配置信息时需通过 app 解析 Config 对象从中获取到相干的配置信息;
如上的代码相对来说较为简单,并没有特地简单的设计;咱们通过下面的代码来理解一下大家平时如何浏览源码的;
———————————– 光彩的分割线 —————————————-
很多同志对源码剖析的时候本人不晓得要剖析一些什么,而后就从最开始的 index.php 中的办法一个个点点点点,点到最初发现绕圈子而后,而后就 … 从开始到放弃(我说的不是你,如果你也是在评论去给个 666)
比方下面的代码中;习惯性的同志就会开始从 new App()
开始,对new App() -> app.__construct() -> app.registerCoreIocBinding -> ioc.bind()
整个链路的办法全副点击一次再说;
实现之后就开始第二个办法 $app->run()
,而后又开始对其链接的每个办法都点击一次; 从$app->run() 到 app.make()
唉这个时候发现又到本人的 make 办法外面了(这里就会存在小纳闷),当这里走完了好不容易理解了就进入到 Db.select()
中,持续点击 Db.select()->Db.connection()->Db.configuration()->app.make()
好家伙而后又回去了 …
02. 技巧总结
对于源码的浏览,是有技巧的,技巧次要是;
- 先确定指标
- 源码摸索考究适量而止,切记不可死磕往死里点
- 肯定要看办法名,肯定要看正文;
- 不明来历看继承,看初始化、看非凡办法及相干特色
- 巧用程序提供的打印函数
- 记录过程,及调度链
- 临时跳过不会的,不晓得的,或者尝试猜想
其中是 1,2,3 点是十分重要基本上对于很多相干的语言的第一浏览都是能够使用到的,一样实用,特地不能运行代码的时候十分有帮忙;
其中 4 和 5 须要对程序有肯定的理解才行,当你对一个语言理解如何运行及根本的机制也能够尝试去浏览
细节拆分具体思路
- 先确定指标
这是十分要害要害要害!!!的第一个点,因为很多同志,晓得我要去看源码一通点击下又回归原点,最初本人被本人转晕了;
其关键问题是不分明本人到底是想看那个性能实现过程没有什么指标,因而对于指标的清晰是十分重要的事件;
- 源码摸索考究适量而止,切记不可死磕往死里点
这一步,次要是针对摸索的过程设计,第一次看的同时往往是看到一个办法就点一个办法,而后发现还有办法再点一个办法,就这样始终点击上来;
也忘了本人是谁,是在哪里,为什么我要看源码?(是的话评论扣个 666)
对于源码浏览肯定要留神适量而止,源码的浏览须要基于第一个点为主线;主线中往往会随带较多的分支,而分支多了就会蛊惑大家,这里倡议对于每个办法最多点击三级,在第一次对办法剖析的时候;
三级次要是指比方 A 办法,在 A 办法中含有(B,C,D)等办法;对于 A 办法的查阅视为 1 级,第二级则是对 B,C,D 的点击浏览,第三级就是对 B,C,D 办法外部调用的办法去查看;
当大略理解了 A 办法中 B,C,D 办法的状况再根据其源码浏览带来的信息去分辨主线,这样就防止死磕往死里点;避免出现爱的魔力转圈圈
- 肯定要看办法名,肯定要看正文;
对于这一条,大部分同志对于开源程序,很少去关注(我已经也是);
首先咱们须要了解;一个办法的封装(且不谈办法外面是何种牛马蛇神)那么是具备其要害性质的性能及作用的;
而优良的开源程序的程序往往会把办法的性能及相干的阐明以正文和办法名的形式传播给了阅读者;
当然,也不排除有英语不是很会的也问题不大,翻译安顿
联合第二步,当咱们点击了一个办法之后能够先看看办法的正文,并对办法名翻译理解它干了什么;有一些办法咱们理论只须要看办法名就即可,再实再不了解的时候才进一步点击往下看,往下看的时候也需次要适量而止;
- 不明来历看继承,看初始化、看非凡办法及相干特色
在源码中妨碍咱们对于程序了解的就是这些非凡存在;
在办法中不乏还有变量调用有如全局 / 部分属性,留神也蕴含办法;最麻烦的问题次要是那些“来历不明的办法和属性”
比方在 PHP 框架中存在 facade 非凡设定,然而在 facade 的对象中的确办法空洞无物,那办法从哪来???
这就须要看语言的一些初始化,继承,以及非凡办法及相干特色了;在 facade 中办法次要是通过 __call()
相干的魔术办法实现,在不同语言的实现形式不一样因而须要时刻警觉非凡存在去查找 不明来历
的源头
- 巧用程序提供的打印函数
这是我认为对程序代码调试比拟好的技巧;在程序源码浏览中,在第四点提到会存在不明来历的属性和办法;
那么咱们能够借助打印办法能够尝试确定对应属性的起源或者其不明办法的作用是什么;
能够把相干调用的变量利用打印函数打印参数信息以此来察看和探索属性背地的源头,但这个也要分语言不是特地万能;
然而有一点是很有用的,就是咱们能够利用打印理解程序的执行和参数的变动过程
比方:
function A($a){var_dump($a); // 解决前
// 对 a 解决
var_dump($a); // 解决后
}
- 记录过程,及调度链
这一点次要是不便本人回顾;
在开源程序及开源框架中整体的调度链个别都很简单,因而对于每个办法的作用和调度过程,倡议能够绘制流程图以及相干的阐明这样就能够不便日后回顾而舒适
对于第 6 点往往应该是配合一个整体的调度流程,本文例子 …. 这个作者很懒,懒得画了;
- 临时跳过不会的,不晓得的 或者尝试猜想
在浏览源码的时候常常会遇到不会的状况,以及看不懂不是特地能了解为什么是这样或者是做什么的;
这个时候我的倡议是如果你通过后面 6 个步骤还是不了解,就间接跳过;
程序的学习并不是肯定要把某个点了解好了才往后学习,在咱们理论学习中可能临时不会然而之后就会了;“用着,用着,用着就会了”
另外也能够在看的时候尝试依据办法名或可能看到理解的相干信息作为线索猜想其作用
03. 实际使用
- 实际对象:laravel5.7.* 版本
- 浏览原理:申请到控制器执行
好开始~~~
03.01 入口
对于 PHP 来说框架的入口文件根本都是从 public/index.php 开始
<?php
define('LARAVEL_START', microtime(true));
require __DIR__.'/../vendor/autoload.php';
// 初始化框架利用对象 application
$app = require_once __DIR__.'/../bootstrap/app.php';
// 获取对 http 申请解决的外围实例
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// 执行申请解决
$response = $kernel->handle($request = Illuminate\Http\Request::capture()
);
// 响应输入
$response->send();
// 程序完结,并完结相干中间件
$kernel->terminate($request, $response);
?>
在一开始咱们能够通过对 public/index.php 的浏览能够理解到整个框架程序的流程及过程;
- 初始化框架利用对象 application
- 获取对 http 申请解决的外围实例
- 执行申请解决
- 响应输入
- 程序完结,并完结相干中间件
接下来咱们进入 bootstrap/app.php 中理解 application 初始化的过程
03.02 application 利用初始化
<?php
// 初始化 application
$app = new Illuminate\Foundation\Application($_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
// 绑定 http/debug/console 外围处理器
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
// 返回
return $app;
?>
这里咱们的指标是为了理解 Application 在初始化的过程,因而咱们就须要点击看 Application 的结构过程;
浏览思路剖析:针对下面的代码,基于咱们的指标能够必定的是 Application 对象的构造函数是咱们须要看的理解的
而后续中调用 Application 中的 singleton 置信第一次看的不是很了解,基于 7 点的准则咱们能够先跳过以 Application 的初始化过程为主
好进入 Application 办法
blog\vendor\laravel\framework\src\Illuminate\Foundation\Application.php
<?php
class Application
{
// ..
public function __construct($basePath = null)
{if ($basePath) {$this->setBasePath($basePath);
}
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();}
// ...
}
?>
看到办法的时候依据准则“3. 肯定要看办法名,肯定要看正文;6. 记录过程,及调度链”
而后一下子不就明了吗 ” 是不是 ”, 你:“对对对”
<?php
// Create a new Illuminate application instance.
// 创立一个新的照明应用程序实例。public function __construct($basePath = null)
{
// 判断是否有设置利用门路
if ($basePath) {
// 设置利用门路
$this->setBasePath($basePath);
}
// 注册利用绑定
$this->registerBaseBindings();
// 注册利用服务提供者
$this->registerBaseServiceProviders();
// 注册外围容器别名
$this->registerCoreContainerAliases();}
?>
再后续会间接在代码上使用 3,6 点;作者他不想每次都引到,心领神会就好
好接下来咱们基于“2. 源码摸索考究适量而止,切记不可死磕往死里点”再进一步理解每个办法的大略作用
先看 setBasePath
<?php
public function setBasePath($basePath)
{
// 解决门路不为空
$this->basePath = rtrim($basePath, '\/');
// 绑定门路到容器里
$this->bindPathsInContainer();
return $this;
}
// 绑定容器中的所有应用程序的门路
protected function bindPathsInContainer()
{$this->instance('path', $this->path());
$this->instance('path.base', $this->basePath());
$this->instance('path.lang', $this->langPath());
//..
}
?>
对于 setBasePath 浏览咱们在整体的思路上就能够理解到是对利用的外围门路去进行设置, 依据 2 准则适量而止因而咱们进行对 bindPathsInContainer
函数的浏览
再看 registerBaseBindings
<?php
// 注册根本绑定到容器中
protected function registerBaseBindings()
{static::setInstance($this);
$this->instance('app', $this);
$this->instance(Container::class, $this);
$this->instance(PackageManifest::class, new PackageManifest(new Filesystem, $this->basePath(), $this->getCachedPackagesPath()));
}
?>
到此咱们发现 instance 是一个较为外围的存在,基本上哪儿都有它的身影,那咱们须要对它剖析嘛?????
须要,然而不当初;因为咱们目前的关键在于理解 Application 初始化过程;
依据 7 能够大略猜想它的作用是为了注册实例,也就是注册对象;再联合办法名即可了解,该办法就是把外围实例 application 注册到容器中;
$this->instance(PackageManifest::class, new PackageManifest(new Filesystem, $this->basePath(), $this->getCachedPackagesPath()));
这一段干了啥????你晓得嘛??你:“什么鬼怎么扯我了,我就是看文章的怎么晓得”;
“ 我当然晓得你不晓得 ” 不晓得咋办,当然是跳过呀。。。依据 7 的准则
持续看 registerBaseServiceProviders
<?php
// 注册所有根底服务提供商
protected function registerBaseServiceProviders()
{$this->register(new \Illuminate\Events\EventServiceProvider($this));
$this->register(new \Illuminate\Log\LogServiceProvider($this));
$this->register(new \Illuminate\Routing\RoutingServiceProvider($this));
}
?>
进入到这个办法依据意思“注册所有根底服务提供商”,看了这个办法依据 event 翻译 事件
,log
,routing
依据这三个关键词的信息提供,这不就晓得这个办法干了啥嘛;
就是注册框架事件、框架日志、框架路由的服务提供者嘛;而后就 OK 了须要看 register 嘛,这是之后的事件和目前的主题关系不是很大;
最初看 registerCoreContainerAliases
<?php
public function registerCoreContainerAliases()
{
foreach ([
'app' => [self::class, \Illuminate\Contracts\Container\Container::class,
// ... 万能的三点
\Illuminate\Contracts\View\Factory::class],
] as $key => $aliases) {foreach ($aliases as $alias) {$this->alias($key, $alias);
}
}
}
?>
这一看不就晓得,以后这个办法就是吧外围容器注册到容器中嘛,其中就蕴含 view/url/db/redis。。。
而后 application 初始化 ok 了
application 初识总结
总结工夫到了,依据刚刚的过程基于 6 点准则,做好总结
总结:在 application 初始化中会先设置整个利用的系统目录地址,在设置实现之后而后就会去注册并绑定外围利用 Application 到容器中,还注册 event/log/routing 等服务提供者,最初实现外围容器的注册
03.04 回到 index.php 看 http-kernel
理解到了 Application 之后咱们再看看 http 的过程
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle($request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
有同志可能就说这 $kernel 是那个对象????这个时候咱们要用准则 5 打印
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
var_dump($kernel);
$response = $kernel->handle($request = Illuminate\Http\Request::capture()
);
看下这不就晓得是那个对象了嘛;
调用的就是App/Http/Kernel.php
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{// ..}
然而发现它并没有 Kernel 办法,依据准则 4 而后发现这最终办法是在 Illuminate\Foundation\Http\Kernel
中
可能你要跟我讲Illuminate\Foundation\Http\Kernel
“ 这在哪??”
既然你披肝沥胆的提问了那我就大发慈悲的通知你,当你的编辑器不太能间接点击查阅办法的时候,能够看 vendor/composer/autoload_classmap.php
它会通知你答案
进入 Foundation/Http/Kernel.php 中
blog\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php
而后查找 kernel 办法
// 解决传入的 http 申请
public function handle($request)
{
try {$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {// 可能是解决 xxx 谬误} catch (Throwable $e) {// 可能是解决 xxx 谬误}
// 申请事件
$this->app['events']->dispatch(new Events\RequestHandled($request, $response)
);
return $response;
}
依据办法的整体构造咱们根本能够推断外围解决申请的办法肯定在 enableHttpMethodParameterOverride 或 sendRequestThroughRouter
中
因为就这里失常,其余都是处理错误,因而基于准则 2 别离查看一下这两个办法,后果 enableHttpMethodParameterOverride
可能查不到 …
没方法只能翻译办法名 ”http 办法参数笼罩 ”,依据这个意思很显然是解决 request 申请对象的,那和申请解决就没关系;那假相只有一个
就决定是你了 sendRequestThroughRouter
protected function sendRequestThroughRouter($request)
{
// 这个不必去看了,晓得是设置 request 绑定到容器里
$this->app->instance('request', $request);
// facade 明确实例,不理解就跳过
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
接下来看 bootstrap
最初咱们就只剩下上面两个了,先看 bootstrap();
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
public function bootstrap()
{if (! $this->app->hasBeenBootstrapped()) {$this->app->bootstrapWith($this->bootstrappers());
}
}
protected function bootstrappers()
{return $this->bootstrappers;}
这里为了缩小篇幅间接安顿相干的调度放到一起,依据“bootstrap”翻译是“驱动 / 启动”再联合 bootstrappers 属性中的关键词能够揣测是;对框架中的系统配置,错误处理,facade,服务提供者等去进行加载启动;
通过 $this->app->bootstrapWith($this->bootstrappers())
实现;
等等..$this->app?? 哪来的?是谁?;依据 4 准则发现是构造函数传递的
public function __construct(Application $app, Router $router)
{$this->app = $app;}
这个时候你有一个抉择能够再看看 $this->app->bootstrapWith
或者跳过,这里咱们抉择看
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {$this['events']->dispatch('bootstrapping:'.$bootstrapper, [$this]);
$this->make($bootstrapper)->bootstrap($this);
$this['events']->dispatch('bootstrapped:'.$bootstrapper, [$this]);
}
}
好能够发现,前后都是进行 event,唯有中间件的$this->make($bootstrapper)->bootstrap($this)
才是真的再搞闲事,这里你就能够联合在 ’http::kernel::bootstrap’ 传入的参数再进一步的去查看;然而呢能够适可而止,因为够了;再看就偏题了
回到 sendRequestThroughRouter 中
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
目前持续剖析,这里会较为难的剖析;因为看起来有些简单;个别闭包办法看谁??
看外部外部传递的调度办法, 因而筛选就只剩 $this->app->shouldSkipMiddleware()
和$this->dispatchToRouter()
shouldSkipMiddleware 属于 Application,是利用层面咱们能够先不看,先看 dispatchToRouter;因为它是 Kernel 中的办法,而 kernel 的次要性能是解决申请因而要看他,是他,是他,就是他;
咱们看 dispatchToRouter
// 线路调度程序回调
protected function dispatchToRouter()
{return function ($request) {$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
好依据咱们的教训看的是不是就是$this->router->dispatch($request)
仔细的发现 $this->router
在初始化的时候传递了就是 Illuminate\Routing\Router
对象;
对 router 施展咱们的准则技巧
上面我会省略之前的技巧使用阐明;因为最终要的还是本人学会,因而前面我就提出要害的办法本人尝试依据我说过的技巧准则去实际吧;实际上我是懒得写了,太多了太累了,唉又不能给个赞
先看如何查找路由
blog\vendor\laravel\framework\src\Illuminate\Routing\Router.php
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
public function dispatchToRoute(Request $request)
{return $this->runRoute($request, $this->findRoute($request));
}
protected function findRoute($request)
{$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route);
return $route;
}
protected function runRoute(Request $request, Route $route)
{$request->setRouteResolver(function () use ($route) {return $route;});
$this->events->dispatch(new Events\RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
依据整体来看路由的匹配是通过 findRoute 实现的,runRoute 则是运行;而联合对 findRoute 浏览能够确定的是调用 RouteCollection::match 进行解析查找的
public function match(Request $request)
{
// 能够试试用打印的办法
$routes = $this->get($request->getMethod());
// 查找到路由,这里是关键性的路由查找办法
$route = $this->matchAgainstRoutes($routes, $request);
if (! is_null($route)) {return $route->bind($request);
}
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0) {return $this->getRouteForMethods($request, $others);
}
throw new NotFoundHttpException;
}
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
// 这里看不懂就打印 $fallbacks, $routes 就行了,用 var_dump
[$fallbacks, $routes] = collect($routes)->partition(function ($route) {return $route->isFallback;});
// 这里是闭包,遇到闭包间接看外部调用的办法
return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
// $value 不晓得就用 var_dump 打印
return $value->matches($request, $includingMethod);
});
}
通过下面的步骤根本就能够找到,是调用的那个办法最终会在赋值给 $route 变量并返回
最初看如何查调用
回到 blog\vendor\laravel\framework\src\Illuminate\Routing\Router.php
中
protected function runRoute(Request $request, Route $route)
{$request->setRouteResolver(function () use ($route) {return $route;});
$this->events->dispatch(new Events\RouteMatched($route, $request));
return $this->prepareResponse($request,
// 一样的准则先看它
$this->runRouteWithinStack($route, $request)
);
}
protected function runRouteWithinStack(Route $route, Request $request)
{$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
// 一样的准则又是看它
return $this->prepareResponse(
// $route 在下面传参了
$request, $route->run());
});
}
而后咱们就能够看到调用的中央了 Route::run
办法
public function run()
{
$this->container = $this->container ?: new Container;
try {if ($this->isControllerAction()) {return $this->runController();
}
return $this->runCallable();} catch (HttpResponseException $e) {return $e->getResponse();
}
}
好了到此咱们就看完了
04. 续
心愿本次对源码解读的过程能够帮忙到你
.