laravel
为我们提供便携的重定向功能,可以由门面 Redirect
,或者全局函数 redirect()
来启用,本篇文章将会介绍重定向功能的具体细节及源码分析。
重定向功能是由类 UrlGenerator
所实现,这个类需要 request
来进行初始化:
$url = new UrlGenerator(
$routes = new RouteCollection,
$request = Request::create('http://www.foo.com/')
);
to
函数:$this->assertEquals('http://www.foo.com/foo/bar', $url->to('foo/bar'));
$this->assertEquals('https://www.foo.com/foo/bar/baz/boom', $url->to('foo/bar', ['baz', 'boom'], true));
$this->assertEquals('https://www.foo.com/foo/bar/baz?foo=bar', $url->to('foo/bar?foo=bar', ['baz'], true));
如果我们想要重定向到 https
,我们可以设置第三个参数为 true
:
$this->assertEquals('https://www.foo.com/foo/bar', $url->to('foo/bar', [], true));
或者使用 forceScheme
函数:
$url->forceScheme('https');
$this->assertEquals('https://www.foo.com/foo/bar', $url->to('foo/bar');
$url->forceRootUrl('https://www.bar.com');
$this->assertEquals('https://www.bar.com/foo/bar', $url->to('foo/bar');
$url->formatPathUsing(function ($path) {
return '/something'.$path;
});
$this->assertEquals('http://www.foo.com/something/foo/bar', $url->to('foo/bar'));
重定向另一个非常重要的功能是重定向到路由所在的地址中去:
$route = new Route(['GET'], '/named-route', ['as' => 'plain']);
$routes->add($route);
$this->assertEquals('http:/www.bar.com/named-route', $url->route('plain'));
laravel
路由重定向可以选择重定向后的地址是否仍然带有域名,这个特性由第三个参数决定:
$route = new Route(['GET'], '/named-route', ['as' => 'plain']);
$routes->add($route);
$this->assertEquals('/named-route', $url->route('plain', [], false));
路由重定向可以允许带有 request
自己的端口:
$url = new UrlGenerator(
$routes = new RouteCollection,
$request = Request::create('http://www.foo.com:8080/')
);
$route = new Route(['GET'], 'foo/bar/{baz}', ['as' => 'bar', 'domain' => 'sub.{foo}.com']);
$routes->add($route);
$this->assertEquals('http://sub.taylor.com:8080/foo/bar/otwell', $url->route('bar', ['taylor', 'otwell']));
如果路由中含有参数,可以将需要的参数赋给 route
第二个参数:
$route = new Route(['GET'], 'foo/bar/{baz}', ['as' => 'foobar']);
$routes->add($route);
$this->assertEquals('http://www.foo.com/foo/bar/taylor', $url->route('foobar', 'taylor'));
也可以根据参数的命名来指定参数绑定:
$route = new Route(['GET'], 'foo/bar/{baz}/breeze/{boom}', ['as' => 'bar']);
$routes->add($route);
$this->assertEquals('http://www.foo.com/foo/bar/otwell/breeze/taylor', $url->route('bar', ['boom' => 'taylor', 'baz' => 'otwell']));
还可以利用 defaults
函数为重定向提供默认的参数来绑定:
$url->defaults(['locale' => 'en']);
$route = new Route(['GET'], 'foo', ['as' => 'defaults', 'domain' => '{locale}.example.com', function () {
}]);
$routes->add($route);
$this->assertEquals('http://en.example.com/foo', $url->route('defaults'));
当在 route
函数中赋给的参数多于路径参数的时候,多余的参数会被添加到 querystring
中:
$route = new Route(['GET'], 'foo/bar/{baz}/breeze/{boom}', ['as' => 'bar']);
$routes->add($route);
$this->assertEquals('http://www.foo.com/foo/bar/taylor/breeze/otwell?fly=wall', $url->route('bar', ['taylor', 'otwell', 'fly' => 'wall']));
$route = new Route(['GET'], 'foo/bar#derp', ['as' => 'fragment']);
$routes->add($route);
$this->assertEquals('/foo/bar?baz=%C3%A5%CE%B1%D1%84#derp', $url->route('fragment', ['baz' => 'åαф'], false));
我们不仅可以通过路由的别名来重定向,还可以利用路由的控制器方法来重定向:
$route = new Route(['GET'], 'foo/bam', ['controller' => 'foo@bar']);
$routes->add($route);
$this->assertEquals('http://www.foo.com/foo/bam', $url->action('foo@bar'));
可以设定重定向控制器的默认命名空间:
$url->setRootControllerNamespace('namespace');
$route = new Route(['GET'], 'foo/bar', ['controller' => 'namespace\foo@bar']);
$routes->add($route);
$route = new Route(['GET'], 'something/else', ['controller' => 'something\foo@bar']);
$routes->add($route);
$this->assertEquals('http://www.foo.com/foo/bar', $url->action('foo@bar'));
$this->assertEquals('http://www.foo.com/something/else', $url->action('\something\foo@bar'));
可以为重定向传入 UrlRoutable
类型的参数,重定向会通过类方法 getRouteKey
来获取对象的某个属性,进而绑定到路由的参数中去。
public function testRoutableInterfaceRoutingWithSingleParameter()
{
$url = new UrlGenerator(
$routes = new RouteCollection,
$request = Request::create('http://www.foo.com/')
);
$route = new Route(['GET'], 'foo/{bar}', ['as' => 'routable']);
$routes->add($route);
$model = new RoutableInterfaceStub;
$model->key = 'routable';
$this->assertEquals('/foo/routable', $url->route('routable', $model, false));
}
class RoutableInterfaceStub implements UrlRoutable
{
public $key;
public function getRouteKey()
{
return $this->{$this->getRouteKeyName()};
}
public function getRouteKeyName()
{
return 'key';
}
}
在说重定向的源码之前,我们先了解一下一般的 uri
基本组成:
scheme://domain:port/path?queryString
也就是说,一般 uri
由五部分构成。重定向实际上就是按照各种传入的参数以及属性的设置来重新生成上面的五部分:
public function to($path, $extra = [], $secure = null)
{
if ($this->isValidUrl($path)) {
return $path;
}
$tail = implode('/', array_map(
'rawurlencode', (array) $this->formatParameters($extra))
);
$root = $this->formatRoot($this->formatScheme($secure));
list($path, $query) = $this->extractQueryString($path);
return $this->format(
$root, '/'.trim($path.'/'.$tail, '/')
).$query;
}
重定向的 scheme
由函数 formatScheme
生成:
public function formatScheme($secure)
{
if (! is_null($secure)) {
return $secure ? 'https://' : 'http://';
}
if (is_null($this->cachedSchema)) {
$this->cachedSchema = $this->forceScheme ?: $this->request->getScheme().'://';
}
return $this->cachedSchema;
}
public function forceScheme($schema)
{
$this->cachedSchema = null;
$this->forceScheme = $schema.'://';
}
可以看出来, scheme
的生成存在优先级:
to
传入的 secure
参数forceScheme
设置的 schema
参数request
自带的 scheme
重定向的 domain
由函数 formatRoot
生成:
public function formatRoot($scheme, $root = null)
{
if (is_null($root)) {
if (is_null($this->cachedRoot)) {
$this->cachedRoot = $this->forcedRoot ?: $this->request->root();
}
$root = $this->cachedRoot;
}
$start = Str::startsWith($root, 'http://') ? 'http://' : 'https://';
return preg_replace('~'.$start.'~', $scheme, $root, 1);
}
public function forceRootUrl($root)
{
$this->forcedRoot = rtrim($root, '/');
$this->cachedRoot = null;
}
与 scheme
类似,root
的生成也存在优先级:
to
传入的 root
参数forceRootUrl
设置的 root
参数request
自带的 root
重定向的 path
由三部分构成,一部分是 request
自带的 path
,一部分是函数 to
原有的 path
,另一部分是函数 to
传入的参数:
public function formatParameters($parameters)
{
$parameters = array_wrap($parameters);
foreach ($parameters as $key => $parameter) {
if ($parameter instanceof UrlRoutable) {
$parameters[$key] = $parameter->getRouteKey();
}
}
return $parameters;
}
protected function extractQueryString($path)
{
if (($queryPosition = strpos($path, '?')) !== false) {
return [
substr($path, 0, $queryPosition),
substr($path, $queryPosition),
];
}
return [$path, ''];
}
相对于 uri
的重定向来说,路由重定向的 scheme
、root
、path
、queryString
都要以路由自身的属性为第一优先级,此外还要利用额外参数来绑定路由的 uri
参数:
public function route($name, $parameters = [], $absolute = true)
{
if (! is_null($route = $this->routes->getByName($name))) {
return $this->toRoute($route, $parameters, $absolute);
}
throw new InvalidArgumentException("Route [{$name}] not defined.");
}
public function to($route, $parameters = [], $absolute = false)
{
$domain = $this->getRouteDomain($route, $parameters);
$uri = $this->addQueryString($this->url->format(
$root = $this->replaceRootParameters($route, $domain, $parameters),
$this->replaceRouteParameters($route->uri(), $parameters)
), $parameters);
if (preg_match('/\{.*?\}/', $uri)) {
throw UrlGenerationException::forMissingParameters($route);
}
$uri = strtr(rawurlencode($uri), $this->dontEncode);
if (! $absolute) {
return '/'.ltrim(str_replace($root, '', $uri), '/');
}
return $uri;
}
路由的重定向 scheme
需要先判断路由的 scheme
属性:
protected function getRouteScheme($route)
{
if ($route->httpOnly()) {
return 'http://';
} elseif ($route->httpsOnly()) {
return 'https://';
} else {
return $this->url->formatScheme(null);
}
}
public function to($route, $parameters = [], $absolute = false)
{
$domain = $this->getRouteDomain($route, $parameters);
$uri = $this->addQueryString($this->url->format(
$root = $this->replaceRootParameters($route, $domain, $parameters),
$this->replaceRouteParameters($route->uri(), $parameters)
), $parameters);
...
}
protected function getRouteDomain($route, &$parameters)
{
return $route->domain() ? $this->formatDomain($route, $parameters) : null;
}
protected function formatDomain($route, &$parameters)
{
return $this->addPortToDomain(
$this->getRouteScheme($route).$route->domain()
);
}
protected function addPortToDomain($domain)
{
$secure = $this->request->isSecure();
$port = (int) $this->request->getPort();
return ($secure && $port === 443) || (! $secure && $port === 80)
? $domain : $domain.':'.$port;
}
protected function replaceRootParameters($route, $domain, &$parameters)
{
$scheme = $this->getRouteScheme($route);
return $this->replaceRouteParameters(
$this->url->formatRoot($scheme, $domain), $parameters
);
}
可以看出路由重定向时,域名的生成主要先经过函数 getRouteDomain
, 判断路由是否有 domain
属性,如果有域名属性,则将会作为 formatRoot
函数的参数传入,否则就会默认启动 1uri
重定向的域名生成方法。
路由重定向可以利用函数 replaceRootParameters
在域名当中参数绑定,,也可以在路径当中利用函数 replaceRouteParameters
进行参数绑定。参数绑定分为命名参数绑定与匿名参数绑定:
protected function replaceRouteParameters($path, array &$parameters)
{
$path = $this->replaceNamedParameters($path, $parameters);
$path = preg_replace_callback('/\{.*?\}/', function ($match) use (&$parameters) {
return (empty($parameters) && ! Str::endsWith($match[0], '?}'))
? $match[0]
: array_shift($parameters);
}, $path);
return trim(preg_replace('/\{.*?\?\}/', '', $path), '/');
}
对于命名参数绑定,程序会分别从变量列表、默认变量列表中获取并替换路由参数对应的数值,若不存在该参数,则直接返回:
protected function replaceNamedParameters($path, &$parameters)
{
return preg_replace_callback('/\{(.*?)\??\}/', function ($m) use (&$parameters) {
if (isset($parameters[$m[1]])) {
return Arr::pull($parameters, $m[1]);
} elseif (isset($this->defaultParameters[$m[1]])) {
return $this->defaultParameters[$m[1]];
} else {
return $m[0];
}
}, $path);
}
命名参数绑定结束后,剩下的未被替换的路由参数将会被未命名的变量按顺序来替换。
如果变量列表在绑定路由后仍然有剩余,那么变量将会作为路由的 queryString
:
protected function addQueryString($uri, array $parameters)
{
if (! is_null($fragment = parse_url($uri, PHP_URL_FRAGMENT))) {
$uri = preg_replace('/#.*/', '', $uri);
}
$uri .= $this->getRouteQueryString($parameters);
return is_null($fragment) ? $uri : $uri."#{$fragment}";
}
protected function getRouteQueryString(array $parameters)
{
if (count($parameters) == 0) {
return '';
}
$query = http_build_query(
$keyed = $this->getStringParameters($parameters)
);
if (count($keyed) < count($parameters)) {
$query .= '&'.implode(
'&', $this->getNumericParameters($parameters)
);
}
return '?'.trim($query, '&');
}
路由 uri
构建完成后,将会继续判断是否存在违背绑定的路由参数,是否显示 absolute
的路由地址
public function to($route, $parameters = [], $absolute = false)
{
...
if (preg_match('/\{.*?\}/', $uri)) {
throw UrlGenerationException::forMissingParameters($route);
}
$uri = strtr(rawurlencode($uri), $this->dontEncode);
if (! $absolute) {
return '/'.ltrim(str_replace($root, '', $uri), '/');
}
return $uri;
}