如何使用 Laravel Collections 類編寫神級程式碼

liuqing_hu發表於2018-07-10

本文首發於 如何使用 Laravel Collections 類編寫神級程式碼,轉載請註明出處。

Laravel 提供了一些超讚的元件,在我看來,它是目前所有 Web 框架中提供元件支援最好的一個。它不僅提供了開箱即用的檢視(views)、身份認證(authentication)、會話(sessions)、快取(caching)、Eloquent、佇列(queues)、資料校驗(data validation)等元件。甚至還提供了開發工具(Valet 和 Homestead)。

但是,這個框架功能中最強大的一個特性常常被萌新們視而不見 - Collection(集合) 類。在這篇文章,我們將探尋如何使用集合提升編碼效率、程式碼的易讀行,及編寫出更精簡的編碼。

預覽

最初接觸到使用集合的場景來自於研發人員使用 Eloquent 執行資料庫查詢,並從返回資料中使用 foreach 語句遍歷獲取模型集合。

不過,初學者可能並沒有注意到,集合提供了超過 90 個以上的方法來操作底層資料。更妙的是幾乎所有的方法都支援鏈式操作,能夠讓你的程式碼讀起來就像一篇散文一樣。這樣使得你的程式碼更易閱讀,無論是你還是其他使用者都是如此。

還沒有進入正題?好吧,讓我們回顧一個簡單的程式碼片段,來看看我們如何使用集合編寫粗、快、猛的程式碼吧。

程式碼示例

讓我們構建一個真實的世界。假設我們查詢某些 API 介面並獲取到如下以陣列儲存的結果集:

<?php
// API 請求返回的結果
$data = [
    ['first_name' => 'John', 'last_name' => 'Doe', 'age' => 'twenties'],
    ['first_name' => 'Fred', 'last_name' => 'Ali', 'age' => 'thirties'],
    ['first_name' => 'Alex', 'last_name' => 'Cho', 'age' => 'thirties'],
];

我們看到陣列包含名字(first name)、姓氏(last name) 和年齡(age)範圍。現在,我們假設從記錄中獲取一名 年齡(age)30 歲(thirties) 的使用者,然後依據 姓氏(last name) 進行 排序(sort)。最後,我們還希望返回的結果為 一個字串(single string),這樣每個使用者獨佔 一行(new line)。最後,我們還希望返回的結果為

這個需求看起來不難實現,現在讓我們看看使用 PHP 如何實現這一功能:

// 依據姓氏排序
usort($data, function ($item1, $item2) {
    return $item1['last_name'] <=> $item2['last_name'];
});

// 依據年齡範圍分組
$new_data = [];

foreach ($data as $key => $item) {
    $new_data[$item['age']][$key] = $item;
}

ksort($new_data, SORT_NUMERIC);

// 從年齡為 30 歲組裡獲取使用者全名
$result = array_map(function($item) {
    return $item['first_name'].' '.$item['last_name'];
}, $new_data['thirties']);

// 將陣列轉換為字串並以行分隔符分隔
$final = implode("\n", $result);

// 譯註:原文是 $final = implode($results, "\n"); implode函式接收兩種順序的引數,為了保持與文件一致所以我這邊做了調整。

我們的實現程式碼超過 20 行,並且很不優雅。移除掉註釋及換行相關程式碼,這段程式碼會變得難以閱讀。再者,我們還需要藉助臨時變數以及 PHP 中內建的不友好的 sort 方法。

現在,讓我們看下藉助 Collection 類實現起來是多麼簡單吧:

collect($data)->where('age', 'thirties')
                 ->sortBy('last_name')
                 ->map(function($item){
                    return $item['first_name'].' '.$item['last_name'];
                 })
                 ->implode("\n");

哇哦!我們的程式碼從 20 行變成了 6 行。現在的程式碼不僅順暢不少,並且在方法實現時無需藉助註釋告訴我們它們在處理什麼問題。

不過,還存在一個問題阻止我們的程式碼不如完美階段... 就是用於比較 first name 和 last name 的 map 方法。坦白說,這真的不是什麼大問題,但是它為我們探索 macro(宏) 概念提供了動力。

擴充套件集合(Extending Collections)

Collection 類,同其它 Laravel 元件一樣,支援宏(macroable),就是說你可以給它新增方法隨後使用。

提示: 如果你希望新方法隨處可用,你應該將它們新增到服務提供中。我喜歡建立一個 MacroServiceProvider 實先這個功能,對於你來說隨你喜歡就好。

讓我們新增一個方法它會連線由陣列提供的任意數量的欄位並返回字串結果:

Collection::macro('toConcatenatedString', function ($fields = [], $separator = ' ') {
    return $this->map(function($item) use ($fields, $separator) {
        return implode($separator, array_map(function ($el) use ($item) {
                return $item[$el];
            }, $fields)
        );
    })->implode("\n");
});

新增完這個方法後,我們的程式碼基本上就完美了:

collect($data)->where('age', 'thirties')
              ->sortBy('last_name')
              ->toConcatenatedString(['first_name', 'last_name']);

我們的程式碼從混亂的 20 多行精簡到了 3 行,程式碼乾淨整潔功能清晰任何人都可以立馬理解。

又一個示例

現在讓我們看下第二個示例,假設我們一個使用者列表,我們需要基於角色(role)過濾出來,然後進一步如果他們的註冊時間為 5 年或以上且 last name 以字母 A-M 開始的僅獲取第一個使用者。

資料類似如下:

<?php
// API 請求返回的結果
$users = [
    ['name' => 'John Doe', 'role' => 'vip', 'years' => 7],
    ['name' => 'Fred Ali', 'role' => 'vip', 'years' => 3],
    ['name' => 'Alex Cho', 'role' => 'user', 'years' => 9],
];

如果我們使用的是 PHP 實現,我們的程式碼看下來如下:

$subset = [];
foreach ($users as $user) {
    if ($user['role'] === 'vip' && $user['years'] >= 5) {
        if (preg_match('/\s[A-Z]/', $user['name'])) {
            $subset[] = $user;
        }
    }
}
return reset($subset)

注意: 你可以將第二個 if 語句移至第一個裡面,但是我個人喜歡在單個 if 語句中使用不超過兩個條件語句,因為我認為超過 2 個條件語句回事程式碼難以閱讀。

這段程式碼不至於太糟糕,但是我們依然需要使用臨時變數,我們還需要使用 reset 函式將指標重置到第一個使用者。我們的程式碼還有四層縮排,這使得程式碼解析變得更有挑戰性。

相反,我們來看看集合是如何處理這個問題的:

collect($users)->where('role', 'vip')
              ->map(function($user) {
                  return preg_match('/\s[A-Z]/', $user['name']);
              })
              ->firstWhere('years', '>=', '5');

我們將程式碼簡化到了之前的一般左右,每一步過濾處理清晰明瞭,並且我們不需要引入臨時變數。

遺憾的是目前集合還不支援正則匹配,所以我們使用 map 方法,不過我們可以為這個功能建立一個宏:

Collection::macro('whereRegex', function($expression, $field) {
    return $this->map(function ($item) use ($expression, $field) {
        return preg_match($expression, $item[$field]);
    })
});

得益於宏方法,我們的程式碼現在看起來如下:

collect($users) -> where('role', 'vip')
                -> whereRegex('/\s[A-Z]/', 'name')
                -> firstWhere('years', '>=', 5);

注意: 為了簡單起見,我們的宏僅僅適用於陣列集合。如果你計劃讓它們可以在 Eloquent 集合上使用,你需要在此場景下做相應的程式碼處理才行。

不同的視角

我們可以繼續列出無數的示例,但仍然無法涵蓋所有可用的集合方法,並且這從來都不是本文的真正目的。

需要注意的是,透過使用 Collection 類,您不僅可以獲得一個方法庫來簡化程式設計工作,還可以選擇一種從根本上改善程式碼的方法。

你會情不自禁的將你的程式碼結構從程式碼塊重構簡化成一行,同時減少程式碼的縮排,臨時變數的使用和技巧性方法,另外你還可以使用鏈式程式設計方法,這讓你的程式碼更加便於閱讀和解析,此外最重要的是減少了編碼工作!

檢視官方文件獲取更多這個迷人的類庫的使用細節:https://learnku.com/docs/laravel/collections

提示: 你還可以獲取這個 Collection 類獨立安裝包,在使用非 laravel 專案是會非常有幫助。感謝 Tighten Co 團隊做出的努力 https://github.com/tightenco/collect

感謝閱讀,快樂編碼!

如果你有興趣,可以 follow 我 @mattkingshott

原文

How Laravel Collections lead to Zen Code

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章