iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
Software Development

一套真實運作中的 Laravel 系統,拆解它的原生機制系列 第 2

Day 02:Eloquent 模型設計——一段字串處理邏輯,為什麼後來搬進了 Model

  • 分享至 

  • xImage
  •  

前言:這段邏輯到底該寫在 View 裡,還是 Model 裡?

「Blade 裡直接寫 Str::limit($content, 100) 有什麼問題嗎?反正畫面上看起來是對的。」

這是很多人第一次寫「截取內容前幾個字當摘要」這種需求時的直覺寫法——在 Blade 檔案裡直接呼叫字串處理函式,畫面渲染出來確實沒問題。真正的問題不會馬上出現,而是等到同一段邏輯在第二個地方也需要用到的時候才浮現。

昨天介紹過,這個系列的素材是一個真實運作的大學官網入口網站,今天要看的正是一段真實發生過的重構:一段原本寫在 Blade View 裡的字串截取邏輯,後來為什麼、又是怎麼搬進 Model 的 Attribute accessor 裡。

今日目標

  • 理解「View 裡直接處理資料」這種寫法什麼時候開始出問題
  • 認識 Laravel 的 Attribute::get() accessor 語法怎麼把邏輯收斂進 Model
  • 看一組真實案例:從「畫面上兩處各寫一次截字邏輯」到「Model 提供一個 accessor」
  • 建立一個判斷基準:什麼樣的邏輯該留在 View,什麼樣的該搬進 Model

本文主體

問題是怎麼冒出來的

這個系統的首頁有好幾個區塊都要顯示新聞的「摘要」跟「短標題」——首頁跑馬燈要顯示短標題,焦點新聞區塊要顯示摘要。這兩個區塊分別在不同的 Blade 元件裡,最早的寫法很直覺:哪個畫面需要,就在那個畫面的 Blade 檔案裡直接處理一次。

問題是,「摘要」的定義不只是「截取前 100 字」這麼單純——這個系統的新聞內容存成 JSON(content.body 欄位存 HTML),要先把 HTML 標籤拿掉、把 HTML entity 解碼,才能截出乾淨的純文字摘要。這段邏輯如果在兩個 Blade 檔案裡各寫一次,維護起來的風險是:哪天摘要規則要改(例如字數上限從 100 改成 80),必須記得兩個地方都要改,漏改一處,兩個區塊顯示的摘要規則就會悄悄不一致。

❌ 邏輯留在 View 裡

{{-- headline.blade.php --}}
<p>{{ Str::limit(html_entity_decode(strip_tags($featured->content['body'] ?? '')), 100) }}</p>

{{-- marquee.blade.php(另一個檔案,重複同一段邏輯) --}}
<span>{{ Str::limit($announcement->title, 100) }}</span>

這樣寫,View 檔案要知道 Model 內部的資料結構(content['body'] 這個 array key),字串處理規則也散落在畫面層,沒有一個統一的地方可以查、可以測試。

✅ 邏輯收斂進 Model 的 Attribute accessor

// app/Models/News.php
protected function excerpt(): Attribute
{
    return Attribute::get(function () {
        $content = is_array($this->content)
            ? ($this->content['body'] ?? '')
            : ($this->content ?? '');

        return Str::limit(html_entity_decode(strip_tags($content)), 100);
    });
}

protected function shortTitle(): Attribute
{
    return Attribute::get(fn () => Str::limit($this->title, 100));
}
{{-- headline.blade.php --}}
<p>{{ $featured->excerpt }}</p>

{{-- marquee.blade.php --}}
<span>{{ $announcement->short_title }}</span>

Blade 檔案現在完全不需要知道「摘要怎麼算出來」這件事,只要用 $news->excerpt 就好。字數上限要改,只要改一個地方;要幫這個邏輯寫測試,也只要對著 Model 寫,不用渲染整個畫面才能驗證。

同一次重構裡,還順手處理的另一組邏輯

這次重構不只搬了 excerptshort_title,同一批 commit 也把「取得新聞的精選圖片」這段邏輯搬進了 Model:

protected function featuredImage(): Attribute
{
    return Attribute::get(fn () => $this->getMedia('images')->last());
}

protected function featuredImageUrl(): Attribute
{
    return Attribute::get(fn () => $this->featured_image?->getUrl());
}

protected function featuredImageAlt(): Attribute
{
    return Attribute::get(fn () => $this->featured_image?->getCustomProperty('alt') ?? $this->title);
}

原本 Blade 裡要呼叫 $news->getMedia('images')->last()->getUrl() 這種鏈式呼叫,現在只要 $news->featured_image_url這種累加式重構值得注意的地方是:它不是一次規劃好的架構決策,是每次遇到「同一段邏輯又要在另一個地方用一次」時,才順手把它抽出來的結果。 一開始不會知道最終會累積出十幾個 accessor,是踩過重複的痛之後,一段一段搬出來的。

什麼時候該搬、什麼時候不該搬

不是所有 View 裡的邏輯都該搬進 Model。判斷基準很單純:這段邏輯是不是「這個資料本身該有的樣子」,還是「這個畫面想怎麼呈現它」。摘要怎麼截、圖片網址怎麼組,屬於前者——不管哪個畫面用到這個新聞的資料,摘要的算法應該一致,這種邏輯適合收進 Model。至於「這個區塊要不要顯示邊框」「這個按鈕文字要顯示什麼顏色」,屬於畫面呈現本身的決定,留在 View 裡才對。

今日思考題

回想你的專案裡,有沒有一段資料處理邏輯,你在兩個以上的 View 檔案裡各寫了一次?如果現在要幫其中一份加一個新規則,你有把握兩個地方都會記得改嗎?

今日重點回顧

  • 邏輯寫在 View 裡,一開始沒問題,問題在同一段邏輯要在第二個地方用到時才浮現
  • Attribute::get() 讓 Model 可以提供一個乾淨的唯讀衍生欄位,View 不用知道背後怎麼算出來的
  • 真實案例:excerptshort_titlefeatured_image_url 這幾個 accessor 都是「用到第二次才抽出來」的產物,不是一開始就規劃好的
  • 判斷基準:資料本身該有的樣子搬進 Model,畫面怎麼呈現它留在 View

明日預告

明天要看同一個系統裡,一次啟用 Model::shouldBeStrict() 之後,一口氣抓出來的真實 N+1 查詢問題跟一個藏了很久的 typo。


上一篇
Day 01:系列介紹——一套真實運作中的 Laravel 系統,拆解它的原生機制
系列文
一套真實運作中的 Laravel 系統,拆解它的原生機制2
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言