laravel系列之lumen一个完整请求分析[生命周期]
今天分析下lumen(laravel分支下的一个轻量版本是和纯粹API开发的)的一个完整的请求生命周期,前期工作是你知道laravel的自动加载是通过composer来实现的,以及对laravel有些许了解,(争取新手也能看懂)
1、(index.php)同样按照惯例所有的入口文件都是index.php我们看到文件里面只有两行代码
$app=require__DIR__.'/../bootstrap/app.php'; $app->run();
2、(bootstrap/app.php)接下来看这个文件,第一行代码就是包含了autoload.php文件,这个文件是composer自动生成的
require_once __DIR__.'/../vendor/autoload.php';
3、(autoload.php)这个文件里面又引用了这个文件
require_once__DIR__.'/composer/autoload_real.php';
并最终调用了一个方法,类似这样的类下面的一个方法
return ComposerAutoloaderInit974c5c143f184f4f159efac86bfe75a8::getLoader();
4、(autoload_real.php)接下来分析文件的getLoader()方法,这个方法里面有这样一句自动注册的代码,注册的方法是本类下面的 loadClassLoader()
spl_autoload_register:这个方法是重点,你要理解透彻,当有对象被实例化也就是new的时候就会自动调用这个方法。
spl_autoload_register(array('ComposerAutoloaderInit974c5c143f184f4f159efac86bfe75a8', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader();
所以这里new \Composer\Autoload\ClassLoader()的代码执行后就会调用spl_autoload_register()触发下面这个方法
public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } }
这个方法应该是安全性检测,检测实例化的第一个对象是不是Composer\Autoload\ClassLoader。
通过上面的实例化后 self::$loader已经是Composer\Autoload\ClassLoader这个类的实例对象了。
5、(Composer\Autoload\ClassLoader)我们对这个类进行简单的分析,发现有很多个私有属性,名称类似Psr0Psr4和ClassMap等,这里的这些私有属性就是用来装后面要用到的各种相对应的自动加载的对应关系。
接着我们回到第4步骤的文件中继续分析:
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit974c5c143f184f4f159efac86bfe75a8::getInitializer($loader));
这里做一个php版本的判断,针对特殊情况的一个处理(php大于5.6版本,并且不是HHVM平台,并且没有使用Zend Guard编码),这里的autoload_static.php文件里面就是各种对应关系,这里包含了你框架内的所有对应关系,包括你安装的第三方的和自己定义的,然后调用这个文件里面的getInitializer()方法。我们发现这里的方法的作用就是将上面的各种对应关系装入到self::$loader的对象中。
如果不满足特殊条件就分三次不同的方法将对应关系扔到self::$loader对象里面。
} else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } }
autoload_namespaces.php和/autoload_psr4.php文件都是 命名空间和文件路径(文件夹)的对应关系,而autoload_classmap.php文件是命名空间和文件的对应关系。
6、到这里自动加载所需的所有文件都注册填装好了,但是还没有注册接下来的关键是:
$loader->register(true); 将所有的加载都注册了,上面所有的填充都是为这部做准备。
7、下面的代码就是完全以require的形式将自定的几个自动加载文件包含进来即可,就是框架里composer.json的autoload.files节点里面的文件。
if ($useStaticLoader) { $includeFiles = Composer\Autoload\ComposerStaticInit974c5c143f184f4f159efac86bfe75a8::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequire974c5c143f184f4f159efac86bfe75a8($fileIdentifier, $file); } return $loader;
8、经过以上的步骤,所有需要的东西都已就自动注册好了,接下来我们回到第二步骤继续分析:
try { (new Dotenv\Dotenv(__DIR__.'/../'))->load(); } catch (Dotenv\Exception\InvalidPathException $e) { // }
这里开始加载.env这个配置文件了,Dotenv.php文件的loadData()方法里面又调用了Loader类和这个类里面的load()方法,我们直接分析这个类才是重点:
调用这个类的时候给了两个参数到构造方法,一个是文件路径$filePath和另一个boolen类型的参数,然后我们看load()方法
public function load() { $this->ensureFileIsReadable(); $filePath = $this->filePath; $lines = $this->readLinesFromFile($filePath); foreach ($lines as $line) { if (!$this->isComment($line) && $this->looksLikeSetter($line)) { $this->setEnvironmentVariable($line); } } return $lines; }
首先做一个常规的检测判断给的文件是否是正常可读的文件,是否存在,紧接着将给的文件通过php自带的file()方法读成一个数组,这是时候返回的是带有.env里面注释的行,紧接着遍历这个数组,处理注释行,也就是#开头的行。
然后将处理好的行通过setEnvironmentVariable()这个方法(具体你可以详细分析)将配置文件里面的配置信息分别放到全局数组$_ENV和$_SERVER中。
最后返回处理好的数组,放到Dotenv.php文件的$loader成员属性中,到这里配置文件已经加载完了。
9、接着回到第二步骤的文件分析:
$app = new Laravel\Lumen\Application( realpath(__DIR__.'/../') )
这个文件继承了Container类,这个就是laravel的核心容器类了。
这个文件还使用了两个核心的trait文件
use Concerns\RoutesRequests, Concerns\RegistersExceptionHandlers;
一个是路由相关的trait和异常相关的trait
好的我们开始分析他的构造方法:
public function __construct($basePath = null) { date_default_timezone_set(env('APP_TIMEZONE', 'UTC')); $this->basePath = $basePath; $this->bootstrapContainer(); $this->registerErrorHandling(); }
首先这里设置了市区,这个也是必须的要么会有时间戳不正确的问题,你可以看到他开始使用env配置文件了,因为之前我们的env配置文件已经加载好了所以这里完全可以使用。然后设置网站路径为$basePath。
bootstrapContainer()方法:
protected function bootstrapContainer() { static::setInstance($this); $this->instance('app', $this); $this->instance('Laravel\Lumen\Application', $this); $this->instance('path', $this->path()); $this->registerContainerAliases(); }
首先将本类放进$instance成员属性 static::setInstance($this);后面方便使用,其实就是为了方便后面的app()方法的使用,剩下的几行代码就是接着往容器里面装东西(实例)和别名。
接下来是这个方法registerErrorHandling()的分析:
protected function registerErrorHandling() { error_reporting(-1); set_error_handler(function ($level, $message, $file = '', $line = 0) { if (error_reporting() & $level) { throw new ErrorException($message, 0, $level, $file, $line); } }); set_exception_handler(function ($e) { $this->handleUncaughtException($e); }); register_shutdown_function(function () { $this->handleShutdown(); }); }
首先是启用php的所有错误报告“error_reporting(-1)”,然后自定义了错误信息处理,异常信息处理,和当php中止后执行一个函数就是$this->handleShutdown();方法。简单理解这个方法就是laravel将php抛出的错误,异常进行了接管,自定义处理了,所以你能能看见那个经典的错误页面。
10、在回到第2步骤的app.php文件继续分析
$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class ); $app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class );
接下来又绑定了两个类到bindings成员属性里,后面会用到,先分析singleton()方法实际上是bind()方法
$this->dropStaleInstances($abstract);
如果以前已经绑定了 先删除调以前的实例 在重新绑定
if (!$concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete); }
如果给定的第二个参数不是一个闭包,则组装成闭包,返回。
$this->bindings[$abstract] = compact('concrete', 'shared');
装到bindings属性内。
protected function getClosure($abstract, $concrete) { return function ($c, $parameters = []) use ($abstract, $concrete) { $method = ($abstract == $concrete) ? 'build' : 'make'; return $c->$method($concrete, $parameters); }; }
这个方法组装成一个闭包返回,并且给比表两个参数,一个是$c另一个是$parameters,在调用的时候会给出,$c就是容器本身,$parameters是所需参数。
11、回到步骤2的文件app.php继续分析,接下来的代码是路由相关的:
$app->group(['namespace' => 'App\Http\Controllers'], function ($app) { require __DIR__.'/../app/Http/routes.php'; });
这段代码比较接单就是通过group方法分离出中间件(如果有的话)并引用routes.php文件,接下来看routes.php这个路有文件,里面一般就是我们方路由的地方各种的 get, post,any等方法的路由。
以欢迎页为例:
$app->get('/', function () use ($app) { return $app->version(); });
调用了get()这个方法
public function get($uri, $action) { $this->addRoute('GET', $uri, $action); return $this; }
这里就是路由的核心了,通过addRoute方法添加你对应的路由,addRoute()的第一个参数也可以是个数组,any()方法就是这么实现的,
通过分析代码我们可以看见 any()还是比较消耗内存的,会将每条路由分别存储多条,所以不推荐用any
if (is_array($method)) { foreach ($method as $verb) { $this->routes[$verb . $uri] = ['method' => $verb, 'uri' => $uri, 'action' => $action]; } } else { $this->routes[$method . $uri] = ['method' => $method, 'uri' => $uri, 'action' => $action]; }
通过addRoute()方法已经将你所有写的路由都放在了routes属性内了。
12、最有一个步骤,我们回到第1步的index.php文件,调用run()方法,启动应用就可以了,那么现在再来分析下这个启动方法:
public function run($request = null) { $response = $this->dispatch($request); if ($response instanceof SymfonyResponse) { $response->send(); } else { echo (string)$response; } if (count($this->middleware) > 0) { $this->callTerminableMiddleware($response); } }
可以看到重点就在dispatch()这个方法上面,去处理请求,并且返回合适的响应,最开始的时候$request默认是null好了接下来看dispatch()方法
首先看到是parseIncomingRequest()这个方法,他的作用就是解析出当前请求的请求方法和pathinfo,那么具体是怎么解析的呢? ($_SERVER就是这个php全局数组,你需要的请求信息都在里面),当然这里还要提一下laravel有一个请求欺骗的_method字段的要说一下,就是在你发送post请求的时候表单多添加一个这个字段比如给一个put值那么这个请求laravel就认为是put了,并且这个请求欺骗的优先级高于全局数组的优先级。
try { return $this->sendThroughPipeline($this->middleware, function () use ($method, $pathInfo) { if (isset($this->routes[$method . $pathInfo])) { return $this->handleFoundRoute([true, $this->routes[$method . $pathInfo]['action'], []]); } return $this->handleDispatcherResponse( $this->createDispatcher()->dispatch($method, $pathInfo) ); }); } catch (Exception $e) { return $this->sendExceptionToHandler($e); } catch (Throwable $e) { return $this->sendExceptionToHandler($e); }
如果路由存在则调用handleFoundRoute()方法继续处理返回正确的response,如果不存在这则返回对应的响应,是404还是方法不存在的区别。