说起PHP框架,就不得不提大名鼎鼎的Lavarel。作为一个“专为Web艺术家而创造”的框架,其优雅、简洁的开发体验吸引了一大批Web开发者,并成为PHP社区中使用最为广泛的全栈框架之一。虽然随着golang、nodejs等server化后台语言的大行其道,让传统的fast-cgi模式框架日渐式微,但Lavarel中采用的组件化开发、依赖注入、横向代理等设计思想,依然值得我们学习与借鉴。笔者在阅读Laravel框架源码的过程,总结了一些自己的理解与体会同大家分享。
本次分享内容如下:
1、框架结构 2、请求生命周期
Laravel的安装方式有很多种,在此推荐官网使用的composer。Composer是目前主流的php依赖管理工具之一,其作用类似于nodejs的NPM,通过它能实现符合PSR-4/0规范的文件自动加载和分类,具体安装及使用方式可以参考这里。安装好composer之后,即可通过命令行一键安装部署Laravel:
composer create-project laraval/laravel --prefer-dist
安装完成后得到的项目文件目录如下:
这里简要介绍一下各个目录的作用:
任何一个web框架最重要的工作就是对网络请求的响应、处理及回包,因此理清请求生命周期是关键。Laravel的处理一次请求的工作流程可以大致分为七步:文件自动加载,服务容器启动与基础服务注册,web内核加载,请求初始化,请求处理与响应,响应发送,程序终止。
我们跟随程序入口文件index.php来梳理一下这个过程:
require __DIR__.'/../bootstrap/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);
如果再把这七步流程合并一下,laravel的整个生命周期大致可分为程序启动准备、请求处理、响应发送与程序终止三个阶段。下面我们分三个小节来分别介绍各个阶段的工作原理。
程序启动阶段主要进行文件自动加载器注册,服务容器初始化以及核心类的实例化。我们来看看\bootstrap\app.php中服务容器是如何初始化的:
// \bootstrap\app.php
$app = new Illuminate\Foundation\Application(
realpath(__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;
// Illuminate\Foundation\Application.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存储共享实例,即整个程序中唯一实例:
// Illuminate\Foundation\Application.php
public function \_\_construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();
}
// Illuminate\Container\Container.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()方法。
// Illuminate\Foundation\Application.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),也是整个服务容器中最核心的部分:
// Illuminate\Container\Container.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);
}
通过服务容器注入的实例类统称为服务提供者类(ServiceProvider)。服务提供者在提供工厂接口构造实例之前,往往还需要完成类内部自定义的一些服务注册及启动工作,这是通过服务容器在其注册时调用服务提供者的register()和boot()接口完成的。
// Illuminate\Foundation\Application.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在程序一开始主要注册了事件、日志、路由三个基础服务,分别用于管理程序的事件触发回调、日志格式化及持久化、请求路由。
// Illuminate\Foundation\Application.php
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
请求处理的核心是kernel。在bootstrap/app.php文件中laravel使用单例模式注册了一个App\Http\Kernel类的实例来提供服务。我们先来看下类定义:
可以看到他的构造函数依赖于app和router两个对象,然鹅在public/index.php文件中我们只是调用$app->make(Illuminate\Contracts\Http\Kernel::class),并没像其传递这两个参数——因为服务容器已经帮我们“解决“”了这两个依赖。
Kernel内部定义还定义$middleware和$routeMiddleware两个中间件数组,前者是全局性的、对所有请求都会生效,而后者仅在请求命中相应路由时被调用。
class Kernel extends HttpKernel
{
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
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,
];
}
此外,还有一个很重要的成员数组$bootstrappers,主要用于kernel实例化之前的一些准备工作。可以看到bootstrap包括加载环境变量、加载配置文件、异常处理、服务提供者注册和启动服务提供者六个步骤。限于篇幅,这里就不多做展开了。
完成kernel的实例化之后,便可以开始处理请求了。在public/index.php文件的第4行中我们通过Illuminate\Http\Request::capture()来获取收到的Http请求实例。
// 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源码。
程序调用kernel的handle()方法来处理上面部分中捕捉到的请求对象request,并生成相应的响应对象response。
// 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 Events\RequestHandled($request, $response)
);
return $response;
}
handle()函数主要做了三件事,一是启用CSRF保护,二是通过路由传输请求实例,最后调用events服务触发RequestHandled事件。重点关注一下sendRequestThroughRouter()这个过程:
// 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是怎么把中间件和业务接口打包在一起并处理中间件的前后关系呢?答案就是 array_reduce()+标准化闭包:
//
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这个打包函数的处理过程如下:
这里的闭包栈想要最终跑起来,必须满足两个前提:一是每一个pipe要么是闭包,要么具有名为{this->method}的函数;二是这个闭包或者名为{this->method}函数必须接受参数passable和stack,并返回新的passable。
我们以middlewares数组中的CheckForMaintenanceMode为例,看到确实有一个handle()方法满足这样的条件:
// 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数组中定义时一致。
上述代码展示的是全局中间件的调用过程,而路由中间件转发过程和上面处理基本一致,只是多了一个路由匹配业务接口的过程:
// 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);
}
响应的发送包括两部分内容,分别是响应头和响应主体(如果有)的发送。其中sendHeaders()函数主要遍历response对象的headers数组并用header()设置;sendContents()直接echo响应内容到输出缓存区。最后调用原生的fastcgi_finish_request()函数或自定义的closeOutputBuffers()方法冲刷所有响应的数据给客户端并结束请求。
// 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)。
// 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工作流程了。其中还有许多细节值的研究,笔者将在后续的文章中探讨。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。