lumen 的路由具体实现

杜河
2023-12-01

lumen 的路由实现

前言

       最近公司项目中使用的框架换成了 lumen , 想到以前面试的时候,面试官都喜欢问一些框架的底层的逻辑,也提到过,laravel 的controller 路由事怎么实现的。
       虽然对于它的路由配置已经很是熟悉了,但是对它的底层原理却很陌生,所以下决心把路由的完整的注册过程,以及分发过程给捋一遍。

代码分析

首先看看我们一般使用时候的配置:
bootstrap/app.php

/** api */
$app->group(['namespace' => 'App\Http\Controller\Api','prefix' =>'/api'], function ($app) {
    require __DIR__ . '/../routes/api.php';
});

route/api.php

/** api */
$app->group(['prefix' => '/xx', 'middleware' => ['param.xx', 'xx']], function () use ($app) {
    $app->get('/user/login', 'UserController@login');
    $app->get('/user/record', 'UserController@userRecord');
}

具体的路由配置不详细解释,可以参考 https://learnku.com/docs/lumen/5.3/routing/1880
大概写完上面两步,然后我们就去建 UserController 控制器,在控制器里面建 login 和 userRecord 方法,然后访问URI资源为 /api/xx/user/login (或 /api/user/record)就可以看到我们写的东西了

但是如何通过这两个URI资源访问到这个里面呢?相信很多忙于写业务代码的同行们所忽略的,下面是我对路由的注册和分发的一些理解,如果有什么理解不对的地方,请各位大神斧正 [手动抱拳]

路由的注册

根据上面的两段代码,我们来看看lumen的代码具体是怎么执行的,具体类 vendor/laravel/lumen-framework/src/Concerns/RoutesRequests.php
首先调用了路由的group方法

public function group(array $attributes, Closure $callback)
{
    if (isset($attributes['middleware']) && is_string($attributes['middleware'])) {
        $attributes['middleware'] = explode('|', $attributes['middleware']);
    }

    $this->updateGroupStack($attributes);

    call_user_func($callback, $this);

    array_pop($this->groupStack);
}

group方法中,首先将middleware属性非数组的形式拆分成数组的形式,然后调用updateGroupStack方法

protected function updateGroupStack(array $attributes)
{
    if (! empty($this->groupStack)) {
        $attributes = $this->mergeWithLastGroup($attributes);
    }

    $this->groupStack[] = $attributes;
}

使用新增加的属性更新当前路由对象中的groupStack属性。更新的时候,是与当前groupStack中最后一个元素进行合并,然后将合并后的属性添加到groupStack的尾部(代码参考mergeWithLastGroup方法,这里就不贴出来了)。一般来说groupStack属性都为空,存在这种情况只会在group嵌套的时候发生,子group会拥有父group的所有属性(我们前面的两段代码则属于子父group嵌套的),兄弟group之间,他们的属性之间没有任何关系(在最后调用了array_pop方法)。当更新好当前的groupStack后,会立即调用当前group所定义的闭包,在这个闭包中我们通常就是调用相关的路由方法,或者定义子group。这样,在当前group中所定义的路由,都会拥有group所定义的路由属性。

嵌套groupgroupStack属性值

array (size=2)
  0 => 
    array (size=2)
      'namespace' => string 'App\Http\Controller\Api' 
      'prefix' => string '/api' 
  1 => 
    array (size=3)
      'middleware' => 
        array (size=2)
          0 => string 'param.xx' 
          1 => string 'xx' 
      'prefix' => string 'api/xx' 
      'namespace' => string 'App\Http\Controller\Api' 

未嵌套groupgroupStack属性值


$this->groupStack 组信息中的属性,包含中间件,命名空间,路由前缀,路由后缀,as(别名?)

array (size=3)
  'prefix' => string 'api'
  'suffix' => string ''
  'middleware' => 
    array (size=2)
      0 => string 'param.valid' 
      1 => string 'logdb.send' 
  'namespace' => string 'App\Http\Controller\Api' 

接着看我们经常所使用的 get,post,put,delete,option 等一系列 http 动作的方法,他们的原型都是路由中的 addRoute 方法,来添加路由

public function addRoute($method, $uri, $action)
{
	// 把 action 解析为数组形式
	$action = $this->parseAction($action);
	//包含中间件,命名空间,前缀
	$attributes = null;
	
	if ($this->hasGroupStack()) {
	    $attributes = $this->mergeWithLastGroup([]);
	}
	
	if (isset($attributes) && is_array($attributes)) {
	    if (isset($attributes['prefix'])) {
	        $uri = trim($attributes['prefix'], '/').'/'.trim($uri, '/');
	    }
	
	    if (isset($attributes['suffix'])) {
	        $uri = trim($uri, '/').rtrim($attributes['suffix'], '/');
	    }
	
	    $action = $this->mergeGroupAttributes($action, $attributes);
	}
	
	$uri = '/'.trim($uri, '/');
	
	if (isset($action['as'])) {
	    $this->namedRoutes[$action['as']] = $uri;
	}
	
	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];
	}
}

首先调用parseAction把我们定义的行为action解析成数组形式,从parseAction方法中可以看出我们定义行为action也可以写成['uses' => 'UseController@login', 'middleware' => ['param.xx']]

protected function parseAction($action)
{
    if (is_string($action)) {
        return ['uses' => $action];
    } elseif (! is_array($action)) {
        return [$action];
    }

    if (isset($action['middleware']) && is_string($action['middleware'])) {
        $action['middleware'] = explode('|', $action['middleware']);
    }

    return $action;
}

然后判断是否存在group,存在的话需要把组的属性,赋给当前的行为,并且对行为action合并当前组的属性,再判断是否存在别名,存在别名则加入到nameRoutes中,最后把路由放进routes

$this->nameRoutes 别名路由中的信息

array (size=1)
  'user.login' => string '/api/xx/user/login'

$this->routes 路由数组中的属性,method + uri 作为键

'POST/api/user/login' => 
    array (size=3)
      'method' => string 'POST' 
      'uri' => string '/api/user/login' 
      'action' => 
        array (size=1)
          'uses' => string 'App\Http\Controllers\Api\UserController@login' 
          'middleware' => 
		    array (size=2)
		      0 => string 'param.valid'
		      1 => string 'logdb.send'
'GET/api/user/record' => 
    array (size=3)
      'method' => string 'GET' 
      'uri' => string '/api/user/record'
      'action' => 
        array (size=1)
          'uses' => string 'App\Http\Controllers\Api\UserController@userRecord'
          'middleware' => 
			    array (size=1)
			      0 => string 'logdb.send'

到这里基本上就知道,lumen里面的路由是如何注册的了。它把所有的路由都放在属性routes

路由的分发
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);
    }
}

路由注册完成之后,调用了$app->run(),运行程序并发送response,首先调用dispatch分发获取到的请求,然后输出结果信息,最后判断是否有中间件,调用结束进程的中间件

首先看看路由分发dispatch方法

public function dispatch($request = null)
{
    list($method, $pathInfo) = $this->parseIncomingRequest($request);

    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)
            );
        });
    } 
    //处理异常
    ...
}

首先解析了request请求,获取到methodpathInfo,调用sendThroughPipeline方法使用给定的回调函数,通过管道发送request请求,存在中间件的时候,需要使用管道(管道后面再说吧),不存在则直接调用传入的函数

protected function sendThroughPipeline(array $middleware, Closure $then)
{
    if (count($middleware) > 0 && ! $this->shouldSkipMiddleware()) {
        return (new Pipeline($this))
            ->send($this->make('request'))
            ->through($middleware)
            ->then($then);
    }

    return $then();
}

回调函数中处理的是:存在method + pathInfo的路由,直接调用handleFoundRoute处理转发的路由,否则调用handleDispatcherResponse(根据动态路由来确定,是否存在路由信息?)

先看看直接找到的路由信息的处理方式

protected function handleFoundRoute($routeInfo)
{
    $this->currentRoute = $routeInfo;

    $this['request']->setRouteResolver(function () {
        return $this->currentRoute;
    });

    $action = $routeInfo[1];

    // Pipe through route middleware...
    if (isset($action['middleware'])) {
        $middleware = $this->gatherMiddlewareClassNames($action['middleware']);

        return $this->prepareResponse($this->sendThroughPipeline($middleware, function () {
            return $this->callActionOnArrayBasedRoute($this['request']->route());
        }));
    }

    return $this->prepareResponse(
        $this->callActionOnArrayBasedRoute($routeInfo)
    );
}

这里的核心处理方法就是调用callActionOnArrayBasedRoute处理请求,调用这个方法,判断是否存在uses,存在的话调用callControllerAction,不存在的话,调用执行绑定的回调函数

protected function callActionOnArrayBasedRoute($routeInfo)
{
    $action = $routeInfo[1];

    if (isset($action['uses'])) {
        return $this->prepareResponse($this->callControllerAction($routeInfo));
    }

    foreach ($action as $value) {
        if ($value instanceof Closure) {
            $closure = $value->bindTo(new RoutingClosure);
            break;
        }
    }

    try {
        return $this->prepareResponse($this->call($closure, $routeInfo[2]));
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}

这个方法里面再执行行为绑定的类方法或者回调函数

执行业务方法的核心方法callControllerAction

protected function callControllerAction($routeInfo)
{
    $uses = $routeInfo[1]['uses'];

    if (is_string($uses) && ! Str::contains($uses, '@')) {
        $uses .= '@__invoke';
    }

    list($controller, $method) = explode('@', $uses);

    if (! method_exists($instance = $this->make($controller), $method)) {
        throw new NotFoundHttpException;
    }

    if ($instance instanceof LumenController) {
        return $this->callLumenController($instance, $method, $routeInfo);
    } else {
        return $this->callControllerCallable(
            [$instance, $method], $routeInfo[2]
        );
    }
}

其实路由走到最后都会调用两个核心的方法,调用类方法,或者执行回调的函数,call以及callClasscallClass最后也是会调用call方法

//调用给定的函数方法或者类方法,并注入其中的依赖
public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
{
    if (static::isCallableWithAtSign($callback) || $defaultMethod) {
        return static::callClass($container, $callback, $parameters, $defaultMethod);
    }

    return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
        return call_user_func_array(
            $callback, static::getMethodDependencies($container, $callback, $parameters)
        );
    });
}

//根据字符串调用类方法
protected static function callClass($container, $target, array $parameters = [], $defaultMethod = null)
{
    $segments = explode('@', $target);

    $method = count($segments) == 2
                    ? $segments[1] : $defaultMethod;

    if (is_null($method)) {
        throw new InvalidArgumentException('Method not provided.');
    }

    return static::call(
        $container, [$container->make($segments[0]), $method], $parameters
    );
}
注入依赖

下面是获取依赖的参数的方法,使用反射类获取方法的参数,根据参数来赋值或实例化不同的参数

//获取注入的参数
protected static function getMethodDependencies($container, $callback, array $parameters = [])
{
    $dependencies = [];

    foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
        static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
    }

    return array_merge($dependencies, $parameters);
}
//获取类的反射信息
protected static function getCallReflector($callback)
{
    //...
    return is_array($callback)
                    ? new ReflectionMethod($callback[0], $callback[1])
                    : new ReflectionFunction($callback);
}
//添加依赖的参数信息
protected static function addDependencyForCallParameter($container, $parameter,
                                                            array &$parameters, &$dependencies)
{
    if (array_key_exists($parameter->name, $parameters)) {
        $dependencies[] = $parameters[$parameter->name];

        unset($parameters[$parameter->name]);
    } elseif ($parameter->getClass()) {
        $dependencies[] = $container->make($parameter->getClass()->name);
    } elseif ($parameter->isDefaultValueAvailable()) {
        $dependencies[] = $parameter->getDefaultValue();
    }
}

方法最后执行的就是我们写的业务代码了,路由分析到这里大概就差不多了,还有动态路由什么的这里没有具体看

request、response 对象解析

在一个http生命周期中,我们最关心的还是输入request,以及输出response,下面简单的看看lumen里面的输入和输出

requset 处理

在前面parseIncomingRequest方法中,我们看到了对request的赋值:Request::capture();,我们来看看这个方法里面捕获了些什么,具体赋值应该是在:vendor/symfony/http-foundation/Request.php:460没有具体分析

public static function capture()
{
	//开启方法参数覆盖?
    static::enableHttpMethodParameterOverride();
	//创建一个 SymfonyRequest 的request
    return static::createFromBase(SymfonyRequest::createFromGlobals());
}
//根据 SymfonyRequest的实例创建一个 Illuminate的实例 request
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;
}
response 处理

在看路由分发的时候,最后一直是调用prepareResponse这个方法的,方法里面有三个if判断,分别处理PsrResponseInterfaceSymfonyResponseBinaryFileResponse

public function prepareResponse($response)
{
    if ($response instanceof PsrResponseInterface) {
        $response = (new HttpFoundationFactory)->createResponse($response);
    } elseif (! $response instanceof SymfonyResponse) {
        $response = new Response($response);
    } elseif ($response instanceof BinaryFileResponse) {
        $response = $response->prepare(Request::capture());
    }

    return $response;
}
处理PsrResponseInterface
public function createResponse(ResponseInterface $psrResponse)
{
    $response = new Response(
        $psrResponse->getBody()->__toString(),
        $psrResponse->getStatusCode(),
        $psrResponse->getHeaders()
    );
    $response->setProtocolVersion($psrResponse->getProtocolVersion());

    foreach ($psrResponse->getHeader('Set-Cookie') as $cookie) {
        $response->headers->setCookie($this->createCookie($cookie));
    }

    return $response;
}
处理SymfonyResponse

前两个最后都是处理成Symfony\Component\HttpFoundation\Response

$response = new Response($response);

use Symfony\Component\HttpFoundation\Response as BaseResponse;

class Response extends BaseResponse
{
...
处理BinaryFileResponse

有兴趣的去看看吧,我没有深入研究
vendor/symfony/http-foundation/Response.php:265

后记

有些地方可能写的不是特别清晰,大家自己最照着源码大概看一看吧!有什么可以指教的,欢迎联系我。QQ:1109563194

 类似资料: