一支 WordPress 外掛的骨架,其實只需要幾行程式碼就能跑起來,這也是外掛開發最容易被誤解的地方,不必安裝任何鷹架產生器,也不必先讀完一整套框架文件,一個資料夾配一支 PHP 檔案就算數。這聽起來太簡單,反而讓不少人懷疑自己是不是漏看了什麼步驟。
真正該問的問題,不是「外掛的程式碼要寫多少」,而是「這幾行程式碼,跟你直接塞進佈景主題 functions.php 裡的那幾行,差別在哪裡」。多數人第一次要幫網站加一個小功能時,會直覺打開 functions.php 補一段程式碼進去,因為不用另外建資料夾、不用寫標頭註解,感覺上省了一道手續。這段程式碼在網站上跑得好好的,直到某一天佈景主題換版、或是主題本身更新,功能突然消失,甚至頁面直接報錯——這時才發現,問題出在一開始就選錯了地方。
先從 WordPress 允許改動網站的 3 個地方講起:核心、佈景主題、外掛。只有其中一個地方寫的東西,不會被使用者自己的操作或系統更新意外洗掉。
外掛獨立於佈景主題之外,換版型或更新都不會消失
WordPress 官方的外掛開發手冊開宗明義就定了一條規則,不要修改 WordPress 核心檔案來新增功能。原因很直接,WordPress 每次版本更新,都會用新版本整套覆蓋核心檔案,你手動改進去的東西不會被保留,還可能在下一次更新時整個消失,或跟新版核心衝突。要新增或調整網站功能,正確的做法是透過外掛實作,外掛可以簡單到只有一支 PHP 檔案,官方內建的範例外掛 Hello Dolly 就是這樣,也可以做得很複雜,重點都是同一件事,也就是在不動核心的前提下擴充功能。
不少人剛開始接觸 WordPress 客製化時,會習慣把自訂功能直接寫進佈景主題的 functions.php,小改動確實方便,牽動全站的功能卻不切實際,原因有 2 個。一是 functions.php 裡的程式碼能不能運作,取決於這個佈景主題目前是不是啟用中的,只要換了佈景主題,寫在裡面的功能就跟著消失,如果網站其他地方還在呼叫一個已經不存在的函式,還會直接觸發錯誤,讓頁面整個掛掉。二是佈景主題本身的更新,除非用的是子佈景主題,否則主題更新也會把 functions.php 裡自訂的程式碼一起覆蓋掉,客製化的功能可能一夜之間就不見。
外掛的定位剛好跟這 2 個問題相反。它獨立安裝、獨立啟用,跟佈景主題脫鉤,換佈景主題、換版型不會影響外掛的功能,外掛更新也只更新外掛自己的程式碼,不會去動佈景主題那一層。這也是為什麼真正要新增功能,而不只是調整版面樣式,開發社群普遍會走外掛開發這條路,而不是持續往 functions.php 裡塞程式碼。認清這個前提之後,實際動手的第一步,就是把外掛最基本的骨架建出來。
外掛的最小骨架,從資料夾到後台啟用
一支 WordPress 看得懂的外掛,最少只需要 3 件事到位,一個專屬資料夾、一支帶標頭註解的 PHP 檔案、還有後台按下的那個「啟用」。這 3 步走完,程式碼才會真正開始被 WordPress 執行。

資料夾與主檔案的命名慣例
外掛檔案要放的位置是固定的,路徑固定在 wp-content/plugins/ 底下,再加一個以外掛名稱命名的資料夾,資料夾裡通常放一支同名的 PHP 主檔案,例如 plugin-name 資料夾底下放 plugin-name.php。WordPress 載入外掛清單時,會去搜尋 plugins 資料夾,包括它底下的每個子資料夾,尋找帶有外掛標頭註解的 PHP 檔案,只要格式對,就會出現在後台的外掛清單裡。
命名這件事看似瑣碎,卻直接影響外掛能不能正常被辨識,也關係到會不會跟其他外掛撞名。如果整支外掛真的只有一支 PHP 檔案,可以像官方範例外掛 Hello Dolly 那樣,直接把這支檔案放在 plugins 資料夾根目錄,不另外建資料夾;但更常見、也更推薦的做法,還是幫外掛建一個專屬資料夾,一方面日後要加圖示、加子檔案都有地方放,一方面資料夾名稱本身就是最直覺的辨識度來源,取一個跟別的外掛不容易混淆的名稱,遠比事後才發現撞名要省事。
標頭註解裡必填與常用的欄位
標頭註解是一段格式固定的 PHP 區塊註解,寫在主檔案最上方,WordPress 就是靠解析這段註解,判斷這支 PHP 檔案是不是一支外掛,並把裡面的名稱、版本這些資訊,拿去後台外掛清單顯示。標頭註解唯一必填的欄位只有 Plugin Name,最低限度只要有這一行就能被 WordPress 辨識:
/*
* Plugin Name: 外掛名稱
*/Code language: PHP (php)
但實務上,外掛通常會多寫幾個常用欄位,讓資訊更完整。Description 是後台顯示的簡短說明,建議控制在 140 字元以內;Version 記版本號,例如 1.0 或 1.0.3;Requires at least 標示這支外掛最低支援的 WordPress 版本;Requires PHP 標示最低支援的 PHP 版本;License 與 License URI 記授權方式;Text Domain 則是多語系翻譯要用到的辨識字串。一段比較完整的標頭註解,大致長這樣:
/*
* Plugin Name: My Basics Plugin
* Description: Handle the basics with this plugin.
* Version: 1.0.3
* Requires at least: 6.7
* Requires PHP: 7.4
* Author: 王小明
* License: GPL v2 or later
* License URI: 授權條文的網址
* Text Domain: my-basics-plugin
*/Code language: PHP (php)
有一件事容易被忽略,如果外掛的資料夾裡有多支 PHP 檔案,標頭註解只該放在其中一支,通常就是那支跟資料夾同名的主檔案,其餘檔案不要重複放這段註解,避免 WordPress 誤判成好幾支外掛。
把外掛資料夾上傳到 wp-content/plugins 並按下啟用
外掛的檔案結構寫好、存檔之後,理論上就能在 WordPress 看到它了。本機開發環境最直接,把整個外掛資料夾放進 wp-content/plugins 底下,回到後台點左側導覽的外掛,就會看到新外掛列在已安裝的外掛清單裡。這裡有個容易忽略的細節,程式碼要等你按下啟用之後,才會真正被 WordPress 執行,存檔跟出現在清單裡只代表 WordPress 認得這支外掛,不代表它已經在運作。
正式站或是想先在本機打包測試的情況,比較常見的做法是走後台上傳。先把整個外掛資料夾壓縮成 zip 檔,進到後台外掛的新增外掛頁面,按上傳外掛,選擇剛才壓縮好的 zip,按立即安裝,安裝完成後一樣要按啟用,這支外掛才會真正開始在網站前台跑起來。這條路徑不需要 FTP 或後台檔案總管,對不方便直接動伺服器檔案的情境特別方便。
用 action 與 filter 掛上功能,寫出可以運作的短代碼
外掛要真正產生效果,靠的是掛上 WordPress 執行過程中預先定義好的時間點,這套機制叫做 hook,分成 action 與 filter 2 種,是外掛開發最核心的部分,WordPress 核心本身的預設功能,也大量靠這套機制運作。
action 負責新增動作,filter 負責修改資料後回傳
action 允許外掛在 WordPress 執行到特定時間點時,插進去做一件事,用 add_action() 掛上去,回呼函式不需要回傳任何值,通常拿來輸出內容,或是把資料寫進資料庫。換句話說,action 接收到訊號之後,做完該做的事就結束了,不用把東西交回去給誰。
filter 則是反過來,用來修改 WordPress 既有的資料再交回去,用 add_filter() 掛上去,回呼函式一定要把處理過的值 return 回去,這件事不能省略,因為 filter 預期一定要有東西被回傳,後續的程式碼才能拿去用。舉例來說,the_content 是核心常見的 filter,wp_footer 則是核心常見的 action,同樣是掛在既定的時間點上,一個要交回內容,一個不用。實際掛上 the_content 的做法,是把回呼函式接收到的原始內容$content,串接上想附加的文字,再用 return 交回去:
function myplugin_append_note( $content ) {
$content .= '<p>本段文字由外掛附加。</p>';
return $content;
}
add_filter( 'the_content', 'myplugin_append_note' );Code language: PHP (php)

把回呼函式掛進正確時機的 hook 上
把函式掛進 action 或 filter,語法上是同一套,呼叫 add_action() 或 add_filter(),第一個參數是要掛入的 hook 名稱,第二個參數是要執行的函式名稱,這 2 個是必填。第三個參數是優先權,預設值是 10,數字愈小代表愈早於同一個 hook 上其他函式之前執行,如果沒有特別需求,多半留預設值就好。第四個參數是這個函式可以接收的參數數量,預設是 1,通常也不用特別調整。
另一個常被問到的問題是,這些程式碼該掛在哪個時間點上,答案要看做的事情屬於哪一類。像註冊自訂文章類型、註冊短代碼這種初始化性質的程式碼,通常掛在 init 這個 hook 上,因為它是 WordPress 核心大致載入完成、可以安全開始註冊各種功能的時間點。官方文件裡一段常見的示範,就是把 register_post_type() 包進一支函式,再把這支函式掛到 init 上:
function pluginprefix_setup_post_type() {
register_post_type( 'book', array( 'public' => true ) );
}
add_action( 'init', 'pluginprefix_setup_post_type' );Code language: PHP (php)
先把要註冊的東西包進一支函式,再用 add_action 掛到 init 上,是外掛裡最常見的寫法,後面講到啟用鉤子時還會用到同一支函式。
用 add_shortcode() 註冊一個真的能輸出內容的短代碼
短代碼是外掛開發裡很實用的一個功能,能讓使用者在文章編輯器裡貼一組標籤,就自動被換成外掛產生的內容。註冊短代碼用 add_shortcode(),語法是帶入短代碼的標籤字串,跟要執行的回呼函式。回呼函式常見會接收 3 個參數,$atts 是使用者在標籤裡加的屬性,$content 是短代碼包住的內容,$tag 是短代碼本身的名稱。
這裡最容易寫錯的一條規則是,回呼函式一定要用 return 把要顯示的字串傳回去,不能直接 echo。WordPress 在解析文章內容時,短代碼 API 會找出已註冊的短代碼,把屬性跟內容拆解出來傳給對應的回呼函式,函式回傳的字串,才會被插入到文章內容裡,取代原本的短代碼標籤本身。如果在回呼函式裡直接 echo,內容就可能印在文章不該出現的位置,例如頁首,而不是短代碼原本所在的段落。一個真的能跑的短代碼,大致長這樣:
add_shortcode( 'myplugin_hello', 'myplugin_hello_shortcode' );
function myplugin_hello_shortcode( $atts = array(), $content = null ) {
return '<p>目前看到的內容,是外掛透過短代碼輸出的。</p>';
}Code language: PHP (php)
寫好這段程式碼、外掛也啟用之後,要在前台看到效果,還得回到文章或頁面編輯器,把[myplugin_hello]這樣的標籤貼進內容裡,WordPress 才會在顯示這篇文章時,把標籤換成回呼函式回傳的內容,光是把函式寫好、沒有在文章裡貼標籤,是不會有任何東西顯示出來的。
用啟用與停用鉤子,讓外掛自己收拾善後
外掛除了平常運作要用的功能,還有 2 個特殊時間點值得處理,一個是使用者剛按下啟用的那一刻,一個是按下停用的那一刻,WordPress 各自提供專屬的鉤子,讓外掛能在這 2 個時間點做該做的事。
啟用時該做的事,以及必須在主檔案裡呼叫的限制
啟用鉤子用 register_activation_hook() 設定,第一個參數要指向外掛的主檔案,也就是放置標頭註解的那一支,第二個參數是要執行的函式名稱。常見的用途包括寫入預設的選項值,或是在外掛裡註冊了自訂文章類型之後,重新整理 WordPress 的固定網址規則,也就是 flush_rewrite_rules(),藉此避免使用者一啟用外掛,前台就出現 404 的情況。
這裡有個容易踩到的限制,register_activation_hook() 不能在掛入 plugins_loaded、init 或任何其他 hook 的函式內部呼叫,必須直接從帶有標頭註解的主檔案呼叫。延續前面註冊自訂文章類型的例子,完整的啟用鉤子寫法是這樣:
function pluginprefix_setup_post_type() {
register_post_type( 'book', array( 'public' => true ) );
}
add_action( 'init', 'pluginprefix_setup_post_type' );
function pluginprefix_activate() {
pluginprefix_setup_post_type();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'pluginprefix_activate' );Code language: PHP (php)
啟用鉤子裡先呼叫一次 pluginprefix_setup_post_type(),是因為固定網址規則要先知道有這個文章類型存在,才能正確重新整理,順序顛倒的話,flush_rewrite_rules() 做的事就沒有意義。
停用時該清的是暫存資料,跟解除安裝不是同一件事
停用鉤子用 register_deactivation_hook() 設定,用法跟啟用鉤子相同,第一個參數指向主檔案,第二個參數是要執行的函式。停用時該處理的,是移除暫時性的資料,例如快取、暫存檔案跟資料夾,延續前面的自訂文章類型範例,停用時通常會做的事是解除註冊、再重新整理一次固定網址:
function pluginprefix_deactivate() {
unregister_post_type( 'book' );
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'pluginprefix_deactivate' );Code language: PHP (php)
容易搞混的是,停用跟解除安裝是 2 件不同的事。停用鉤子的回呼函式執行的當下,外掛其實仍處於啟用狀態,這是執行外掛完整運作邏輯的最後機會,該清的只是暫存性質的資料。真正要把外掛寫進資料庫的永久資料,例如選項表裡的設定值整個清掉,是使用者在後台按下刪除、觸發解除安裝流程之後才會做的事,這一步通常另外寫在 uninstall.php 裡處理,跟停用鉤子分開,別在停用鉤子裡就把使用者的設定資料一併刪掉,那些資料使用者可能只是暫時關閉外掛,之後還想繼續用。
外掛安全的最低限度,擋掉直接存取與清乾淨使用者輸入
外掛檔案是在 WordPress 核心已經先執行、環境已經準備好之後,才被載入執行的,這中間有一個常數叫 ABSPATH,是 WordPress 在 wp-config.php 裡定義的,代表這個 WordPress 安裝目錄的絕對路徑。正常情況下,外掛檔案被執行的時候,ABSPATH 一定已經被定義好了,如果一支 PHP 檔案被執行的當下,ABSPATH 卻沒有被定義,就代表這支檔案是被直接用瀏覽器網址存取,繞過了 WordPress 正常的載入流程。
直接用網址存取外掛檔案,有 2 個實際風險,一是這種存取方式多半會觸發 PHP 錯誤,錯誤訊息裡常常會洩漏伺服器上 WordPress 的安裝路徑,等於把內部結構攤開給任何看得到這個網址的人;二是如果程式碼本身剛好有漏洞,直接存取更可能被拿來嘗試利用這個漏洞。擋掉這件事的做法很簡單,在主檔案最上方加這幾行:
if ( ! defined( 'ABSPATH' ) ) {
exit;
}Code language: PHP (php)
外掛安全的另一半功課,是處理不受信任的資料,使用者從表單送進來的輸入、第三方網站傳來的資料,甚至自己資料庫裡存的舊資料,都不該直接拿來用,用之前都要先檢查過。原則上驗證優先於淨化,因為驗證更明確,能直接判斷資料格式對不對,但像一個文字輸入欄位,內容本來就可以是任何字,很難靠驗證篩掉,這時候淨化就是次佳的選擇。
最常見的淨化寫法是 sanitize_text_field(),用在存進資料庫之前:
$title = sanitize_text_field( $_POST['title'] );
update_post_meta( $post->ID, 'title', $title );Code language: PHP (php)
sanitize_text_field() 背後做的事包括檢查無效的 UTF-8 字元、把單一的小於符號轉成實體字元、剝除所有 HTML 標籤、移除換行跟 tab 跟多餘的空白,還有剝除八進位跳脫序列。這支函式只是眾多淨化函式裡最常見的一支,實際要用哪一支,取決於資料本身是文字、網址、還是電子郵件,這已經是另一個題目的深度,這篇只需要記住一件事,任何要存進資料庫或印到頁面上的資料,中間都該先過一手,不要原封不動直接用。

外掛長大之後,加前綴防撞名比分資料夾更優先
一支 PHP 檔案,加上前面講的標頭註解、幾個 hook、一個短代碼,就足夠讓外掛跑起來,這也是為什麼外掛的骨架能這麼精簡。但功能一多,全部擠在同一支檔案裡,很快就會變得難維護,改一個功能得在幾百行程式碼裡找對地方,這時候就該把程式碼依功能拆進不同資料夾。
常見的切法是,admin 資料夾放後台相關的程式碼,includes 資料夾放共用邏輯,public 資料夾放前台輸出,各自負責各自的部分。子檔案寫好之後,用 require_once 把它載入主檔案,搭配 plugin_dir_path( __FILE__ ) 取得外掛所在資料夾的完整路徑,寫法大致是這樣:
require_once plugin_dir_path( __FILE__ ) . 'includes/myplugin-functions.php';Code language: PHP (php)
拆檔案之前,還有一件事比資料夾結構更該優先處理,替函式、變數、類別名稱都加上外掛專屬的前綴,前面範例用的 myplugin 就是這個用意,避免跟其他外掛或 WordPress 核心裡的名稱撞在一起,一旦撞名,輕則某個功能悄悄失效,重則整個網站直接報錯。剛起步的小型外掛,不必一開始就套用完整的分層結構,功能真的變多、變複雜之後再逐步拆分,反而更務實,檔案切得太細、又用一堆 require 手動引入,常見的後遺症是新增檔案時忘記引入,程式碼寫好了卻怎麼樣都跑不動。
外掛開發最容易被誤解的一點,不是門檻高不高,而是多數人以為要先學會很多東西才能動手,實際上骨架加幾個 hook 就足夠讓第一支外掛真正運作起來,複雜的分層架構、進階的資料庫操作,都是功能長大之後才需要面對的下一關。與其等到把所有觀念學完才開始寫,不如先讓一支簡單的外掛在自己的網站上跑起來,之後遇到新的需求,再回頭補需要的那一塊知識,反而學得比較踏實。
