Laravel HTTP——路由

leoyang發表於2017-07-18

前言

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...

作為一個 web 後臺框架,路由無疑是極其重要的一部分。本部落格接下來幾篇文章都將會圍繞路由這一主題來展開討論,分別講述:

  • 路由的使用
  • 路由屬性註冊
  • 路由的正則編譯與匹配
  • 路由的中介軟體
  • 路由的控制器與引數繫結
  • RESTful 路由

和之前一樣,第一篇將會利用單元測試樣例說明我們在平時可能用到的 route 的 api 函式用法,後面幾篇文章將會剖析 laravel 的 route 原始碼。下面開始介紹 laravel 中路由的各種用法。


路由屬性註冊

所有 Laravel 路由都定義在位於 routes 目錄下的路由檔案中,這些檔案通過框架自動載入。routes/web.php 檔案定義了 web 介面的路由,這些路由被分配了 web 中介軟體組,從而可以提供 session 和 csrf 防護等功能。routes/api.php 中的路由是無狀態的,被分配了 api 中介軟體組。

對大多數應用而言,都是從 routes/web.php 檔案開始定義路由。

路由 method 方法

我們可以註冊路由來響應任何 HTTP 請求:

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

有時候還需要註冊路由響應多個 HTTP 請求——這可以通過 match 方法來實現。或者,可以使用 any 方法註冊一個路由來響應所有 HTTP 請求:

Route::match(['get', 'post'], '/', function () {
    //
});

Route::any('foo', function () {
    //
});

值得注意的是,一般的HTML表單僅僅支援getpost,並不支援putpatchdelete等動作,這時候就需要在前端新增一個隱藏的 _method 欄位到給表單中,其值被用作 HTTP 請求方法名:

<input type="hidden" name="_method" value="PUT">

在 web 路由檔案中所有請求方式為PUTPOSTDELETE的 HTML 表單都會包含一個CSRF令牌欄位,否則,請求會被拒絕。關於 CSRF 的更多細節,可以參考 淺談CSRF攻擊方式

<form method="POST" action="/profile">
    {{ csrf_field() }}
    ...
</form>

路由 scheme 協議

對於 web 後臺框架來說,路由的 scheme 底層協議一般使用 httphttps:

Route::get('foo/{bar}', ['http', function () {
    }]);
Route::get('foo/{bar}', ['https', function () {
    }]);

路由 domain 子域名

子域名可以像 URI 一樣被分配給路由引數,子域名可以通過路由屬性中的 domain 來指定:

Route::domain('api.name.bar')
     ->get('foo/bar', function ($name) {
        return $name;
    });

Route::get('foo/bar', ['domain' => 'api.name.bar', function ($name) {
        return $name;
    }]);

路由 prefix 字首

可以為路由新增一個給定 URI 字首,通過利用路由屬性的 prefix 指定:

Route::prefix('pre')
     ->get('foo/bar', function () {
    });

Route::get('foo/bar', ['prefix' => 'pre', function () {
    }]);

Route::get('foo/bar', function () {
    })->prefix('pre');

路由 where 正則約束

可以為路由的 URI 引數指定正則約束:

Route::get('{one}', ['where' => ['one' => '(.+)'], function () {
    }]);

Route::get('{one}', function () {
    })->where('one', '(.+)');

如果想要路由引數在全域性範圍內被給定正規表示式約束,可以使用 pattern 方法。在 RouteServiceProvider 類的 boot 方法中定義約束模式:

public function boot()
{
    Route::pattern('one', '(.+)');
    parent::boot();
}

路由 middleware 中介軟體

為路由新增中介軟體,通過利用路由屬性的 middleware 指定:

Route::middleware('web')
     ->get('foo/bar', function () {
    });

Route::get('foo/bar', ['middleware' => 'web', function () {
    }]);

Route::get('foo/bar', function () {
    })->middleware('web');

路由 namespace 屬性

可以為路由的控制器新增 namespace 來指定控制器的名稱空間:

Route::namespace('Namespace\\Example\\')
     ->get('foo/bar', function () {
    });

Route::get('foo/bar', ['namespace' => 'Namespace\\Example\\', function () {
    }]);

路由 uses 屬性

可以為路由新增 URI 對應的執行邏輯,例如閉包或者控制器:

Route::get('foo/bar', ['uses' => function () {
    }]);

Route::get('foo/bar', ['uses' => ‘Illuminate\Tests\Routing\RouteTestControllerStub@index’]);

Route::get('foo/bar')->uses(function () {
    });

Route::get('foo/bar')->uses(‘Illuminate\Tests\Routing\RouteTestControllerStub@index’);

路由 as 別名

可以為路由指定別名,通過路由屬性的 as 來指定:

Route::as('Foo')
     ->get('foo/bar', function () {
    });

Route::name('Foo')
     ->get('foo/bar', function () {
    });

Route::get('foo/bar', ['as' => 'Foo', function () {
    }]);

Route::get('foo/bar', function () {
    })->name('Foo');

路由 group 群組屬性

可以為一系列具有類似屬性的路由歸為同一組,利用 group 將這些路由歸併到一起:

Route::group(['domain'     => 'group.domain.name',
              'prefix'     => 'grouppre',
              'where'      => ['one' => '(.+)'], 
              'middleware' => 'groupMiddleware',
              'namespace'  => 'Namespace\\Group\\', 
              'as'         => 'Group::',]
              function () {
                  Route::get('/replace',‘domain’ => 'route.domain.name',
                                        'uses'   => function () {
                                        return 'replace';
                  });

                 Route::get('additional/{one}/{two}', 'prefix'     => 'routepre',
                                                      'where'      => '['one' => '([0-9]+)','two' => '(.+)']',
                                                      'middleware' => 'routeMiddleware',
                                                      'namespace'  => 'Namespace\\Group\\', 
                                                      'as'         => 'Route',
                                                      'use         => 'function () {
                                                      return 'additional';
                 });
});

$this->assertEquals('replace', $router->dispatch(Request::create('http://route.domain.name/grouppre/replace', 'GET'))->getContent());

$this->assertEquals('additional', $router->dispatch(Request::create('http://group.domain.name/routepre/grouppre/additional/111/add', 'GET'))->getContent());

$routes = $router->getRoutes()->getRoutes();
$action = $routes[0]->getAction();
$this->assertEquals('Namespace\\Group\\', $action['namespace']);
$this->assertEquals('Group::', $action['as']);

$routes = $router->getRoutes()->getRoutes();
$action = $routes[1]->getAction();
$this->assertEquals(['groupMiddleware', 'routeMiddleware'], $action['middleware']);
$this->assertEquals('Namespace\\Group\\Namespace\\Group\\', $action['namespace']);
$this->assertEquals('Group::Route', $action['as']);

group 群組的屬性分為兩類:替換型、遞增型。當群組屬性與路由屬性重複的時候,替換型屬性會用路由的屬性替換群組的屬性,遞增型的屬性會綜合路由和群組的屬性。

在上面的例子可以看出:

  • domain 這個屬性是替換型屬性,路由的屬性會覆蓋和替換群組的這幾個屬性;
  • prefixmiddlewarenamespaceaswhere 這幾個屬性是遞增型屬性,路由的屬性和群組屬性會相互結合。

另外值得注意的是:

  • 路由的 prefix 屬性具有優先順序,因此上面第二個路由的 uriroutepre/grouppre/additional/111/add,而不是 grouppre/routepre/additional/111/add
  • where屬性對於相同的路由引數會替換,不同的路由引數會結合,因此上面 whereone 被替換,two 被結合進來

路由引數與匹配

laravel 允許在註冊定義路由的時候設定路由引數,以供控制器或者閉包所用。路由引數可以設定在 URI 中,也可以設定在 domain 中。

路由編碼匹配

對於已編碼的請求 URI,框架會自動進行解碼然後進行匹配:

$router = $this->getRouter();
 $router->get('foo/bar/åαф', function () {
     return 'hello';
});
$this->assertEquals('hello', $router->dispatch(Request::create('foo/bar/%C3%A5%CE%B1%D1%84', 'GET'))->getContent());

$router = $this->getRouter();
$route = $router->get('foo/{file}', function ($file) {
    return $file;
});
$this->assertEquals('oxygen%20', $router->dispatch(Request::create('http://test.com/foo/oxygen%2520', 'GET'))->getContent());

路由引數

路由引數總是通過花括號進行包裹,這些引數在路由被執行時會被傳遞到路由的閉包。路由引數不能包含 - 字元,需要的話可以使用 _ 替代。

$router = $this->getRouter();
$route = $router->get('foo/{age}', ['domain' => 'api.{name}.bar', function ($name, $age) {
   return $name.$age;
}]);
$this->assertEquals('taylor25', $router->dispatch(Request::create('http://api.taylor.bar/foo/25', 'GET'))->getContent());

$route = new Route('GET', 'images/{id}.{ext}', function () {
    });

$request1 = Request::create('images/1.png', 'GET');
$this->assertTrue($route->matches($request1));
$route->bind($request1);
$this->assertTrue($route->hasParameter('id'));
$this->assertFalse($route->hasParameter('foo'));
$this->assertEquals('1', $route->parameter('id'));
$this->assertEquals('png', $route->parameter('ext'));        

路由可選引數

有時候可能需要指定可選的路由引數,這可以通過在引數名後加一個 ? 標記來實現,這種情況下需要給相應的變數指定預設值:

$router = $this->getRouter();
$router->get('{foo?}/{baz?}', function ($name = 'taylor', $age = 25) {
     return $name.$age;
});
$this->assertEquals('fred25', $router->dispatch(Request::create('fred', 'GET'))->getContent());

$router->get('default/{foo?}/{baz?}', function ($name, $age = 25) {
     return $name.$age;
})->default('name', 'taylor');
$this->assertEquals('fred25', $router->dispatch(Request::create('fred', 'GET'))->getContent());

路由引數正則約束

可以使用路由例項上的 where 方法來約束路由引數的格式。where 方法接收引數名和一個正規表示式來定義該引數如何被約束:

Route::get('user/{name}', function ($name) {
    //
})->where('name', '[A-Za-z]+');

如果想要路由引數在全域性範圍內被給定正規表示式約束,可以使用 pattern 方法。在 RouteServiceProvider 類的 boot 方法中定義約束模式:

public function boot()
{
    Route::pattern('id', '[0-9]+');
    parent::boot();
}

值得注意的是,路由引數是不允許出現 / 字元的,例如:

$router->get('{one?}', [
            'uses' => function ($one = null){
                 return $one;
            },
        ]);
$request = Request::create('foo/bar/baz', 'GET');
$this->assertFalse($route->matches($request));

上例中 one 只能匹配 foo,不能匹配 foo/bar/baz,這時就需要對 one 進行正則約束:

public function testLeadingParamDoesntReceiveForwardSlashOnEmptyPath()
{
    $router = $this->getRouter();
    $router->get('{one?}', [
            'uses' => function ($one = null){
                 return $one;
            },
            'where' => ['one' => '(.+)'],
        ]);

    $this->assertEquals('foo', $router->dispatch(Request::create('/foo', 'GET'))->getContent());
    $this->assertEquals('foo/bar/baz', $router->dispatch(Request::create('/foo/bar/baz', 'GET'))->getContent());
}

路由中介軟體

HTTP 中介軟體為過濾進入應用的 HTTP 請求提供了一套便利的機制。例如,Laravel 內建了一箇中介軟體來驗證使用者是否經過認證,如果使用者沒有經過認證,中介軟體會將使用者重定向到登入頁面,否則如果使用者經過認證,中介軟體就會允許請求繼續往前進入下一步操作。

Laravel框架自帶了一些中介軟體,包括認證、CSRF 保護中介軟體等等。所有的中介軟體都位於 app/Http/Middleware 目錄。

中介軟體之前/之後/終止

一箇中介軟體是請求前還是請求後執行取決於中介軟體本身。比如,以下中介軟體會在請求處理前執行一些任務:

class BeforeMiddleware
{
    public function handle($request, Closure $next)
    {
        // 執行動作

        return $next($request);
    }
}

class AfterMiddleware
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        // 執行動作

        return $response;
    }
}

有時候中介軟體可能需要在 HTTP 響應傳送到瀏覽器之後做一些工作。比如,Laravel 內建的“session”中介軟體會在響應傳送到瀏覽器之後將 Session 資料寫到儲存器中,為了實現這個功能,需要定義一個終止中介軟體並新增 terminate 方法到這個中介軟體:

class StartSession
{
    public function handle($request, Closure $next)
    {
        return $next($request);
    }

    public function terminate($request, $response)
    {
        // 儲存session資料...
    }
}

全域性中介軟體

如果你想要中介軟體在每一個 HTTP 請求期間被執行,只需要將相應的中介軟體類設定到 app/Http/Kernel.php 的陣列屬性 $middleware 中即可。

protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

路由中介軟體

如果你想要分配中介軟體到指定路由,可以傳遞完整的類名:

use App\Http\Middleware\CheckAge;

Route::get('admin/profile', function () {
    //
})->middleware(CheckAge::class);

或者可以給中介軟體提供一個別名:

public function testDefinedClosureMiddleware()
 {
    $router = $this->getRouter();
    $router->get('foo/bar', ['middleware' => 'foo', function () {
        return 'hello';
    }]);
    $router->aliasMiddleware('foo', function ($request, $next) {
        return 'caught';
    });
    $this->assertEquals('caught', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent());
 }

也可以應該在 app/Http/Kernel.php 檔案中分配給該中介軟體一個 key,預設情況下,該類的 $routeMiddleware 屬性包含了 Laravel 自帶的中介軟體,要新增你自己的中介軟體,只需要將其追加到後面併為其分配一個 key,例如:

protected $routeMiddleware = [
    'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];

Route::get('admin/profile', function () {
    //
})->middleware('auth');

使用陣列分配多箇中介軟體到路由:

Route::get('/', function () {
    //
})->middleware('first', 'second');

中介軟體組

有時候你可能想要通過指定一個鍵名的方式將相關中介軟體分到同一個組裡面,從而更方便將其分配到路由中,這可以通過使用 HTTP Kernel$middlewareGroups 屬性實現。

Laravel 自帶了開箱即用的 webapi 兩個中介軟體組以分別包含可以應用到 Web UIAPI 路由的通用中介軟體:

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

    'api' => [
        'throttle:60,1',
        'auth:api',
    ],
];

Route::get('/', function () {
    //
})->middleware('web');

值得注意的是,中介軟體組中可以迴圈巢狀中介軟體組:

 public function testMiddlewareGroupsCanReferenceOtherGroups()
 {
    unset($_SERVER['__middleware.group']);
    $router = $this->getRouter();
    $router->get('foo/bar', ['middleware' => 'web', function () {
        return 'hello';
    }]);

    $router->aliasMiddleware('two', 'Illuminate\Tests\Routing\RoutingTestMiddlewareGroupTwo');
    $router->middlewareGroup('first', ['two:abigail']);
    $router->middlewareGroup('web', ['Illuminate\Tests\Routing\RoutingTestMiddlewareGroupOne', 'first']);

    $this->assertEquals('caught abigail', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent());
    $this->assertTrue($_SERVER['__middleware.group']);

    unset($_SERVER['__middleware.group']);
}

中介軟體引數

中介軟體還可以接收額外的自定義引數,例如,如果應用需要在執行給定動作之前驗證認證使用者是否擁有指定的角色,可以建立一個 CheckRole 來接收角色名作為額外引數。

額外的中介軟體引數會在 $next 引數之後傳入中介軟體:

namespace App\Http\Middleware;

use Closure;

class CheckRole
{
    public function handle($request, Closure $next, $role)
    {
        if (! $request->user()->hasRole($role)) {
            // Redirect...
        }

        return $next($request);
    }

}

Route::put('post/{id}', function ($id) {
    //
})->middleware('role:editor');

中介軟體的順序

router 中有多箇中介軟體的時候,中介軟體的執行順序並不是嚴格按照中介軟體陣列進行的,框架中存在一個陣列 $middlewarePriority,規定了這個陣列中各個中介軟體的順序:

protected $middlewarePriority = [
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \Illuminate\Auth\Middleware\Authenticate::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ];

當我們使用了上面其中多箇中介軟體的時候,框架會自動按照上面的陣列進行排序:

 public function testMiddlewarePrioritySorting()
{
    $middleware = [
        Placeholder1::class,
        SubstituteBindings::class,
        Placeholder2::class,
        Authenticate::class,
        Placeholder3::class,
    ];

    $router = $this->getRouter();

    $router->middlewarePriority = [Authenticate::class, SubstituteBindings::class, Authorize::class];

    $route = $router->get('foo', ['middleware' => $middleware, 'uses' => function ($name) {
        return $name;
    }]);

    $this->assertEquals([
        Placeholder1::class,
        Authenticate::class,
        SubstituteBindings::class,
        Placeholder2::class,
        Placeholder3::class,
    ], $router->gatherRouteMiddleware($route));
}

控制器

控制器類

更普遍的方法是使用控制器來組織管理這些行為。控制器可以將相關的 HTTP 請求封裝到一個類中進行處理。通常控制器存放在 app/Http/Controllers 目錄中.

所有的 Laravel 控制器應該繼承自 Laravel 自帶的控制器基類 Controller,控制器基類提供了一些很方便的方法如 middleware,用於新增中介軟體到控制器動作:

class UserController extends Controller
{
    public function show($id)
    {
        return view('user.profile', ['user' => User::findOrFail($id)]);
    }
}

Route::get('user/{id}', 'UserController@show');

單動作控制器

如果想要定義一個只處理一個動作的控制器,可以在這個控制器中定義 __invoke 方法,當為這個單動作控制器註冊路由的時候,不需要指定方法:

public function testDispatchingCallableActionClasses()
{
    $router = $this->getRouter();
    $router->get('foo/bar', 'Illuminate\Tests\Routing\ActionStub');

    $this->assertEquals('hello', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent());

    $router->get('foo/bar2', [
        'uses' => 'Illuminate\Tests\Routing\ActionStub@func',
    ]);

    $this->assertEquals('hello2', $router->dispatch(Request::create('foo/bar2', 'GET'))->getContent());
}

class ActionStub extends Controller
{    
    public function __invoke()
    {
        return 'hello';
    }
}

控制器中介軟體

將中介軟體放在控制器建構函式中更方便,在控制器的建構函式中使用 middleware 方法你可以很輕鬆的分配中介軟體給該控制器。你甚至可以限定該中介軟體應用到該控制器類的指定方法:

class UserController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('log')->only('index');
        $this->middleware('subscribed')->except('store');
    }
}

callAction 方法

值得注意的是每次執行控制器方法都會先執行控制器的 callAction 函式:

public function callAction($method, $parameters)
{
    return call_user_func_array([$this, $method], $parameters);
}

測試樣例:


unset($_SERVER['__test.controller_callAction_parameters']);
$router->get(($str = str_random()).'/{one}/{two}', 'Illuminate\Tests\Routing\RouteTestAnotherControllerWithParameterStub@oneArgument');
$router->dispatch(Request::create($str.'/one/two', 'GET'));
 $this->assertEquals(['one' => 'one', 'two' => 'two'], $_SERVER['__test.controller_callAction_parameters']);

class RouteTestAnotherControllerWithParameterStub extends Controller
{
    public function callAction($method, $parameters)
    {
        $_SERVER['__test.controller_callAction_parameters'] = $parameters;
    }

    public function oneArgument($one)
    {
    }
}

__call方法

和普通類一樣,若控制器中沒有對應 classname@method 中的 method ,則會呼叫類的 __call 函式。

public function testCallableControllerRouting()
{
    $router = $this->getRouter();

    $router->get('foo/bar', 'Illuminate\Tests\Routing\RouteTestControllerCallableStub@bar');
    $router->get('foo/baz', 'Illuminate\Tests\Routing\RouteTestControllerCallableStub@baz');

    $this->assertEquals('bar', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent());
    $this->assertEquals('baz', $router->dispatch(Request::create('foo/baz', 'GET'))->getContent());
}

class RouteTestControllerCallableStub extends Controller
{
    public function __call($method, $arguments = [])
    {
        return $method;
    }
}

路由引數依賴注入與繫結

Laravel 使用服務容器解析所有的 Laravel 控制器,因此,可以在控制器的建構函式中型別宣告任何依賴,這些依賴會被自動解析並注入到控制器例項中。路由的引數繫結可以分為兩種:顯示繫結與隱示繫結。

路由隱示繫結

  • 控制器方法期望輸入路由引數,只需要將路由引數放到其他依賴之後
Route::put('user/{id}', 'UserController@update');

class UserController extends Controller
{
    public function update(Request $request, $id)
    {
    }
}
  • 可以在控制器的動作方法中進行依賴的型別提示,例如,我們可以在某個方法中型別提示 Illuminate\Http\Request 例項:
class UserController extends Controller
{
    public function store(Request $request)
    {
        $name = $request->input('name');
    }
}
  • 可以為控制器的動作方法中新增資料庫模型的主鍵,框架會自動利用主鍵來獲取對應的記錄,需要注意的是,route定義路由的路由引數必須和控制器內的變數名相同,例如下例中路由引數 userid 和控制器引數 userid:
Route::put('user/{userid}', 'UserController@update');

class UserController extends Controller
{
    public function update(UserModel $userid)
    {
        $userid->name = 'taylor';
        $userid->update();
    }
}

綜合測試樣例:

public function testImplicitBindingsWithOptionalParameter()
{
    unset($_SERVER['__test.controller_callAction_parameters']);
    $router->get(($str = str_random()).'/{user}/{defaultNull?}/{team?}', [
        'middleware' => SubstituteBindings::class,
        'uses' => 'Illuminate\Tests\Routing\RouteTestAnotherControllerWithParameterStub@withModels',
    ]);

    $router->dispatch(Request::create($str.'/1', 'GET'));

    $values = array_values($_SERVER['__test.controller_callAction_parameters']);

    $this->assertInstanceOf('Illuminate\Http\Request', $values[0]);
    $this->assertEquals(1, $values[1]->value);
    $this->assertNull($values[2]);
    $this->assertInstanceOf('Illuminate\Tests\Routing\RoutingTestTeamModel', $values[3]);
}

class RouteTestAnotherControllerWithParameterStub extends Controller
{
    public function callAction($method, $parameters)
    {
        $_SERVER['__test.controller_callAction_parameters'] = $parameters;
    }

    public function withModels(Request $request, RoutingTestUserModel $user, $defaultNull = null, RoutingTestTeamModel $team = null)
    {
    }
}

class RoutingTestUserModel extends Model
{
    public function getRouteKeyName()
    {
        return 'id';
    }

    public function where($key, $value)
    {
        $this->value = $value;

        return $this;
    }

    public function first()
    {
        return $this;
    }

    public function firstOrFail()
    {
        return $this;
    }
}

class RoutingTestTeamModel extends Model
{
    public function getRouteKeyName()
    {
        return 'id';
    }

    public function where($key, $value)
    {
        $this->value = $value;

        return $this;
    }

    public function first()
    {
        return $this;
    }

    public function firstOrFail()
    {
        return $this;
    }
}

路由顯示繫結

除了隱示地轉化路由引數外,我們還可以給路由引數顯示提供繫結。顯示繫結有 bindmodel 兩種方法。

  • 通過 bind 為引數繫結閉包函式:
public function testRouteBinding()
{
    $router = $this->getRouter();
    $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) {
         return $name;
    }]);
    $router->bind('bar', function ($value) {
        return strtoupper($value);
    });
    $this->assertEquals('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent());
}
  • 通過 bind 為引數繫結類方法,可以指定 classname@method,也可以直接使用類名,預設會呼叫類的 bind 函式:
public function testRouteClassBinding()
{
    $router = $this->getRouter();
    $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) {
        return $name;
    }]);
    $router->bind('bar', 'Illuminate\Tests\Routing\RouteBindingStub');
    $this->assertEquals('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent());
}

public function testRouteClassMethodBinding()
{
    $router = $this->getRouter();
    $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) {
        return $name;
    }]);
    $router->bind('bar', 'Illuminate\Tests\Routing\RouteBindingStub@find');
    $this->assertEquals('dragon', $router->dispatch(Request::create('foo/Dragon', 'GET'))->getContent());
}

class RouteBindingStub
{
    public function bind($value, $route)
    {
        return strtoupper($value);
    }

    public function find($value, $route)
    {
        return strtolower($value);
    }
}
  • 通過 model 為引數繫結資料庫模型,路由的引數就不需要和控制器方法中的變數名相同,laravel 會利用路由引數的值去呼叫 where 方法查詢對應記錄:
if ($model = $instance->where($instance->getRouteKeyName(), $value)->first()) {
     return $model;
}

測試樣例如下:

public function testModelBinding()
{
    $router = $this->getRouter();
    $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) {
        return $name;
    }]);
    $router->model('bar', 'Illuminate\Tests\Routing\RouteModelBindingStub');
    $this->assertEquals('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent());
}

class RouteModelBindingStub
{
    public function getRouteKeyName()
    {
        return 'id';
    }

    public function where($key, $value)
    {
        $this->value = $value;

        return $this;
    }

    public function first()
    {
        return strtoupper($this->value);
    }
}
  • 若繫結的 model 並沒有找到對應路由引數的記錄,可以在 model 中定義一個閉包函式,路由引數會呼叫閉包函式:
public function testModelBindingWithCustomNullReturn()
{
    $router = $this->getRouter();
    $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) {
        return $name;
    }]);
    $router->model('bar', 'Illuminate\Tests\Routing\RouteModelBindingNullStub', function () {
        return 'missing';
    });
    $this->assertEquals('missing', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent());
}

public function testModelBindingWithBindingClosure()
{
    $router = $this->getRouter();
    $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) {
        return $name;
    }]);
    $router->model('bar', 'Illuminate\Tests\Routing\RouteModelBindingNullStub', function ($value) {
        return (new RouteModelBindingClosureStub())->findAlternate($value);
    });
    $this->assertEquals('tayloralt', $router->dispatch(Request::create('foo/TAYLOR', 'GET'))->getContent());
}

class RouteModelBindingNullStub
{
    public function getRouteKeyName()
    {
        return 'id';
    }

    public function where($key, $value)
    {
        return $this;
    }

    public function first()
    {
    }
}

class RouteModelBindingClosureStub
{
    public function findAlternate($value)
    {
        return strtolower($value).'alt';
    }
}

router擴充套件方法

router支援新增自定義的方法,只需要利用 macro 函式來註冊對應的函式名和函式實現:

public function testMacro()
{
    $router = $this->getRouter();
    $router->macro('webhook', function () use ($router) {
        $router->match(['GET', 'POST'], 'webhook', function () {
            return 'OK';
        });
    });
    $router->webhook();
    $this->assertEquals('OK', $router->dispatch(Request::create('webhook', 'GET'))->getContent());
    $this->assertEquals('OK', $router->dispatch(Request::create('webhook', 'POST'))->getContent());
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章