這邊以一個台灣電商最常見的需求:在結帳時讓客人輸入發票統一編。使用者在結帳時可以選擇「開立三聯式發票」,勾了之後要填統編,這個值要通過驗證、存進訂單並在後台看得到,並且稍後串接電子發票時還要能讀出來。
WooCommerce 從 8.9 起提供這個函式:woocommerce_register_additional_checkout_field,(8.6 的實驗版是 __experimental_woocommerce_blocks_register_checkout_field,已棄用):
add_action(
'woocommerce_blocks_loaded',
function () {
woocommerce_register_additional_checkout_field(
array(
'id' => 'my-plugin/invoice-tax-id',
'label' => __( '統一編號', 'my-plugin' ),
'location' => 'order',
'type' => 'text',
'required' => false,
)
);
}
);
一次註冊,前端表單、Store API schema、驗證、訂單儲存全部到位,不用自己寫 React:

上面那段是官方文件的寫法,放在外掛裡沒問題,但放在佈景主題的 functions.php會沒有效果,原因是 woocommerce_blocks_loaded 在 plugins_loaded 階段就跑完了,佈景主題的 functions.php 比它更晚載入,解法是改掛 init:
// 佈景主題載入時 woocommerce_blocks_loaded 已經跑完,所以掛 init。
add_action( 'init', 'block_theme_register_checkout_fields' );
init 在外掛裡也一樣安全,因為 woocommerce_register_additional_checkout_field() 本身有防呆,它會先看 did_action(),太早呼叫就自己重新掛一次:
$woocommerce_blocks_loaded_ran = did_action( 'woocommerce_blocks_loaded' );
if ( ! $woocommerce_blocks_loaded_ran ) {
add_action(
'woocommerce_blocks_loaded',
function () use ( $options ) {
woocommerce_register_additional_checkout_field( $options );
}
);
return;
}
直接看 WooCommerce 的驗證原始碼(src/Blocks/Domain/Services/CheckoutFields.php 的 validate_options()),把規則整理出來:
id 必須是 namespace/name 格式。
if ( count( explode( '/', $options['id'] ) ) < 2 ) {
// A checkout field id must consist of namespace/name.
}
沒有斜線會直接被拒絕,連錯誤訊息都寫死了,用自己的外掛前綴當 namespace,這是避免跟其他外掛撞名的機制。
location 只有三個合法值。
| location | 顯示位置 | 資料存到 |
|---|---|---|
contact |
聯絡資訊區(電子郵件那一區) | 訂單 + 顧客 |
address |
帳單與運送地址區(兩邊各出現一次) | 訂單 + 顧客地址 |
order |
額外資訊區(訂單備註附近) | 訂單 |
還有一個 additional,那是 8.9 之前的名稱,現在會收到棄用警告並自動轉成 order,統一編號屬於訂單資訊而不是地址的一部分,所以用 order。
type 只支援三種。
private $supported_field_types = [ 'text', 'select', 'checkbox' ];
沒有日期、沒有檔案上傳、沒有數字,需要那些就得另外想辦法(例如用 text 加驗證、或走 extensions 自己畫 UI),萬一 AI 寫了 'type' => 'number' 並不會出現任何欄位,也不會有明顯錯誤訊息。
label 必填。
回到需求本身:統編欄位不應該預設出現,要等顧客勾了「開立三聯式發票」才出現,而且一出現就是必填。直覺會想這得寫 React,但 required 與 hidden 這兩個參數除了吃 true / false,也接收一段 schema rule。
結帳頁每次資料變動都會拿目前的狀態去比對這段規則,比對成立就顯示或設為必填,全部在 WooCommerce 自己的前端跑完,不用你寫一行 JavaScript。
先註冊那個 checkbox:
woocommerce_register_additional_checkout_field(
array(
'id' => 'block-theme/invoice-triplicate',
'label' => __( '開立三聯式發票', 'block-theme' ),
// 沒有這行,結帳頁會自動在標籤後面接上「(optional)」。
'optionalLabel' => __( '開立三聯式發票', 'block-theme' ),
'location' => 'order',
'type' => 'checkbox',
)
);
再把規則包成一個函式,required 跟 hidden 各用一次、傳相反的值:
/**
* 產生一段對應發票 checkbox 狀態的 schema rule。
*
* @param bool $checked 要比對的 checkbox 狀態。
* @return array
*/
function block_theme_invoice_checkbox_rule( $checked ) {
return array(
'type' => 'object',
'properties' => array(
'checkout' => array(
'type' => 'object',
'properties' => array(
'additional_fields' => array(
'type' => 'object',
'properties' => array(
'block-theme/invoice-triplicate' => array( 'const' => $checked ),
),
),
),
),
),
);
}
woocommerce_register_additional_checkout_field(
array(
'id' => 'block-theme/invoice-tax-id',
'label' => __( '統一編號', 'block-theme' ),
'location' => 'order',
'type' => 'text',
'required' => block_theme_invoice_checkbox_rule( true ), // 勾了才必填.
'hidden' => block_theme_invoice_checkbox_rule( false ), // 沒勾就不顯示.
)
);
規則裡那串巢狀路徑對應的是 WooCommerce 內部那個 document object 的結構(src/Blocks/Domain/Services/CheckoutFieldsSchema/DocumentObject.php):
{
cart: { items, items_count, coupons, totals, needs_shipping, ... },
customer: { id, billing_address, shipping_address, additional_fields },
checkout: { additional_fields, ... }
}
這裡最容易寫錯的是欄位放在哪一層:location 是 order 的欄位在 checkout.additional_fields,contact 與 address 的則在 customer.additional_fields。路徑寫錯不會報錯,只會條件永遠不成立,然後你就會盯著一個永遠不出現的欄位發呆。
實際跑起來的效果是:沒勾的時候統編欄位連 DOM 都不存在,勾下去就出現而且標記為必填,取消勾選又整個消失。

統編要驗鄭格式有兩個層次可以做,首先是欄位自己的驗證,用註冊時的 callback:
woocommerce_register_additional_checkout_field(
array(
'id' => 'my-plugin/invoice-tax-id',
'label' => __( '統一編號', 'my-plugin' ),
'location' => 'order',
'type' => 'text',
'sanitize_callback' => function ( $value ) {
return preg_replace( '/\D/', '', (string) $value ); // 只留數字.
},
'validate_callback' => function ( $value ) {
if ( '' === $value ) {
return; // 非必填,空值放行.
}
if ( ! preg_match( '/^\d{8}$/', $value ) ) {
return new WP_Error(
'invalid_tax_id',
__( '統一編號必須是 8 位數字。', 'my-plugin' )
);
}
},
)
);
sanitize_callback 先跑、validate_callback 後跑,回傳 WP_Error 就會在結帳頁顯示錯誤並擋下訂單。注意這兩個 callback 都在伺服器端執行,前端的 React 表單只做基本的必填檢查,真正的驗證永遠在後端,在請 AI 開發任何驗證值的行為時,務必要確保有在後端做驗證。
其次是跨欄位的驗證用 hook,因為 callback 只拿得到自己的值:
add_action(
'woocommerce_validate_additional_field',
function ( $errors, $field_key, $field_value ) {
if ( 'my-plugin/invoice-type' !== $field_key ) {
return;
}
// 例如:選了三聯式,統編就不能空白.
},
10,
3
);
註冊完之後值會存進訂單的 meta,但 key 不是你給的 id,WooCommerce 會加上前綴,看原始碼裡的常數:
const BILLING_FIELDS_PREFIX = '_wc_billing/';
const SHIPPING_FIELDS_PREFIX = '_wc_shipping/';
const OTHER_FIELDS_PREFIX = '_wc_other/';
所以我們那個欄位實際存成 _wc_other/my-plugin/invoice-tax-id。location 是 address 的欄位會存兩份(帳單一份、運送一份,各自帶前綴)。
要讀值時不要自己拼字串,用官方的取值方法:
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
$checkout_fields = Package::container()->get( CheckoutFields::class );
$tax_id = $checkout_fields->get_field_from_object( 'my-plugin/invoice-tax-id', $order, 'other' );
第三個參數是 group(other / billing / shipping),對應上面那三個前綴,用這個方法的好處是前綴哪天改了你不會壞掉,事實上 _wc_additional/ 就是在 8.9 改成 _wc_other/ 的,萬一之前是用寫死的前綴就會出錯。
註冊的欄位預設會出現在後台訂單編輯頁的「額外欄位」區塊,不用額外處理:

如果要控制它出不出現在訂單確認頁(thank you page)與通知信,用註冊時的 show_in_order_confirmation 參數。
前面說過,直接問「怎麼在 WooCommerce 結帳頁加欄位」,AI 大機率給你 woocommerce_checkout_fields 的舊寫法,有效的做法是強調區塊結帳:
在區塊結帳頁註冊一個統一編號欄位,location 用 order,加上 sanitize 與 validate callback。
然後 review 時盯這四件事:
id 有沒有 namespace(沒有斜線就是錯的)type 是不是那三種之一(number、date、email 都會安靜失敗)woocommerce_blocks_loaded,改 init)_wc_other/ 字串就要改掉)還有一個測試建議:寫一個 PHPUnit 測試把 validate_callback 測起來。做法跟之前一樣,直接呼叫你的 callback 丟各種輸入進去,斷言合法值放行、非法值回傳 WP_Error。結帳欄位的驗證邏輯是那種「壞掉了不會有人發現,直到收到一堆爛資料」的東西,需要用測試來檢驗。
欄位處理完了,接下來要處理電商最核心也最不能出錯的部分,下一篇我們先把金流的運作原理講清楚:WC_Payment_Gateway 是什麼、process_payment() 要回傳什麼、以及為什麼「使用者付款完導回來的那個請求」永遠不能當作付款成功的依據。
文章目錄:https://oberonlai.blog/category/2026-ithome/
Hi, 我是 Oberon Lai,十多年前我從一個不懂程式的平面設計師,一頭栽進 WordPress 的世界。從佈景主題到外掛開發,從接案到自研產品,這段旅程讓我深刻理解:好的技術不只是寫出能跑的程式碼,而是真正解決人的問題。
我積極投入參與社群,公開演講紀錄如下:
我專精 WordPress 開發,從企業形象網站的設計與開發、佈景主題客製化,到既有網站的改版升級,提供完整的 WordPress 建置服務。開發面涵蓋外掛開發與維護、區塊編輯器(Gutenberg)客製區塊、ACF 與 Custom Post Type 的資料架構設計,以及 REST API 整合與 Multisite 多站架構建置。同時也協助網站效能改善與 SEO 調校、安全性檢測與強化,並導入自動化部署與版本控制流程,搭配長期的技術顧問與維運支援,讓網站上線後也能穩定運作。
亦提供 WooCommerce 商店的建置與設定,並串接綠界、LINE Pay、藍新等台灣主流金流。可依需求進行結帳頁面客製化、訂單狀態自動化流程設計,以及商品管理與庫存系統的客製開發;也支援 WooCommerce Subscription 定期定額、REST API 應用開發、報表與數據匯出等進階需求。此外,透過購物流程 UX 改善、電商網站效能調校與 HPOS 高效能訂單儲存相容開發,全面提升營運效率,並提供電商營運技術顧問服務。
AI 浪潮席捲而來,我選擇擁抱而非恐懼,我把 AI 融入開發工作流以及客戶的產品中,也持續累積「AI 看不見的部分」:真實踩坑經驗、最新漏洞情報那些只有第一線工程師才看得見的細節,如果你有任何 WordPress 的客製化需求或是 AI 開發相關的問題非常歡迎加入 LINE 官方帳號與我聯繫:
https://page.line.me/vrf7844t?oat_content=url&openQrModal=true
如果想要獲取 AI 開發實戰經驗也能訂閱我的電子報,每週五上午準時出刊:
https://oberonlai.blog/wordpress-newsletter/