Laravel 的 Helper 函式官方文件列了上百個,逐條讀完不切實際,也沒有人會全部背下來。今天換個角度:挑出最常用的幾十個整理成速查表,外加 Laravel 11 新增的 once()——知道「有這些工具可以用」比「背熟每個函式簽名」重要得多。
Illuminate\Support\Arr 是處理陣列最常用的靜態方法集合。
use Illuminate\Support\Arr;
// get:安全地取巢狀陣列的值,取不到給預設值,不會噴 undefined index 錯誤
$title = Arr::get($post, 'meta.title', '未命名文章');
// only / except:只取或排除特定 key
$summary = Arr::only($post, ['id', 'title', 'published_at']);
$withoutTimestamps = Arr::except($post, ['created_at', 'updated_at']);
// has / exists:判斷 key 是否存在(has 支援點記法巢狀判斷)
if (Arr::has($post, 'author.name')) { /* ... */ }
// map:對每個元素做轉換,同時保留 key
$titles = Arr::map($posts, fn ($post) => $post['title']);
// sort:依值排序,回傳新陣列
$sorted = Arr::sort($posts, fn ($post) => $post['published_at']);
// first / last:取第一個/最後一個符合條件的元素
$latest = Arr::first($posts, fn ($post) => $post['status'] === 'published');
// flatten:把多維陣列壓平成一維
$flat = Arr::flatten(['posts' => ['title' => 'A'], 'meta' => ['views' => 10]]);
// pluck:從一組陣列/物件裡取出指定欄位組成新陣列,跟 collect()->pluck() 效果相同
$titles = Arr::pluck($posts, 'title');
$titlesById = Arr::pluck($posts, 'title', 'id'); // 第二個參數指定要用什麼當 key
// random:隨機取出一個或多個元素
$randomPost = Arr::random($posts);
還有兩個常被忽略、但處理巢狀資料時很好用的全域函式:
// data_get:跟 Arr::get 類似但支援萬用字元 *
$allTitles = data_get($posts, '*.title');
// data_set:安全地設定巢狀陣列的值,路徑中間層不存在會自動建立
data_set($config, 'seo.meta.description', '這是文章摘要');
Illuminate\Support\Str 是這系列後面幾天會頻繁用到的字串工具集(例如 Day 9 產生文章網址代稱的 Str::slug()),這裡先把常用方法集中整理:
use Illuminate\Support\Str;
Str::slug('Laravel 13 上手筆記'); // 中文不會原樣保留!詳見下方說明
Str::limit($post->body, 100); // 截斷字串並加上 "...",Day 12 的 Blade 元件範例會用到
Str::words($post->body, 20); // 依「字詞」數量截斷(跟 limit 依字元數截斷不同)
Str::title('my first post'); // "My First Post",每個字首字母大寫
Str::camel('post_title'); // "postTitle"
Str::snake('postTitle'); // "post_title"
Str::studly('post_title'); // "PostTitle",常用於動態組出類別名稱
Str::plural('post'); // "posts"
Str::singular('posts'); // "post"
Str::contains($post->title, 'Laravel'); // 判斷是否包含子字串,PHP 8 起也有原生 str_contains() 可用
Str::startsWith($post->slug, 'draft-'); // 判斷開頭
Str::random(32); // 產生隨機字串,適合當作暫時性 Token
Str::mask($email, '*', 3); // 遮蔽字串中間部分,例如信箱顯示成 "abc****@example.com"
Str::uuid(); // 產生一組 UUID(v4),Day 11 會講到的 HasUuids trait 底層用的就是類似機制
Str::slug() 遇到中文:一個中文圈開發者一定要知道的細節Str::slug() 是這份清單裡最容易被誤解的一個,而且誤解的代價不小,值得單獨拉出來講。它的方法簽名長這樣:
Str::slug($title, $separator = '-', $language = 'en', $dictionary = ['@' => 'at']);
關鍵在第三個參數 $language 預設是 'en'。只要這個參數不是 null,slug() 內部就會先把字串丟進 Str::ascii() 做「轉寫成 ASCII」的處理,之後才做小寫化跟連字號替換。這代表中文字不會原樣保留在網址代稱裡:
Str::slug('Laravel 13 上手筆記');
// 中文會先被轉寫(transliterate)成拉丁字母的近似拼音,
// 大致會得到 "laravel-13-shang-shou-bi-ji" 這種結果,而不是 "laravel-13-上手筆記"
// 想保留中文字本身,要明確把 $language 傳成 null,跳過 ASCII 轉寫這一步
Str::slug('Laravel 13 上手筆記', '-', null);
// "laravel-13-上手筆記"
為什麼傳 null 就能保留?因為跳過 ascii() 之後,slug() 最後那道清理用的正規表達式保留的是 Unicode 的「字母」(\pL)跟「數字」(\pN),中文字本身就屬於 \pL,自然會被留下來。
實務上該選哪一種,沒有標準答案,取決於你要的網址長相:
/posts/laravel-13-shang-shou-bi-ji):用預設行為即可,但要有心理準備轉寫結果的可讀性通常不高,純中文標題轉出來的拼音對讀者幾乎沒有意義。/posts/laravel-13-上手筆記):傳 null 保留中文,瀏覽器會自動做百分號編碼,實際 URL 會變成一長串 %E4%B8%8A...,複製貼上時觀感較差,但在網址列顯示時是可讀的中文。id 當網址識別。這件事在 Day 9 會有實際後果——那天 Post 的 Observer 會用 Str::slug($post->title) 自動產生代稱,如果 blog-app 的文章標題是純中文(例如「今天天氣真好」),預設行為轉寫出來的結果可能很短、甚至在極端情況下為空字串,而 slug 欄位有唯一索引,兩篇標題轉寫後撞在一起就會直接寫入失敗。Day 9 會再回頭處理這個防呆。
如果你需要對同一個字串連續做好幾個操作,Str::of()(或簡寫 str())提供 Fluent(鏈式)風格的 API,比連續巢狀呼叫 Str::xxx(Str::yyy($value)) 好讀得多:
$excerpt = str($post->body)
->stripTags()
->limit(150)
->finish('...')
->toString();
// 等效但不用鏈式寫法,可讀性明顯較差
$excerpt = Str::finish(Str::limit(strip_tags($post->body), 150), '...');
Str::of() 回傳的是一個 Stringable 物件,串接的每個方法都回傳新的 Stringable,最後用 ->toString()(或直接在 Blade 裡輸出,會自動轉字串)取出結果。這個模式跟 Day 10 會提到的 Collection 鏈式操作是同一套設計哲學——把「一連串轉換步驟」寫成一行可以從左到右閱讀的鏈式呼叫,而不是由內而外層層包裹的巢狀函式呼叫。
Illuminate\Support\Number 處理數字轉換成人類易讀格式:
use Illuminate\Support\Number;
Number::format(1234567.891, precision: 2); // "1,234,567.89"
Number::currency(1999, in: 'TWD'); // "NT$1,999.00"
Number::fileSize(1024 * 1024 * 3); // "3 MB"
Number::forHumans(1500000); // "1.5 million"
Number::percentage(45.5); // "45.50%"
Number::abbreviate(1200); // "1.2K"
Number::clamp(150, min: 0, max: 100); // 100,超出範圍時夾回邊界值,例如限制分頁參數不能亂傳超出範圍的數字
Number::ordinal(3); // "3rd"(英文序數,中文情境較少用到)
blog-app 之後如果要顯示文章瀏覽次數、檔案上傳大小,這幾個函式比自己手刻格式化邏輯省事很多,也自動處理了千分位、單位換算這些細節。Number::clamp() 特別適合處理使用者可控的分頁參數(例如 ?per_page=99999 這種惡意或誤植的輸入),一行把數值限制在合理範圍內,比寫一串 if/min/max 巢狀判斷乾淨。
app_path('Models/Post.php'); // .../app/Models/Post.php
base_path('composer.json'); // 專案根目錄
config_path('services.php'); // .../config/services.php
database_path('migrations'); // .../database/migrations
public_path('images/logo.png'); // .../public/images/logo.png
resource_path('views/posts'); // .../resources/views/posts
storage_path('app/public'); // .../storage/app/public
這些函式的價值在於跨環境的路徑一致性——不管你的專案部署在哪台主機、哪個路徑下,這些函式永遠回傳正確的絕對路徑,不需要自己手刻字串拼接。
route('posts.show', $post); // 依命名路由產生 URL(Day 7 建立的 posts.show)
url('/posts'); // 直接組出絕對網址
asset('images/logo.png'); // 對應 public/ 目錄下的靜態資源網址
secure_asset('images/logo.png'); // 強制 HTTPS 版本
to_route('posts.index'); // 產生一個導向該命名路由的 RedirectResponse
這個分類光官方文件就超過 50 個,這裡只挑幾乎每個 Laravel 專案都會用到的:
// dd():dump and die,除錯時印出變數內容並中斷執行
dd($post);
// dump():印出變數內容但不中斷執行,適合想連續看多個變數
dump($post->title);
// config() / app() / request() / auth():分別讀取設定、解析容器綁定、取得目前請求、取得認證使用者
$locale = config('app.locale');
$user = auth()->user();
// abort():直接中斷請求並回傳指定的 HTTP 狀態碼
abort_if(! $post->isPublished(), 404);
// collect():把陣列包裝成 Collection,取得一整套鏈式操作方法
$publishedTitles = collect($posts)->where('status', 'published')->pluck('title');
// 安全地存取可能是 null 的物件屬性:現在首選 PHP 8 原生的 nullsafe 運算子 ?->
// (早期常用的 optional() helper 仍然可用,但單純取屬性沒有理由捨棄語言內建語法)
$authorName = $post->author?->name ?? '匿名';
// tap():對一個值執行副作用操作後,回傳原本的值本身(適合鏈式呼叫中插入 side effect)
$post = tap(Post::create($data))->notifyAuthor();
// now():取得目前時間的 Carbon 實例
$post->published_at = now();
// old():Blade 表單重新填值,配合驗證失敗後保留使用者輸入
<input value="{{ old('title', $post->title) }}">
// blank() / filled():比 empty() 更精準地判斷「值是否有意義」,會正確處理空字串、只有空白的字串、空陣列
blank(''); // true
blank(' '); // true(純空白字串也算 blank,empty() 不會這樣判斷)
filled($request->input('title')); // 判斷欄位確實有填內容
// with():把一個值傳進閉包做運算,語意上類似「先算出一個中繼值再接著用」
$result = with(Post::count(), fn ($count) => $count > 0 ? "共 {$count} 篇" : '目前沒有文章');
// rescue():執行一段可能拋出例外的邏輯,失敗時回傳預設值而不是讓例外往外拋
$data = rescue(fn () => json_decode(file_get_contents($path), true), []);
// retry():自動重試一段可能因為暫時性問題失敗的邏輯
$response = retry(3, fn () => Http::get('https://api.example.com/posts'), 100);
rescue() 跟 retry() 這兩個函式值得多花一點篇幅——它們處理的是「這段程式碼有機率失敗,但失敗的處理方式不一樣」的兩種情境。rescue() 適合「失敗了給個安全的預設值,程式繼續往下跑就好」(例如讀取一個可能不存在的快取檔);retry() 適合「失敗可能只是暫時性的(網路抖動、外部服務短暫不穩),值得再試幾次」,這在 Day 26 呼叫外部 API、Day 29 串接 LINE/OpenAI 這類依賴網路的操作時特別實用。
Laravel 11 新增的小工具,解決一個常見痛點:確保一段邏輯在單次請求中只執行一次,之後的呼叫直接回傳快取的結果,不需要自己手動維護靜態變數或屬性來做記憶化(memoization)。
function expensiveCalculation(): int
{
return once(function () {
// 假設這裡有一段很花時間的運算,例如複雜的統計查詢
return Post::published()->count();
});
}
expensiveCalculation(); // 實際執行運算
expensiveCalculation(); // 直接回傳快取結果,不會重新查詢
expensiveCalculation(); // 一樣是快取結果
once() 的快取只在單次請求的生命週期內有效,跟 Cache::remember() 那種跨請求持久化的快取是不同層級的東西——如果你只是想避免同一次請求裡重複計算同一個值(例如同一個 view 裡好幾個地方都要用到「目前使用者的文章總數」),once() 比自己手動加一個靜態變數要乾淨得多。原本 Laravel 生態圈裡很多專案會另外裝 spatie/once 這個第三方套件做同樣的事,Laravel 11 把它內建進框架後,這個第三方套件就不需要了(如果你的 composer.json 還有它,可以移除以避免功能重複)。
once() 也可以在類別方法內使用,這時候的記憶化範圍是「同一個物件實例」,不是全域共用:
class Post extends Model
{
public function readingTime(): int
{
return once(fn () => (int) ceil(str_word_count($this->body) / 200));
}
}
同一個 $post 物件實例在單次請求裡多次呼叫 $post->readingTime(),只有第一次會真的計算字數,後續呼叫直接回傳快取值——如果這個方法在同一個 view 裡被呼叫好幾次(例如列表頁跟側邊欄都要顯示閱讀時間),這個機制能省下重複計算的成本。
dd():dd() 會直接中斷整個請求並輸出除錯資訊給前台看到,忘記移除是很常見但很尷尬的疏失,養成 commit 前搜尋一次 dd(/dump( 的習慣。once() 誤用成跨請求快取:once() 只在單一請求生命週期內有效,每次新的 HTTP 請求都會重新執行一次,不要拿它取代 Cache::remember()。Arr::get() 的點記法路徑打錯:Arr::get($data, 'a.b.c') 只要中間任何一層不是陣列(例如是物件),就會直接回傳預設值而不是報錯,除錯時容易誤以為「資料本來就沒有」,其實是路徑格式不對。empty() 判斷字串是否有內容,卻誤判純空白字串:empty(' ') 會回傳 false(因為字串長度不是 0),如果你的驗證邏輯用 empty() 判斷使用者是否真的填了內容,純空白的輸入會被誤判成「有填」,這正是 blank()/filled() 存在的理由。retry() 沒有限制重試的錯誤類型,導致明確不該重試的錯誤也被重試:例如驗證失敗、404 這類「重試也不會成功」的錯誤,如果整段邏輯都包進 retry(),會白白浪費時間重試注定失敗的操作,實務上建議搭配例外類型判斷,只對真正「可能是暫時性問題」的錯誤重試。Helper 函式的價值不在於背熟每一個,而在於知道「遇到這種情境,Laravel 通常已經幫你寫好一個現成的工具」,真正動手寫的時候再回頭查文件確認簽名即可。今天除了 Arr/Number/Path/URL 這些分類,也補上了 Str 的鏈式 API、blank()/filled()/rescue()/retry() 這幾個實務上很常用但容易被忽略的函式,並把 once() 併進來講——這是這系列「新功能就近安插進相關主題」的一個小例子。
他山之石,可以攻錯 — 《詩經・小雅》
Day 9 開始進入 Eloquent 的世界,我們會正式建立 blog-app 的 Post 與 User 模型與資料表,為後面好幾天的內容打地基。