前言
本篇文章逻辑较长,只说明和响应生命周期相关的必要代码。
本文主要内容顺序为:
1、执行上文管道中的then方法指定的闭包,路由的分发
2、在路由器中(Router类)找到请求($request 也就是经过全局中间件处理的请求)匹配的路由规则
3、说明路由规则的加载(会跳转到框架的boot过程),注意这部分是在处理请求之前完成的,因为一旦当我们开始处理请求,就意味着所有的路由都应该已经加载好了,供我们的请求进行匹配
4、执行请求匹配到的路由逻辑
5、生成响应,并发送给客户端
6、最后生命周期的结束
7、基本响应类的使用
前文说道,如果一个请求顺利通过了全局中间件那么就会调用管道then方法中传入的闭包
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() "htmlcode">public function dispatch(Request $request) { $this->currentRequest = $request; // 将请求分发到路由 // 跳转到dispatchToRoute方法 return $this->dispatchToRoute($request); } public function dispatchToRoute(Request $request) { // 先跳转到findRoute方法 return $this->runRoute($request, $this->findRoute($request)); } // 见名之意 通过给定的$request 找到匹配的路由 protected function findRoute($request) { // 跳转到Illuminate\Routing\RouteCollection::match方法 $this->current = $route = $this->routes->match($request); $this->container->instance(Route::class, $route); return $route; }查看Illuminate\Routing\RouteCollection::match方法
/** * Find the first route matching a given request. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Routing\Route * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ public function match(Request $request) { // 根据请求动作找到全局匹配的路由 // 可以自行打印下$routes $routes = $this->get($request->getMethod()); // 匹配路由 下面查看框架如何生成的路由规则!!! $route = $this->matchAgainstRoutes($routes, $request); if (! is_null($route)) { return $route->bind($request); } $others = $this->checkForAlternateVerbs($request); if (count($others) > 0) { return $this->getRouteForMethods($request, $others); } throw new NotFoundHttpException; }下面说明框架如何加载的路由规则
Application::boot方法
// 主要逻辑是调用服务提供者的boot方法 array_walk($this->serviceProviders, function ($p) { $this->bootProvider($p); });App\Providers\RouteServiceProvider::boot方法
public function boot() { // 调用父类Illuminate\Foundation\Support\Providers\RouteServiceProvider的boot方法 parent::boot(); }Illuminate\Foundation\Support\Providers\RouteServiceProvider::boot方法
public function boot() { $this->setRootControllerNamespace(); if ($this->routesAreCached()) { $this->loadCachedRoutes(); } else { // 就看这个loadRoutes方法 $this->loadRoutes(); $this->app->booted(function () { // dd(get_class($this->app['router'])); $this->app['router']->getRoutes()->refreshNameLookups(); $this->app['router']->getRoutes()->refreshActionLookups(); }); } } /** * Load the application routes. * 看注释就知道我们来对了地方 * @return void */ protected function loadRoutes() { // 调用App\Providers\RouteServiceProvider的map方法 if (method_exists($this, 'map')) { $this->app->call([$this, 'map']); } }App\Providers\RouteServiceProvider::map方法
public function map() { // 为了调试方便我注释掉了api路由 // $this->mapApiRoutes(); // 这两个都是加载路由文件 这里查看web.php $this->mapWebRoutes(); } protected function mapWebRoutes() { // 调用Router的__call方法 返回的是RouteRegistrar实例 Route::middleware('web') ->namespace($this->namespace) // 调用RouteRegistrar的namespace方法 触发__call魔术方法 // 依然是挂载属性 可自行打印 // Illuminate\Routing\RouteRegistrar {#239 ▼ // #router: Illuminate\Routing\Router {#34 "middleware" => array:1 [▼ // 0 => "web" // ] // "namespace" => "App\Http\Controllers" // ] // #passthru: array:7 ["htmlcode">public function __call($method, $parameters) { if (static::hasMacro($method)) { return $this->macroCall($method, $parameters); } if ($method === 'middleware') { // 调用了RouteRegistrar的attribute方法 只是挂载路由属性 return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) "htmlcode">public function __call($method, $parameters) { if (in_array($method, $this->passthru)) { // 当使用get post等方法的时候 return $this->registerRoute($method, ...$parameters); } if (in_array($method, $this->allowedAttributes)) { if ($method === 'middleware') { return $this->attribute($method, is_array($parameters[0]) "htmlcode">public function group($callback) { // dd($this->attributes, $callback); // array:2 [▼ // "middleware" => array:1 [▼ // 0 => "web" // ] // "namespace" => "App\Http\Controllers" // ] // "/home/vagrant/code/test1/routes/web.php" // 查看Router的group方法 $this->router->group($this->attributes, $callback); }Router::group方法
public function group(array $attributes, $routes) { $this->updateGroupStack($attributes); // 查看loadRoutes方法 /home/vagrant/code/test1/routes/web.php $this->loadRoutes($routes); array_pop($this->groupStack); } protected function loadRoutes($routes) { if ($routes instanceof Closure) { // 用于闭包嵌套 laravel的路由是可以随意潜逃组合的 $routes($this); } else { // 加载路由文件 /home/vagrant/code/test1/routes/web.php (new RouteFileRegistrar($this))->register($routes); } }Illuminate\Routing\RouteFileRegistrar 文件
protected $router; public function __construct(Router $router) { $this->router = $router; } public function register($routes) { $router = $this->router; // 终于加载到了路由文件 // require("/home/vagrant/code/test1/routes/web.php"); // 看到这里就到了大家熟悉的Route::get()等方法了 // 道友们可能已经有了有趣的想法: 可以在web.php等路由文件中继续require其他文件 // 便可实现不同功能模块的路由管理 require $routes; }了解了理由加载流程,下面举个简单例子,laravel如何注册一个路由
// web.php中 Route::get('routecontroller', "\App\Http\Controllers\Debug\TestController@index"); // 跳转到Router的get方法 /** * Register a new GET route with the router. * * @param string $uri * @param \Closure|array|string|callable|null $action * @return \Illuminate\Routing\Route */ public function get($uri, $action = null) { // dump($uri, $action); // $uri = routecontroller // $action = \App\Http\Controllers\Debug\TestController@index // 跳转到addRoute方法 return $this->addRoute(['GET', 'HEAD'], $uri, $action); } /** * Add a route to the underlying route collection. * * @param array|string $methods * @param string $uri * @param \Closure|array|string|callable|null $action * @return \Illuminate\Routing\Route */ // ['GET', 'HEAD'], $uri, $action public function addRoute($methods, $uri, $action) { // routes是routecollection实例 // 跳转到createRoute方法 // 跳转到RouteCollection的add方法 return $this->routes->add($this->createRoute($methods, $uri, $action)); } /** * Create a new route instance. * * @param array|string $methods * @param string $uri * @param mixed $action * @return \Illuminate\Routing\Route */ // ['GET', 'HEAD'], $uri, $action protected function createRoute($methods, $uri, $action) { // 跳转到actionReferencesController方法 if ($this->actionReferencesController($action)) { $action = $this->convertToControllerAction($action); // dump($action); // array:2 [▼ // "uses" => "\App\Http\Controllers\Debug\TestController@index" // "controller" => "\App\Http\Controllers\Debug\TestController@index" // ] } // 创建一个对应路由规则的Route实例 并且添加到routes(collection)中 // 返回到上面的addRoute方法 // 请自行查看Route的构造方法 $route = $this->newRoute( // dump($this->prefix); // routecontroller $methods, $this->prefix($uri), $action ); if ($this->hasGroupStack()) { $this->mergeGroupAttributesIntoRoute($route); } $this->addWhereClausesToRoute($route); return $route; } /** * Determine if the action is routing to a controller. * * @param array $action * @return bool */ // 判断是否路由到一个控制器 protected function actionReferencesController($action) { // 在此例子中Route::get方法传递的是一个字符串 if (! $action instanceof Closure) { // 返回true return is_string($action) || (isset($action['uses']) && is_string($action['uses'])); } return false; }RouteCollection的add方法
/** * Add a Route instance to the collection. * * @param \Illuminate\Routing\Route $route * @return \Illuminate\Routing\Route */ public function add(Route $route) { // 跳转吧 $this->addToCollections($route); $this->addLookups($route); // 最终一路返回到Router的get方法 所以我们可以直接打印web.php定义的路由规则 return $route; } /** * Add the given route to the arrays of routes. * * @param \Illuminate\Routing\Route $route * @return void */ protected function addToCollections($route) { $domainAndUri = $route->getDomain().$route->uri(); // dump($route->getDomain(), $route->uri()); null routecontroller foreach ($route->methods() as $method) { // 将路由规则挂载到数组 方便匹配 $this->routes[$method][$domainAndUri] = $route; } // 将路由规则挂载的数组 方便匹配 $this->allRoutes[$method.$domainAndUri] = $route; }至此就生成了一条路由 注意我这里将注册api路由进行了注释,并且保证web.php中只有一条路由规则
以上是路由的加载 这部分是在$this->bootstrap()方法中完成的,还远没有到达路由分发和匹配的阶段,希望大家能够理解,至此路由规则生成完毕 保存到了RouteCollection实例中,每个路由规则都是一个Route对象,供请求进行匹配
下面根据此条路由进行匹配,并执行返回结果
我们回到Illuminate\Routing\RouteCollection::match方法
public function match(Request $request) { // 获取符合当前请求动作的所有路由 // 是一个Route对象数组 每一个对象对应一个route规则 $routes = $this->get($request->getMethod()); // 匹配到当前请求路由 $route = $this->matchAgainstRoutes($routes, $request); if (! is_null($route)) { // 将绑定了请求的Route实例返回 return $route->bind($request); } $others = $this->checkForAlternateVerbs($request); if (count($others) > 0) { return $this->getRouteForMethods($request, $others); } throw new NotFoundHttpException; } // 该方法中大量使用了collect方法 请查看laravel手册 protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true) { // dump(get_class_methods(get_class(collect($routes)))); // dump(collect($routes)->all()); // items数组 protected属性 // dump(collect($routes)->items); // items属性是一个数组 // 当注册一个兜底路由的时候 (通过Route::fallback方法)对应$route的isFallback会被设为true // partition方法根据传入的闭包将集合分成两部分 // 具体实现可以查看手册 集合部分 [$fallbacks, $routes] = collect($routes)->partition(function ($route) { return $route->isFallback; }); // 将兜底路由放到集合后面 并且通过first方法找到第一个匹配的路由 return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) { return $value->matches($request, $includingMethod); }); }Router文件
protected function findRoute($request) { // 可以打印$route 你会发现和你在web.php中打印的是同一个Route对象 $this->current = $route = $this->routes->match($request); // 将匹配到的路由实例挂载到容器 $this->container->instance(Route::class, $route); return $route; } public function dispatchToRoute(Request $request) { // 跳转到runRoute方法 return $this->runRoute($request, $this->findRoute($request)); } protected function runRoute(Request $request, Route $route) { // 给request帮顶当前的route 可以使用$request->route()方法 获取route实例 // 你也可以随时在你的业务代码中通过容器获得当前Route实例 // app(Illuminate\Routing\Route::class) $request->setRouteResolver(function () use ($route) { return $route; }); $this->events->dispatch(new RouteMatched($route, $request)); // 开始准备响应了 return $this->prepareResponse($request, // 跳转到runRouteWithinStack方法 $this->runRouteWithinStack($route, $request) ); } protected function runRouteWithinStack(Route $route, Request $request) { $shouldSkipMiddleware = $this->container->bound('middleware.disable') && $this->container->make('middleware.disable') === true; $middleware = $shouldSkipMiddleware "htmlcode">public function run() { $this->container = $this->container "htmlcode">/** * Dispatch a request to a given controller and method. * * @param \Illuminate\Routing\Route $route * @param mixed $controller * @param string $method * @return mixed */ public function dispatch(Route $route, $controller, $method) { $parameters = $this->resolveClassMethodDependencies( $route->parametersWithoutNulls(), $controller, $method ); if (method_exists($controller, 'callAction')) { // 执行基类控制器中的callAction方法并返回执行结果 return $controller->callAction($method, $parameters); } return $controller->{$method}(...array_values($parameters)); }控制器方法返回的结果到Router::runRouteWithinStack方法
protected function runRouteWithinStack(Route $route, Request $request) { $shouldSkipMiddleware = $this->container->bound('middleware.disable') && $this->container->make('middleware.disable') === true; $middleware = $shouldSkipMiddleware "htmlcode">// Symfony\Component\HttpFoundation\Response public function __construct($content = '', int $status = 200, array $headers = []) { // 可以看到基本什么都没做 $this->headers = new ResponseHeaderBag($headers); // 调用Illuminate\Http\Response的setContent方法 设置响应内容呗 $this->setContent($content); $this->setStatusCode($status); $this->setProtocolVersion('1.0'); } // Illuminate\Http\Response::setContent public function setContent($content) { $this->original = $content; // shouldBeJson方法将实现了特定接口的response或者是一个array的response转换为 // 并设置响应头 if ($this->shouldBeJson($content)) { $this->header('Content-Type', 'application/json'); // morphToJson方法保证最终给此响应设置的响应内容为json串 $content = $this->morphToJson($content); } elseif ($content instanceof Renderable) { $content = $content->render(); } // Symfony\Component\HttpFoundation\Response 如果最终设置的响应内容不是null或者字符串或者实现了__toString方法的类 那么跑出异常, 否则设置响应内容 parent::setContent($content); return $this; } // Symfony\Component\HttpFoundation\Response::setContent方法 public function setContent($content) { if (null !== $content && !\is_string($content) && !is_numeric($content) && !\is_callable([$content, '__toString'])) { // php官方建议不要使用gettype方法获取变量的类型 throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', \gettype($content))); } // (string) 会触发__toString方法 如何对象允许的话 $this->content = (string) $content; return $this; }拿到响应后执行return $response->prepare($request);
/** * Prepares the Response before it is sent to the client. * * This method tweaks the Response to ensure that it is * compliant with RFC 2616. Most of the changes are based on * the Request that is "associated" with this Response. * * @return $this */ // 总的来说就是设置各种响应头 注意此时并未发送响应 public function prepare(Request $request) { $headers = $this->headers; // 如果是100 204 304系列的状态码 就删除响应数据 删除对应的数据头 if ($this->isInformational() || $this->isEmpty()) { $this->setContent(null); $headers->remove('Content-Type'); $headers->remove('Content-Length'); } else { // Content-type based on the Request if (!$headers->has('Content-Type')) { $format = $request->getPreferredFormat(); if (null !== $format && $mimeType = $request->getMimeType($format)) { $headers->set('Content-Type', $mimeType); } } // Fix Content-Type $charset = $this->charset "htmlcode">响应返回到Router::runRoute方法 再返回到Router::dispatchToRoute方法 再返回到Router::dispatch方法 再返回到Illuminate\Foundation\Http::sendRequestThroughRouter方法 (注意只要是通过了管道都要注意中间件的类型) 最终返回到index.php中 $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send(); $kernel->terminate($request, $response);我们来看send方法 Symfony\Component\HttpFoundation\Response::send
public function send() { // 先发送响应头 $this->sendHeaders(); // 再发送响应主体 $this->sendContent(); if (\function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } elseif (!\in_array(\PHP_SAPI, ['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; } // headers foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { $replace = 0 === strcasecmp($name, 'Content-Type'); foreach ($values as $value) { // 将之前设置的各种头发送出去 header($name.': '.$value, $replace, $this->statusCode); } } // cookies foreach ($this->headers->getCookies() as $cookie) { // 告诉客户端要设置的cookie header('Set-Cookie: '.$cookie, false, $this->statusCode); } // status // 最后发送个status 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; } // 至此真的响应了客户端了$kernel->terminate($request, $response);
Illuminate\Foundation\Http\Kernel::terminate方法
/** * Call the terminate method on any terminable middleware. * * @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Response $response * @return void */ public function terminate($request, $response) { // 调用实现了terminate方法的中间件 $this->terminateMiddleware($request, $response); // 执行注册的callback $this->app->terminate(); }laravel将控制器(闭包)返回的数据封装成response对象
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); }观察上面的代码发现:
1 上面代码的作用是将路由节点返回的数据封装成Response对象等待发送
2 并且上面的代码存在大量的instanceof判断 (为什么要这样呢 是因为一旦我们从控制器中返回一个实现了
laravel指定接口的实例,laravel就知道该如何渲染这些响应给客户端 此时你可能还不清楚,请看下面的例子)
3 而且没有else分支(这是因为laravel允许我们直接返回reponse对象,当我们直接返回Resposne实例的时候会直接走到方法的最后一句话)
4 并且最终都调用的都是Symfony Response的prepare方法
我们先来看Responsable接口 在laravel中任何一个实现了此接口的对象 都可以响应给客户端
<"\App\Http\Controllers\Debug\TestController@checkStatus"); 2 创建响应 namespace App\Responses; use App\Models\Order; use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\JsonResponse; class OrderStatusRes implements Responsable { protected $status; public function __construct(Order $order) { $this->status = $order->status; } public function toResponse($request) { if ($this->status) { // 订单以完成 return new JsonResponse('order completed', 200); } // 订单未结算 return view('needToCharge'); } } 3 创建控制器 <"nofollow" target="_blank" href="https://crnkovic.me/laravel-behind-the-scenes-interfaces/">老哥的博客通过十篇水文,分享了从类的自动加载,到走完laravel的生命周期。
第一次写博客不足太多,不爱看大量代码的道友,可以查看这位外国老哥的博客,其代码较少,但逻辑清晰明了。发现错误,欢迎指导,感谢!!!
collection文档
laravel中使用psr7
总结
免责声明:本站文章均来自网站采集或用户投稿,网站不提供任何软件下载或自行开发的软件! 如有用户或公司发现本站内容信息存在侵权行为,请邮件告知! 858582#qq.com狼山资源网 Copyright www.pvsay.com暂无“Laravel Reponse响应客户端示例详解”评论...