Laravel HTTP—— 重定向的使用與原始碼分析

leoyang發表於2017-08-08

前言

本文 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 的重定向來說,路由重定向的 schemerootpathqueryString 都要以路由自身的屬性為第一優先順序,此外還要利用額外引數來繫結路由的 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 協議》,轉載必須註明作者和本文連結

相關文章