分析 Collection(3)--Higher Order Messages

Higher Order Messages 是一個像魔法一般的功能。

來看看官方提供的範例:

$invoices->each(function($invoice) {
$invoice->pay();
});

// 下面的呼叫結果相同
$invoices->each->pay();

// ------------

$employees->reject(function($employee) {
return $employee->retired;
})->each(function($employee){
$employee->sendPayment();
});

// 下面的呼叫結果相同
$employees->reject->retired->each->sendPayment();

原本要寫一堆 callback,現在只要使用一連串屬性的取法,就能得到一樣的結果。

分析

以上例第一個例子為例:

$invoices->each->pay();

可以猜想得到,each 會是一個委任出去的角色,這從原始碼最上面註解的屬性可以找得到:

/**
* @property-read HigherOrderCollectionProxy $each
*/

通常 Laravel 或大部分開源的原始碼,會寫在最上面的屬性和方法,通常都是 magic method 實作出來的。本例是屬性,所以會是由 __get() 實作:

protected static $proxies = [
'average', 'avg', 'contains', 'each', 'every', 'filter', 'first',
'flatMap', 'groupBy', 'keyBy', 'map', 'max', 'min', 'partition',
'reject', 'sortBy', 'sortByDesc', 'sum', 'unique',
];

public function __get($key)
{
if (! in_array($key, static::$proxies)) {
throw new Exception("Property [{$key}] does not exist on this collection instance.");
}

return new HigherOrderCollectionProxy($this, $key);
}

這裡直接建構出 HigherOrderCollectionProxy 出來用,而帶入的 $key 以上例來說,會是 each。這個元件的實作超級簡單,除了建構子單純是保存狀態之外,就只有實作兩個 magic method:

註:下面原始碼的 $this->method,即為上面建構帶入的的 $key

public function __get($key)
{
return $this->collection->{$this->method}(function ($value) use ($key) {
return is_array($value) ? $value[$key] : $value->{$key};
});
}

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

由原始碼可以看出,原本要寫的 callback,將會由委任的物件建立。同時也可以得知,要成為 Higher Order Messages 的一員,參數必須要有 callback。

而哪時會使用 __get(),哪時會使用 __call(),下面是一個比較容易理解的比較:

// __get()
$invoices->each->name;

// __call()
$invoices->each->pay();

接著來看 __get() 實際在做的事,裡面的 $key 指的是上例的 name。因此原始碼與等價的程式碼比對如下:

return $this->collection->{$this->method}(function ($value) use ($key) {
return is_array($value) ? $value[$key] : $value->{$key};
});

return $this->collection->each(function ($value) {
return is_array($value) ? $value['name'] : $value->name;
});

同理,__call()$keypay。原始碼與等價的程式碼比對如下:

return $this->collection->{$this->method}(function ($value) use ($method, $parameters) {
return $value->{$method}(...$parameters);
});

return $this->collection->each(function ($value) {
return $value->pay();
});

從上述等價原始碼可以了解,使用 Higher Order Messages 的時機會是,當方法是在 proxy 的列表裡,且 callback 裡面單純只回傳一個特定值、或呼叫一個特定方法時,就能考慮使用它。反之,需要兩行以上的程式碼時,就無法使用這個功能了。

Higher Order Messages 是個很有趣的實作,短短不到 100 行程式碼,就能讓使用它的開發者少寫非常多程式碼,而且可讀性也不差,真的是非常厲害。