浅谈 Laravel Collections

这两天看了两本书《Laravel Collections Unraveled》和 《Refactoring to Collections》。

学习了如何将数组 items 重构成 Collection,以及为什么这么做。

其中,一个核心思想就是:Never write another loop again。

下面把学到的知识简单梳理出来,重点学习 Laravel 使用的 Collection

为何重构

我不想把重构说成是包治百病的万灵丹,但可以帮助你始终良好地控制自己的代码。重构是个工具,它可以(并且应该)用于以下几个目的。

重构改进软件设计

同样一件事,设计不良的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同的语句做同样的事。因此改进设计的一个重要方向就是消除重复代码。这个动作的重要性在于方便未来的修改。代码量减少并不会使系统运行更快,因为这对程序的运行轨迹几乎没有任何明显影响。然而代码量减少将使未来可能的程序修改动作容易得多。

重构使软件更容易理解

所谓程序设计,很大程度上就是与计算机交谈:你编写代码告诉计算机做什么事,它的响应则是精确按照你的指示行动。你得及时填补“想要他做什么”和“告诉它做什么”之间的缝隙。这种编程模式的核心就是“准确说出我所要的”。除了计算机外,你的源码还有其他读者:几个月后可能会有另一个程序员尝试读懂你的代码并做一些修改。我们很容易忘记第二位读者,但他才是最重要的。计算机是否多花了几个小时才编译,又有什么关系呢?如果一个程序员花费一周时间来修改某段代码,那才要命呢——如果他理解了你的代码,这个修改原来只需一小时。

总之一句话,不要让你的代码成为下一个接盘者嘴里的:垃圾代码

重构帮助找到 bug

对代码的理解,可以帮助我找到 bug。我承认我不太擅长调试。有些人只要盯着一大段代码就可以找出里面的 bug,我可不行。但我发现,如果对代码进行重构,我就可以深入理解代码的作为,并恰到好处地把新的理解反馈回去。搞清楚程序结构的同时,我也清楚了自己所做的一些假设,于是想不把 bug 揪出来都难。

这让我想起了 Kent Beck 经常形容自己的一句话:“我不是个伟大的程序员,我只是个有着一些优秀习惯的好程序员。”重构能够帮助我更有效地写出强健的代码。

重构提高编程速度

我绝对相信:良好的设计是快速开发的根本——事实上,拥有良好设计才可能做到快速开发。如果没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,无法添加新功能。修改时间愈来愈长,因为你必须花愈来愈多的时间去理解系统、寻找重复代码。随着你给最初程序打上一个又一个的补丁,新特性需要等多代码才能实现。真是个恶性循环。

良好设计是维持软件开发速度的根本。重构可以帮助你更快速地开发软件,因为它阻止系统腐败变质,它甚至还可以提高设计质量。

我相信这也是为什么很多优秀的框架能得到很多人的认可和使用,因为他们的框架可以提高我们的编程速度,要不我们为什么要去使用他们呢?其中 Laravel 就是其中的代表。

以上主要摘自《重构——改善既有代码的设计》,推荐大家看看此书。

Refactoring to Collection 三要素

本着「Never write another loop again」此重构原则,我们需要找出 array 使用频率最多的「循环语句」,封装它,然后做成各种通用的高阶函数,最后形成 Collection 类。最后我们在使用 array 时,只要转变成 Collection 对象,就可以尽可能的 Never write another loop again。

循环语句

在对数组 items 进行操作时,我们避免不了使用循环语句去处理我们的逻辑。

如,我们想拿到所有用户的邮箱地址,也许我们这么写:

function getUserEmails($users) {

    // 1. 创建空数组用于保存结果
    $emails = [];

    // 2. 初始化变量 $i,用于遍历所有用户
    for ($i = 0; $i < count($users); $i++) {
        $emails[] = $$users[$i]->email;
    }

    return $emails;
}

又如,我们要对数组每个元素 *3 计算:

function cheng3($data) {
    for ($i = 0; $i < count($data); $i++) {
        $data[$i] *= 3;
    }
}

又如,我们要把贵的商品挑出来:

function expensive($products) {
    $expensiveProducts = [];

    foreach ($products as $product) { 
        if ($product->price > 100) { 
            $expensiveProducts[] = $product; 
        } 
    }

    return $expensiveProducts;
}

对数组的操作,这类例子太多了,终究都是通过循环来对数组的每个元素进行操作。

而我们重构的思路就是:把循环的地方封装起来,这样最大的避免我们在写业务逻辑时,自己去写循环语句 (让循环语句见鬼去吧)。

Higher Order Functions

俗称:高阶函数 A higher order function is a function that takes another function as a parameter, returns a function, or does both.

使用高阶函数对上面四个例子进行改造。

第一个例子,主要的业务逻辑在于这条语句,获取每个用户的邮箱:

$emails[] = $$users[$i]->email;

将其他代码封装成如下 map 函数:

function map($items, $func) { 
    $results = [];

    foreach ($items as $item) { 
        $results[] = $func($item); 
    }

    return $results;
}

这样使用该 map 函数进行重构就简单:

function getUserEmails($users) {
    return $this->map($users, function ($user) {
        return $user->email;
    });
}

相比较刚开始的写法,明显简单多了,而且也避免了不必要的变量。

一样的,对第二个例子进行重构,将循环语句封装成 each 函数:

function each($items, $func) {
    foreach ($items as $item) {
        $func($item);
    } 
}

这个 each 和 map 函数最大的区别在于,each 函数是对每个元素的处理逻辑,且没有返回新的数组。

使用 each 函数就比较简单:

function cube($data) {
    $this->each($data, function ($item) {
       return $item * 3;
    });
}

同样的对第三个例子进行重构,重构的对象在于价格的筛选判断上

if ($product->price > 100) { 
    $expensiveProducts[] = $product; 
}

我们参考 map 函数进行重构:

function filter($items, $func) { 
    $result = [];

    foreach ($items as $item) { 
        if ($func($item)) { 
            $result[] = $item; 
        } 
    }

    return $result;
}

当满足于 $func($item)条件的 item 都放入 $result 数组中。

使用就很简单:

return $this->filter($products, function ($product) {
    return $product->price > 100;
});

这里的 filter 函数和 map 函数的区别在于,map 函数是获取原有数组对应的属性集或者计算产生的新数组;而 filter 更多的是通过筛选符合条件的 item,构成的数组。

构造 Collection 类

我们把这些 map、each、filter 方法整合在一起构成一个 Collection 类

A collection is an object that bundles up an array and lets us perform array operations by calling methods on the collection instead of passing the array into functions.

其中 items 是唯一属性。核心的都是对 items 遍历,做各种各样的操作,具体看代码:

class Collection {
    protected $items;

    public function __construct($items) {
        $this->items = $items;
    }

    function map($items, $func) {
        $results = [];

        foreach ($items as $item) {
            $results[] = $func($item);
        }

        return $results;
    }

    function each($items, $func) {
        foreach ($items as $item) {
            $func($item);
        }
    }

    function filter($items, $func) {
        $result = [];

        foreach ($items as $item) {
            if ($func($item)) {
                $result[] = $item;
            }
        }

        return $result;
    }

    public function toArray() {
        return $this->items;
    }

}

当然到目前为止,自己封装的 Collection 雏形就已经有了,但还是达不到可以通用的水平。所以我们需要看看别人是怎么写的,当然这时候要祭出大招 —— Laravel 使用的

Illuminate\Support\Collection

解说 Illuminate\Support\Collection.

The Illuminate\Support\Collection class provides a fluent, convenient wrapper for working with arrays of data.

Collection 主要实现了以下几个接口:

  1. ArrayAccess
  2. Countable
  3. IteratorAggregate
  4. JsonSerializable and Laravel's own Arrayable and Jsonable

下面让我来一个个解说这几个接口的作用。

ArrayAccess

interface ArrayAccess {
    public function offsetExists($offset);
    public function offsetGet($offset);
    public function offsetSet($offset, $value);
    public function offsetUnset($offset);
}

实现这四个函数:

/**  
 * Determine if an item exists at an offset. 
 * 
 * @param  TKey  $key  
 * @return bool  
 */  
public function offsetExists($key): bool  
{  
    return isset($this->items[$key]);  
}  
  
/**  
 * Get an item at a given offset. 
 * 
 * @param  TKey  $key  
 * @return TValue  
 */  
public function offsetGet($key): mixed  
{  
    return $this->items[$key];  
}  
  
/**  
 * Set the item at a given offset. 
 * 
 * @param  TKey|null  $key  
 * @param  TValue  $value  
 * @return void  
 */  
public function offsetSet($key, $value): void  
{  
    if (is_null($key)) {  
        $this->items[] = $value;  
    } else {  
        $this->items[$key] = $value;  
    }  
}  
  
/**  
 * Unset the item at a given offset. 
 * 
 * @param  TKey  $key  
 * @return void  
 */  
public function offsetUnset($key): void  
{  
    unset($this->items[$key]);  
}

这个接口更多的职责是让 Collection 类看起来像是个 array,主要是对 items 进行增删查和判断 item 是否存在。

Enumerable

所谓迭代器接口,把相关的接口做了一次“聚合”:

interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable  
{  
    /**  
     * Create a new collection instance if the value isn't one already.     
     *     
     * @template TMakeKey of array-key  
     * @template TMakeValue  
     *  
     * @param  \Illuminate\Contracts\Support\Arrayable<TMakeKey, TMakeValue>|iterable<TMakeKey, TMakeValue>|null  $items  
     * @return static<TMakeKey, TMakeValue>  
     */  
    public static function make($items = []);  
  
    /**  
     * Create a new instance by invoking the callback a given amount of times.     
     *     
     * @param  int  $number  
     * @param  callable|null  $callback  
     * @return static  
     */  
    public static function times($number, callable $callback = null);  
  
    /**  
     * Create a collection with the given range.     
     *     
     * @param  int  $from  
     * @param  int  $to  
     * @return static  
     */  
    public static function range($from, $to);  
  
    /**  
     * Wrap the given value in a collection if applicable.     
     *     
     * @template TWrapValue  
     *  
     * @param  iterable<array-key, TWrapValue>|TWrapValue  $value  
     * @return static<array-key, TWrapValue>  
     */  
    public static function wrap($value);  
  
    /**  
     * Get the underlying items from the given collection if applicable.     
     *     
     * @template TUnwrapKey of array-key  
     * @template TUnwrapValue  
     *  
     * @param  array<TUnwrapKey, TUnwrapValue>|static<TUnwrapKey, TUnwrapValue>  $value  
     * @return array<TUnwrapKey, TUnwrapValue>  
     */  
    public static function unwrap($value);  
  
    /**  
     * Create a new instance with no items.     
     *     
     * @return static  
     */  
    public static function empty();  
  
    /**  
     * Get all items in the enumerable.     
     *     
     * @return array  
     */  
    public function all();  
  
    /**  
     * Alias for the "avg" method.     
     *     
     * @param  (callable(TValue): float|int)|string|null  $callback  
     * @return float|int|null  
     */  
    public function average($callback = null);  
  
    /**  
     * Get the median of a given key.     
     *     
     * @param  string|array<array-key, string>|null  $key  
     * @return float|int|null  
     */  
    public function median($key = null);  
  
    /**  
     * Get the mode of a given key.     
     *     
     * @param  string|array<array-key, string>|null  $key  
     * @return array<int, float|int>|null  
     */  
    public function mode($key = null);  
  
    /**  
     * Collapse the items into a single enumerable.     
     *     
     * @return static<int, mixed>  
     */  
    public function collapse();  
  
    /**  
     * Alias for the "contains" method.     
     *     
     * @param  (callable(TValue, TKey): bool)|TValue|string  $key  
     * @param  mixed  $operator  
     * @param  mixed  $value  
     * @return bool  
     */  
    public function some($key, $operator = null, $value = null);  
  
    /**  
     * Determine if an item exists, using strict comparison.     
     *     
     * @param  (callable(TValue): bool)|TValue|array-key  $key  
     * @param  TValue|null  $value  
     * @return bool  
     */  
    public function containsStrict($key, $value = null);  
  
    /**  
     * Get the average value of a given key.     
     *     
     * @param  (callable(TValue): float|int)|string|null  $callback  
     * @return float|int|null  
     */  
    public function avg($callback = null);  
  
    /**  
     * Determine if an item exists in the enumerable.     
     *     
     * @param  (callable(TValue, TKey): bool)|TValue|string  $key  
     * @param  mixed  $operator  
     * @param  mixed  $value  
     * @return bool  
     */  
    public function contains($key, $operator = null, $value = null);  
  
    /**  
     * Determine if an item is not contained in the collection.     
     *     
     * @param  mixed  $key  
     * @param  mixed  $operator  
     * @param  mixed  $value  
     * @return bool  
     */  
    public function doesntContain($key, $operator = null, $value = null);  
  
    /**  
     * Cross join with the given lists, returning all possible permutations.     
     *     
     * @template TCrossJoinKey  
     * @template TCrossJoinValue  
     *  
     * @param  \Illuminate\Contracts\Support\Arrayable<TCrossJoinKey, TCrossJoinValue>|iterable<TCrossJoinKey, TCrossJoinValue>  ...$lists  
     * @return static<int, array<int, TValue|TCrossJoinValue>>  
     */  
    public function crossJoin(...$lists);  
  
    /**  
     * Dump the collection and end the script.     
     *     
     * @param  mixed  ...$args  
     * @return never  
     */  
    public function dd(...$args);  
  
    /**  
     * Dump the collection.     
     *     
     * @return $this  
     */  
    public function dump();  
  
    /**  
     * Get the items that are not present in the given items.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue>  $items  
     * @return static  
     */  
    public function diff($items);  
  
    /**  
     * Get the items that are not present in the given items, using the callback.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue>  $items  
     * @param  callable(TValue, TValue): int  $callback  
     * @return static  
     */  
    public function diffUsing($items, callable $callback);  
  
    /**  
     * Get the items whose keys and values are not present in the given items.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @return static  
     */  
    public function diffAssoc($items);  
  
    /**  
     * Get the items whose keys and values are not present in the given items, using the callback.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @param  callable(TKey, TKey): int  $callback  
     * @return static  
     */  
    public function diffAssocUsing($items, callable $callback);  
  
    /**  
     * Get the items whose keys are not present in the given items.     *     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @return static  
     */  
    public function diffKeys($items);  
  
    /**  
     * Get the items whose keys are not present in the given items, using the callback.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @param  callable(TKey, TKey): int  $callback  
     * @return static  
     */  
    public function diffKeysUsing($items, callable $callback);  
  
    /**  
     * Retrieve duplicate items.     
     *     
     * @param  (callable(TValue): bool)|string|null  $callback  
     * @param  bool  $strict  
     * @return static  
     */  
    public function duplicates($callback = null, $strict = false);  
  
    /**  
     * Retrieve duplicate items using strict comparison.     
     *     
     * @param  (callable(TValue): bool)|string|null  $callback  
     * @return static  
     */  
    public function duplicatesStrict($callback = null);  
  
    /**  
     * Execute a callback over each item.     
     *     
     * @param  callable(TValue, TKey): mixed  $callback  
     * @return $this  
     */  
    public function each(callable $callback);  
  
    /**  
     * Execute a callback over each nested chunk of items.     
     *     
     * @param  callable  $callback  
     * @return static  
     */  
    public function eachSpread(callable $callback);  
  
    /**  
     * Determine if all items pass the given truth test.     
     *     
     * @param  (callable(TValue, TKey): bool)|TValue|string  $key  
     * @param  mixed  $operator  
     * @param  mixed  $value  
     * @return bool  
     */  
    public function every($key, $operator = null, $value = null);  
  
    /**  
     * Get all items except for those with the specified keys.     
     *     
     * @param  \Illuminate\Support\Enumerable<array-key, TKey>|array<array-key, TKey>  $keys  
     * @return static  
     */  
    public function except($keys);  
  
    /**  
     * Run a filter over each of the items.     
     *     
     * @param  (callable(TValue): bool)|null  $callback  
     * @return static  
     */  
    public function filter(callable $callback = null);  
  
    /**  
     * Apply the callback if the given "value" is (or resolves to) truthy.     
     *     
     * @template TWhenReturnType as null  
     *  
     * @param  bool  $value  
     * @param  (callable($this): TWhenReturnType)|null  $callback  
     * @param  (callable($this): TWhenReturnType)|null  $default  
     * @return $this|TWhenReturnType  
     */  
    public function when($value, callable $callback = null, callable $default = null);  
  
    /**  
     * Apply the callback if the collection is empty.     
     *     
     * @template TWhenEmptyReturnType  
     *  
     * @param  (callable($this): TWhenEmptyReturnType)  $callback  
     * @param  (callable($this): TWhenEmptyReturnType)|null  $default  
     * @return $this|TWhenEmptyReturnType  
     */  
    public function whenEmpty(callable $callback, callable $default = null);  
  
    /**  
     * Apply the callback if the collection is not empty.     
     *     
     * @template TWhenNotEmptyReturnType  
     *  
     * @param  callable($this): TWhenNotEmptyReturnType  $callback  
     * @param  (callable($this): TWhenNotEmptyReturnType)|null  $default  
     * @return $this|TWhenNotEmptyReturnType  
     */  
    public function whenNotEmpty(callable $callback, callable $default = null);  
  
    /**  
     * Apply the callback if the given "value" is (or resolves to) truthy.     
     *     
     * @template TUnlessReturnType  
     *  
     * @param  bool  $value  
     * @param  (callable($this): TUnlessReturnType)  $callback  
     * @param  (callable($this): TUnlessReturnType)|null  $default  
     * @return $this|TUnlessReturnType  
     */  
    public function unless($value, callable $callback, callable $default = null);  
  
    /**  
     * Apply the callback unless the collection is empty.     
     *     
     * @template TUnlessEmptyReturnType  
     *  
     * @param  callable($this): TUnlessEmptyReturnType  $callback  
     * @param  (callable($this): TUnlessEmptyReturnType)|null  $default  
     * @return $this|TUnlessEmptyReturnType  
     */  
    public function unlessEmpty(callable $callback, callable $default = null);  
  
    /**  
     * Apply the callback unless the collection is not empty.     
     *     
     * @template TUnlessNotEmptyReturnType  
     *  
     * @param  callable($this): TUnlessNotEmptyReturnType  $callback  
     * @param  (callable($this): TUnlessNotEmptyReturnType)|null  $default  
     * @return $this|TUnlessNotEmptyReturnType  
     */  
    public function unlessNotEmpty(callable $callback, callable $default = null);  
  
    /**  
     * Filter items by the given key value pair.     
     *     
     * @param  string  $key  
     * @param  mixed  $operator  
     * @param  mixed  $value  
     * @return static  
     */  
    public function where($key, $operator = null, $value = null);  
  
    /**  
     * Filter items where the value for the given key is null.     
     *     
     * @param  string|null  $key  
     * @return static  
     */  
    public function whereNull($key = null);  
  
    /**  
     * Filter items where the value for the given key is not null.     
     *     
     * @param  string|null  $key  
     * @return static  
     */  
    public function whereNotNull($key = null);  
  
    /**  
     * Filter items by the given key value pair using strict comparison.     
     *     
     * @param  string  $key  
     * @param  mixed  $value  
     * @return static  
     */  
    public function whereStrict($key, $value);  
  
    /**  
     * Filter items by the given key value pair.     
     *     
     * @param  string  $key  
     * @param  \Illuminate\Contracts\Support\Arrayable|iterable  $values  
     * @param  bool  $strict  
     * @return static  
     */  
    public function whereIn($key, $values, $strict = false);  
  
    /**  
     * Filter items by the given key value pair using strict comparison.     
     *     
     * @param  string  $key  
     * @param  \Illuminate\Contracts\Support\Arrayable|iterable  $values  
     * @return static  
     */  
    public function whereInStrict($key, $values);  
  
    /**  
     * Filter items such that the value of the given key is between the given values.     
     *     
     * @param  string  $key  
     * @param  \Illuminate\Contracts\Support\Arrayable|iterable  $values  
     * @return static  
     */  
    public function whereBetween($key, $values);  
  
    /**  
     * Filter items such that the value of the given key is not between the given values.     
     *     
     * @param  string  $key  
     * @param  \Illuminate\Contracts\Support\Arrayable|iterable  $values  
     * @return static  
     */  
    public function whereNotBetween($key, $values);  
  
    /**  
     * Filter items by the given key value pair.     
     *     
     * @param  string  $key  
     * @param  \Illuminate\Contracts\Support\Arrayable|iterable  $values  
     * @param  bool  $strict  
     * @return static  
     */  
    public function whereNotIn($key, $values, $strict = false);  
  
    /**  
     * Filter items by the given key value pair using strict comparison.     
     *     
     * @param  string  $key  
     * @param  \Illuminate\Contracts\Support\Arrayable|iterable  $values  
     * @return static  
     */  
    public function whereNotInStrict($key, $values);  
  
    /**  
     * Filter the items, removing any items that don't match the given type(s).     
     *     
     * @template TWhereInstanceOf  
     *  
     * @param  class-string<TWhereInstanceOf>|array<array-key, class-string<TWhereInstanceOf>>  $type  
     * @return static<TKey, TWhereInstanceOf>  
     */  
    public function whereInstanceOf($type);  
  
    /**  
     * Get the first item from the enumerable passing the given truth test.     
     *     
     * @template TFirstDefault  
     *  
     * @param  (callable(TValue,TKey): bool)|null  $callback  
     * @param  TFirstDefault|(\Closure(): TFirstDefault)  $default  
     * @return TValue|TFirstDefault  
     */  
    public function first(callable $callback = null, $default = null);  
  
    /**  
     * Get the first item by the given key value pair.     
     *     
     * @param  string  $key  
     * @param  mixed  $operator  
     * @param  mixed  $value  
     * @return TValue|null  
     */  
    public function firstWhere($key, $operator = null, $value = null);  
  
    /**  
     * Get a flattened array of the items in the collection.     
     *     
     * @param  int  $depth  
     * @return static  
     */  
    public function flatten($depth = INF);  
  
    /**  
     * Flip the values with their keys.     
     *     
     * @return static<TValue, TKey>  
     */  
    public function flip();  
  
    /**  
     * Get an item from the collection by key.     
     *     
     * @template TGetDefault  
     *  
     * @param  TKey  $key  
     * @param  TGetDefault|(\Closure(): TGetDefault)  $default  
     * @return TValue|TGetDefault  
     */  
    public function get($key, $default = null);  
  
    /**  
     * Group an associative array by a field or using a callback.     
     *     
     * @param  (callable(TValue, TKey): array-key)|array|string  $groupBy  
     * @param  bool  $preserveKeys  
     * @return static<array-key, static<array-key, TValue>>  
     */  
    public function groupBy($groupBy, $preserveKeys = false);  
  
    /**  
     * Key an associative array by a field or using a callback.     
     *     
     * @param  (callable(TValue, TKey): array-key)|array|string  $keyBy  
     * @return static<array-key, TValue>  
     */  
    public function keyBy($keyBy);  
  
    /**  
     * Determine if an item exists in the collection by key.     
     *     
     * @param  TKey|array<array-key, TKey>  $key  
     * @return bool  
     */  
    public function has($key);  
  
    /**  
     * Determine if any of the keys exist in the collection.     
     *     
     * @param  mixed  $key  
     * @return bool  
     */  
    public function hasAny($key);  
  
    /**  
     * Concatenate values of a given key as a string.     
     *     
     * @param  callable|string  $value  
     * @param  string|null  $glue  
     * @return string  
     */  
    public function implode($value, $glue = null);  
  
    /**  
     * Intersect the collection with the given items.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @return static  
     */  
    public function intersect($items);  
  
    /**  
     * Intersect the collection with the given items by key.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @return static  
     */  
    public function intersectByKeys($items);  
  
    /**  
     * Determine if the collection is empty or not.     
     *     
     * @return bool  
     */  
    public function isEmpty();  
  
    /**  
     * Determine if the collection is not empty.     
     *     
     * @return bool  
     */  
    public function isNotEmpty();  
  
    /**  
     * Determine if the collection contains a single item.     
     *     
     * @return bool  
     */  
    public function containsOneItem();  
  
    /**  
     * Join all items from the collection using a string. The final items can use a separate glue string.     
     *     
     * @param  string  $glue  
     * @param  string  $finalGlue  
     * @return string  
     */  
    public function join($glue, $finalGlue = '');  
  
    /**  
     * Get the keys of the collection items.     
     *     
     * @return static<int, TKey>  
     */  
    public function keys();  
  
    /**  
     * Get the last item from the collection.     
     *     
     * @template TLastDefault  
     *  
     * @param  (callable(TValue, TKey): bool)|null  $callback  
     * @param  TLastDefault|(\Closure(): TLastDefault)  $default  
     * @return TValue|TLastDefault  
     */  
    public function last(callable $callback = null, $default = null);  
  
    /**  
     * Run a map over each of the items.     
     *     
     * @template TMapValue  
     *  
     * @param  callable(TValue, TKey): TMapValue  $callback  
     * @return static<TKey, TMapValue>  
     */  
    public function map(callable $callback);  
  
    /**  
     * Run a map over each nested chunk of items.     
     *     
     * @param  callable  $callback  
     * @return static  
     */  
    public function mapSpread(callable $callback);  
  
    /**  
     * Run a dictionary map over the items.     
     *     
     * The callback should return an associative array with a single key/value pair.     
     *     
     * @template TMapToDictionaryKey of array-key  
     * @template TMapToDictionaryValue  
     *  
     * @param  callable(TValue, TKey): array<TMapToDictionaryKey, TMapToDictionaryValue>  $callback  
     * @return static<TMapToDictionaryKey, array<int, TMapToDictionaryValue>>  
     */  
    public function mapToDictionary(callable $callback);  
  
    /**  
     * Run a grouping map over the items.     
     *     
     * The callback should return an associative array with a single key/value pair.     
     *     
     * @template TMapToGroupsKey of array-key  
     * @template TMapToGroupsValue  
     *  
     * @param  callable(TValue, TKey): array<TMapToGroupsKey, TMapToGroupsValue>  $callback  
     * @return static<TMapToGroupsKey, static<int, TMapToGroupsValue>>  
     */  
    public function mapToGroups(callable $callback);  
  
    /**  
     * Run an associative map over each of the items.     
     *     
     * The callback should return an associative array with a single key/value pair.     
     *     
     * @template TMapWithKeysKey of array-key  
     * @template TMapWithKeysValue  
     *  
     * @param  callable(TValue, TKey): array<TMapWithKeysKey, TMapWithKeysValue>  $callback  
     * @return static<TMapWithKeysKey, TMapWithKeysValue>  
     */  
    public function mapWithKeys(callable $callback);  
  
    /**  
     * Map a collection and flatten the result by a single level.     
     *     
     * @template TFlatMapKey of array-key  
     * @template TFlatMapValue  
     *  
     * @param  callable(TValue, TKey): (\Illuminate\Support\Collection<TFlatMapKey, TFlatMapValue>|array<TFlatMapKey, TFlatMapValue>)  $callback  
     * @return static<TFlatMapKey, TFlatMapValue>  
     */  
    public function flatMap(callable $callback);  
  
    /**  
     * Map the values into a new class.     
     *     
     * @template TMapIntoValue  
     *  
     * @param  class-string<TMapIntoValue>  $class  
     * @return static<TKey, TMapIntoValue>  
     */  
    public function mapInto($class);  
  
    /**  
     * Merge the collection with the given items.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @return static  
     */  
    public function merge($items);  
  
    /**  
     * Recursively merge the collection with the given items.     
     *     
     * @template TMergeRecursiveValue  
     *  
     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TMergeRecursiveValue>|iterable<TKey, TMergeRecursiveValue>  $items  
     * @return static<TKey, TValue|TMergeRecursiveValue>  
     */  
    public function mergeRecursive($items);  
  
    /**  
     * Create a collection by using this collection for keys and another for its values.     
     *     
     * @template TCombineValue  
     *  
     * @param  \Illuminate\Contracts\Support\Arrayable<array-key, TCombineValue>|iterable<array-key, TCombineValue>  $values  
     * @return static<TValue, TCombineValue>  
     */  
    public function combine($values);  
  
    /**  
     * Union the collection with the given items.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @return static  
     */  
    public function union($items);  
  
    /**  
     * Get the min value of a given key.     
     *     
     * @param  (callable(TValue):mixed)|string|null  $callback  
     * @return mixed  
     */  
    public function min($callback = null);  
  
    /**  
     * Get the max value of a given key.     
     *     
     * @param  (callable(TValue):mixed)|string|null  $callback  
     * @return mixed  
     */  
    public function max($callback = null);  
  
    /**  
     * Create a new collection consisting of every n-th element.     
     *     
     * @param  int  $step  
     * @param  int  $offset  
     * @return static  
     */  
    public function nth($step, $offset = 0);  
  
    /**  
     * Get the items with the specified keys.     
     *     
     * @param  \Illuminate\Support\Enumerable<array-key, TKey>|array<array-key, TKey>|string  $keys  
     * @return static  
     */  
    public function only($keys);  
  
    /**  
     * "Paginate" the collection by slicing it into a smaller collection.     
     *     
     * @param  int  $page  
     * @param  int  $perPage  
     * @return static  
     */  
    public function forPage($page, $perPage);  
  
    /**  
     * Partition the collection into two arrays using the given callback or key.     
     *     
     * @param  (callable(TValue, TKey): bool)|TValue|string  $key  
     * @param  mixed  $operator  
     * @param  mixed  $value  
     * @return static<int<0, 1>, static<TKey, TValue>>  
     */  
    public function partition($key, $operator = null, $value = null);  
  
    /**  
     * Push all of the given items onto the collection.     
     *     
     * @param  iterable<array-key, TValue>  $source  
     * @return static  
     */  
    public function concat($source);  
  
    /**  
     * Get one or a specified number of items randomly from the collection.     
     *     
     * @param  int|null  $number  
     * @return static<int, TValue>|TValue  
     *  
     * @throws \InvalidArgumentException  
     */  
    public function random($number = null);  
  
    /**  
     * Reduce the collection to a single value.     
     *     
     * @template TReduceInitial  
     * @template TReduceReturnType  
     *  
     * @param  callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType  $callback  
     * @param  TReduceInitial  $initial  
     * @return TReduceReturnType  
     */  
    public function reduce(callable $callback, $initial = null);  
  
    /**  
     * Reduce the collection to multiple aggregate values.     
     *     
     * @param  callable  $callback  
     * @param  mixed  ...$initial  
     * @return array  
     *  
     * @throws \UnexpectedValueException  
     */  
    public function reduceSpread(callable $callback, ...$initial);  
  
    /**  
     * Replace the collection items with the given items.     
     *     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @return static  
     */  
    public function replace($items);  
  
    /**  
     * Recursively replace the collection items with the given items.     
     *     
     * @param  \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>  $items  
     * @return static  
     */  
    public function replaceRecursive($items);  
  
    /**  
     * Reverse items order.     
     *     
     * @return static  
     */  
    public function reverse();  
  
    /**  
     * Search the collection for a given value and return the corresponding key if successful.     *     * @param  TValue|callable(TValue,TKey): bool  $value  
     * @param  bool  $strict  
     * @return TKey|bool  
     */  
    public function search($value, $strict = false);  
  
    /**  
     * Shuffle the items in the collection.     
     *     
     * @param  int|null  $seed  
     * @return static  
     */  
    public function shuffle($seed = null);  
  
    /**  
     * Create chunks representing a "sliding window" view of the items in the collection.     
     *     
     * @param  int  $size  
     * @param  int  $step  
     * @return static<int, static>  
     */  
    public function sliding($size = 2, $step = 1);  
  
    /**  
     * Skip the first {$count} items.     
     *     
     * @param  int  $count  
     * @return static  
     */  
    public function skip($count);  
  
    /**  
     * Skip items in the collection until the given condition is met.     
     *     
     * @param  TValue|callable(TValue,TKey): bool  $value  
     * @return static  
     */  
    public function skipUntil($value);  
  
    /**  
     * Skip items in the collection while the given condition is met.     
     *     
     * @param  TValue|callable(TValue,TKey): bool  $value  
     * @return static  
     */  
    public function skipWhile($value);  
  
    /**  
     * Get a slice of items from the enumerable.     
     *     
     * @param  int  $offset  
     * @param  int|null  $length  
     * @return static  
     */  
    public function slice($offset, $length = null);  
  
    /**  
     * Split a collection into a certain number of groups.     
     *     
     * @param  int  $numberOfGroups  
     * @return static<int, static>  
     */  
    public function split($numberOfGroups);  
  
    /**  
     * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception.     
     *     
     * @param  (callable(TValue, TKey): bool)|string  $key  
     * @param  mixed  $operator  
     * @param  mixed  $value  
     * @return TValue  
     *  
     * @throws \Illuminate\Support\ItemNotFoundException  
     * @throws \Illuminate\Support\MultipleItemsFoundException  
     */  
    public function sole($key = null, $operator = null, $value = null);  
  
    /**  
     * Get the first item in the collection but throw an exception if no matching items exist.     
     *     
     * @param  (callable(TValue, TKey): bool)|string  $key  
     * @param  mixed  $operator  
     * @param  mixed  $value  
     * @return TValue  
     *  
     * @throws \Illuminate\Support\ItemNotFoundException  
     */  
    public function firstOrFail($key = null, $operator = null, $value = null);  
  
    /**  
     * Chunk the collection into chunks of the given size.     
     *     
     * @param  int  $size  
     * @return static<int, static>  
     */  
    public function chunk($size);  
  
    /**  
     * Chunk the collection into chunks with a callback.     
     *     
     * @param  callable(TValue, TKey, static<int, TValue>): bool  $callback  
     * @return static<int, static<int, TValue>>  
     */  
    public function chunkWhile(callable $callback);  
  
    /**  
     * Split a collection into a certain number of groups, and fill the first groups completely.     *     * @param  int  $numberOfGroups  
     * @return static<int, static>  
     */  
    public function splitIn($numberOfGroups);  
  
    /**  
     * Sort through each item with a callback.     
     *     
     * @param  (callable(TValue, TValue): int)|null|int  $callback  
     * @return static  
     */  
    public function sort($callback = null);  
  
    /**  
     * Sort items in descending order.     
     *     * @param  int  $options  
     * @return static  
     */  
    public function sortDesc($options = SORT_REGULAR);  
  
    /**  
     * Sort the collection using the given callback.     
     *     
     * @param  array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string  $callback  
     * @param  int  $options  
     * @param  bool  $descending  
     * @return static  
     */  
    public function sortBy($callback, $options = SORT_REGULAR, $descending = false);  
  
    /**  
     * Sort the collection in descending order using the given callback.     
     *     
     * @param  array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string  $callback  
     * @param  int  $options  
     * @return static  
     */  
    public function sortByDesc($callback, $options = SORT_REGULAR);  
  
    /**  
     * Sort the collection keys.     
     *     * @param  int  $options  
     * @param  bool  $descending  
     * @return static  
     */  
    public function sortKeys($options = SORT_REGULAR, $descending = false);  
  
    /**  
     * Sort the collection keys in descending order.     
     *     
     * @param  int  $options  
     * @return static  
     */  
    public function sortKeysDesc($options = SORT_REGULAR);  
  
    /**  
     * Sort the collection keys using a callback.     
     *     * @param  callable(TKey, TKey): int  $callback  
     * @return static  
     */  
    public function sortKeysUsing(callable $callback);  
  
    /**  
     * Get the sum of the given values.     
     *     
     * @param  (callable(TValue): mixed)|string|null  $callback  
     * @return mixed  
     */  
    public function sum($callback = null);  
  
    /**  
     * Take the first or last {$limit} items.     
     *     
     * @param  int  $limit  
     * @return static  
     */  
    public function take($limit);  
  
    /**  
     * Take items in the collection until the given condition is met.     
     *     
     * @param  TValue|callable(TValue,TKey): bool  $value  
     * @return static  
     */  
    public function takeUntil($value);  
  
    /**  
     * Take items in the collection while the given condition is met.     
     *     * @param  TValue|callable(TValue,TKey): bool  $value  
     * @return static  
     */  
    public function takeWhile($value);  
  
    /**  
     * Pass the collection to the given callback and then return it.     
     *     
     * @param  callable(TValue): mixed  $callback  
     * @return $this  
     */  
    public function tap(callable $callback);  
  
    /**  
     * Pass the enumerable to the given callback and return the result.     
     *     
     * @template TPipeReturnType  
     *  
     * @param  callable($this): TPipeReturnType  $callback  
     * @return TPipeReturnType  
     */  
    public function pipe(callable $callback);  
  
    /**  
     * Pass the collection into a new class.     
     *     
     * @template TPipeIntoValue  
     *  
     * @param  class-string<TPipeIntoValue>  $class  
     * @return TPipeIntoValue  
     */  
    public function pipeInto($class);  
  
    /**  
     * Pass the collection through a series of callable pipes and return the result.     
     *     
     * @param  array<callable>  $pipes  
     * @return mixed  
     */  
    public function pipeThrough($pipes);  
  
    /**  
     * Get the values of a given key.     
     *     
     * @param  string|array<array-key, string>  $value  
     * @param  string|null  $key  
     * @return static<int, mixed>  
     */  
    public function pluck($value, $key = null);  
  
    /**  
     * Create a collection of all elements that do not pass a given truth test.     
     *     
     * @param  (callable(TValue, TKey): bool)|bool|TValue  $callback  
     * @return static  
     */  
    public function reject($callback = true);  
  
    /**  
     * Convert a flatten "dot" notation array into an expanded array.     
     *     
     * @return static  
     */  
    public function undot();  
  
    /**  
     * Return only unique items from the collection array.     
     *     
     * @param  (callable(TValue, TKey): mixed)|string|null  $key  
     * @param  bool  $strict  
     * @return static  
     */  
    public function unique($key = null, $strict = false);  
  
    /**  
     * Return only unique items from the collection array using strict comparison.     
     *     
     * @param  (callable(TValue, TKey): mixed)|string|null  $key  
     * @return static  
     */  
    public function uniqueStrict($key = null);  
  
    /**  
     * Reset the keys on the underlying array.     
     *     
     * @return static<int, TValue>  
     */  
    public function values();  
  
    /**  
     * Pad collection to the specified length with a value.     
     *     
     * @template TPadValue  
     *  
     * @param  int  $size  
     * @param  TPadValue  $value  
     * @return static<int, TValue|TPadValue>  
     */  
    public function pad($size, $value);  
  
    /**  
     * Get the values iterator.     
     *     
     * @return \Traversable<TKey, TValue>  
     */  
    public function getIterator(): Traversable;  
  
    /**  
     * Count the number of items in the collection.     
     *     
     * @return int  
     */  
    public function count(): int;  
  
    /**  
     * Count the number of items in the collection by a field or using a callback.     
     *     
     * @param  (callable(TValue, TKey): array-key)|string|null  $countBy  
     * @return static<array-key, int>  
     */  
    public function countBy($countBy = null);  
  
    /**  
     * Zip the collection together with one or more arrays.     
     *     
     * e.g. new Collection([1, 2, 3])->zip([4, 5, 6]);     
     *      => [[1, 4], [2, 5], [3, 6]]     
     *     
     * @template TZipValue  
     *  
     * @param  \Illuminate\Contracts\Support\Arrayable<array-key, TZipValue>|iterable<array-key, TZipValue>  ...$items  
     * @return static<int, static<int, TValue|TZipValue>>  
     */  
    public function zip($items);  
  
    /**  
     * Collect the values into a collection.     
     *     
     * @return \Illuminate\Support\Collection<TKey, TValue>  
     */  
    public function collect();  
  
    /**  
     * Get the collection of items as a plain array.     
     *     
     * @return array<TKey, mixed>  
     */  
    public function toArray();  
  
    /**  
     * Convert the object into something JSON serializable.     
     *     
     * @return mixed  
     */  
    public function jsonSerialize(): mixed;  
  
    /**  
     * Get the collection of items as JSON.     
     *     
     * @param  int  $options  
     * @return string  
     */  
    public function toJson($options = 0);  
  
    /**  
     * Get a CachingIterator instance.     
     *     
     * @param  int  $flags  
     * @return \CachingIterator  
     */  
    public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING);  
  
    /**  
     * Convert the collection to its string representation.     
     *     
     * @return string  
     */  
    public function __toString();  
  
    /**  
     * Indicate that the model's string representation should be escaped when __toString is invoked.     
     *     
     * @param  bool  $escape  
     * @return $this  
     */  
    public function escapeWhenCastingToString($escape = true);  
  
    /**  
     * Add a method to the list of proxied methods.     
     *     
     * @param  string  $method  
     * @return void  
     */  
    public static function proxy($method);  
  
    /**  
     * Dynamically access collection proxies.     
     *     
     * @param  string  $key  
     * @return mixed  
     *  
     * @throws \Exception  
     */  
    public function __get($key);  
}

几乎把所有能想到的功能都罗列和实现了,比如数组的个数:

/**  
 * Count the number of items in the collection. * * @return int  
 */  
public function count(): int  
{  
    return count($this->items);  
}

又比如 toJson,在 EnumeratesValues 实现:

/**  
 * Get the collection of items as JSON. * * @param  int  $options  
 * @return string  
 */  
public function toJson($options = 0)  
{  
    return json_encode($this->jsonSerialize(), $options);  
}

上面的接口,在 Collection 里都有实现细节,大家可以具体研究学习。

如果这些常规方法还满足不了你,你也可以对 Collection 类使用 Collection::macro 方法进行扩展:

use Illuminate\Support\Str;

Collection::macro('toUpper', function () {
    return $this->map(function ($value) {
        return Str::upper($value);
    });
});

$collection = collect(['first', 'second']);

$upper = $collection->toUpper();

// ['FIRST', 'SECOND']

具体实现看 Macroable

trait Macroable  
{  
    /**  
     * The registered string macros.     
     *     
     * @var array  
     */  
    protected static $macros = [];  
  
    /**  
     * Register a custom macro.     
     *     
     * @param  string  $name  
     * @param  object|callable  $macro  
     * @return void  
     */  
    public static function macro($name, $macro)  
    {  
        static::$macros[$name] = $macro;  
    }  
  
    /**  
     * Mix another object into the class.     
     *     
     * @param  object  $mixin  
     * @param  bool  $replace  
     * @return void  
     *  
     * @throws \ReflectionException  
     */  
    public static function mixin($mixin, $replace = true)  
    {  
        $methods = (new ReflectionClass($mixin))->getMethods(  
            ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED  
        );  
  
        foreach ($methods as $method) {  
            if ($replace || ! static::hasMacro($method->name)) {  
                static::macro($method->name, $method->invoke($mixin));  
            }  
        }  
    }  
  
    /**  
     * Checks if macro is registered.     
     *     
     * @param  string  $name  
     * @return bool  
     */  
    public static function hasMacro($name)  
    {  
        return isset(static::$macros[$name]);  
    }  
  
    /**  
     * Flush the existing macros.     
     *     
     * @return void  
     */  
    public static function flushMacros()  
    {  
        static::$macros = [];  
    }  
  
    /**  
     * Dynamically handle calls to the class.     
     *     
     * @param  string  $method  
     * @param  array  $parameters  
     * @return mixed  
     *  
     * @throws \BadMethodCallException  
     */  
    public static function __callStatic($method, $parameters)  
    {  
        if (! static::hasMacro($method)) {  
            throw new BadMethodCallException(sprintf(  
                'Method %s::%s does not exist.', static::class, $method  
            ));  
        }  
  
        $macro = static::$macros[$method];  
  
        if ($macro instanceof Closure) {  
            $macro = $macro->bindTo(null, static::class);  
        }  
  
        return $macro(...$parameters);  
    }  
  
    /**  
     * Dynamically handle calls to the class.     
     *     
     * @param  string  $method  
     * @param  array  $parameters  
     * @return mixed  
     *  
     * @throws \BadMethodCallException  
     */  
    public function __call($method, $parameters)  
    {  
        if (! static::hasMacro($method)) {  
            throw new BadMethodCallException(sprintf(  
                'Method %s::%s does not exist.', static::class, $method  
            ));  
        }  
  
        $macro = static::$macros[$method];  
  
        if ($macro instanceof Closure) {  
            $macro = $macro->bindTo($this, static::class);  
        }  
  
        return $macro(...$parameters);  
    }  
}

具体代码解读,之后再继续。

总结

从这个 Collection 类我们可以看出 Laravel 的用心,和为什么我们能优雅的使用 Laravel 框架了。

只要涉及到 array 的操作和使用,我们都建议先转成 collect($items) —— Collection 对象,这样可以很方便的对数组进行操作。

接下来我们再好好学习学习用 Collection 作为基类的 Eloquent: Collections 的使用。

参考

1. Never write another loop again. https://adamwathan.me/refactoring-to-collections/

2. 《laravel collections unraveled》

3. 《重构——改善既有代码的设计》

如果觉得文章内容对您有用
打赏

Buy Me A Coffee