### 簡介
為了方便你自定義框架的核心組件功能,甚至是完全替換它們,Laravel 提供了大量可以對應用進行擴展的地方。例如,哈希服務實現了 `Illuminate\Contracts\Hashing\Hasher` 契約,你可以按照自己應用的需求來重新實現它。你還可以繼承 `Request` 對象類,添加自己用的順手的方法。你甚至可以添加全新的用戶認證、緩存和會話驅動!
Laravel 組件功能通常有兩種擴展方式:在服務容器里面綁定新實現,或者通過采用工廠模式實現的 `Manager` 類注冊一個自定義的擴展。在本章中,我們將探索擴展框架核心功能的不同方式,并檢查都需要些什么代碼。
> 注:關于工廠模式可以參考學院提供的[設計模式系列教程中的工廠模式](https://xueyuanjun.com/post/2465.html)。
### 管理類和工廠
Laravel 有多個 `Manager` 類用來管理基于驅動的組件的創建。這些組件包括緩存、會話、用戶認證、隊列組件等。管理類負責根據應用程序的配置來創建特定的驅動實例。例如,`CacheManager` 可以創建 APC、Memcached、Redis 以及其他不同的緩存驅動的實現。
同時,每個管理類都包含 `extend` 方法,該方法可用于將新的驅動解決方案注入到管理類中。下面我們將逐個介紹這些管理類,并向你展示如何將自定義的驅動注入它們。
> 了解你的管理類:請花點時間看看 Laravel 中每個 `Manager` 類的代碼,比如 `CacheManager` 和 `SessionManager`。通過閱讀這些代碼能讓你對Laravel 底層工作原理有更加全面的了解。所有管理類都繼承自`Illuminate\Support\Manager` 基類,該基類每個管理類提供了一些有用的通用功能(適用于 Laravel 4,Laravel 5并非如此)。
### 緩存
要擴展 Laravel 的緩存服務,需要使用 `CacheManager` 里的 `extend` 方法,該方法用來綁定自定義的緩存驅動到管理類。在所有管理類中都是這個邏輯,所以擴展其他的管理類也是按照這個思路來。例如,我們想注冊一個新的緩存驅動,名叫「mongo」,代碼可以這樣寫:
```php
Cache::extend('mongo', function($app) {
// Return Illuminate\Cache\Repository instance...
});
```
`extend` 方法的第一個參數是自定義緩存驅動的名字。該名字對應 `config/cache.php` 配置文件中的 `driver` 配置項。第二個參數是一個會返回 `Illuminate\Cache\Repository` 實例的匿名函數,傳入該匿名函數的 `$app` 參數是 `Illuminate\Foundation\Application` 的實例,即全局服務容器。
要創建自定義的緩存驅動,首先要實現 `Illuminate\Contracts\Cache\Store` 接口。所以,基于 MongoDB 實現的緩存驅動代碼結構如下:
```php
use Illuminate\Contracts\Cache\Store;
class MongoStore implements Store
{
public function get($key)
{
// TODO: Implement get() method.
}
public function many(array $keys)
{
// TODO: Implement many() method.
}
public function put($key, $value, $minutes)
{
// TODO: Implement put() method.
}
public function putMany(array $values, $minutes)
{
// TODO: Implement putMany() method.
}
public function increment($key, $value = 1)
{
// TODO: Implement increment() method.
}
public function decrement($key, $value = 1)
{
// TODO: Implement decrement() method.
}
public function forever($key, $value)
{
// TODO: Implement forever() method.
}
public function forget($key)
{
// TODO: Implement forget() method.
}
public function flush()
{
// TODO: Implement flush() method.
}
public function getPrefix()
{
// TODO: Implement getPrefix() method.
}
}
```
我們只需使用 MongoDB 連接來實現上面的每一個方法即可。一旦實現完畢,就可以像下面這樣完成自定義驅動的注冊:
```php
use Illuminate\Cache\Repository;
use Illuminate\Support\Facades\Cache;
Cache::extend('mongo', function($app)
{
return new Repository(new MongoStore);
}
```
正如上面的例子所示,在創建自定義驅動的時候你可以直接使用 `Illuminate\Cache\Repository` 基類。通常你不需要創建自己的 Repository 類。
如果你不知道要把自定義的緩存驅動代碼放到哪里,可以考慮將其放到擴展包然后發布到 [Packagist](https://packagist.org/) 。或者,你也可以在應用的 `app` 目錄下創建一個 `Extensions` 目錄,然后將 `MongoStore.php` 放到該目錄下。不過,Laravel 并沒有對應用程序的目錄結構做硬性規定,所以你完全可以按照自己喜歡的方式組織應用程序的代碼。
> 如果你還為在哪里存放注冊代碼發愁,服務提供者是個不錯的地方。我們之前就講過,使用服務提供者來管理框架擴展代碼是一個非常不錯的方式,將相應代碼放到服務提供者的 `boot` 方法即可。
### 會話(Session)
通過自定義會話驅動擴展 Laravel 功能和擴展緩存系統一樣簡單。和剛才一樣,我們還是使用 `extend` 方法來注冊自定義的代碼:
```php
Session::extend('mongo', function($app)
{
// Return implementation of SessionHandlerInterface
});
```
注意,我們自定義的會話驅動需要實現 `SessionHandlerInterface` 接口。這個接口在 PHP 5.4 以上版本才有。但如果你用的是 PHP 5.3,也別擔心,Laravel 會自動幫你定義這個接口的。該接口包含了一些需要我們實現的方法。基于 MongoDB 來實現的會話驅動的代碼結構就像下面這樣:
```php
class MongoHandler implements \SessionHandlerInterface
{
public function close()
{
// TODO: Implement close() method.
}
public function destroy($session_id)
{
// TODO: Implement destroy() method.
}
public function gc($maxlifetime)
{
// TODO: Implement gc() method.
}
public function open($save_path, $name)
{
// TODO: Implement open() method.
}
public function read($session_id)
{
// TODO: Implement read() method.
}
public function write($session_id, $session_data)
{
// TODO: Implement write() method.
}
}
```
這些方法不像前面的 `Illuminate\Contracts\Cache\Store` 接口定義的那么容易理解。下面我們簡單講講這些方法都是干什么的:
- `close` 方法和 `open` 方法通常都不是必需的。對大部分驅動來說都不必實現。
- `destroy` 方法會將與 `$sessionId` 相關聯的數據從持久化存儲系統中刪除。
- `gc` 方法會將所有存在時間超過參數 `$lifetime` 設定值的數據全都刪除,該參數是一個 UNIX 時間戳。如果你使用的是諸如 Memcached 或Redis 這種自主管理過期數據的系統,那么該方法可以留空。
- `open` 方法一般在基于文件的會話存儲系統中才會用到。Laravel 自帶的 `file` 會話驅動使用的就是 PHP 原生的基于文件的會話系統,你可能永遠也不需要在這個方法里寫東西,所以留空就好。另外這也是一個接口設計的反面教材(稍后我們會討論這一點),因為 PHP 總是要求我們實現它,即使大部分時候不需要實現它。
- `read` 方法會返回與給定 `$sessionId` 關聯的會話數據的字符串版本。在你的會話驅動中,無論讀取還是存儲會話數據,都不需要做任何序列化和編碼操作,因為 Laravel 已經替你做了。
- `write` 方法會將給定的 `$data` 字符串關聯到對應的 `$sessionId`,然后將其寫入到一個持久化存儲系統中,例如 MongoDB、數據庫、Redis 等。
一旦 `SessionHandlerInterface` 被實現,我們就可以將其注冊到會話管理器:
```php
Session::extend('mongo', function($app) {
return new MongoHandler;
});
```
注冊完畢后,我們就可以在 `config/session.php` 配置文件里使用`mongo` 驅動了。
> 你要是寫了個自定義的會話處理器,別忘了在 Packagist 上分享它!
### 用戶認證
用戶認證的擴展方式和緩存、會話的擴展方式一樣,使用認證管理類上的 `extend` 方法就可以了:
```php
Auth::extend('riak', function($app) {
// Return implementation of Illuminate\Contracts\Auth\UserProvider
});
```
`Illuminate\Contracts\Auth\UserProvider` 接口的實現類負責從某個持久化存儲系統(如 MySQL、Riak 等)中獲取 `Illuminate\Contracts\Auth\Authenticatable` 接口的實現類實例。這兩個接口使得 Laravel 的用戶認證機制得以在不用關心用戶數據如何存儲以及使用何種類型表示用戶的情況下繼續工作。
下面我們來看看 `Illuminate\Contracts\Auth\UserProvider` 接口的代碼:
```php
<?php
namespace Illuminate\Contracts\Auth;
interface UserProvider
{
/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier);
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* @param mixed $identifier
* @param string $token
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token);
/**
* Update the "remember me" token for the given user in storage.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $token
* @return void
*/
public function updateRememberToken(Authenticatable $user, $token);
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials);
/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials
* @return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials);
}
```
`retrieveById` 方法通常接受一個表示用戶ID的數字鍵,比如 MySQL 數據庫的自增 ID。該方法會返回與給定 ID 匹配的 `Illuminate\Contracts\Auth\Authenticatable` 的實現類實例,如 `User` 模型類實例。
當用戶嘗試登錄到應用時,`retrieveByCredentials` 方法會接受傳遞給 `Auth::attempt` 方法的認證憑證數組。然后該方法會「查詢」底層的持久化存儲系統,來找到與給定憑證信息匹配的用戶。通常,該方法會執行一個帶有「where」條件的查詢來匹配參數里的 `$credentials['username']`。**該方法不應該嘗試做任何密碼驗證**。
`validateCredentials` 方法會通過比較給定 `$user` 和`$credentials` 來認證用戶。例如,該方法會比較 `$user->getAuthPassword()` 方法返回的字符串和 `$credentials['password']` 經過 `Hash::make` 處理后的結果,如果相等,則認為認證通過,否則認證失敗。
`retrieveByToken` 方法和 `updateRememberToken` 則用于在登錄認證時實現「記住我」的功能,讓用戶在 Token 有效期內不用輸入登錄憑證即可自動登錄。
現在,我們已經探索了 `Illuminate\Contracts\Auth\UserProvider` 接口的每一個方法,接下來,我們來看看 `Illuminate\Contracts\Auth\Authenticatable` 接口。別忘了,`Authenticatable` 接口實現的實例是通過是 `UserProvider` 實現實例的 `retrieveById` 和 `retrieveByCredentials` 方法返回的:
```php
<?php
namespace Illuminate\Contracts\Auth;
interface Authenticatable
{
/**
* Get the name of the unique identifier for the user.
*
* @return string
*/
public function getAuthIdentifierName();
/**
* Get the unique identifier for the user.
*
* @return mixed
*/
public function getAuthIdentifier();
/**
* Get the password for the user.
*
* @return string
*/
public function getAuthPassword();
/**
* Get the token value for the "remember me" session.
*
* @return string
*/
public function getRememberToken();
/**
* Set the token value for the "remember me" session.
*
* @param string $value
* @return void
*/
public function setRememberToken($value);
/**
* Get the column name for the "remember me" token.
*
* @return string
*/
public function getRememberTokenName();
}
```
這個接口很簡單。`getAuthIdentifier` 方法返回用戶的「主鍵」。如果在 MySQL 數據庫中,就是自增主鍵了。`getAuthPassword` 方法返回經過散列處理的用戶密碼。`getAuthIdentifierName` 方法會返回用戶的唯一標識,比如用戶名或郵箱信息。其他幾個方法都是和「記住我」功能相關的。
有了這個接口,用戶認證系統就可以處理任何用戶類,而不用關心該用戶類使用了什么 ORM 框架或者存儲抽象層。默認情況下,Laravel 已經在 `app` 目錄下提供了實現 `Authenticatable` 接口的 `User` 類。所以你可以將這個類作為實現示例。
最后,當我們實現了 `Illuminate\Contracts\Auth\UserProvider` 接口后,就可以將對應擴展注冊進 `Auth` 里面:
```php
Auth::extend('riak', function($app) {
return new RiakUserProvider($app['riak.connection']);
});
```
使用 `extend` 方法注冊好驅動以后,你就可以在 `config/auth.php` 配置文件中切換到新的驅動了(對應配置項是 `providers.users.driver`)。
### 容器默認綁定
幾乎所有 Laravel 框架自帶的服務提供者都會綁定一些對象到服務容器里。你可以在 `config/app.php` 配置文件里找到服務提供者列表。如果你有時間的話,你應該大致過一遍每個服務提供者的源碼。這么做的好處是你可以對每個服務提供者有更深的理解,明白它們都往框架里加了什么東西,以及對應的綁定到服務容器的鍵是什么,通過這些鍵我們就可以從容器中解析相應的服務。
> 學院君注:由于 Laravel 5.5 中新增了包自動發現功能,所以 `config/app.php` 配置文件的 `providers` 數組提供的服務服務者列表并不全,最全的列表在 `bootstrap/cache/services.php` 的 `providers` 數組中。
舉個例子,`AuthServiceProvider` 向服務容器內綁定了一個 `auth` 鍵,通過這個鍵解析出來的服務是一個 `Illuminate\Auth\AuthManager` 的實例。你可以在自己的應用中通過覆蓋這個服務容器綁定來輕松實現擴展并重寫該類。例如,你可以創建一個繼承自 `AuthManager` 類的子類:
```php
namespace App\Extensions;
class MyAuthManager extends Illuminate\Auth\AuthManager
{
//
}
```
子類寫好以后,你可以在服務提供者 `AuthServiceProvider` 的 `boot` 方法中覆蓋默認的 `auth`:
```php
public function boot()
{
...
$this->app->singleton('auth', function ($app) {
return new MyAuthManager;
});
}
```
```
public function boot()
```
2
```
{
```
3
```
...
```
4
```
```
5
```
$this->app->singleton('auth', function ($app) {
```
6
```
return new MyAuthManager;
```
7
```
});
```
8
```
}
```
這就是擴展綁定進容器的任意核心類的通用方法。基本上每一個核心類都以這種方式綁定進了容器,都可以被重寫。還是那一句話,讀一遍框架自帶的服務提供者源碼可以幫助你熟悉各種類是怎么綁定進容器的,都綁定到哪些鍵上。這是學習 Laravel 框架底層究竟如何運轉的最佳實踐。
### 請求
由于這是框架里面非常基礎的部分,并且在請求生命周期中很早就被實例化,所以擴展 `Request` 類的方法與之前擴展其他服務的示例相比是有些不同的。
首先,還是要寫個繼承自 `Illuminate\Http\Request` 的子類:
```php
namespace App\Extensions;
class CustomRequest extends Illuminate\Http\Request
{
// Custom, helpful methods here...
}
```
子類寫好后,打開 `bootstrap/app.php` 文件。該文件是每次對應用發起請求時最早被載入的幾個文件之一。該文件中執行的第一個動作是創建 Laravel 的 `$app` 實例:
```php
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
```
當新的應用實例創建后,它將會創建一個 `Illuminate\Http\Request` 的實例并且將其綁定到服務容器里,鍵名為 `request`。所以,我們需要找個方法來將一個自定義的類指定為「默認的」請求類,對不對?在 Laravel 4 中,應用實例上有一個 `requestClass` 方法,可以用來指定自定義的請求類,但是在 Laravel 5 中,沒有這個類,所以我們需要自己來實現,打開 `public/index.php`,在
```php
app = require_once __DIR__.'/../bootstrap/app.php';
```
這行代碼之后添加如下代碼覆蓋默認的 `request` 指向:
```php
$app->alias('request', \App\Extensions\CustomRequest::class);
```
然后還要修改
```php
$request = Illuminate\Http\Request::capture()
```
這段代碼為
```php
$request = App\Extensions\CustomRequest::capture()
```
指定好自定義的請求類后,Laravel 會在任何創建 `Request` 實例的時候都使用這個自定義的類,以便你始終擁有自定義的請求類,即使在單元測試中也不例外!
> 學院君注:以上擴展 Request 請求類之后,控制器注入處都要改成 `CustomRequest`,此外,表單請求類也要作調整,請求表單請求類的祖先類中不包含 `CustomRequest`。