Laravel HTTP—— RESTFul 風格路由的使用與原始碼分析

leoyang發表於2017-08-04

前言

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/laravel-source-analysis
我們在前面的文章已經講了整個路由與控制器的原始碼,我們今天這個文章開始向大家介紹在 laravel 中建立 RESTFul 風格的控制器。

關於什麼是RESTFul風格及其規範可參考這篇文章:理解RESTful架構

關於 laravelRESTFul 風格控制器的建立簡要介紹 : HTTP控制器例項教程 —— 建立 RESTFul 風格控制器實現文章增刪改查

 

建立 RESTFul 風格控制器

要想在 laravel 中建立 RESTFul 風格控制器,只需要一句:

Route::resource('post','PostController');

該路由包含了指向多個動作的子路由:

方法 路徑 動作 路由名稱
GET /post index post.index
GET /post/create create post.create
POST /post store post.store
GET /post/{post} show post.show
GET /post/{post}/edit edit post.edit
PUT/PATCH /post/{post} update post.update
DELETE /post/{post} destroy post.destroy

這種用法既簡單又方便,接下來,我們將會說一下 laravel 為我們提供的更加靈活的用法。

字首 RESTFul 路由

可以為 RESTFul 路由定義字首:

$router->resource('prefix/foos', 'FooController');

$this->assertEquals('prefix/foos/{foo}', $routes[3]->uri());

多引數 RESTFul 路由

laravel 允許定義擁有多個引數的 RESTFul 路由:

$router->resource('foos.bars', 'FooController');

$this->assertEquals('foos/{foo}/bars/{bar}', $routes[3]->uri());

引數自定義命名

一般來說,RESTFul 路由的引數命名規則是路由單數,符號 - 轉為 _,例如下面例子中 bars,和 foo-baz

$router->resource('foos', 'FooController');
$this->assertEquals('foos/{foo}', $routes[3]->uri());

$router->resource('foo-bar.foo-baz', 'FooController', ['only' => ['show']]);
$this->assertEquals('foo-bar/{foo_bar}/foo-baz/{foo_baz}', $routes[0]->uri());

我們可以利用 parameters 強制這種單數模式:

$router->resource('foos', 'FooController', ['parameters' => 'singular']);
$this->assertEquals('foos/{foo}', $routes[3]->uri());

我們也可以利用 singularParameters 來強制:

ResourceRegistrar::singularParameters(true);

$router->resource('foos', 'FooController', ['parameters' => 'singular']);
$this->assertEquals('foos/{foo}', $routes[3]->uri());

我們還可以不使用單數,利用 parameters 用自己自定義的名字來定義引數:

$router->resource('bars.foos.bazs', 'FooController', ['parameters' => ['foos' => 'oof', 'bazs' => 'b']]);

$this->assertEquals('bars/{bar}/foos/{oof}/bazs/{b}', $routes[3]->uri());

同時,我們仍然可以利用 setParameters 函式來自定義引數命名:

ResourceRegistrar::setParameters(['foos' => 'oof', 'bazs' => 'b']);

$router->resource('bars.foos.bazs', 'FooController');
$this->assertEquals('bars/{bar}/foos/{oof}/bazs/{b}', $routes[3]->uri());

RESTFul 路由動詞控制

laravelRESTFul 路由生成了兩個帶有動詞的路由: createedit,分別用於載入訂單的建立頁面與編輯頁面,這兩個動詞 laravel 是允許修改的:

ResourceRegistrar::verbs([
    'create' => 'ajouter',
    'edit' => 'modifier',
]);

$router->resource('foo', 'FooController');
$routes = $router->getRoutes();

$this->assertEquals('foo/ajouter', $routes->getByName('foo.create')->uri());
$this->assertEquals('foo/{foo}/modifier', $routes->getByName('foo.edit')->uri());

控制器方法約束

一般情況下,我們都會一次性想要上面所生成的七個路由,然而,有時候,我們只需要其中幾個,或者不想要其中幾個。這時候就可以利用 only 或者 except:

$router = $this->getRouter();
$router->resource('foo', 'FooController', ['only' => ['show', 'destroy']]);
$routes = $router->getRoutes();

$this->assertCount(2, $routes);
$router = $this->getRouter();
$router->resource('foo', 'FooController', ['except' => ['show', 'destroy']]);
$routes = $router->getRoutes();

$this->assertCount(5, $routes);

RESTFul 路由名稱自定義

RESTFul 路由的每個路由都要自己預設的路由名稱,laravel 允許我們對路由名稱進行修改:

我們可以用 as 來為路由名稱新增字首:

$router->resource('foo-bars', 'FooController', ['only' => ['show'], 'as' => 'prefix']);

$this->assertEquals('prefix.foo-bars.show', $routes[0]->getName());

當有多個路由引數的時候,路由引數預設新增到了路由名稱中:

$router->resource('prefix/foo.bar', 'FooController');

$this->assertTrue($router->getRoutes()->hasNamedRoute('foo.bar.index'));

可以利用 names 為單個路由來命名:

$router->resource('foo', 'FooController', ['names' => [
    'index' => 'foo',
    'show' => 'bar',
]]);

$this->assertTrue($router->getRoutes()->hasNamedRoute('foo'));
$this->assertTrue($router->getRoutes()->hasNamedRoute('bar'));

還可以利用 names 為所有路由來命名:

$router->resource('foo', 'FooController', ['names' => 'bar']);

$this->assertTrue($router->getRoutes()->hasNamedRoute('bar.index'));

 

RESTFul 路由原始碼分析

RESTFul 路由的建立工作由類 ResourceRegistrar 負責,這個類為預設為使用者建立七個路由,函式方法 register 是建立路由的主函式:

class ResourceRegistrar
{
    public function register($name, $controller, array $options = [])
    {
        if (isset($options['parameters']) && ! isset($this->parameters)) {
            $this->parameters = $options['parameters'];
        }

        if (Str::contains($name, '/')) {
            $this->prefixedResource($name, $controller, $options);

            return;
        }

        $base = $this->getResourceWildcard(last(explode('.', $name)));

        $defaults = $this->resourceDefaults;

        foreach ($this->getResourceMethods($defaults, $options) as $m) {
            $this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options);
        }
    }
}

這個函式主要流程分為三段:

  • 判斷是否由字首
  • 獲取路由的基礎引數
  • 新增路由

擁有字首的 RESTFul 路由

如果我們為 RESTFul 路由新增了字首,那麼 laravel 將會以 group 的形式新增路由:

protected function prefixedResource($name, $controller, array $options)
{
    list($name, $prefix) = $this->getResourcePrefix($name);

    $callback = function ($me) use ($name, $controller, $options) {
        $me->resource($name, $controller, $options);
    };

    return $this->router->group(compact('prefix'), $callback);
}

protected function getResourcePrefix($name)
{
    $segments = explode('/', $name);

    $prefix = implode('/', array_slice($segments, 0, -1));

    return [end($segments), $prefix];
}

獲取基礎 RESTFul 路由引數

在新增各種路由之前,我們需要先獲取路由的基礎引數,也就是當存在多引數情況下,最後的引數。獲取引數後,如果使用者有自定義命名,則獲取自定義命名:

public function getResourceWildcard($value)
{
    if (isset($this->parameters[$value])) {
        $value = $this->parameters[$value];
    } elseif (isset(static::$parameterMap[$value])) {
        $value = static::$parameterMap[$value];
    } elseif ($this->parameters === 'singular' || static::$singularParameters) {
        $value = Str::singular($value);
    }

    return str_replace('-', '_', $value);
}

新增各種路由

新增路由主要有三個步驟:

  • 計算路由 uri
  • 獲取路由屬性
  • 建立路由
protected function addResourceIndex($name, $base, $controller, $options)
{
    $uri = $this->getResourceUri($name);

    $action = $this->getResourceAction($name, $controller, 'index', $options);

    return $this->router->get($uri, $action);
}

當計算路由 uri 時,由於存在多引數的情況,需要迴圈計算路由引數:

public function getResourceUri($resource)
{
    if (! Str::contains($resource, '.')) {
        return $resource;
    }

    $segments = explode('.', $resource);

    $uri = $this->getNestedResourceUri($segments);

    return str_replace('/{'.$this->getResourceWildcard(end($segments)).'}', '', $uri);
}

protected function getNestedResourceUri(array $segments)
{  
    return implode('/', array_map(function ($s) {
        return $s.'/{'.$this->getResourceWildcard($s).'}';
    }, $segments));
}

當計算路由的屬性時,最重要的是獲取路由的名字,路由的名字可以是預設,也可以是使用者利用 names 或者 as 屬性來自定義:

protected function getResourceAction($resource, $controller, $method, $options)
{
    $name = $this->getResourceRouteName($resource, $method, $options);

    $action = ['as' => $name, 'uses' => $controller.'@'.$method];

    if (isset($options['middleware'])) {
        $action['middleware'] = $options['middleware'];
    }

    return $action;
}

protected function getResourceRouteName($resource, $method, $options)
{
    $name = $resource;

    if (isset($options['names'])) {
        if (is_string($options['names'])) {
            $name = $options['names'];
        } elseif (isset($options['names'][$method])) {
            return $options['names'][$method];
        }
    }

    $prefix = isset($options['as']) ? $options['as'].'.' : '';

    return trim(sprintf('%s%s.%s', $prefix, $name, $method), '.');
}

值得注意的是,如果單獨為某一個方法命名,那麼直接回返回命名,而不會受 as 和方法名 'method' 的影響。

相關文章