WooCommerce Store API 實戰(四)- 程式碼結構

前情提要:上一篇文章測試了所有在結帳流程中會使用到的 WooCommerce Store API,我們知道了該傳哪些參數以及請求方法,同時也知道了它的限制,因此我們需要自己設計新的 API 來滿足之後開發購物車外掛的需求

Store API 沒有提供以下三種我們需要的資料:付款方式列表、運送方式的附加資料像是滿額免運的金額,以及會員登入註冊功能。從前幾篇的文章我們可以知道前兩個需求是用 GET 方法來取得,而會員登入註冊需要用 POST 方法來寫入資料。

我們先從第一個取得付款方式來說明如何建立自己的 API。

取得付款方式列表

新增 API 的步驟有三個,第一步先使用 register_rest_route() 來設定 API 的請求網址、請求方法、以及處理任務的回呼函式:

function register_woocat_api_route() {
    register_rest_route(
        'woocat/v1',
        '/gateway',
        array(
            'methods'  => 'GET',
            'callback' => function() {
                return rest_ensure_response( 'Hello World' );
            },
        )
    );
}

woocat/v1/gateway 是我的請求網址,method 用 GET 來取得資料,callback 為要取得付款列表的函式,我們這邊先回傳 Hello World 就好,rest_ensure_response() 是 WP REST API 專用的回應方法,裡面可以接受的參數是 WP_REST_ResponseWP_ErrorWP_HTTP_Response 或是純文字與陣列,而他最後會回傳 WP_REST_Response 這個 API 專用的回應物件。

第二個步驟我們把這個函式掛在勾點 rest_api_init 上面:

add_action( 'rest_api_init', 'register_woocat_api_route' );

這樣當我們用 Postman 以 GET 方法請求網址 https://wc.test/wp-json/woocat/v1/gateway 就可以看到輸出 Hello World 的回傳結果:

https://oberonlai.blog/wp-content/uploads/woocommerce-store-api-practice-3/woocommerce-store-api-practice-03.jpg

最後一個步驟我們來取得 WooCommerce 已啟用的付款方式列表,我把回呼函式改成以下內容:

function register_woocat_api_route() {
    register_rest_route(
        'woocat/v1',
        '/gateway',
        array(
            'methods'  => 'GET',
            'callback' => function() {
                $gateways = WC()->payment_gateways->payment_gateways();
                $enabled  = array();
                foreach ( $gateways as $gateway ) {
                    if ( $gateway->enabled === 'yes' ) {
                        $enabled[ $gateway->id ] = $gateway->title;
                    }
                }
                return rest_ensure_response( $enabled );
            },
        )
    );
}

使用 WC()->payment_gateways->payment_gateways() 來取得所有的付款方式,然後把有啟用的放到要回傳的陣列裡面,這個陣列由付款方式的 ID 以及標題組成的,完成後再次請求相同的 API 位置就可以得到下圖中的結果:

https://oberonlai.blog/wp-content/uploads/woocommerce-store-api-practice-3/woocommerce-store-api-practice-05.jpg

這邊需要注意的是「綠界信用卡(分期)」這個付款方式還需要加入分期期數的資料,而從資料庫裡面得知這個期數資料是放在 wp_optionwoocommerce_ry_ecpay_credit_installment_settings 裡面,並且用序列化的方式保存:

https://oberonlai.blog/wp-content/uploads/woocommerce-store-api-practice-3/woocommerce-store-api-practice-06.jpg

number_of_periods 就是分期期數資料,而裡面似乎還有其他我們可能會需要的資訊,像是付款方式名稱、描述,最小訂購金額等等,這些也許之後會派得上用場,另一方面從資料庫裡面也可以觀察到每個付款方式的設定欄位名稱都是以 woocommerce_ + gateway ID + _settings 所組成的,因此乾脆改成回傳付款方式所有的設定內容而不只有付款名稱。

再次回到 API 的回呼函式,將 $enable 陣列的內容修改如下:

function register_woocat_api_route() {
    register_rest_route(
        'woocat/v1',
        '/gateway',
        array(
            'methods'  => 'GET',
            'callback' => function() {
                $gateways = WC()->payment_gateways->payment_gateways();
                $enabled  = array();
                foreach ( $gateways as $gateway ) {
                    if ( $gateway->enabled === 'yes' ) {
                        $enabled[ $gateway->id ] = get_option( 'woocommerce_' . $gateway->id . '_settings' );
                    }
                }
                return rest_ensure_response( $enabled );
            },
        )
    );
}

再次請求 API 就能看到該付款方式的相關設定,連我們需要的分期期數都在裡面了:

https://oberonlai.blog/wp-content/uploads/woocommerce-store-api-practice-3/woocommerce-store-api-practice-07.jpg

透過以上步驟就完成了我們第一支自己設計的 API,但為了更融入 WooCommerce Store API 的體系之中,我們來學習一下該如何採用官方的邏輯來新增 API 。

StoreAPI 資料夾結構

撰文的當下採用的是 WooCommerce Blocks 8.1.0 版本來進行說明。

不管是在 WooCommerce Blocks 還是 WooCommerce 本身都可以看到 StoreAPI 的資料夾,它已經內建在 WooCommerce 的主程式之中,路徑位於 plugins/woocommerce/packages/woocommerce-blokcs/src/StoreApi,結構如下:

StoreApi
├── Authentication.php
├── Exceptions
├── Formatters
├── Formatters.php
├── Legacy.php
├── Payments
├── Routes
├── RoutesController.php
├── SchemaController.php
├── Schemas
├── StoreApi.php
├── Utilities
├── deprecated.php
└── functions.php

首先,StoreApi.php 是載入所有功能的主程式,RoutesController.php 控制所有 Store API 的請求路徑,Routes 資料夾是所有請求路徑的實作:

├── Routes
│   ├── RouteInterface.php
│   └── V1
│       ├── AbstractCartRoute.php
│       ├── AbstractRoute.php
│       ├── AbstractTermsRoute.php
│       ├── Batch.php
│       ├── Cart.php
│       ├── CartAddItem.php
│       ├── CartApplyCoupon.php
│       ├── CartCoupons.php
│       ├── CartCouponsByCode.php
│       ├── CartExtensions.php
│       ├── CartItems.php
│       ├── CartItemsByKey.php
│       ├── CartRemoveCoupon.php
│       ├── CartRemoveItem.php
│       ├── CartSelectShippingRate.php
│       ├── CartUpdateCustomer.php
│       ├── CartUpdateItem.php
│       ├── Checkout.php
│       ├── ProductAttributeTerms.php
│       ├── ProductAttributes.php
│       ├── ProductAttributesById.php
│       ├── ProductCategories.php
│       ├── ProductCategoriesById.php
│       ├── ProductCollectionData.php
│       ├── ProductReviews.php
│       ├── ProductTags.php
│       ├── Products.php
│       └── ProductsById.php
├── RoutesController.php

每個檔案定義了該路徑的請求方法與可接收參數,而 AbstractRoute.php 為請求路徑的抽象類別,其他所有的路徑都是透過它繼承而來,因此假設我們需要增加自己的路徑,看起來可能就是要繼承該類別來進行處理。

至於請求該路徑後會回傳什麼資料主要定義在 SchemaController.phpSchemas 資料夾之中:

SchemaController.php
├── Schemas
│   ├── ExtendSchema.php
│   └── V1
│       ├── AbstractAddressSchema.php
│       ├── AbstractSchema.php
│       ├── BatchSchema.php
│       ├── BillingAddressSchema.php
│       ├── CartCouponSchema.php
│       ├── CartExtensionsSchema.php
│       ├── CartFeeSchema.php
│       ├── CartItemSchema.php
│       ├── CartSchema.php
│       ├── CartShippingRateSchema.php
│       ├── CheckoutSchema.php
│       ├── ErrorSchema.php
│       ├── ImageAttachmentSchema.php
│       ├── OrderCouponSchema.php
│       ├── ProductAttributeSchema.php
│       ├── ProductCategorySchema.php
│       ├── ProductCollectionDataSchema.php
│       ├── ProductReviewSchema.php
│       ├── ProductSchema.php
│       ├── ShippingAddressSchema.php
│       └── TermSchema.php

結構描述 ( Schema ) 是一種說明資料格式的文件,我們在註冊 register_rest_route() 的時候可以帶入結構描述,它的主要用途是讓開發人員可以預期回傳資料的內容,藉此在處理前端時能將各種情境考量進去,另外一個好處是他能擴充同一個請求路徑的回傳資料,以及做資料過濾與驗證。

Store API 採用的是 JSON Schema 第四版,更多詳細解釋可以參考這篇:
https://github.com/Hsueh-Jen/blog/issues/2

至於其他的檔案跟我們的需求好像沒有直接相關,所以先跳過等有需要的時候再回來爬吧XD,我們先來看看主程式裡面做了什麼事情~

主程式 StoreApi.php

可以看到我們上面使用過的勾點 resp_api_init 註冊了三個功能,分別是 AuthenticationLegacy 以及 RoutesController

public function init() {
    add_action(
        'rest_api_init',
        function() {
            self::container()->get( Authentication::class )->init();
            self::container()->get( Legacy::class )->init();
            self::container()->get( RoutesController::class )->register_all_routes();
        }
    );
}

Authentication 負責登入會員的 Cookie 設定,通常在會員已登入的情況下會產生一組 LOGGED_IN_COOKIE,而這 Cookie 必須在登入後頁面重整後才拿得到,而隨機數產生的函式 wp_create_nonce() 會根據會員 ID 來做加密,因此為了讓 Store API 不用重整頁面就能拿到這個 Cookie 來做隨機數的驗證就必須手動將 LOGGED_IN_COOKIE 寫入:

add_action( 'set_logged_in_cookie', function( $logged_in_cookie ) {
    if ( ! defined( 'LOGGED_IN_COOKIE' ) || ! $this->is_request_to_store_api() ) {
        return;
    }
    $_COOKIE[ LOGGED_IN_COOKIE ] = $logged_in_cookie;
}

Legacy 負責相容性的部分,主要是針對在沒有支援 Checkout API 付款方式的狀況下來做相容處理,但之前實測 WooCommerce 內建的付款方法都已經可以透過 Store API 完成結帳了,在想這支類別應該是早期開發 WooCommerce Block 所使用的,我們就先忽略它吧。

另外就是 RoutesController 這個類別,它的 register_all_routes() 就是註冊所有 Store API 的方法,這邊一直讓我看不懂的地方在於 self::container->get( RouterContorller )->regiseter_all_routes() 這樣的寫法,往下看到有一個靜態方法叫做 container(),裡面宣告了 Container 類別。

回想起曾經看過的文章,發現到這應該就是傳說中的 Registry 設計模式,而 Store API 這邊是以依賴注入容器的方式來使用,目的在於雖然 Store API 已經內建在 WooCommerce 之中,但還是以 packages 的形式存放,萬一哪天要把它整合進核心檔案就可以只更換 WooCommerce 所使用的容器,藉此達成更改檔案目錄卻不用重寫類別之間的組合。

$container = new Container();
$container->register(
    RoutesController::class,
    function ( $container ) {
        return new RoutesController(
            $container->get( SchemaController::class )
        );
    }
);

容器的 register() 方法把類別連同命名空間一起存進去,存進去的值則是該類別的實例,因此之後就可以用 get() 來呼叫已經註冊類別的實例,也就是 rest_api_init 裡面的用法。而 Store API 主要由四個類別相互依賴:

  • 建立請求路徑 Routes 需要結構資料 Schema
  • 建立結構資料 Schema 需要結構資料擴充 ExtendSchema
  • 建立結構資料擴充 ExtendSchema 需要格式化 Formatters

因此在註冊 RoutesController 實例時,需要將 SchemaController 做為參數傳入,同理,在註冊 SchemaController 也需要將 ExtendSchema 傳入:

$container->register(
    RoutesController::class,
    function ( $container ) {
        return new RoutesController(
            $container->get( SchemaController::class )
        );
    }
);
$container->register(
    SchemaController::class,
    function ( $container ) {
        return new SchemaController(
            $container->get( ExtendSchema::class )
        );
    }
);
$container->register(
    ExtendSchema::class,
    function ( $container ) {
        return new ExtendSchema(
            $container->get( Formatters::class )
        );
    }
);

一個接一個就像是列車一樣,好羨慕可以寫出如此精簡的結構,能將整個架構簡化成幾個類別就能搞定這背後不知道做花了多少時間與心血,也有可能重構了 N 次才能變成現在的結果。總之我們已經掌握到要新增路徑的關鍵就是在 Routes 以及 Schemas 裡面,接下來我們就把取得付款列表的路徑用繼承的方式改寫。

在 WooCommerce Blocks 存放庫裡面有 Handbook 可以參考,也有說明該如何利用 ExtendSchema 來新增回傳資料,讓我們可以在既有的路徑底下新增付款方式列表,這樣就不用再建立新的,也許這也是一條可行的路,實作看看就知道了,我們下一篇繼續!

文章標籤Store API

目錄

發佈留言

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

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料

賴俊吾 / Oberon Lai
賴俊吾 / Oberon Lai

現為全職 WordPress 工程師,網站開發經歷 11 年,專攻前端工程與 WordPress 佈景主題、外掛客製化開發

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!

專注於分享 WordPress 開發、接案技巧、專案管理等自由工作者必備知識與心得

© 2024 想點創意科技有限公司

想點創意科技有限公司 | 統一編號 90516823
Designed by Hend Design | 隱私權政策

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!