使用 Laravel Manager 類別 - 原理篇

許久沒寫文章了,今天寫個 Laravel 相關的主題,主要是 Manager 的使用方法。

這個類別在官方文件並沒有出現,而框架本身有在使用。首先先來分析原始碼,它是一段含註解不到 200 行的類別:

abstract class Manager
{
// Laravel Container
protected $container;

// Laravel Config
protected $config;

public function __construct(Container $container)
{
$this->container = $container;
$this->config = $container->make('config');
}

abstract public function getDefaultDriver();
}

它是一個抽象類別,裡面依賴了 Container 實例,同時 Container 必須註冊 config,裡面放的是 Config 的實例;而抽象類所要求要實作的方法是 getDefaultDriver(),這裡的 Driver 看下文可以了解,它指的可能是一群行為有相關的「實作」,而這個抽象方法是要取得預設的實作。

這個抽象類別在框架裡面的實作有三個:

綜合上面這些觀察可以發現,Manager 想要達成的其中一個目標是,要統一管理多個「建立實例的方法」,而實例的樣貌有三種類型如下:

  1. 相同的類別,但是不同的初始化方法。例如:SessionManager 一律都是回傳 Illuminate\Session\Store,但它建立實體的方法會隨著定義不同而有所不同;
  2. 相同的介面,但是不同的實作。例如: HashManager 產生的都是實作 Illuminate\Contracts\HashingHasher 介面的實體。
  3. 完全不相關的實作。例如 ChannelManager 產生的實體並沒有被任何介面約束,雖然程式裡可以查得到產生出來的實體都有實作 send() 方法,但實際上並沒有實作任何介面。

所以實際上 Manager 是一個實作工廠模式(Factory Pattern)或建造者模式(Builder Pattern)的「管理者」。當不同的實作需要透過同一個實體管理時,Manager 會是個很好的工具。

Manager 如何產生實體?

繼續看其他原始碼,首先是取得實體的入口 driver()

public function driver($driver = null)
{
// 如果沒有指定要取得哪個實作,則會拿預設的實作
$driver = $driver ?: $this->getDefaultDriver();

// 必須要指定,否則就丟例外
if (is_null($driver)) {
throw new InvalidArgumentException(sprintf(
'Unable to resolve NULL driver for [%s].', static::class
));
}

// 這裡使用了 Registry Singleton Pattern
// 指定實作若在記憶體有存在,則直接回傳,否則就建一個新的回傳
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}

return $this->drivers[$driver];
}

這裡可以看到有個 $this->drivers 的屬性,這是要存放已產生過的實體。相同的請求,在第二次進來的時候,會從記憶體直接取得並回傳,這是 Registry of Singleton Pattern 的實作。

再來看 createDriver() 實際建立實體的方法:

protected function createDriver($driver)
{
// 當有設定自定義建立器的時候,就使用它
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
} else {
// 如果沒有的話就看有沒有定義建立實體的方法
// 如 `createSessionDriver()` 是否存在
$method = 'create'.Str::studly($driver).'Driver';

if (method_exists($this, $method)) {
return $this->$method();
}
}

// 都沒有的話,就丟例外
throw new InvalidArgumentException("Driver [$driver] not supported.");
}

這段比較 Magic。一般使用 Manager 是為了要管理實體產生的方法,接著需要透過實作方法(指類別的 Method)來讓 Manager 在需要實體的實作來呼叫並產生對應的實體。

可以從上面這段程式碼裡面的 $method 變數看得出來,這個實作的方法名稱會跟 Driver 的名稱有關。實際的從 Driver 名稱轉換到方法名稱的範例如下:

cache => createCacheDriver
session => createSessionDriver
database => createDatabaseDriver
my_new-class => createMyNewClassDriver

擴充實體生成方法

在實作 Manager 的時候,會先預設好可能的實體生成方法,並先實作完成。例如 SessionManager 這個類別,已經實作好 File、Redis 與 Database 等實體的生成方法。但在兩個情境下,有可能會需要為實作的 Manager 擴充新的方法:

  1. 有新的實體生成方法,如 HashManager 有新的演算法。
  2. 原有的生成方法不滿意,因此想使用新的方法取代它。

Manager 擴充方法是透過呼叫 extend() 方法來達成:

public function extend($driver, Closure $callback)
{
$this->customCreators[$driver] = $callback;

return $this;
}

從行為上來看,這個方法的介面是透過註冊一個新的建立實體方法在 $this->customCreators 屬性裡。 而在前面呼叫 createDriver() 的時候會先確認 Driver 的名稱有沒有註冊,如果有的話會透過 callCustomCreator() 呼叫:

protected function callCustomCreator($driver)
{
return $this->customCreators[$driver]($this->container);
}

因此可以想像註冊的方法可能會是像這樣:

$manager->extend('new', function(Container $app) {
return new Some($app->make(Dep::class));
});

代理模式

Manager 除了當管理實體角色外,它也可以做為 Proxy Pattern 使用,因為它實作了一層可以作為代理的魔術方法(Magic Method):

public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}

所有 Manager 不存在的方法,全都會轉去呼叫預設實作(因為 driver() 沒指定會拿到預設實作)的對應方法。

這個實作模式在 Laravel 很常見,像是 LogManager 可以把它當 PSR-3 來使用,實際上它代理了預設 Logger 的實作。

注意事項

前面有提到 Laravel 框架,剛好用在三個不同場景,因此這篇文章就不特別說明完整範例,只說明要注意的重點為何。

2022/11/12:實作的部分後來有額外寫一篇可以參考--使用 Laravel Manager 類別 - 實戰篇

實務上在使用時,建議把它作為同一類型的實體產生器,如前面提到的第一個情境,相同類別,不同的初始化方法,或是第二個情境,相同介面,不同實作。這就如同把 Manager 作為 Factory Pattern 來使用--回傳的實體具有一致的介面或行為。

但因為 PHP 沒有泛型,所以有辦法創造出第三種情境--呼叫 driver() 但回傳不同類型的實體。以我目前的經驗來說,是不建議這麼做的,因為在 Manager 回傳實作不明確時,在使用上就有可能造成非預期的結果。