本篇文章更新時間:2023/02/22
如有資訊過時或語誤之處,歡迎使用 Contact 功能通知。
一介資男的 LINE 社群開站囉!歡迎入群聊聊~
如果本站內容對你有幫助,歡迎使用 BFX Pay 加密貨幣 或 新台幣 贊助支持。
這需求應該算很冷門。但剛好最近研究到,能讓安裝了 WooCommerce 外掛的使用者,不只有商品可以加入購物車,還可以讓網站上的文章、頁面,甚至是自定義的內容類型(Custom Post Type, 縮寫為 CPT)也都可以進入結帳流程下單購買,覺得很有意思!
WooCommerce 的「商品」有人會是用來關聯線下實體的物品,有人會是當作數位作品等。當然還可以是「門票」、「贊助」或「課程」等這些變形。
如果網站只是預設上述其中一種來使用沒什麼大問題,但如果今天會混用這些類型的話,原先定義的「商品」,就其實不太夠用了。
切入正題
WooCommerce 3.0 以上的版本後,有強制檢查下單的物件是否為「product」這個類型,整體來說難度就是要滿足檢查的這些條件,讓加入結帳的其他非商品物件可以完成結帳到訂單這部分。
首先是繼承 WC_Product
,包裝我自定義的 MXP_CPT
類型為商品類別。
class MXP_CPT_PRODUCT extends WC_Product {
protected $post_type = 'MXP_CPT';
public function get_type() {
return 'MXP_CPT';
}
public function __construct($product = 0) {
$this->supports[] = 'ajax_add_to_cart';
parent::__construct($product);
}
// 僅覆寫 WC_Product 必要方法,有其他對這個 Type 想操作的方法可以補在這後面
}
然後補上新加入的 MXP_CPT
類型的,資料庫操作方法
class MXP_CPT_Data_Store_CPT extends WC_Product_Data_Store_CPT {
public function read(&$product) {
$product->set_defaults();
$post_object = get_post($product->get_id());
if (!$product->get_id() || !$post_object || 'MXP_CPT' !== $post_object->post_type) {
throw new Exception(__('Invalid product.', 'woocommerce'));
}
$product->set_props(
array(
'name' => $post_object->post_title,
'slug' => $post_object->post_name,
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp($post_object->post_date_gmt) : null,
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp($post_object->post_modified_gmt) : null,
'status' => $post_object->post_status,
'description' => $post_object->post_content,
'short_description' => $post_object->post_excerpt,
'parent_id' => $post_object->post_parent,
'menu_order' => $post_object->menu_order,
'reviews_allowed' => 'open' === $post_object->comment_status,
)
);
$this->read_attributes($product);
$this->read_downloads($product);
$this->read_visibility($product);
$this->read_product_data($product);
$this->read_extra_data($product);
$product->set_object_read(true);
}
// 此處也並非完整的繼承覆寫 WC_Product_Data_Store_CPT 方法,有需要再補
}
定義新內容類型的訂單物件(Order Item),不然加入訂單時會檢查到出錯
class MXP_CPT_WC_Order_Item_Product extends WC_Order_Item_Product {
public function set_product_id($value) {
if ($value > 0 && 'MXP_CPT' !== get_post_type(absint($value))) {
$this->error('order_item_product_invalid_product_id', __('Invalid product ID', 'woocommerce'));
}
$this->set_prop('product_id', absint($value));
}
}
以上定義部分都處理好後,開始進入 hack 安插的階段。
// 註冊新的商品類型的讀取方法
function mxp_cpt_woocommerce_data_stores($stores) {
// 這邊會需要 product-$type 的寫法
$stores['product-MXP_CPT'] = 'MXP_CPT_Data_Store_CPT';
return $stores;
}
add_filter('woocommerce_data_stores', 'mxp_cpt_woocommerce_data_stores', 11, 1);
// 註冊商品的類別
function mxp_add_new_woocommerce_product_class($class_name, $product_type, $product_id) {
if ($product_type == 'MXP_CPT') {
$class_name = 'MXP_CPT_PRODUCT';
}
return $class_name;
}
add_filter('woocommerce_product_class', 'mxp_add_new_woocommerce_product_class', 25, 3);
// 把 CPT 的 Type 加入判斷
function mxp_add_product_type_hack($false, $product_id) {
if ($false === false) {
global $post;
if (is_object($post) && !empty($post)) {
if ($post->post_type == 'MXP_CPT' && $post->ID == $product_id) {
return 'MXP_CPT';
} else {
$product = get_post($product_id);
if (is_object($product) && !is_wp_error($product)) {
if ($product->post_type == 'MXP_CPT') {
return 'MXP_CPT';
}
}
}
} else if (wp_doing_ajax()) {
$product_post = get_post($product_id);
if ($product_post->post_type == 'MXP_CPT') {
return 'MXP_CPT';
}
} else {
$product = get_post($product_id);
if (is_object($product) && !is_wp_error($product)) {
if ($product->post_type == 'MXP_CPT') {
return 'MXP_CPT';
}
}
}
}
return false;
}
add_filter('woocommerce_product_type_query', 'mxp_add_product_type_hack', 12, 2);
// 處理到訂單部分的時候,每一個 Line item 物件也會有判斷,並對應處理的類別
function mxp_woocommerce_checkout_create_order_line_item_object($item, $cart_item_key, $values, $order) {
$product = $values['data'];
if ($product->get_type() == 'MXP_CPT') {
return new MXP_CPT_WC_Order_Item_Product();
}
return $item;
}
add_filter('woocommerce_checkout_create_order_line_item_object', 'mxp_woocommerce_checkout_create_order_line_item_object', 20, 4);
function mxp_woocommerce_checkout_create_order_line_item($item, $cart_item_key, $values, $order) {
if ($values['data']->get_type() == 'MXP_CPT') {
$item->update_meta_data('_CPT', 'MXP_CPT'); // 這邊加入識別判斷用的關鍵字
return;
}
}
add_action('woocommerce_checkout_create_order_line_item', 'mxp_woocommerce_checkout_create_order_line_item', 20, 4);
function mxp_woocommerce_get_order_item_classname($classname, $item_type, $id) {
global $wpdb;
// 撈取欄位判斷關鍵字是否為自定義的類型
$is_MXP_CPT = $wpdb->get_var("SELECT meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = {$id} AND meta_key = '_CPT'");
// 如果是自定義類型,就回傳前面定義好的客製化訂單類別
if ('MXP_CPT' === $is_MXP_CPT) {
$classname = 'MXP_CPT_WC_Order_Item_Product';
}
return $classname;
}
add_filter('woocommerce_get_order_item_classname', 'mxp_woocommerce_get_order_item_classname', 20, 3);
到目前這階段已經可以實現主功能沒問題了。其他小細節的部分還有撈取新定義商品售價的功能:
function mxp_woocommerce_product_get_price($price, $product) {
if ($product->get_type() == 'MXP_CPT') {
$price = 99; // 此處可以實作撈取對應 CPT 的 meta,自定一個欄位表示「售價」。範例這邊就是預設都是 99 元
}
return $price;
}
add_filter('woocommerce_get_price', 'mxp_woocommerce_product_get_price', 20, 2);
add_filter('woocommerce_product_get_price', 'mxp_woocommerce_product_get_price', 10, 2);
又或是要在哪裡擺放「加入購物車」的按鈕..等等。後續這些都比較偏向主觀設計與商業邏輯,至少現在直接連結 https://www.mxp.tw/?add-to-cart=123
這樣的方式,就可以測試新定義類型加入購物車到結帳的整個環節沒問題了。
後記
這樣的技術,以限制會員觀看內容的案例為例,每篇文章都開放給人下單,有下單才可以預覽完整版本。又或是一個問答論壇,想觀看解答就要有人付費解鎖。
另外還可以延伸,如果想利用 WooCommerce 建立一個多人同時銷售商品的平台,這樣也沒問題,每個人註冊這平台就配置一個 CPT,把這 CPT 註冊 WooCommerce 後,所有人都可以建立自己的商品讓人在網站上一起下單結帳。
以上都是研究這技術時的簡單發想,可以把一套工具延伸出其他可能性,真的是很不錯,正如同 WordPress 使用它來架的站也是非常多元~
參考資料:
- woocommerce add custom post type to cart
- How to integrate Custom Post Type in WooCommerce and update pricing (作者也有分享現成的付費外掛 WooCommerce Custom Post)
- How to add a non-existent product to the WooCommerce cart with an html form? 研究過程中有一個想法是「能不能加入一個動態、程式參數化不存在資料庫的商品加入購物車」,答案是:不行!
- Data Stores WooCommerce 的商品資料結構需要透過這個 Data Stores 來負責存取資料。就算試圖在這邊動手腳,想模擬一個商品,也還是會碰到後面的判斷機制導致失敗。
- Add a new product type in Woocommerce 3 這參考資料說的是「新增一個商品類型」不是指讓一個內容類型變成商品,查資料的時候會有這樣類似的答案。