寶塔網站目錄怎麼選:專案根目錄與public運行目錄的差別
把Laravel專案上傳到寶塔,網站卻顯示目錄、404,甚至把不該公開的檔案送給訪客,先核對「專案放在哪裡」和「網站從哪裡提供檔案」。兩者可以是不同目錄。框架把公開入口放在public時,不能只因整包程式已上傳,就讓網站直接指向專案根目錄。 以下依寶塔、Laravel、Nginx及PHP官方文件整理設定方法。目錄樹是說明用示例,請換成自己的實際路徑;不同專案的入口結構不同,不能看到public這個名稱,就替所有網站套用同一設定。 先找入口,不先猜目錄名稱 先看專案的部署文件,確認哪個目錄包含對外入口。以Laravel為例,官方部署文件要求Web伺服器把請求導向public/index.php,並明確提醒不要把index.php移到專案根目錄,否則可能暴露敏感設定。 my-project/ app/ bootstrap/ storage/ vendor/ .env public/ index.php images/ 這個示例中,my-project是完整程式所在目錄,public才是網站公開入口。app、vendor與.env不應因為上傳到同一專案,就成為瀏覽器可以直接取得的網站檔案。不要用「先把網站開起來再說」為理由,將所有內容放進公開入口。 如果是純HTML網站,入口可能就在上傳目錄;如果是其他框架,則以它的部署文件為準。也確認目前資料夾不是壓縮包解開後多套一層的結果,例如你以為指向專案,實際卻指向只包含另一個專案目錄的外層。 寶塔的網站目錄與運行目錄分開核對 寶塔官方網站目錄說明,把網站目前部署目錄與運行目錄分開列出。運行目錄用來指定二級目錄,文件以ThinkPHP5與Laravel這類程式為例。這不是要求把完整專案搬進public,而是讓Web入口落在正確的位置。 在網站清單選中正確站點,查看它的網站目錄,再核對運行目錄。先記下變更前的完整路徑,避免同一台伺服器有多個相似站點時,把另一個網站的目錄改掉。若清單顯示的路徑和你上傳的位置不同,先處理這個差異。 假設專案放在/www/wwwroot/example.com,且公開入口確實是它下面的public,核對結果應讓網站從該public提供內容。介面中的相對目錄和最後生效的完整路徑要一起看,不要只憑下拉選單裡有public就判定完成。 使用Nginx時,查看最後生效的root Nginx官方root指令說明,檔案路徑一般由root所設目錄與請求URI組合。這表示相同的網址,在不同root下會查找不同檔案。若你讀到的是另一份server配置,或者相關location另有設定,單看一個目錄值仍不足以確認所有請求。 Laravel官方的Nginx部署示例把root指向專案的public,並配合請求轉交入口程式的規則。只改root而漏掉框架路由規則,可能使首頁能開,內頁仍404。應依實際Web服務和框架版本核對整套部署要求,不從網上隨便貼一段配置覆蓋現有站點。 這篇用Nginx說明檔案入口原理;若站點使用Apache或其他服務,請核對相應的文件根目錄與路由配置。不要把Nginx指令貼到另一種服務,或把寶塔欄位的顯示值當成所有服務都採用相同配置的證據。 看到404,先區分靜態檔案與框架路由 先挑一個你確認存在的公開靜態檔案,例如public/images內的測試圖片,再看它的網址是否符合公開目錄結構。當public已是文件根目錄,網址通常不再額外帶一層public;不要把磁碟上的完整路徑原樣接到域名後面。 如果靜態檔案也404,優先核對選中的站點、公開目錄、檔名大小寫及檔案是否真正存在。如果靜態檔案正常,但框架內頁404,再查請求如何交給入口程式。這個順序可以避免一看到404,就反覆更改運行目錄。 也不要只檢查首頁。首頁可能由現存HTML檔案提供,並沒有經過你打算使用的PHP入口。應分別核對首頁、已知靜態資源與一條合法的框架路由,記下各自結果,再決定下一個要處理的設定。 防跨站設定不代替公開入口 寶塔網站目錄頁也提供防跨站攻擊設定,涉及PHP的open_basedir。PHP官方文件將它描述為限制PHP可存取檔案的位置,並提醒這只是額外安全措施,不能作為完整安全保證。 因此,即使啟用防跨站,也不能把專案根目錄當作公開目錄,期待它替Web伺服器擋住所有敏感檔案。瀏覽器取得靜態檔案和PHP程式讀取檔案,是不同的行為;公開入口仍需依框架要求設定。 若改入口後PHP報出路徑限制錯誤,核對錯誤涉及哪個檔案和允許範圍,再按專案需要處理。不要為了消除一行錯誤就關閉所有限制,更不要把其他網站的目錄加入,讓本來分開的站點互相讀取資料。 搬站時重新核對路徑,不沿用舊機器的字串 從另一台主機搬過來時,舊配置中的絕對路徑不一定適用新站點。先確認新機器上的專案位置,再核對公開入口和相關程式路徑;不能因域名相同,就認為磁碟目錄、PHP環境與檔案權限也完全相同。 若目前站點已有正常服務,先記錄配置及可還原的版本,再安排範圍明確的變更。更改運行目錄後,使用剛才列出的靜態資源與合法路由重新檢查;出現異常時保留錯誤,不要連續嘗試多個猜測路徑,讓原本可用的設定也失去追蹤。 目錄正確後,再處理執行條件 正確的public入口不代表整個部署已完成。Laravel仍有PHP版本、擴充功能、依賴、環境設定與可寫目錄等要求。官方文件特別列出storage與bootstrap/cache的寫入權限;這些是程式執行條件,不應以公開整個專案來迴避。 遇到500時,保留發生時間、網址與相應錯誤日誌,先判斷是入口配置、PHP執行,還是程式本身的問題。不要在無法辨認原因時同時改目錄、版本與權限,否則即使恢復,也很難知道是哪個變更起作用。 最後保存專案完整路徑、公開入口、Web服務類型與核對結果。下次更新專案時,完整程式仍按原結構部署,對外目錄保持明確;維護者就不會把「程式放置位置」誤當成「所有檔案都可公開的位置」。 參考資料寶塔官方:網站目錄與運行目錄Laravel:部署與public入口Nginx:root指令PHP:open_basedir與核心設定寶塔官方:新增PHP網站
Laravel查不到資料還讀欄位:first的null與firstOrFail
商品詳情頁偶爾報錯「無法讀取null的屬性」,但同一段程式在別的商品又正常。先查單筆查詢是否真的有找到模型。Eloquent的 first()沒有找到資料時會回null;接著直接讀 $item->name,就把「查無資料」變成另一個程式錯誤。 你可以用明確的null分支處理沒有資料,也可以在「這個入口必須有一筆」時使用 firstOrFail(),讓未找到以例外表達。兩者是不同的流程選擇,不是只因為方法名字看起來較強,就全部替換。 first只取第一筆,不保證一定有 下面的查詢以主鍵篩選。如果資料存在,first得到模型;若主鍵不存在,結果是null。這與查詢沒有連上資料庫不同,也不代表資料庫操作拋出例外。 $item = Item::whereKey($id)->first(); if ($item === null) { // 依這個入口的需求處理查無資料。 } else { $name = $item->name; } 把讀屬性的動作放在確認模型存在的分支後,能讓維護者直接看出條件。不要只在每個欄位讀取前加可選操作符,讓整頁默默顯示空值,卻沒有決定這條商品URL不存在時應怎麼回應。 也不要把資料庫故障與查無資料混成同一個空畫面。first正常返回null,與連線或SQL錯誤是不同情況;後者仍可能拋出例外,需要沿專案既有錯誤處理方式處理。一次廣泛catch後回空字串,很容易把真正的服務故障藏起來。 必須存在的資源可用firstOrFail 對商品詳情、會員資源等需要明確模型的入口,firstOrFail可以讓查無資料時丟出 ModelNotFoundException。找到資料時仍回傳模型,並不是讓成功資料有另一種形狀。 $item = Item::whereKey($id)->firstOrFail(); $name = $item->name; Laravel官方文件說明,這個例外在應用程式一般HTTP處理中若未被捕捉,會產生404回應。本文本機案例只核Eloquent例外類型,沒有啟動完整HTTP控制器;接回你的網站後,仍要核專案的例外處理、回應格式與路由配置。 如果你已經有自訂handler,把所有例外都轉成200或其他格式,不能只看firstOrFail方法就認為最終HTTP必然正確。查詢層表達未找到,與應用程式把它轉成對使用者的回應,是接續的兩層。 用存在與不存在ID做對照 在專用測試資料庫建立一筆合成商品,名稱Demo。先用它的主鍵查,再用一個確定不存在的主鍵查,不接正式商品庫,也不需要刪掉既有商品來製造情況。 $found = Item::whereKey($existingId)->first(); $missing = Item::whereKey($missingId)->first(); dump($found?->name); dump($missing === null); try { Item::whereKey($missingId)->firstOrFail(); } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { dump(get_class($e)); } 本文隔離案例使用PHP8.4.2、Laravel13.34.0與記憶體SQLite。存在ID的first得到名稱Demo;不存在ID的first確實是null;同一不存在ID的firstOrFail拋出完整的Eloquent ModelNotFoundException類型。沒有任何正式資料被改動。 這三項比較回答的是「查詢方法如何表達未找到」。它不包含瀏覽器404頁、API錯誤格式或權限控制的驗收。讀者可以在自己的測試路由再做真請求,分別確認成功回應與未找到回應,避免把本機例外輸出當成完整網站行為。 未找到可能來自查詢範圍 資料表裡看得到一筆,應用程式卻查不到,不一定是first有問題。查詢還可能包含狀態、租戶、軟刪除或全域scope。先看真正執行的條件,確認這份資料是否應在目前入口的可見範圍內。 不要為了讓查詢拿到資料就移除所有scope或授權條件。它們可能正是防止跨租戶或未公開資料被讀取的界線。若結果與需求不符,依具體條件修正,而不是把where清空後拿第一筆任意資料。 如果查的是email、slug等非主鍵欄位,也要核對它們是否真正唯一。first只取符合條件的第一筆,並不會替你確認只有一筆。遇到重複資料時,任意取第一筆可能掩蓋資料品質問題,不能把「沒有報錯」當成資源解析正確。 列表無結果,和詳情不存在不同 搜尋清單沒有符合項目,通常可以顯示空結果;單一資源URL找不到對應項目,則可能適合未找到回應。不能為了統一方法,把所有查詢都改成firstOrFail,讓正常的零筆搜尋也成為異常。 同樣地,first不是拿來代表一份完整列表。列表查詢應依需求取得集合或分頁,並在集合層處理零筆;單筆查詢才看模型或null。先分清UI到底要一個資源還是多筆結果,再選對應方法。 在實際網站中,無結果訊息可引導使用者調整篩選;未找到資源則應提供返回合理入口的方式。這是各入口的產品行為,查詢方法只提供資料層的條件,不會自動幫你設計畫面。 別在缺資料時製造假的成功模型 有些修法會用空Item或預設名稱替代null,讓屬性讀取不報錯。如果這條URL本來應對應一筆真資料,假模型可能讓頁面回正常成功,卻顯示不存在的商品。只有需求本來允許預設資料時,才應採這種策略。 修改或刪除入口更需要確認模型存在,不能在null時繼續執行後續動作。先把資源解析清楚,再核權限與操作條件,最後才寫入。firstOrFail不會代替授權,也不會證明這筆資料可由目前使用者修改。 如果在查詢與寫入之間,資料可能被另一個操作變更,還需按業務規則處理競爭情況。本文的合成案例沒有併發,不把單次查到模型推成整段修改流程永遠有效。對需要交易或鎖的功能,另依真正的資料一致性要求設計。 從讀屬性的位置往前查 遇到null屬性錯誤,先找到模型變數從哪裡來,核查詢與未找到分支。若應容許不存在,就明確處理null;若必須存在,就用適合的未找到流程,並核HTTP層的真結果。不要在整篇畫面每個欄位加預設值,卻留下資源其實沒有找到的問題。 保存存在ID與不存在ID兩份固定案例,往後調scope或路由時一起重測。這樣能分辨是資料範圍有意改變,還是查詢流程意外失效,也讓下一位維護者知道查無資料應該走哪條路。 最後確認你用的是Eloquent模型查詢。DB::table()的Query Builder成功取到單筆時,返回的是一般物件,不是帶有模型方法與關聯的Item。兩者都能讀某些欄位,不代表可直接交換使用;也不能對一般物件呼叫模型的保存或關聯方法。本文比較的是Item模型的first與firstOrFail,維護另一種查詢時應按它自己的返回型別核對。 參考資料...
Laravel翻到第二頁篩選消失:分頁連結保留限定query
商品清單選了coffee分類,第一頁正確,按第二頁卻變成所有商品。先把滑鼠移到分頁連結,看看網址是否只剩 ?page=2。後端第一個請求有讀分類,不代表Paginator產生下一頁時會自動保留那份篩選。 要修的是連結攜帶的條件,以及下一個請求如何使用條件。Laravel的 appends()可以把指定query加入分頁URL;若只需要category與q,保留這兩個即可,不必把所有目前請求參數都複製出去。 第一頁的查詢條件不會留在下一個請求 假設第一頁網址是 /items?category=coffee&q=C%2B%2B,控制器讀取category與q後查到清單。頁面結束後,下一次按分頁會發出新請求;如果href沒有這些query,控制器也就拿不到原條件。 這不是資料庫忽然忘記篩選,而是URL契約沒有完整傳遞。查修時先看第一頁的實際網址與第二頁href,再看控制器讀哪幾個query。不要只在控制器固定加coffee,讓第一個問題暫時消失,卻讓使用者無法切其他分類。 $items = Item::query() // 這裡沿用你的已驗證篩選條件。 ->paginate(12); $items->appends(['category' => 'coffee']); 這段示意連結追加的位置。真實查詢仍要把category用在資料查詢中;只加到URL卻沒使用它,不會讓下一頁自動篩對資料。分頁清單與連結要共用同一份已核對的條件。 明確保留需要的query 如果這個入口允許category與q,可先從query資料選出這兩個key,再交給Paginator。這讓回傳URL與頁面的篩選契約一致,也避免把無關參數一起帶到每一個連結。 $filters = collect($request->query()) ->only(['category', 'q']) ->all(); $items->appends($filters); 選key不是驗證值。category是否存在、q是否符合長度與格式、是否有權讀取對應資料,仍要在你的請求與查詢流程中處理。本文只示意保留篩選的連結機制,不把任意query當作可信資料庫條件。 Laravel也提供 withQueryString(),方便保留目前請求的全部query。若頁面設計確實需要全部參數,這是可用選項;若只想保留兩個篩選,明確appends通常更容易知道每個連結帶了什麼。不要為了少寫一行,把不相關的追蹤或內部測試參數都長期附在分頁上。 用URL比較,不必先接完整商品資料 可以在自有Laravel測試專案建立一個LengthAwarePaginator,總數6、每頁2、目前第1頁。資料只放合成ID,專門觀察第二頁URL。 use Illuminate\Pagination\LengthAwarePaginator; $p = new LengthAwarePaginator( [['id' => 1], ['id' => 2]], 6, 2, 1, ['path' => 'https://example.test/items'] ); dump($p->url(2)); $p->appends(['category' => 'coffee', 'q' => 'C++ beans']); dump($p->url(2)); Laravel13.34.0的本機案例中,第一個URL是 /items?page=2;追加後包含 category=coffee&q=C%2B%2B%20beans&page=2。再解析query,得到category coffee、q C++ beans、page2,說明加號與空白沒有被手動拼接弄壞。 這個例子核的是Paginator的URL,不是商品查詢或真頁面呈現。接回網站時,再按真正的第二頁連結,確認它發出的query與第一頁一致,結果確實仍屬同一個分類與搜尋條件。兩層都核,才能完整回答「翻頁條件是否保持」。 別手動拼出含加號的搜尋網址 搜尋字串可能含空白、加號與其他符號。直接把文字接在 &q=後面,容易產生不同解讀;例如原本的C++可能被當成空白。讓框架的query生成方法編碼參數,會比自己零散替換字元更清楚。 也不要把已經編碼的字串再當成原值交給appends,造成重複編碼。這個範例送進去的是原字串C++ beans,生成器才輸出相應URL。若你看到網址裡的百分比編碼不符合預期,先核資料在哪一層已被編碼,而不是再加一輪encode。 URL中的文字與實際查詢也要分開:使用者讀到C++,後端查詢取得的應是原字串,不是 %2B字面文字。Network、控制器輸入與Paginator輸出各看一眼,通常就能定位是哪一步改變了值。 切換篩選時,回到合適頁碼 保留query解決了翻頁丟條件,卻不代表所有舊頁碼都適合新篩選。使用者在第8頁改到只有一頁的分類,若表單又把page8帶過去,就可能得到空結果。切換篩選通常應把頁碼重設到第一頁,再由新清單的分頁連結繼續。 這是前端篩選提交與URL設計的工作,不能只在所有連結加appends就算完成。確認提交篩選時沒有保留不適用的page,再測新條件第一頁與第二頁。清單沒有資料,也應讓使用者知道目前條件與結果,不必自動把條件清掉來掩蓋空頁。 如果同一頁有兩組Paginator,Laravel支援替各組設定不同pageName。這時保留query要核對各組頁碼是否互相影響。本文只有一組預設page,不把單組範例推成所有多列表頁面都能照抄。 排序與篩選是另外的穩定條件 第二頁有保留category,仍可能因查詢沒有穩定排序而出現重複或跳項。這不是appends的責任;它只保留URL條件,不安排資料庫排序。查詢需要明確排序與必要的唯一鍵,以符合你的分頁資料要求。...
Laravel收到false字串卻當成true:用Request boolean讀表單開關
設定表單送來 enabled=false,後端卻把開關打開。若程式用了 (bool)$request->input('enabled'),先看輸入是否為字串。PHP的布林轉型不會把英文單字false理解為否;非空字串false反而會轉成true。 Laravel的Request提供 boolean()來讀取常見開關值。這能讓字串false與on等表單值按明確方式轉換,但它不是驗證器:不認得的值也可能得到false。先分清「把輸入轉成布林」與「拒絕不符合契約的輸入」,再接回設定保存。 先比較型別,不只看文字 下面兩個值在人眼看來都像false,型別卻不同。JSON布林值是布林,表單或查詢字串通常是文字;PHP轉型因此有不同結果。 dump((bool) false); // false dump((bool) 'false'); // true dump((bool) '0'); // false 這不是Laravel偷偷改了開關,而是一般PHP轉型規則。若你把任何非空文字都當作有效的開關,banana也會變true。不要為了這個問題只把false特判掉,再留下其他未定義的值可以任意打開設定。 在查修紀錄裡,同時記合成輸入值與型別。單看一行輸出的false,有可能其實是字串,也可能是布林。用測試資料先重現,不需要把真使用者的整份表單寫進日誌。 用Request的boolean讀常見開關值 在一般請求裡,可以直接讀你需要的欄位。Laravel官方文件列出1、字串1、true、字串true、on、yes等會被讀為true,其他值讀為false。 $enabled = $request->boolean('enabled'); 例如HTML checkbox未指定value時,勾選常會送on;明確送字串false則得到false。這比PHP的任意非空字串轉型更貼近常見表單開關,但你仍應確認前端真的採用這份契約,不要假定不同客戶端會送相同型別。 如果API只接受真正的JSON布林,就應要求呼叫端送 true或 false,並按那個契約驗證。boolean方法讓你方便取值,不代表所有可被它轉換的字串都必須允許進入API。 用八種輸入跑同一個Request 在自有Laravel測試專案,可以建立合成Request,不連到正式設定資料。每次分別放入一種輸入,對照一般PHP轉型與Request boolean。 use Illuminate\Http\Request; $request = Request::create('/demo', 'POST', ['enabled' => 'false']); dump((bool) $request->input('enabled')); dump($request->boolean('enabled')); 本文在PHP8.4.2、Laravel13.34.0測了missing、字串false、字串true、on、0、1、布林false與banana。字串false的PHP轉型是true,Request boolean是false;banana也出現true對false。true字串、on與1兩邊都是true,0、布林false與missing兩邊都是false。 這份比較看的是解讀方式,不是設定更新成功。尤其banana得到false,並沒有產生驗證錯誤。如果你的要求是非法值必須拒絕,就不能把「最後得到false」當成「輸入有效且使用者明確關閉」。 真HTTP請求還可能先經過字串整理等中介層。本文合成Request用固定測試值,沒有聲稱已涵蓋所有表單序列化與中介層設定。把程式接回網站後,用Network核實送出的值,再看伺服器實際讀到什麼,才能對上完整流程。 轉換與boolean驗證不是同一份允許集合 Laravel驗證的 boolean規則有自己的允許值,官方文件列出true、false、1、0以及字串1、字串0。它不等同Request boolean對on、yes等字串的轉換方式。不要以為兩個API都叫boolean,就可以交換使用而結果完全一樣。 例如你先用嚴格的boolean規則驗證,表單送on可能被拒絕;如果需求是標準checkbox輸入,應先設計一致的輸入與驗證流程。也可以讓前端明確送1或0,減少各端的差異。選擇哪種方式,要由這個入口的資料契約決定。 更不應先把任意非法文字轉成false,再驗證轉換後的布林值,結果宣稱原輸入已通過檢查。那樣已經丟失原值,驗證器看不到輸入原本不合理。要拒絕未知值時,先對原輸入做你選定的驗證,再轉成業務要用的布林。 沒送checkbox,可能是關閉,也可能是不改 HTML checkbox未勾選時,常不會送出該欄位。Request boolean在欄位缺席時的預設結果是false,對完整設定表單可能正好代表關閉。但部分更新API的missing可能表示保留原值,兩種情境不能用同一段無條件保存來處理。 如果部分更新契約是「沒送就不改」,先判斷輸入key是否存在,再對有送來的值進行驗證與轉換。不要只是每次把 $request->boolean('enabled')保存,否則只修改標題的請求也可能把開關關掉。 反過來,完整表單若用隱藏欄位與checkbox一起補送值,也要確認同名輸入如何被序列化。不同表單與客戶端可能讓伺服器收到單值或陣列,不應把未知陣列直接當成可用開關。本文八種值都不是陣列,真專案要按自己的輸入契約補測。 程式中的if也要讀正確資料 修正取值後,後續判斷應使用已經解析出的布林,而不是另一處又直接讀原字串。否則保存可能是false,發送通知的if仍把false字串當true,形成看起來很矛盾的行為。 $enabled = $request->boolean('enabled'); if ($enabled) { // 這裡只處理已明確解析為開啟的情況。 } 這段只是判斷示意,真功能的授權、業務條件與保存要各自完成。開關值是false,不表示使用者有權停用某個資源;值為true也不代表可觸發任何外部工作。讓輸入解讀、權限與動作有各自明確的責任。 回到前後端核三條路 至少測勾選、未勾選與明確傳false;若是API,再加missing與未知字串。每條路都看原payload、解析結果與最終設定。設定畫面顯示關閉而資料仍開啟,可能是保存問題;解析已經錯了,則先修輸入層,不必一開始就查快取。 將前端預期型別寫清楚,讓後來加新客戶端的人知道應送布林或哪種字串。Request boolean是合適的取值工具,但只有與驗證和更新契約配合,才能避免把缺席、非法與明確關閉全部混成一個false。 驗證案例別只斷言最後是false...
Laravel更新原email也報重複:unique要排除目前模型
會員編輯頁只改了姓名,email維持原值,送出後卻顯示email已被使用。先查唯一驗證是否把正在編輯的那筆會員也算進去。新增資料需要檢查任何一筆是否已使用這個email;更新資料則要排除目前模型,再檢查其他資料。 排除對象必須來自伺服器已取得並確認可修改的模型,不能直接拿表單送來的ID交給ignore。本文用三個合成email比較原值、另一筆已用值與新值,讓更新規則只放過自己,仍擋住真正的重複。 新增與更新的唯一檢查不同 普通unique規則會在指定資料表與欄位查是否已有相同值。如果你正在編輯Ada,資料庫裡本來就有Ada的email;沒有排除自己,維持原email仍會查到一筆,驗證失敗是預期結果。 use Illuminate\Validation\Rule; $rules = [ 'email' => ['required', 'email', Rule::unique('users', 'email')], ]; 這組適合不該與任何既有會員重複的新資料,不代表可以直接套到每個更新入口。查修時看的是規則查詢範圍,不是先把原email改成另一個值來繞過錯誤。那樣只掩蓋了更新契約沒排除自身的問題。 用取得的模型排除自己 假設控制器已經取得目前的user,並完成修改權限核對,可以把這個模型交給規則。Laravel會從模型取出對應key,安排排除條件。 $rules = [ 'email' => [ 'required', 'email', Rule::unique('users', 'email')->ignore($user), ], ]; 這裡的user不是讓使用者任意選擇的排除對象。它應是伺服器根據路由與既有流程載入的資源,接著核對目前使用者是否能更新。排除自身只處理唯一性,不處理資源所有權;兩件事都需要成立。 Laravel官方文件特別提醒,不應把使用者控制的請求輸入交給ignore。不要寫成 ignore($request->input('id')),也不要為了讓例子短就略過可信模型的來源。讓請求參數控制要排除哪筆資料,會破壞你原本想確認的範圍。 如果模型用不同的主鍵名稱,模型傳入方式也能讓規則取得模型key的資訊。手動指定排除值與欄位時,必須核對表結構,不能把會員ID、另一個資料表ID與email欄位名稱混在一起。最小修改通常是沿用已經取得的模型。 三個email,要有三種合理結果 在自有測試資料庫建立兩筆會員:Ada使用 [email protected],另一筆使用 [email protected]。現在編輯Ada,分別測她的原值、另一筆的值,以及不存在的 [email protected]。這些都是合成地址,不寄出郵件。 $candidates = [ '[email protected]', '[email protected]', '[email protected]', ]; foreach ($candidates as $email) { $v = Validator::make(['email' => $email], [ 'email' => ['required', 'email', Rule::unique('users', 'email')->ignore($user)], ]); dump($email, $v->passes()); } 這段需要在測試專案匯入Validator,並讓user指向Ada那筆模型。不要在正式會員表隨意新增測試資料。若已有專用測試資料,確認它們的ID與email,再跑規則即可;目的只是看排除範圍,不需要觸發完整註冊流程。 本文隔離案例使用PHP8.4.2、Laravel13.34.0與記憶體SQLite。未ignore時,原值與另一筆都失敗,新值通過;ignore可信Ada模型後,原值通過、另一筆仍失敗、新值通過。這三個結果一起看,才能知道不是把unique整個關掉了。 只測原值成功還不夠。若程式誤把別人的模型傳入ignore,可能放過另一筆的email,卻仍拒絕自己。因此保存「自己、別人、新值」三份案例,讓排除邏輯與對象都能被核對。 不要用驗證取代資料庫唯一條件 表單驗證提供較早的錯誤提示,但兩個請求可能同時驗證同一個尚未使用的email。驗證時都查不到,不代表稍後兩次寫入都能安全成立。資料庫的唯一索引仍要依業務要求存在,寫入時也要處理constraint失敗。 本文只比較驗證規則,不執行真正的會員更新,也不模擬併發。規則修好後,測試站還要確認欄位保存、資料庫約束與錯誤回應符合需求。不要把一次Validator通過寫成更新或寄信已成功。 同樣地,如果系統允許在特定租戶內唯一,或只檢查某種狀態的資料,需要在規則與資料庫約束中維持一致範圍。不要隨意加where讓紅字消失,卻讓另一個入口或資料庫的規則與它不同。本文例子採全表email唯一,沒有替你設計租戶或軟刪除策略。 格式與大小寫仍依實際資料規則 unique查的是指定欄位。email是否先整理大小寫、去空白,資料庫的比較與排序規則是什麼,要看既有系統。本文合成值沒有大小寫差異,不把SQLite結果推成所有MySQL設定都用同一種比較方式。 若前端顯示的是整理後email,後端驗證與保存卻使用另一份輸入,也可能在原值更新時出現不一致。把進入驗證與保存的資料流對上,但避免日誌保存完整個人資料;測試可用合成值重現,不需要輸出真會員email。 編輯頁的取消與保存也要分清。用表單送來的舊ID或隱藏欄位,不能代替伺服器取得目前模型。先核實資源,再驗證新值,最後才保存;每一步都保留自己的責任。 回到更新入口確認最小變更...
Laravel篩選集合後JSON變物件:用values整理索引
API原本回傳一個商品陣列,後端加了 filter 後,前端突然不能再用同樣的清單操作。回應內容看起來仍有兩件商品,最外層卻從方括號變成大括號,還多了 "0"、"2" 這些key。先查集合篩選後保留下來的索引,不必立刻修改前端把物件硬轉陣列。 Laravel的集合 filter()會保留原key。PHP把不連續的數字key編碼為JSON時,產生的是物件;如果API契約需要一份清單,在篩選後呼叫 values()重新排列索引,就能讓最外層保持陣列形狀。這裡的索引與資料本身的商品ID是不同東西。 刪掉第二項後,索引不是自動連續 用三筆合成資料重現即可。id是業務資料,active是本例的篩選條件,不接資料庫,也不假設你的商品表一定有同樣欄位。 $items = collect([ ['id' => 11, 'active' => true], ['id' => 12, 'active' => false], ['id' => 13, 'active' => true], ]); $filtered = $items->filter(fn ($row) => $row['active']); dump($filtered->keys()->all()); dump($filtered->toJson()); 留下來的是id11與id13,但外層key是0與2。這是因為篩選保留原本位置的key,沒有把2往前改成1。在PHP8.4.2、Laravel13.34.0的案例裡,toJson()輸出是下面這種形狀。 {"0":{"id":11,"active":true},"2":{"id":13,"active":true}} 這份JSON合法,伺服器沒有編碼失敗;只是它是以key索引的物件,而非前端期待的陣列。不要把JSON是否合法與是否符合API契約混在一起。回應200、能解析JSON,都不能代替檢查資料型別與欄位形狀。 用values重排外層,不改商品ID 如果這個回應是供 v-for、表格或一般清單使用,且API約定最外層是一個陣列,可在篩選之後整理索引。 $result = $items ->filter(fn ($row) => $row['active']) ->values(); dump($result->keys()->all()); dump($result->toJson()); 此時外層key變成0與1,JSON為陣列;兩件商品的id仍是11與13。values()整理的是集合索引,不是重新編號資料列,更不會替資料庫更新主鍵。 [{"id":11,"active":true},{"id":13,"active":true}] 同一組例子還檢查了全部被排除的情況:結果再呼叫 values()->toJson()得到 []。前端因此可以用同樣的陣列契約處理零筆、一筆、多筆,不必看到空結果時又改另一種分支。不過真實API若外面還有 data或分頁欄位,應檢查的是約定的清單位置,不是把整份回應都改成陣列。 在Laravel測試專案的Tinker可以直接執行上述三筆資料範例。比對時同時看 keys()、toJson()與 pluck('id'):前兩個確認形狀,最後一個確認業務ID沒變。只看前端畫面能顯示,可能漏掉回傳順序或ID被誤改的問題。 不是所有集合都應該values 如果資料本來就按明確的業務key索引,保留key可能是正確需求。例如回傳以語系代碼為key的設定,或以商品ID快速查找的映射,物件正好符合契約。這時一律 values()反而會丟掉原本有意義的key,讓呼叫端無法直接查找。 因此修正前先問「這是list,還是map」。list著重順序與項目;map著重key到value的對應。兩者都能包含相同資料,但存取方式不同。不要因為前端某個套件喜歡陣列,就在所有API輸出上加values,尤其既有客戶端可能已經依key存取。 如果一份回應既要清單順序,又需要穩定識別,可以把業務ID留在每個項目裡,外層維持清單。像範例的id欄位就做這件事。用陣列位置代替商品ID會讓篩選、排序、插入後的識別變動,並不是整理索引想解決的問題。 巢狀分組也要逐層判斷。外層用分類slug當key,內層才是項目陣列時,只整理需要當清單的內層;在最外層呼叫values,可能把分類key一起丟掉。不要把單層範例推成任意分組資料的通用改法,先看呼叫端需要哪一層是陣列。 原生JSON與API Resource可能有不同處理 本文比較的是普通Collection直接 toJson()的結果。若回應使用Eloquent API Resource collection,Laravel官方文件說明預設會重設集合的數字key,並提供 preserveKeys來保留key。這是資源層的行為,不能直接當成每個普通集合方法都會重排。 維護API時,先確認最後序列化的是普通集合、ResourceCollection、已轉成PHP陣列,還是自己呼叫...
Laravel欄位沒送也驗證過關:sometimes與present的差別
編輯會員資料時,請求沒有帶 name,驗證卻通過了;另一個表單明明帶了 name: null,又被判為失敗。先不要把兩者都叫做「空欄位」。欄位沒出現在輸入裡、出現但值為 null、出現而值為空字串,是三種不同的資料。 Laravel的 sometimes、present、required 各自回答不同問題。要寫對更新表單,先決定「沒有送來」代表不修改,還是代表這份輸入不完整,再選規則。本文聚焦欄位是否存在;不處理網址格式或模型批量賦值。 沒有送欄位,可以是合理的部分更新 假設會員暱稱原本是 Ada,這次請求只改聯絡電話。name 不在 payload裡,就可能代表保持原值,而不是清空。這類部分更新,可以在該欄位使用 sometimes,讓規則只在欄位出現時執行。 $rules = ['name' => ['sometimes', 'required', 'string']]; 這裡的 sometimes 並沒有把 required 取消。它讓整組規則在 key缺席時跳過;key一旦出現,required仍要求它有合適的非空值,string仍檢查型別。因此「沒傳 name」可以通過,「傳 name但值為null」則不能照樣通過。 這項差異很適合用在讀者可照做的測試上:分別送 {} 與 {"name": null},不要只在表單裡把文字刪光再重送。前端表單、JSON請求和你的序列化程式可能產生不同 payload;光看欄位畫面是空白,不能知道 key到底有沒有被送出。 present要求有key,並不要求有非空文字 如果輸入契約規定每次都必須帶 name,但允許用null表示清空,則需要處理「存在」與「值」兩部分。 $rules = ['name' => ['present', 'nullable', 'string']]; present要求輸入中有這個欄位;nullable讓null可接受;有非null的有效輸入時,再按其他規則檢查。這和 sometimes|required|string 的任務不同:前者不能少key,後者可以少key,但送來時不能空。 也不要把 present單獨當成「必填文字」。它只處理欄位是否存在,不能代替 required,更不能代替長度、格式或授權檢查。若表單每次都應送一個非空文字,使用 required|string通常比較直接。真正的規則應由資料契約決定,不能為了讓紅字消失而挑較寬鬆的一組。 四種輸入,分別跑三組規則 下面可以在自有Laravel測試專案的Tinker執行。測試只跑Validator,不修改資料庫。先準備四份資料,再用相同欄位依序驗證,避免每次手動改字串時把案例搞混。 use Illuminate\Support\Facades\Validator; $inputs = [ 'missing' => [], 'null' => ['name' => null], 'empty' => ['name' => ''], 'value' => ['name' => 'Ada'], ]; $rules...
Laravel交易裡捕捉例外仍寫入?確認rollback的邊界
表單一次要新增訂單和明細,明細寫入失敗,訂單卻留在資料庫。程式已經包了 DB::transaction(),看起來應該一起取消,為什麼仍有半張訂單?先找交易閉包裡的 catch。如果例外在裡面被捕捉,接著正常走到閉包結尾,Laravel看到的是一次成功完成的工作,就可能提交前面的寫入。 交易不會閱讀你回傳的錯誤字串,也不知道畫面上顯示「儲存失敗」。它依靠資料庫操作與例外控制流程決定怎麼收尾。要讓同一交易中的前一步取消,失敗必須離開交易閉包,或由你正確管理手動交易;本文先用自動交易的方法把這件事分清楚。 先認清是哪一個catch接住失敗 下面是容易出問題的寫法。notes只是示範表,故意在第一次寫入後拋出例外,代表下一步失敗。這個例子用來看流程,不包含真實付款或訂單處理。 DB::transaction(function () { try { DB::table('notes')->insert(['title' => 'first step']); throw new RuntimeException('second step failed'); } catch (Throwable $e) { // 只記錄,沒有重新拋出。 report($e); } }); 內層 catch 執行完後,閉包正常返回。對交易方法而言,沒有例外穿過它的邊界。程式雖然記錄了錯誤,第一筆資料仍可能被提交。把 catch 改成回傳 false、一個錯誤陣列或錯誤訊息,並不會自動把「回傳失敗」變成 rollback;這些都是正常的回傳值。 常見的誤判是看到日誌有一筆例外,就認為交易也收到例外。日誌只說明某處呼叫了記錄方法。要核對的是例外最後有沒有再被拋出,以及交易閉包結束時走的是正常回傳還是例外路徑。搜尋 try、catch、return,通常比先調整資料庫隔離層級更快找到問題。 把需要對使用者回應的catch放在外面 讓 Laravel先處理回滾,再把失敗轉成控制器的回應。例外可以保留原始類型與訊息,使用者畫面則不必把資料庫細節顯示出來。 try { DB::transaction(function () { DB::table('notes')->insert(['title' => 'first step']); throw new RuntimeException('second step failed'); }); } catch (Throwable $e) { report($e); // 控制器在這裡回傳合適的失敗回應。 } 第二步的例外離開閉包,交易方法會處理 rollback,然後把例外再交到外面的 catch。外層可以記錄、通知維護者,或依應用程式規則回傳錯誤。這樣不用把 HTTP 回應和交易成功判定混在同一層。 如果業務上需要在閉包裡補上記錄或資源清理,也可以捕捉後 throw $e;,使原例外繼續向外傳。別在這裡為了避免報錯而吞掉所有 Throwable。能恢復的業務情況與必須取消交易的故障,應有各自的處理方式;不應靠「有沒有寫日誌」區分。 用一張測試表看清三種結果 在自有測試環境準備只有 id、title 的 notes...
Laravel表單欄位沒存到?分清fillable、驗證與直接賦值
會員編輯姓名的表單只露出一個輸入框,後端卻可能收到其他欄位。瀏覽器沒有顯示 is_admin,不代表請求裡不能加上它。另一種常見情況恰好相反:表單新增了電話,驗證也通過,儲存後電話卻消失。這兩個問題都值得沿著資料進入模型的路線查,不能只看畫面。 Laravel的 fillable 控制模型接受哪些大量賦值欄位;表單驗證控制輸入的內容與格式;授權則回答這個人能不能修改這筆資料。三者處理不同問題。本文以一般會員修改自己的公開姓名為例,說明欄位白名單怎麼寫,以及哪些寫法會繞過它。以下語法適用本文示範的Laravel 13 Eloquent模型,舊專案請先對照自己的框架版本。 先找出欄位從哪裡進入模型 看到 create()、模型實例的 update() 或 fill(),先看傳入的陣列由哪裡來。如果直接送入 $request->all(),任何請求欄位都會走到模型的大量賦值檢查,維護者必須知道哪些被接受、哪些被丟棄。表單畫面不能代替這份檢查。 class Profile extends Model { protected $fillable = ['name']; } $profile = new Profile; $profile->fill([ 'name' => 'Coffee', 'is_admin' => true, ]); 這個例子在模型屬性裡只留下 name。預設行為下,沒有列在白名單的 is_admin 被丟棄。這不表示使用者已經存進資料庫:fill() 只是填模型屬性,真正寫入仍需要儲存操作。要確認一個實際表單的結果,應在送出後重新讀取資料庫那筆資料,不能只用回傳訊息或記憶體中的物件作結論。 對既有專案,先搜尋模型名稱及它的寫入位置。相同模型可能被會員表單、管理後台與匯入工具共用;把某欄加到白名單會影響所有大量賦值入口。如果只有管理員可以修改會員等級,不應為了讓後台少寫一行程式,就把等級開放給所有送進該模型的陣列。 驗證完成後,只送需要的資料 會員修改姓名的入口可以先用驗證取得指定資料,再交給模型。白名單也保留,讓兩個邊界互相補足。下面假設 $profile 已經是目前登入會員有權修改的資料;查出哪筆資料及授權檢查,仍要在你的控制器或Policy裡處理。 $data = $request->validate([ 'name' => [ 'required', 'string', 'max:80', ], ]); $profile->fill($data); $profile->save(); 不要把驗證當作權限檢查。姓名格式正確,不表示送出的人能編輯其他會員;同樣,模型接受某欄,不代表該欄適合由這個表單修改。如果網站有巢狀資料,還要明確限制允許的陣列鍵,不能因為驗證了上層陣列就假設其中每個鍵都已逐項審核。 若新增電話欄位,至少沿四處核對:前端欄位名稱、驗證規則、送進模型的資料、模型白名單。最後確認資料表真有該欄,以及型別和長度能容納輸入。只改 fillable 不會新增資料庫欄位,反過來只加migration也不會讓表單自動接受資料。 直接指定屬性不受同一白名單限制 下面這段是另一條寫入路徑。它不是把整個輸入陣列交給大量賦值,因此不會因為 is_admin 沒列在 fillable 就被擋下。本文案例中,直接指定後的模型屬性包含 name 與 is_admin。 $profile->is_admin = true; $profile->save(); 這種寫法本身有合理用途,例如伺服器依已核准的管理操作更新狀態。問題是值從哪裡來,以及是否先做授權。如果把請求中的權限欄位直接接到這裡,白名單不能救回這條路徑。對照程式時,不能只搜尋 create() 而漏掉屬性指定、Query Builder更新或原始SQL。...
Laravel送出同一表單兩次:讓資料庫唯一條件擋重複
訪客連按兩次送出,資料表多了兩筆相同詢問。把按鈕停用能減少誤按,但請求也可能被重試,或從另一個視窗送來。若產品要求同一次提交只保存一筆,資料庫需要有能識別這次提交的唯一條件。 Laravel 的 unique 驗證規則可以在保存前提示已有資料;資料庫的 unique constraint 則在真正寫入時阻止違反條件的紀錄。兩者處理的時點不同,不能用一次驗證通過,就保證稍後寫入時仍然沒有重複。 先定義什麼叫同一次提交 先選業務上穩定的識別方式。這篇以同一使用者、同一 submission_key 為例,要求這個組合只能存在一次。兩個不同使用者可以有相同的示例字串,但同一使用者重送同一個 key,不能新增第二筆。 實際網站應由伺服器產生或依既有協定驗證提交識別碼,讓同一次請求的重試沿用相同值。不要每次送出時都重新產生 key,否則兩次請求會被視為不同提交。也不要把「訊息文字完全一樣」直接當作重複,訪客可能隔天真的要再詢問同一個問題。 識別碼的範圍、有效期間和重試規則都要由產品決定。付款、訂單或外部 API 的重試還涉及其他副作用;這篇只處理資料表的一筆保存,不把唯一索引稱為完整冪等機制。 在資料表建立真正的唯一條件 Laravel migration 可替兩個欄位建立複合唯一索引: $table->unsignedBigInteger('user_id'); $table->string('submission_key'); $table->string('message'); $table->unique( ['user_id', 'submission_key'], 'submissions_user_key_unique' ); 這裡兩個識別欄位都不是 nullable。若允許 NULL,唯一限制對 NULL 的行為需看使用的資料庫,不能假定空值也會形成同一個提交。欄位型別、長度與比較方式,也應符合實際 key 的格式。 替已有資料表增加限制前,先盤點目前是否已存在相同組合。不能直接上線 migration,再碰到重複資料才臨時刪除。由維護者確認該保留哪筆、其他資料有沒有引用,以及備份和回復方法,再安排正式變更。 驗證規則仍然保留,條件要與資料庫一致 前置驗證可以讓訪客較早收到清楚提示。複合唯一條件的驗證也要包含使用者範圍,而不是把所有人的 key 當成全站唯一: 'submission_key' => [ 'required', Rule::unique('submissions') ->where(fn ($query) => $query->where('user_id', $request->user()->id) ), ], 這是已驗證登入身分的應用程式片段,Rule 需正確匯入。使用者 ID 應取自伺服器確認的身分,不是由表單任意指定。提交 key 本身也需依產品協定核對,不能只要有字串就認定它代表合法的提交。 驗證與資料庫的條件要一致。驗證查全站唯一,資料庫卻只限制同一使用者,會錯擋合法資料;反過來則可能讓驗證通過,寫入卻被另一個更嚴格的條件擋下。先把實際索引欄位與規則並排核對。 兩次驗證都通過,第二次仍被擋下 在隔離的 Laravel 13 與合成 SQLite 案例中,我們先建立空資料表,令請求 A、B 帶同一組 user_id=1 和 submission_key=demo-request-A。在任何寫入之前,先後執行兩次 Validator,兩次都通過。 接著保存 A,得到一筆資料。保存 B 時,SQLite 回報複合唯一條件衝突,Laravel...
Laravel列表每頁都查很多次資料:找出關聯N+1
列表只有十幾筆資料,資料庫卻被查了幾十次。常見原因是先取得文章,再在每一列讀作者、分類或其他關聯;畫面看起來只是在印一個名稱,實際上卻可能每次都發出新的 SQL。 這類問題稱為 N+1:一次取得 N 筆主資料,接著為每筆再查一次關聯。先看相同 SQL 如何隨列表數量增加,再針對畫面真正使用的關聯預先載入。不要一看到查詢多,就把所有資料全部塞進快取。 先找出迴圈中讀取的關聯 假設列表顯示書名和作者名,Book 透過 belongsTo 對應作者。下面先取得書籍,再讀每筆的 author: $books = Book::orderBy('id')->get(); foreach ($books as $book) { echo $book->title; echo $book->author->name; } 若作者關聯尚未載入,讀取 $book->author 會觸發關聯查詢。Blade 模板、API Resource 或模型 accessor 中的讀取也可能造成同樣情況,不一定寫在控制器的那個迴圈裡。追到真正需要作者名稱的輸出位置,才能找到多出來的查詢。 還要分清關聯屬性與關聯方法。$book->author 取已載入的關聯,$book->author() 則提供可繼續建立查詢的關聯物件;如果每列都在後面呼叫查詢方法,原本的預先載入未必能省掉那一筆。先看實際程式,不只在控制器加一個 with 就結束。 把主查詢與關聯查詢分開數 在開發環境記錄這段取資料與輸出的查詢,保留 SQL 和必要的測試條件。Laravel 可用查詢事件監聽,也可以在有界限的測試區塊查看 query log。記錄範圍要一致,不能把第一次建立資料表的 SQL 算進修改前,修改後卻只算一次讀取。 重點看這類重複形態:先查一次 books,接著多次按作者 ID 查 authors。不同書籍即使共用作者,也可能各自觸發關聯查詢;不要假定同樣的 ID 曾查過,就會自動替每一個獨立模型省掉查詢。 正式站的日誌可能含私人資料和敏感參數,不要把完整 bindings 公開貼出。先在合成資料環境重現;需要正式觀察時,由維護者安排適當的記錄範圍與保留方式,不要永久開啟無界限的完整查詢記錄。 本例六筆資料,從七次變成兩次 我們用 Laravel 13 的 Eloquent 連到另建的合成 SQLite,放入六本書、三位作者。先清空這次查詢記錄,再取得書籍與顯示作者。未預先載入時,讀取 books 一次,六個模型分別查作者,共七次。 改成 Book::with('author') 後,再以相同排序取得和顯示。查 books 一次,再用作者 ID 集合查 authors 一次,共兩次;兩份輸出的六個書名和作者名稱完全相同。 未預先載入:1 次...
Laravel表單更新到別人的資料:用Policy核資源所有權
使用者只能看到自己的編輯按鈕,卻把網址裡的文章編號換掉,就能修改別人的文章。問題不在按鈕藏得不夠好,而是伺服器收到更新請求後,沒有確認這筆資料是否允許由目前使用者修改。 Laravel 的 Policy 可以把「誰能對哪一筆資料做什麼」放到同一個地方。這篇以作者只能修改本人文章為例,先建立規則,再在寫入前執行授權。登入、表單格式與資源所有權各自需要核對,不能只通過其中一項。 先找到真正要更新的那一筆資料 更新入口通常會帶文章 ID。後端要先取得對應的文章,再對這個模型做授權;不要只核對列表頁曾經顯示過哪些 ID。使用者可以改網址,也可以直接發送請求,前端頁面不是伺服器授權的依據。 假設文章的 user_id 表示作者,伺服器目前登入者的 ID 表示操作者,就比較這兩個值。作者欄位應取自資料庫中已保存的文章,不是表單傳入的隱藏欄位。否則使用者把隱藏欄位也改成自己,就可能讓錯誤檢查通過。 多租戶或團隊共用資料時,規則可能不只比作者 ID,還要核對所屬組織、角色或委派權限。先寫清楚你的產品允許誰做什麼,再決定 Policy 的條件。下面只處理最小的「本人文章」規則,不替管理員或團隊設計所有權限。 在 Policy 寫出更新規則 可用 Artisan 產生與文章模型對應的 Policy: php artisan make:policy PostPolicy --model=Post 在 update 方法接收操作者與文章,判斷作者是否一致。模型若使用整數 ID,先確認兩邊的型別設定一致;若用 UUID,就按實際欄位型別處理,不要為了讓比較通過而把所有識別碼隨意轉成整數。 public function update(User $user, Post $post): bool { return $user->id === $post->user_id; } Laravel 可以依符合慣例的模型與 Policy 位置自動尋找對應。若專案目錄或命名不符合慣例,明確註冊對應,並確認應用程式啟動時有執行。只建立 Policy 檔案卻從未呼叫,更新流程不會因此自動受到保護。 Gate::policy(Post::class, PostPolicy::class); 授權必須發生在寫入之前 控制器取得文章後,先執行 Gate::authorize('update', $post),再驗證可修改欄位並更新。拒絕時會拋出授權例外;在正常 Laravel HTTP 處理流程中,預設會轉成 403 回應。不能先保存文章,再用授權結果決定要不要顯示成功提示。 public function update(Request $request, Post $post) { Gate::authorize('update', $post); $validated = $request->validate([ 'title' => ['required',...
- 1
- 2
