[WooCommerce] 支援其他非商品內容類型(Custom Post Type)下單與結帳的功能

本篇文章更新時間: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 使用它來架的站也是非常多元~

參考資料:

  1. woocommerce add custom post type to cart
  2. How to integrate Custom Post Type in WooCommerce and update pricing (作者也有分享現成的付費外掛 WooCommerce Custom Post)
  3. How to add a non-existent product to the WooCommerce cart with an html form? 研究過程中有一個想法是「能不能加入一個動態、程式參數化不存在資料庫的商品加入購物車」,答案是:不行!
  4. Data Stores WooCommerce 的商品資料結構需要透過這個 Data Stores 來負責存取資料。就算試圖在這邊動手腳,想模擬一個商品,也還是會碰到後面的判斷機制導致失敗。
  5. Add a new product type in Woocommerce 3 這參考資料說的是「新增一個商品類型」不是指讓一個內容類型變成商品,查資料的時候會有這樣類似的答案。

Share:

作者: Chun

資訊愛好人士。主張「人人都該為了偷懶而進步」。期許自己成為斜槓到變進度條 100% 的年輕人。[///////////____36%_________]

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *


文章
Filter
Mastodon