前言
本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...laravel
為我們提供便攜的重定向功能,可以由門面 Redirect
,或者全域性函式 redirect()
來啟用,本篇文章將會介紹重定向功能的具體細節及原始碼分析。
URI 重定向
重定向功能是由類 UrlGenerator
所實現,這個類需要 request
來進行初始化:
$url = new UrlGenerator(
$routes = new RouteCollection,
$request = Request::create('http://www.foo.com/')
);
重定向到 uri
- 當我們想要重定向到某個地址時,可以使用
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
如果我們想要重定向到 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'));
重定向路由 querystring 新增
當在 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']));
fragment 重定向
$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));
路由 action 重定向
我們不僅可以透過路由的別名來重定向,還可以利用路由的控制器方法來重定向:
$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 引數繫結
可以為重定向傳入 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 重定向原始碼分析
在說重定向的原始碼之前,我們先了解一下一般的 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
重定向的 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
重定向的 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
重定向的 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
需要先判斷路由的 scheme
屬性:
protected function getRouteScheme($route)
{
if ($route->httpOnly()) {
return 'http://';
} elseif ($route->httpsOnly()) {
return 'https://';
} else {
return $this->url->formatScheme(null);
}
}
路由重定向 domain
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
如果變數列表在繫結路由後仍然有剩餘,那麼變數將會作為路由的 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;
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結