laravel源码浅析

  • laravel版本5.8

入口

当你想要弄清楚任何一件事物的内部构造时,一定需要先找到入口,因此第一件要做的事情就是找到入口文件 public/index.php

<?php
define('LARAVEL_START', microtime(true));
require __DIR__.'/../vendor/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);

精简一下,只有9行代码,非常的简洁,包括<?php在内只有10行代码

  1. 定义常量 表示laravel运行的开始时间
  2. 包含composer的自动加载 曾经有psr0 和psr4规范 但是在我写下这个文档的时间(2019-06-04)已经废弃psr0很久了
  3. 加载laravel引导 (把laravel想象成一个操作系统 此处的引导就是操作系统的那512M字节的数据),并得到laravel的应用变量
  4. 使用$app变量,制作一个Http内核$kernel (没错 就是操作系统的那个内核)
  5. 使用内核($kernel)处理 $request 并得到一个$response
  6. 发送$response
  7. $kernel内核终止请求与响应

至此一次完整的laravel的生命周期就结束了,同时思考一下http协议,一个完整的HTTP事务应该是这个样子

客户端发送请求->服务器处理请求并响应->客户端收到响应

对于一个http请求来说,只有请求和响应两个过程,而laravel的入口恰好完美的诠释了这个神圣而又复杂的过程.

写代码虽然很简单,但是能把代码写的像散文一样一定是非常困难的,而这个入口文件就让人心旷心神

引导

$app = require_once DIR.'/../bootstrap/app.php';

<?php

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

$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;

大多数的其他框架都倾向于把引导放在入口里,但是laravel是单独放了一个文件,并且这个文件也非常的简洁

以代码量计算只有17行,以功能计算只有5个

  1. 根据路径new出一个Application类
  2. 绑定一个Http内核单例
  3. 绑定一个Console内核单例
  4. 绑定一个异常处理类单例
  5. 返回$app实例

不需要知道Application内部做了什么 也不需要知道singleton内部做了什么,仅仅只阅读这几行代码,就能窥豹一斑

laravel的app实例仅仅只有三个功能

  • http请求处理 由http内核提供
  • 控制台命令处理 由Console内核提供
  • app异常处理 由Exceptions\Handler提供

最后返回一个app实例,这个app实例是继承于Container类的,这里运用反射技术来提供了一个根据类名生成类实例的功能,具体实现可以查看laravel/framework/src/illuminate/Container/Container.php的第790行代码

Container容器的具体用法在后面的篇章中会详细的进行解释,此处不多做介绍

http 内核

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

使用$app实例制作一个实现了Http/Kernel契约的内核实例

还记得引导文件中的这一行代码么

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

没错,这个地方返回的内核就是绑定的App\Http\Kernel::class 实例,并且仅初始化了基本的中间件而已

这里make的是一个imterface,因此只要你的Kernel实现了Illuminate\Contracts\Http\Kernel::class这个接口,那么在上面的文件中你完全可以自定义属于自己的内核,虽然没有什么必要

处理请求

$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);

先看这一行代码 ,捕获http请求

$request = Illuminate\Http\Request::capture()

暂时不深入剖析源代码,根据文档中的描述来看以及经验来看,都应该想到这里的request就是

$_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER 中携带的信息,虽然会做一些处理,但是一个基本的http请求中都必须包含这些信息,这是Http协议决定了

接着是内核处理这个请求

$response = $kernel->handle($request);

这个地方就勉为其难的看一下源代码好了,目光定位到Illuminate\Foundation\Http\Kernel 的111行

顺便我们再精简一下,把异常处理去掉,得到代码如下

public function handle($request)
{
    $request->enableHttpMethodParameterOverride();
    $response = $this->sendRequestThroughRouter($request);
    $this->app['events']->dispatch(
        new Events\RequestHandled($request, $response)
    );
    return $response;
}

做了三件事情

  1. 开启http 方法重写 (这个地方其实仅仅是为了支持 传统表单页面中 用_method=put 来代替PUT请求这样的功能的)

    具体代码可以查看Symfony\Component\HttpFoundation\Request 1229行

  2. sendRequestThroughRouter发送请求到路由器并得到响应$response

  3. 分发事件处理

发送请求到路由器

这里是发送请求到路由器 那么接下来肯定是路由分发了

目光定位到Illuminate\Foundation\Http\Kernel 149行代码

protected function sendRequestThroughRouter($request)
{
    $this->app->instance('request', $request);

    Facade::clearResolvedInstance('request');

    $this->bootstrap();

    return (new Pipeline($this->app))
        ->send($request)
        ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
        ->then($this->dispatchToRouter());
}

只需要看最后一行代码就可以了,这里有两个关键词

  • Pipeline 管道
  • dispatchToRouter 路由分发

记得最开始make内核的时候,在App\Http\Kernel 文件里有许多的中间件数组配置,那么这里的Pipeline做的事情只有一件,让这些左右的中间件变成一个队列一样,这些请求依次穿过(through)这些中间件, 注意这里的用词是through 穿过,然后then 路由分发dispatchToRouter , 读起来很有感觉

dispatchToRouter路由分发就更简单了,在laravel中所有的路由都是预先配置好的,通过request请求中的path找到配置好的route,如果命中则交给route中定义的控制器或者闭包来进行处理,如果没有命中,那么则抛出异常

当然 抛出异常这个过程是在上一步处理请求中的 handle中执行的,因为我精简掉了异常处理代码 所以看不到

最后route的dispatch过程,可以把代码定位到Illuminate\Routing\Roouter 730行

经过了一些列的操作,甚至包括中间件

在laravel中 请求和响应是都要经过中间件的 代码分别在

请求: Illuminate\Foundation\Http\Kernel 149行代码

响应: Illuminate\Routing\Roouter 656行

public static function toResponse($request, $response)
{
    if ($response instanceof Responsable) {
        $response = $response->toResponse($request);

    }

    if ($response instanceof PsrResponseInterface) {
        $response = (new HttpFoundationFactory)->createResponse($response);

    } elseif ($response instanceof Model && $response->wasRecentlyCreated) {
        $response = new JsonResponse($response, 201);

    } elseif (! $response instanceof SymfonyResponse &&
        ($response instanceof Arrayable ||
        $response instanceof Jsonable ||
        $response instanceof ArrayObject ||
        $response instanceof JsonSerializable ||
        is_array($response))) {
        $response = new JsonResponse($response);

    } elseif (! $response instanceof SymfonyResponse) {
        $response = new Response($response);

    }

    if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
        $response->setNotModified();
    }

    return $response->prepare($request);
}

这里最后一定会得到一个Illuminate\Http\Response的实例

发送响应

$response->send();

根据上面所有的流程,我们最终得出, 返回了一个Response实例,那么接下来我们一起看看laravel 是怎么把这个发送这个response的

注意看这里 这个响应是自己发送的 并不是内核发送的 内核仅仅是根据请求 并且通过了管道内的中间件 最后得到了一个Response 实例

目光定位到Symfony\Component\HttpFoundation 的 373行代码

精简一下变成这样

public function send()
{
    $this->sendHeaders();
    $this->sendContent();
}
  • $this->sendHeaders(); 这行代码输出头信息 Symfony\Component\HttpFoundation 330行代码
header($name.': '.$value, $replace, $this->statusCode);
header('Set-Cookie: '.$cookie->getName().strstr($cookie, '='), false, $this->statusCode);
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);

这个地方仅仅只是利用header 输出了一些头部信息

  • $this->sendContent(); 这行代码输出文本
public function sendContent()
{
    echo $this->content;
    return $this;
}

和预想中的一样,这个地方仅仅是echo了一些东西而已 没有特殊的地方

通过代码分析 我们可以知道 $response->send();只做了两件事情 输出头信息以及内容, 和输出所有的网页一样,头部和内容

内核终止请求与响应

$kernel->terminate($request, $response);

目光定位到Illuminate\Foundation\Application 中的1006行代码

public function terminate()
{
    foreach ($this->terminatingCallbacks as $terminating) {
        $this->call($terminating);
    }
}

这里的代码很简单,在响应成功之后,执行一些回调函数

总结

抛开所有的类库 框架 mvc等等各种各样的概念来看,laravel其实是一个非常简单的东西,当然http协议也是一样

创建应用实例->接受请求->得到响应->发送响应(header 一些东西 echo 一些东西)->结束

发表评论