源码阅读分析-PHP-laravel

源码阅读分析-PHP-laravel

如何阅读源码?阅读源码有什么用?

这个问题对于工作两年左右的程序来说就会开始去接触并且会有意关注和去了解;大都数的认为源码的阅读是为了更好的去应对面试找更高薪的工作;其实除了这样的效果以外还有的就是,可以更好地理解框架及程序的设计原理和设计思路,设计模式等;对于开发来说还可以通过对源码的阅读从中吸取良好的代码编写规划提高自己代码质量,以及对bug的问题分析和修复能力以及功能扩展的能力等等。

因此就开始尝试对源码的阅读。

01. 遇到的问题;懵,一脸懵;

在对源码的阅读中,新手在没有掌握技巧的时候往往是很懵,又很晕觉得绕来绕去(叮当猫都晕了)

源码阅读分析-PHP-laravel_第1张图片

新手一般看源码的方式是这样的;如下为一段代码【ioc注册与解析并通过app加载启动利用db获取数据的简化过程】

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()好家伙然后又回去了...

源码阅读分析-PHP-laravel_第2张图片

02. 技巧总结

对于源码的阅读,是有技巧的,技巧主要是;

  1. 先确定目标
  2. 源码探索讲究适量而止,切记不可死磕往死里点
  3. 一定要看方法名,一定要看注释;
  4. 不明来历看继承,看初始化、看特殊方法及相关特征
  5. 巧用程序提供的打印函数
  6. 记录过程,及调度链
  7. 暂时跳过不会的,不知道的,或者尝试猜测

其中是1,2,3点是非常重要基本上对于很多相关的语言的第一阅读都是可以运用到的,一样适用,特别不能运行代码的时候非常有帮助;

其中4和5需要对程序有一定的了解才行,当你对一个语言了解如何运行及基本的机制也可以尝试去阅读

细节拆分具体思路

  1. 先确定目标

这是非常关键关键关键!!!的第一个点,因为很多同志,知道我要去看源码一通点击下又回归原点,最后自己被自己转晕了;

其关键问题是不清楚自己到底是想看那个功能实现过程没有什么目标,因此对于目标的清晰是非常重要的事情;

  1. 源码探索讲究适量而止,切记不可死磕往死里点

这一步,主要是针对探索的过程设计,第一次看的同时往往是看到一个方法就点一个方法,然后发现还有方法再点一个方法,就这样一直点击下去;

也忘了自己是谁,是在哪里,为什么我要看源码?(是的话评论扣个666)

对于源码阅读一定要注意适量而止,源码的阅读需要基于第一个点为主线;主线中往往会随带较多的分支,而分支多了就会迷惑大家,这里建议对于每个方法最多点击三级,在第一次对方法分析的时候;

三级主要是指比如A方法,在A方法中含有(B,C,D)等方法;对于A方法的查阅视为1级,第二级则是对B,C,D的点击阅读,第三级就是对B,C,D方法内部调用的方法去查看;

当大概了解了A方法中B,C,D方法的情况再依据其源码阅读带来的信息去分辨主线,这样就避免死磕往死里点;避免出现爱的魔力转圈圈

  1. 一定要看方法名,一定要看注释;

对于这一条,大部分同志对于开源程序,很少去关注(我曾经也是);

首先我们需要理解;一个方法的封装(且不谈方法里面是何种牛马蛇神)那么是具有其关键性质的功能及作用的;

而优秀的开源程序的程序往往会把方法的功能及相关的说明以注释和方法名的方式传达给了阅读者;

当然,也不排除有英语不是很会的也问题不大,翻译安排

结合第二步,当我们点击了一个方法之后可以先看看方法的注释,并对方法名翻译了解它干了什么;有一些方法我们实际只需要看方法名就即可,再实再不理解的时候才进一步点击往下看,往下看的时候也需主要适量而止;

  1. 不明来历看继承,看初始化、看特殊方法及相关特征

在源码中阻碍我们对于程序理解的就是这些特殊存在;

在方法中不乏还有变量调用有如全局/局部属性,注意也包含方法;最麻烦的问题主要是那些“来历不明的方法和属性”

比如在PHP框架中存在facade特殊设定,但是在facade的对象中确实方法空空如也,那方法从哪来???

这就需要看语言的一些初始化,继承,以及特殊方法及相关特征了;在facade中方法主要是通过__call()相关的魔术方法实现,在不同语言的实现方式不一样因此需要时刻警惕特殊存在去查找不明来历的源头

  1. 巧用程序提供的打印函数

这是我认为对程序代码调试比较好的技巧;在程序源码阅读中,在第四点提到会存在不明来历的属性和方法;

那么我们可以借助打印方法可以尝试确定对应属性的来源或者其不明方法的作用是什么;

可以把相关调用的变量利用打印函数打印参数信息以此来观察和探究属性背后的源头,但这个也要分语言不是特别万能;

但是有一点是很有用的,就是我们可以利用打印了解程序的执行和参数的变化过程

比如:

function A($a){
  var_dump($a); // 处理前

  // 对a处理

  var_dump($a); // 处理后
}
  1. 记录过程,及调度链

这一点主要是方便自己回顾;

在开源程序及开源框架中整体的调度链一般都很复杂,因此对于每个方法的作用和调度过程,建议可以绘制流程图以及相关的说明这样就可以方便日后回顾而温馨

对于第6点往往应该是配合一个整体的调度流程,本文例子....这个作者很懒,懒得画了;

  1. 暂时跳过不会的,不知道的 或者尝试猜测

在阅读源码的时候经常会遇到不会的情况,以及看不懂不是特别能理解为什么是这样或者是做什么的;

这个时候我的建议是如果你通过前面6个步骤还是不理解,就直接跳过;

程序的学习并不是一定要把某个点理解好了才往后学习,在我们实际学习中可能暂时不会但是之后就会了;“用着,用着,用着就会了”

另外也可以在看的时候尝试根据方法名或能够看到了解的相关信息作为线索猜测其作用

03. 实践运用

  • 实践对象:laravel5.7.*版本
  • 阅读原理:请求到控制器执行

好开始~~~

03.01 入口

对于PHP来说框架的入口文件基本都是从public/index.php开始

make(Illuminate\Contracts\Http\Kernel::class);
// 执行请求处理
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
// 响应输出
$response->send();
// 程序结束,并结束相关中间件
$kernel->terminate($request, $response);
?>

在一开始我们可以通过对public/index.php的阅读可以了解到整个框架程序的流程及过程;

  1. 初始化框架应用对象application
  2. 获取对http请求处理的核心实例
  3. 执行请求处理
  4. 响应输出
  5. 程序结束,并结束相关中间件

接下来我们进入bootstrap/app.php中了解application初始化的过程

03.02 application应用初始化

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

setBasePath($basePath);
      }
      $this->registerBaseBindings();
      $this->registerBaseServiceProviders();
      $this->registerCoreContainerAliases();
  }
  // ...
}
?>

看到方法的时候根据原则“3. 一定要看方法名,一定要看注释;6. 记录过程,及调度链”

然后一下子不就明了吗"是不是", 你:“对对对”

setBasePath($basePath);
    }
    // 注册应用绑定
    $this->registerBaseBindings();
    // 注册应用服务提供者
    $this->registerBaseServiceProviders();
    // 注册核心容器别名
    $this->registerCoreContainerAliases();
}
?>

再后续会直接在代码上运用3,6点;作者他不想每次都引到,心照不宣就好

好接下来我们基于“2. 源码探索讲究适量而止,切记不可死磕往死里点”再进一步了解每个方法的大概作用

先看setBasePath
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
instance('app', $this);

    $this->instance(Container::class, $this);

    $this->instance(PackageManifest::class, new PackageManifest(
        new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
    ));
}
?>

到此我们发现instance是一个较为核心的存在,基本上哪儿都有它的身影,那我们需要对它分析嘛?????

需要,但是不现在;因为我们目前的关键在于了解Application初始化过程;

源码阅读分析-PHP-laravel_第3张图片

根据7可以大概猜测它的作用是为了注册实例,也就是注册对象;再结合方法名即可理解,该方法就是把核心实例application注册到容器中;

$this->instance(PackageManifest::class, new PackageManifest(
    new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
));

这一段干了啥????你知道嘛??你:“什么鬼怎么扯我了,我就是看文章的怎么知道”;

"我当然知道你不知道"不知道咋办,当然是跳过呀。。。根据7的原则

继续看registerBaseServiceProviders
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
 [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()
);

看下这不就知道是那个对象了嘛;

源码阅读分析-PHP-laravel_第4张图片

调用的就是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. 续

希望本次对源码解读的过程可以帮助到你

.

你可能感兴趣的:(源码阅读分析-PHP-laravel)