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 一些东西)->结束

gitbook配置

gitbook配置



{ "title": "标题", "author": "crisen", "description": "描述", "language": "zh-cn", "gitbook": "3.2.3", "structure": { "readme": "README.md" }, "links": { "sidebar": { "crisen": "https://www.crisen.org" } }, "plugins": [ "-lunr", "-search", "search-plus", "expandable-chapters-small", "edit-link", "advanced-emoji", "anchors", "sitemap-general", "copy-code-button", "alerts", "ace", "splitter", "-sharing", "anchor-navigation-ex-toc", "theme-api" ], "pluginsConfig": { "edit-link": { "base": "http://github.com/crisenchou/docs/edit/master", "label": "编辑" }, "sitemap-general": { "prefix": "http://www.crisen.org" }, "theme-api": { "theme": "light" } } }

gitlab-ci-yml配置

.gitlab-ci.yml

.gitlab-ci.yml是从7.12版本开始启用的项目CI配置文件,本文件应该放在项目的根目录中,里边描述了你的项目应该如何构建。

一个yaml文件定义了一组各不相同的job,并定义了它们应该怎么运行。这组jobs会被定义为yaml文件的顶级元素,并且每个job的子元素中总有一个名为script的节点。

A set of jobs,强调是Set所以名称必须不同。

job1:
    script: "job1的执行脚本命令(shell)“

job2:
    script:
        - "job2的脚本命令1(shell)"
        - “job2的脚本命令2(shell)”

上述的文件是最简单的CI配置例子,每个job都执行了不同的命令,其中job1只执行了1条命令,job2通过数组的定义按顺序执行了两条命令。

当然,每个命令可以直接执行代码(./configure;make;make install)或者在仓库目中运行另一个脚本(test.sh)。

Job配置会被Runner读取并用于构建项目,并且在Runner的环境中被执行。很重要的一点是,每个job都会独立的运行,相互间并不依赖。

Each job is run independently from each other.

下边是一个比较复杂的CI配置文件例子:

image: ruby:2.1
services:
  - postgres

before_script:
  - bundle install

after_script:
  - rm secrets

stages:
  - build
  - test
  - deploy

job1:
  stage: build
  script:
    - execute-script-for-job1
  only:
    - master
  tags:
    - docker

以下是一些保留字,这些单词不能被用于命名job:

保留字 必填 介绍
image 构建使用的Docker镜像名称,使用Docker )作为Excutor时有效}
services 使用的Docker服务,使用Docker 作为Excutor时有效
stages 定义构建的stages
types stages的别名
before_script 定义所有job执行之前需要执行的脚本命令
after_script 定义所有job执行完成后需要执行的脚本命令
variables 定义构建变量
cache 定义一组文件,该组文件会在运行时被缓存,下次运行仍然可以使用

image和services

这两个关键字允许用户自定义运行时使用的Docker image,及一组可以在构建时使用的Service。这个特性的详细说明在 Docker integration – GitLab Documentation中。

before_script

被用于定义所有job被执行之前的命令,包括部署构建环境。它可以是一个数组元素或者一个多行文本元素。

after_script

Gitlab8.7及GitlabRunner1.2以上才支持本关键字

被用于定义所有job被执行完之后要执行的命令。同样可以是一个数组或者一个多行文本。

stages

用于定义可以被job使用的stage. 定义stages可以实现柔性的多stage执行管道。

The specification of stages allows for having flexible multi stage pipelines.

stages定义的元素顺序决定了构建的执行顺序:

  1. 同样stage的job是并行执行的。
  2. 下一个stage的jobs是当上一个stage的josb全部执行成功后才会执行。

让我们看一个简单的例子,以下有3个stage:

stages:
    - build
    - test
    - deploy
  1. 首先,所有stage属性为build的job会被并行执行.
  2. 如果所有stage属性为build的job都执行成功了,stage为test的job会被并行执行。
  3. 如果所有stage为test的job都执行成功了,则stage为deploy的job会被并行执行。
  4. 如果所有stage为deploy的job都执行成功了,则提交被标记为success。
  5. 如果任何一个前置job失败了,则提交被标记为failed并且任何下一个stage的job都不会被执行。

两个有价值的提示:

  1. 如果配置文件中没有定义stages,那么默认情况下的stages属性为build、test和deploy。
  2. 如果一个job没有定义stage属性,则它的stage属性默认为test。

types

stages的别名。

variables

从Runner0.5.0开始支持

GitlabCI允许你在.gitlab-ci.yml文件中设置构建环境的环境变量。这些变量会被存储在git仓库中并用于记录不敏感的项目配置信息,例如:

variables:
  DATABASE_URL: "postgres://postgres@postgres/my_database"

这些变量会在之后被用于执行所有的命令和脚本。Yaml配置的变量同样会被设置于所有被建立的service容器中,这可以让使用更加方便。variables同样可以设置为job级别的属性。

除了用户自定义的变量外,同样有Runner自动设置的变量。例如说CI_BUILD_REF_NAME,这个变量定义了正在构建的git仓库的branch或者tag的名称。

除了在.gitlab-ci.yml中设置的非敏感变量外,Gitlab的UI中还提供了设置敏感变量的功能。

更多关于变量的说明

cache

Runner0.7.0之后被介绍

用于定义一系列需要在构建时被缓存的文件或者目录。只能定义在项目工作环境中的目录或者文件。

cache is used to specify a list of files and directories which should be cached between builds.

默认情况下缓存功能是对每个job和每个branc都开启的。

如果cache在job元素之外被蒂尼,这意味着全局设置,并且所有的job会使用这个设置。以下是一些例子:

  • 缓存所有在binaries目录中的文件和.config文件:
rspec:
  script: test
  cache:
    paths:
    - binaries/
    - .config
  • 缓存所有git未追踪的文件
rspec:
  script: test
  cache:
    untracked: true
  • 缓存所有git未追踪的文件和在binaries目录下的文件
rspec:
  script: test
  cache:
    untracked: true
    paths:
    - binaries/
  • job级别定义的cache设置会负载全局级别的cache配置。该例子将仅缓存目录binaries
cache:
  paths:
  - my/files

rspec:
  script: test
  cache:
    paths:
    - binaries/

cache功能仅提供最努力的支持,但不要指望它总能生效。更多的实现细节,可以参阅GitlabRunner。

The cache is provided on a best-effort basis, so don’t expect that the cache will be always present.

cache:key

Runner1.0.0中被介绍

key允许你在不同的job之间定义cache的种类,例如所有job共享的cache单例、一个job一个的cache、一个branch一个的cache等等。

这允许你更便利的使用缓存,允许你在不同的job甚至不同的branche间共享缓存。

cache:key变量可以使用任何之前定义的变量。

例子

  • 缓存每个job
cache:
  key: "$CI_BUILD_NAME"
  untracked: true

缓存每个branch

cache:
  key: "$CI_BUILD_REF_NAME"
  untracked: true

缓存每个job和branch

cache:
  key: "$CI_BUILD_NAME/$CI_BUILD_REF_NAME"
  untracked: true

缓存每个branch和每个stage

cache:
  key: "$CI_BUILD_STAGE/$CI_BUILD_REF_NAME"
  untracked: true

这段感觉怎么翻译都啰嗦,就这样吧

如果是在Windows环境下开发,需要使用%代替$标识环境变量:

cache:
  key: "%CI_BUILD_STAGE%/%CI_BUILD_REF_NAME%"
  untracked: true

Jobs

.gitlab-ci.yml允许配置无限个job。每个job必须有一个唯一的名称,并且不能是前文所说的任何一个保留字。一个job由一系列定义构建行为的参数组成。

job_name:
  script:
    - rake spec
    - coverage
  stage: test
  only:
    - master
  except:
    - develop
  tags:
    - ruby
    - postgres
  allow_failure: true
关键字 必要性 介绍
script 定义了Runner会执行的脚本命令
image 使用Docker镜像,多内容参考使用Docker镜像
services 使用Docker服务,更多内容参考 使用Docker镜像
stage 定义构建的stage(默认:test
type stage的别名
variables 定义job级别的环境变量
only 定义一组构建会创建的git refs
except 定义一组构建不会创建的git refs
tags 定义一组tags用于选择合适的Runner
allow_failure 允许构建失败。失败的构建不会影响提交状态。
when 定义什么时候执行构建。可选:on_successon_failurealwaysmanual
dependencies 定义当前构建依赖的其他构建,然后你可以在他们之间传递artifacts
artifacts 定义一组构建artifact。
cache 定义一组可以缓存以在随后的工作中共享的文件
before_script 覆写全局的before_script命令
after_script 覆写全局的after_script命令
environment 定义当前构建完成后的运行环境的名称

script

这是一个或一组会被Runner执行的shell脚本。例如:

jobA:
  script: "bundle exec rspec"

jobB:
  script:
    - uname -a
    - bundle exec rspec

有时候,script命令需要被包在双引号或者单引号之间。例如,包含符号(:)的命令需要写在引号中,这样yaml的解析器才能正确的解析,而不会误以为这是一组键值对。在使用包含以下符号的命令时要特别小心:
:{}[],&*#?|-<>=!%@、```

stage

stage 允许分组构建不同的stage。构建相同的stage时是并行进行的。更多关于stages的说明可以参阅stages.

only and except

onlyexcept 是管理job在被构建时的refs策略的的参数。

  1. only 设置了需要被构建的branches和tags的名称。
  2. except 设置了不需要被构建的branches和tags的名称。

这里有一些使用refs策略的规则:

  • onlyexcept 是可以相互包含的。如果一个job中 onlyexcept都被定义了,ref会同时被 only 过滤 except
  • onlyexcept 支持正则表达式。
  • onlyexcept 可以使用这几个关键字: branches, tagstriggers
  • onlyexcept 允许通过定义仓库路径的方式来过滤要fork的job。

在以下的例子中,job将只会启动以issue-开头的refs,并且跳过所有的branches。

job:
    # 使用正则
    only:
        - /^issue-.*$/
  # 使用关键字
    except:
        - branches

在下边的例子中,job将仅对被打了tag的refs,或者是被API触发器触发时
才执行:

job:
  # 使用关键字
  only:
    - tags
    - triggers

仓库路径可以用于让job仅为父仓库执行并且不fork:

The repository path can be used to have jobs executed only for the parent repository and not forks:

job:
  only:
    - branches@gitlab-org/gitlab-ce
  except:
    - master@gitlab-org/gitlab-ce

上述的例子将会为除了master以外的所有的branches运行job

job variables

可以通过使用variables定义job级别的构建变量。

gitlab runner 安装

gitlab runner 安装

  • 平台GNU/LINUX
  • x86_64 架构

获取安装脚本

 # Linux x86-64
sudo wget -O /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

给予可执行权限

 sudo chmod +x /usr/local/bin/gitlab-runner

如果是docker的gitlab 则可选择安装docker

 curl -sSL https://get.docker.com/ | sh

创建gitlab runner用户

 sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

启动gitlab runner服务

 sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
 sudo gitlab-runner start