iT邦幫忙

2026 iThome 鐵人賽

0
Vibe Coding

從一句話到一個網站:用 Vibe Coding 開發 WordPress Block Theme 的 30 天系列 第 34

WooCommerce 客製結帳頁面:用 Checkout Block 的擴充點加自訂欄位

  • 分享至 

  • xImage
  •  

這邊以一個台灣電商最常見的需求:在結帳時讓客人輸入發票統一編。使用者在結帳時可以選擇「開立三聯式發票」,勾了之後要填統編,這個值要通過驗證、存進訂單並在後台看得到,並且稍後串接電子發票時還要能讀出來。

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:

Hook 的載入時機

上面那段是官方文件的寫法,放在外掛裡沒問題,但放在佈景主題的 functions.php會沒有效果,原因是 woocommerce_blocks_loadedplugins_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.phpvalidate_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 必填。

條件欄位不用寫 JS

回到需求本身:統編欄位不應該預設出現,要等顧客勾了「開立三聯式發票」才出現,而且一出現就是必填。直覺會想這得寫 React,但 requiredhidden 這兩個參數除了吃 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',
	)
);

再把規則包成一個函式,requiredhidden 各用一次、傳相反的值:

/**
 * 產生一段對應發票 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, ... }
}

這裡最容易寫錯的是欄位放在哪一層locationorder 的欄位在 checkout.additional_fieldscontactaddress 的則在 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-idlocationaddress 的欄位會存兩份(帳單一份、運送一份,各自帶前綴)。

要讀值時不要自己拼字串,用官方的取值方法:

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 參數。

如果請 AI 開發 WooCommerce 結帳區塊自訂欄位

前面說過,直接問「怎麼在 WooCommerce 結帳頁加欄位」,AI 大機率給你 woocommerce_checkout_fields 的舊寫法,有效的做法是強調區塊結帳:

在區塊結帳頁註冊一個統一編號欄位,location 用 order,加上 sanitize 與 validate callback。

然後 review 時盯這四件事:

  1. id 有沒有 namespace(沒有斜線就是錯的)
  2. type 是不是那三種之一numberdateemail 都會安靜失敗)
  3. hook 掛對了沒(AI 幾乎都會給 woocommerce_blocks_loaded,改 init
  4. 讀值是不是用官方方法(看到硬拼 _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/


上一篇
WooCommerce 購物車與結帳區塊的架構與資料流
下一篇
WooCommerce 金流串接原理介紹
系列文
從一句話到一個網站:用 Vibe Coding 開發 WordPress Block Theme 的 30 天35
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言