说起PHP框架,就不得不提大名鼎鼎的Lavarel。作为一个“专为Web艺术家而创造”的框架,其优雅、简洁的开发体验吸引了一大批Web开发者,并成为PHP社区中使用最为广泛的全栈框架之一。虽然随着golang、nodejs等server化后台语言的大行其道,让传统的fast-cgi模式框架日渐式微,但Lavarel中采用的组件化开发、依赖注入、横向代理等设计思想,依然值得我们学习与借鉴。笔者在阅读Laravel框架源码的过程,总结了一些自己的理解与体会同大家分享。
本次分享内容如下:
1、框架结构 2、请求生命周期
1. 框架结构
1.1 安装
Laravel的安装方式有很多种,在此推荐官网使用的composer。Composer是目前主流的php依赖管理工具之一,其作用类似于nodejs的NPM,通过它能实现符合PSR-4/0规范的文件自动加载和分类,具体安装及使用方式可以参考这里。安装好composer之后,即可通过命令行一键安装部署Laravel:
代码语言:txt复制composer create-project laraval/laravel --prefer-dist
1.2 目录结构
安装完成后得到的项目文件目录如下:
这里简要介绍一下各个目录的作用:
- app——核心业务逻辑代码目录,也就是我们平时主要码码码的地方。初始包括Console(cli命令行模式)、Exception(异常处理)、Http(路由、请求、控制器、中间件等)、Provides(服务组件)四个核心目录。随着项目功能的扩增,还可以artisan命令行工具添加Listeners(事件监听)、Jobs(消息队列)等目录。
- bootstrap——框架启动和自动加载配置的相关文件目录。
- config——应用程序的各模块配置文件目录。
- database——数据库迁移及填充文件目录,这个在项目运维部署的时候很有用。
- public——对外提供访问的地方,包含应用的入口文件index.php,同时包含js、css等静态资源。
- resources——视图文件view的存放目录。
- storage——与应用即时存储相关的文件目录,包括编译后的视图组件、文件缓存、session文件和日志文件等。
- tests——自动化测试文件目录。
- vendor——项目依赖库文件,包括laravel核心等源码,由composer自动生成并更新。 此外,还有两个重要的文件composer.json和.env。前者是composer的包依赖配置文件,通过编写该文件我们可以告诉composer项目所依赖的库及其文件映射形式(PSR0、PSR4、classmap和files四种模式);后者是环境配置文件,当开发环境变更时,可以通过该文件的拷贝及修改实现项目部署的自动变更而无需修改业务代码。
2. 请求生命周期
任何一个web框架最重要的工作就是对网络请求的响应、处理及回包,因此理清请求生命周期是关键。Laravel的处理一次请求的工作流程可以大致分为七步:文件自动加载,服务容器启动与基础服务注册,web内核加载,请求初始化,请求处理与响应,响应发送,程序终止。
我们跟随程序入口文件index.php来梳理一下这个过程:
代码语言:txt复制require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(IlluminateContractsHttpKernel::class);
$response = $kernel->handle(
$request = IlluminateHttpRequest::capture()
);
$response->send();
$kernel->terminate($request, $response);
- 首先,程序引用了boostrap目录下的autoload.php文件,而这个文件又把php的类文件自动加载功能移交给composer的自动加载类Autoloader来完成( 想知道composer是如何实现自动加载的可以移步这里)
- 接着引用boostrap目录下的app.php文件来生成一个名为app的对象作为应用的全局服务容器(AppContainer)。什么是服务容器及其作用会在文章后面阐述。我们只需要知道,在这一步中主要完成了业务代码路径设置、项目基础服务注册、全局类别名注册等工作。
- app对象构造完成后,程序紧接着调用其make()方法获取了一个IlluminatContractsHttpKernel类下的kernel对象。kernel对象即是程序处理http请求的核心。
- 在处理请求之前需要先初始化请求,这个通过IlluminateHttpRequest的静态方法capture()完成。其原理是从PHP的超全局变量($_REQUEST、$_SERVER等)中构造出一个符合PSR规范的标准request对象。
- kernel对象的handle()接口作为一个流式接口,封装了请求路由、中间件链式调用、业务逻辑处理等一系列动作,并最终返回一个符合PSR规范的标准response对象。
- 调用response的send()方法将缓冲区的响应数据发送出去。
- 最终调用kernel的terminate()方法进行程序的收尾工作,如上下文清理、统计上报等。
如果再把这七步流程合并一下,laravel的整个生命周期大致可分为程序启动准备、请求处理、响应发送与程序终止三个阶段。下面我们分三个小节来分别介绍各个阶段的工作原理。
2.1 服务初始化
程序启动阶段主要进行文件自动加载器注册,服务容器初始化以及核心类的实例化。我们来看看bootstrapapp.php中服务容器是如何初始化的:
代码语言:txt复制// bootstrapapp.php
$app = new IlluminateFoundationApplication(
realpath(__DIR__.'/../')
);
$app->singleton(
IlluminateContractsHttpKernel::class,
AppHttpKernel::class
);
$app->singleton(
IlluminateContractsConsoleKernel::class,
AppConsoleKernel::class
);
$app->singleton(
IlluminateContractsDebugExceptionHandler::class,
AppExceptionsHandler::class
);
return $app;
// IlluminateFoundationApplication.php
public function __construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();
}
构造函数主要完成了app目录的设置、自我绑定、基础服务注册、常用类别名注册。这里的注册是什么意思呢?这就得先解释一下什么是服务容器。
在现代的程序设计中,为了解决不同的类之间相互耦合,接口与实现类之间绑定混乱的问题,往往采用依赖注入的方式将类之间的依赖关系从程序内部提到了外部容器来管理,即IoC(Inversion of Control)容器。其作用在于使用接口来统一获取某个类的实例,这个实例可能是该类本身的对象,也有可能是该类的子类的对象,一切取决于你指定的接口和实例的关系。而注册其实就是绑定这个指定的类的实例所需要的构造者的过程,这个构造者既可以是该实例的构造函数,也可以该实例的一个工厂函数。
在laravel中,服务容器以完全限定命名空间名称或用户自定义的别名(aliase)作为索引,将该类已有实例或实例的构造器存放到自身定义的instances和bingdings两个数组属性中。其中,instances存储共享实例,即整个程序中唯一实例:
代码语言:txt复制// IlluminateFoundationApplication.php
public function __construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();
}
// IlluminateContainerContainer.php
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) {
$concrete = $abstract;
}
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
bindings数组用来存储类的构造函数(Closure)。那么服务容器具体又是如何实现 服务名=》实例 的映射呢?答案是依赖解决resolve()方法。
代码语言:txt复制// IlluminateFoundationApplication.php
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
在resolve()函数中,先判断类abstract是否有注册别名,并根据注册名称寻找共享实例数组instances,找到就直接返回,没有则通过getConcrete()从绑定数组bindings中获取其子类。由与抽象类abstract可能嵌套绑定了多层子类,因此这里采用了isBuildabel()判断子类是否可实例化并递归调用make(子类)直到得到一个实例类或类构造器,并最终调用build($concrete)实现注入。接下来就是build函数,这里使用到了反射(ReflectionClass),也是整个服务容器中最核心的部分:
代码语言:txt复制// IlluminateContainerContainer.php
public function build($concrete)
{
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
- 先通过动态反射获取concrete的构造函数及其参数列表。
- 遍历该列表调用resolveDependencies()注入各参数的值。这里的参数如果是标量,先从传递给make()里parameters里找,没有的话再看是否有默认值;如果参数是一个对象,那就再递归调用make()解决依赖(没错,又是递归) 。
- 最后,得到已经填入参数值的参数数组并传给反射对象的newInstanceArgs(),一个concrete的实例就大功告成了。正是通过这个自顶向下的注入过程,有效避免了复杂依赖关系下大量new和make代码的编写。
通过服务容器注入的实例类统称为服务提供者类(ServiceProvider)。服务提供者在提供工厂接口构造实例之前,往往还需要完成类内部自定义的一些服务注册及启动工作,这是通过服务容器在其注册时调用服务提供者的register()和boot()接口完成的。
代码语言:txt复制// IlluminateFoundationApplication.php
public function register($provider, $options = [], $force = false)
{
if (($registered = $this->getProvider($provider)) && ! $force) {
return $registered;
}
if (is_string($provider)) {
$provider = $this->resolveProvider($provider);
}
if (method_exists($provider, 'register')) {
$provider->register();
}
$this->markAsRegistered($provider);
if ($this->booted) {
$this->bootProvider($provider);
}
return $provider;
}
protected function bootProvider(ServiceProvider $provider)
{
if (method_exists($provider, 'boot')) {
return $this->call([$provider, 'boot']);
}
}
回到前面的服务容器构造函数中,我们发现laravel在程序一开始主要注册了事件、日志、路由三个基础服务,分别用于管理程序的事件触发回调、日志格式化及持久化、请求路由。
代码语言:txt复制// IlluminateFoundationApplication.php
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
请求处理的核心是kernel。在bootstrap/app.php文件中laravel使用单例模式注册了一个AppHttpKernel类的实例来提供服务。我们先来看下类定义:
可以看到他的构造函数依赖于app和router两个对象,然鹅在public/index.php文件中我们只是调用$app->make(IlluminateContractsHttpKernel::class),并没像其传递这两个参数——因为服务容器已经帮我们“解决“”了这两个依赖。
Kernel内部定义还定义$middleware和$routeMiddleware两个中间件数组,前者是全局性的、对所有请求都会生效,而后者仅在请求命中相应路由时被调用。
代码语言:txt复制class Kernel extends HttpKernel
{
protected $middlewareGroups = [
'web' => [
AppHttpMiddlewareEncryptCookies::class,
IlluminateCookieMiddlewareAddQueuedCookiesToResponse::class,
IlluminateSessionMiddlewareStartSession::class,
// IlluminateSessionMiddlewareAuthenticateSession::class,
IlluminateViewMiddlewareShareErrorsFromSession::class,
AppHttpMiddlewareVerifyCsrfToken::class,
IlluminateRoutingMiddlewareSubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
protected $routeMiddleware = [
'auth' => IlluminateAuthMiddlewareAuthenticate::class,
'auth.basic' => IlluminateAuthMiddlewareAuthenticateWithBasicAuth::class,
'bindings' => IlluminateRoutingMiddlewareSubstituteBindings::class,
'can' => IlluminateAuthMiddlewareAuthorize::class,
'guest' => AppHttpMiddlewareRedirectIfAuthenticated::class,
'throttle' => IlluminateRoutingMiddlewareThrottleRequests::class,
];
protected $bootstrappers = [
IlluminateFoundationBootstrapLoadEnvironmentVariables::class,
IlluminateFoundationBootstrapLoadConfiguration::class,
IlluminateFoundationBootstrapHandleExceptions::class,
IlluminateFoundationBootstrapRegisterFacades::class,
IlluminateFoundationBootstrapRegisterProviders::class,
IlluminateFoundationBootstrapBootProviders::class,
];
}
此外,还有一个很重要的成员数组$bootstrappers,主要用于kernel实例化之前的一些准备工作。可以看到bootstrap包括加载环境变量、加载配置文件、异常处理、服务提供者注册和启动服务提供者六个步骤。限于篇幅,这里就不多做展开了。
2.2 请求捕捉
完成kernel的实例化之后,便可以开始处理请求了。在public/index.php文件的第4行中我们通过IlluminateHttpRequest::capture()来获取收到的Http请求实例。
代码语言:txt复制// Illuminate/Http/Request.php
public static function capture()
{
static::enableHttpMethodParameterOverride();
return static::createFromBase(SymfonyRequest::createFromGlobals());
}
public static function createFromBase(SymfonyRequest $request)
{
if ($request instanceof static) {
return $request;
}
$content = $request->content;
$request = (new static)->duplicate(
$request->query->all(), $request->request->all(), $request->attributes->all(),
$request->cookies->all(), $request->files->all(), $request->server->all()
);
$request->content = $content;
$request->request = $request->getInputSource();
return $request;
}
// symfony/http-foundation/Request.php
public static function createFromGlobals()
{
$request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);
if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
&& in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH'))
) {
parse_str($request->getContent(), $data);
$request->request = new ParameterBag($data);
}
return $request;
}
这里laravel底层调用了Symfony框架的SymfonyRequest::createFromGlobals()来获取一个Http请求对象request,并通过拷贝该对象的query、request等属性将其转换为Illuminate的Request对象。SymfonyRequest对象构造是通过PHP超全局变量$_GET、$_POST、$_SERVER、$_COOKIE、 $_FILES作为参数来封装的,一方面是为了添加更多的参数处理接口,另一方面是为了使整个request对象符合PSR7规范,感兴趣的同学可以去看一下Symfony源码。
2.3 请求响应
程序调用kernel的handle()方法来处理上面部分中捕捉到的请求对象request,并生成相应的响应对象response。
代码语言:txt复制// Illuminate/Foundation/Http/Kernel.php
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new EventsRequestHandled($request, $response)
);
return $response;
}
handle()函数主要做了三件事,一是启用CSRF保护,二是通过路由传输请求实例,最后调用events服务触发RequestHandled事件。重点关注一下sendRequestThroughRouter()这个过程:
代码语言:txt复制 // Illuminate/Foundation/Http/Kernel.php
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;
- 向管道发送请求对象$request;
- 向管道设置全局中间件$this->middleware;
- 找到匹配请求的路由并打包路由中间件及业务接口(这里先不触发)
- 将上一步打包好的闭包同步骤3中的中间件最终统一打包为一个嵌套的闭包并触发,注册好的所有闭包将按 前置中间件(FILO)->业务接口->后置中间件(FIFO)的次序 依次触发。
这里大家会有疑问,到底pipeline是怎么把中间件和业务接口打包在一起并处理中间件的前后关系呢?答案就是 array_reduce() 标准化闭包:
代码语言:txt复制//
public function send($passable)
{
$this->passable = $passable;
return $this;
}
public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
list($name, $parameters) = $this->parsePipeString($pipe);
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
$parameters = [$passable, $stack];
}
return $pipe->{$this->method}(...$parameters);
};
};
}
这里看到,前面的两步send()、through()其实只是把水(request)和泵(middleware)设置好了,真正的打包和调用是在then()中进行的。then()中利用了php标准库函数——array_reduce(array, callback, initializer),把array数组传递过来的闭包元素进行打包,合并成了一个嵌套N(=数组长度)层的闭包栈$pipeline,最终触发连锁调用。callback这个打包函数的处理过程如下:
- 接收当前的迭代累积值stack和下一个元素pipe,先判断pipe能否直接调用,如果能则直接调用返回,如果不能则继续;
- 判断pipe对象是否生成,如果未生成则通过服务容器获取,如果生成则准备好pipe处理所需的参数passable和stack;
- 最终调用pipe中通过{this->method}指定的某个方法,处理passable和stack并返回。
这里的闭包栈想要最终跑起来,必须满足两个前提:一是每一个pipe要么是闭包,要么具有名为{this->method}的函数;二是这个闭包或者名为{this->method}函数必须接受参数passable和stack,并返回新的passable。
我们以middlewares数组中的CheckForMaintenanceMode为例,看到确实有一个handle()方法满足这样的条件:
代码语言:txt复制// Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php
class CheckForMaintenanceMode
{
protected $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public function handle($request, Closure $next)
{
if ($this->app->isDownForMaintenance()) {
$data = json_decode(file_get_contents($this->app->storagePath().'/framework/down'), true);
throw new MaintenanceModeException($data['time'], $data['retry'], $data['message']);
}
return $next($request);
}
}
这本质上是一种装饰者模式的设计思想。只要每个中间件都提供handle()这个接口并按同样的规则返回下一个闭包next的调用,那我们便可以在不修改原有类的基础上动态的添加或减少处理功能而使框架的可扩展性大大增加。
此外, 在处理array_reduce()函数时通过array_reverse($this->pipes)把中间件数组进行了反转,并调用this->prepareDestination($destination)把业务接口函数放置在了反转数组顶部,这样在生成的函数栈调用次序就能与middlewares数组中定义时一致。
上述代码展示的是全局中间件的调用过程,而路由中间件转发过程和上面处理基本一致,只是多了一个路由匹配业务接口的过程:
代码语言:txt复制// Illuminate/Routing/Router.php
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(
$request, $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();
}
}
public function prepareResponse($request, $response)
{
if ($response instanceof PsrResponseInterface) {
$response = (new HttpFoundationFactory)->createResponse($response);
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response);
}
return $response->prepare($request);
}
2.4 响应发送与程序终止
响应的发送包括两部分内容,分别是响应头和响应主体(如果有)的发送。其中sendHeaders()函数主要遍历response对象的headers数组并用header()设置;sendContents()直接echo响应内容到输出缓存区。最后调用原生的fastcgi_finish_request()函数或自定义的closeOutputBuffers()方法冲刷所有响应的数据给客户端并结束请求。
代码语言:txt复制// symfony/http-foundation/Response.php
public function send()
{
$this->sendHeaders();
$this->sendContent();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (!in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) {
static::closeOutputBuffers(0, true);
}
return $this;
}
public function sendHeaders()
{
// headers have already been sent by the developer
if (headers_sent()) {
return $this;
}
foreach ($this->headers->allPreserveCase() as $name => $values) {
foreach ($values as $value) {
header($name.': '.$value, false, $this->statusCode);
}
}
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
return $this;
}
public function sendContent()
{
echo $this->content;
return $this;
}
public static function closeOutputBuffers($targetLevel, $flush)
{
$status = ob_get_status(true);
$level = count($status);
$flags = defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE) : -1;
while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) {
if ($flush) {
ob_end_flush();
} else {
ob_end_clean();
}
}
}
在完成响应的发送之后,接下来便是整个程序的终止了。这里主要分为全局中间件的清理接口($this->middleware),以及服务容器中注册的各服务回调($this->terminatingCallbackss)。
代码语言:txt复制// Illuminate/Foundation/Http/Kernel.php
public function terminate($request, $response)
{
$this->terminateMiddleware($request, $response);
$this->app->terminate();
}
protected function terminateMiddleware($request, $response)
{
$middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge(
$this->gatherRouteMiddleware($request),
$this->middleware
);
foreach ($middlewares as $middleware) {
if (! is_string($middleware)) {
continue;
}
list($name, $parameters) = $this->parseMiddleware($middleware);
$instance = $this->app->make($name);
if (method_exists($instance, 'terminate')) {
$instance->terminate($request, $response);
}
}
}
public function terminate()
{
foreach ($this->terminatingCallbacks as $terminating) {
$this->call($terminating);
}
}
以上便是一次请求触发的整个laravel工作流程了。其中还有许多细节值的研究,笔者将在后续的文章中探讨。