Day20 - 铁人付外挂设定介面(二)- 全域设定

对於 WordPress 资料库结构有个大概的认识後,我们就能把後台的设定选项归纳为两种,一种是放在 wp_options 的全域设定,另外一种是放在 wp_{xxx}meta 里面的局部设定。在这个金流外挂之中我们会做到以下两种设定栏位:

  • 金流商的商店代号、金钥以及除错设定
  • 订单纪录交易回传结果

第一个种栏位是全域设定,所以我们会把他写在 wp_options 里面,第二种是局部设定,写在 wp_postmeta 里面。这篇文章我们先来处理全域设定的部分。

如果我们要做的设定选项无法关联到 post、user 或是其他资料栏位的话,放在全域设定会是比较好的选择,像是网站的标题、描述、语系、时区这些跟网站本身有关的项目,或是我们自行开发的外挂需要提供一些设定功能时,就会放在 wp_options 里面。

WordPress 的 Settings API 提供了丰富的函式可以来新增後台选单以及设定项的表单栏位,如果我们要制作设定页面完全不用写到半行 HTML 跟处理表单 POST 的问题,WordPress 全都帮我们封装好了,而 WooCommerce 的设定 API 跟 WordPress 不同,它另外写了一支抽象类别 WC_Settings_Page 来让我们继承使用。

由於我们是要开发 WooCommerce 金流外挂为主,所以本篇会用 WooCommerce 的 Settings API 来做说明。先让我们回顾一下目前铁人付外挂的资料夹结构:

iron-pay
├── composer.json
├── composer.lock
├── iron-pay.php
├── src
└── vendor
    ├── a7
    ├── autoload.php
    └── composer

将目录切换到 src 资料夹,并且新增 Options.php:

~/Sites/woocommerce/wp-content/plugins/iron-pay$ cd src
~/Sites/woocommerce/wp-content/plugins/iron-pay$ touch Options.php

关於档案的命名方式,在 WordPress Coding Standard 的建议是采用 class-xxx.php 这样的形式,但由於之後会提到测试 PHPUnit 的部分,为了要遵循 PSR-4 的自动载入机制,使用驼峰再搭配命名空间的写法会比较方便後续的作业,取名为 Options 是为了要跟资料表做呼应,代表这个档案负责处理的是全域设定的相关功能。

WooCommerce 设定页的基本逻辑如下:

/**
 * Add setting page
 *
 * @param array $settings settings.
 */
class IronPay_Setting extends WC_Settings_Page {
	//...
}
function add_setting_page( $settings ) {
	$settings[] = new IronPay_Setting();
	return $settings;
}
add_filter( 'woocommerce_get_settings_pages', 'add_setting_page' );

它是透过一个 Filter 勾点来修改既有的 $settings 阵列,而回呼函式 add_setting_page 要处理的就是增加 $settings 的值後返回它。而新增 $settings 的值是要继承抽象类别 WC_Settings_Page 并返回实例,接下来开启 Opitons.php 贴上以下程序码:

<?php

namespace Irp;

function set_options() {
	class Options extends \WC_Settings_Page {
		// 设定项实作
	}

	return new Options();
}

add_filter( 'woocommerce_get_settings_pages', 'irp\set_options' );

首先,为了可以大量减少在类别或是函式名称增加前缀来避免冲突的做法,以及方便程序码重用,我们改用命名空间的作法来进行辨识,这样如果要用在其他专案只要修改命名空间就好,不然没有前缀像 set_options 或是 Options 这种菜市场名一定每天都会喷错。因为使用了命名空间,所以在需要继承的类别以及函式要注意到路径问题。

其次,为了方便管理,我把继承类别放在函式之中,最後再透过 woocommerce_get_settings_pages 勾点去执行它,之後如果要新增相关的设定栏位,都可以在 Options 这个档案中一并处理。接下来是 Options 类别的实作细节,包含建构式在内一共有七个方法,我们逐一来看:

class Options extends \WC_Settings_Page {
	public function __construct() {
		$this->id = 'irp_setting_gateway';
		add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_tab' ), 51 );
		add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) );
		add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) );
	}

在建构式中首先设定了父类别的 ID 属性,这代表这组全域设定的代称,後面设定了三个勾点,分别是管理 WooCommerce 设定页签、输出设定栏位、储存设定栏位三个功能,第一个方法 add_settgins_tab 增加设定页签:

/**
 * Add a new settings tab to the WooCommerce settings tabs array.
 *
 * @param array $settings_tabs setting tab.
 * @return array $settings_tabs setting tab.
 */
public function add_settings_tab( $settings_tabs ) {
	$settings_tabs['irp_setting_gateway'] = '铁人付设定';
	return $settings_tabs;
}

第二个 output 方法为渲染表单栏位:

/**
 * Render fields html.
 */
public function output() {
	global $current_section;
	$settings = $this->set_fields( $current_section );
	\WC_Admin_Settings::output_fields( $settings );
}

先用 WooCommerce 的 $current_section 来取得所在设定页的 ID,然後传入稍後会实作的 set_fields 方法,最後使用 WC_Admin_Settings 的静态方法把栏位显示在设定页。

第三个 save 方法为储存表单资料:

/**
 * Save fields value.
 */
public function save() {
	global $current_section;
	$settings = $this->set_fields( $current_section );
	\WC_Admin_Settings::save_fields( $settings );
}

逻辑跟 output 相似,透过 save_fields 可以直接把栏位都写进资料库,无需再手动处理,最後最重要的是来新增设定栏位,也就是 set_fields 方法的实作:

/**
 * Get all the settings for this plugin for @see woocommerce_admin_fields() function.
 *
 * @return array Array of settings for @see woocommerce_admin_fields() function.
 */
public function set_fields( $section = null ) {
	$settings = array(
		array(
			'title' => '一般设定',
			'type'  => 'title',
			'id'    => 'irp_general_setting',
		),
		array(
			'title'   => '除错资讯',
			'type'    => 'checkbox',
			'default' => 'no',
			'desc'    => sprintf( '纪录日志於以下路径:<code>%s</code>', wc_get_log_file_path( 'iron-pay' ) ),
			'id'      => 'irp_payment_debug_log_enabled',
		),
		array(
			'type' => 'sectionend',
			'id'   => 'irp_general_setting',
		),
		array(
			'title' => '商家资料设定',
			'type'  => 'title',
			'id'    => 'irp_payment_api_settings',
		),
		array(
			'title' => '测试模式',
			'type'  => 'checkbox',
			'desc'  => '如果要使用测试模式请勾选,未勾选则为正式交易环境',
			'id'    => 'irp_payment_testmode_enabled',
		),
		array(
			'title' => '测试商家',
			'type'  => 'text',
			'desc'  => '请输入测试商家代号',
			'id'    => 'irp_payment_testmode_orgno',
		),
		array(
			'title' => '测试金钥',
			'type'  => 'text',
			'desc'  => '请输入测试金钥',
			'id'    => 'irp_payment_testmode_secret',
		),
		array(
			'title' => '正式商家',
			'type'  => 'text',
			'desc'  => '请输入正式商家代号',
			'id'    => 'irp_payment_orgno',
		),
		array(
			'title' => '正式金钥',
			'type'  => 'text',
			'desc'  => '请输入正式金钥',
			'id'    => 'irp_payment_secret',
		),
		array(
			'type' => 'sectionend',
			'id'   => 'irp_payment_api_settings',
		),
	);
	return $settings;
}

可以看到里面就是使用阵列来定义栏位的类型与名称,比较特别的是 type 为 sectionend 的阵列,它会对应到相同的 ID 来作为一个段落的区分,像是第一个 sectionend 对到的是 irp_general_setting 因此一般设定里面的除错资讯是一个区块。

栏位的参数说明如下:

array(
     'title' => '栏位显示名称',
     'description' => '栏位描述',
     'type' => 'text|password|textarea|checkbox|select|multiselect',
     'default' => '预设值',
     'class' => 'CSS 类别',
     'css' => '行内样式',
     'label' => '选项标签', // 限於 checkbox
     'options' => array(  // select/multiselects 选项
          'key' => 'value'
     )
)

透过这样的方式来设计栏位好处是非常方便,再加上样式也会符合整体风格,以及内建过滤敏感字串,这要自己刻的话会花上不少时间。但如果想要设计自己的设定页面也是完全可行的,不管是使用 WordPress Settings API 还是专门建立 custom fields 的外挂都可实现,但站在使用者角度来看,提供给 WooCommerce 的功能做在其设定页面里面会比较符合操作的逻辑性。

最後完成後的 Options.php 如下:

<?php

namespace Irp;

function set_options() {
	class Options extends \WC_Settings_Page {

		public function __construct() {
			$this->id = 'irp_setting_gateway';
			add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_tab' ), 51 );
			add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) );
			add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) );
		}

		/**
		 * Add a new settings tab to the WooCommerce settings tabs array.
		 *
		 * @param array $settings_tabs setting tab.
		 * @return array $settings_tabs setting tab.
		 */
		public function add_settings_tab( $settings_tabs ) {
			$settings_tabs['irp_setting_gateway'] = '铁人付设定';
			return $settings_tabs;
		}

		/**
		 * Get all the settings for this plugin for @see woocommerce_admin_fields() function.
		 *
		 * @return array Array of settings for @see woocommerce_admin_fields() function.
		 */
		public function set_fields( $section = null ) {
			$settings = array(
				array(
					'title' => '一般设定',
					'type'  => 'title',
					'id'    => 'irp_general_setting',
				),
				array(
					'title'   => '除错资讯',
					'type'    => 'checkbox',
					'default' => 'no',
					'desc'    => sprintf( '纪录日志於以下路径:<code>%s</code>', wc_get_log_file_path( 'iron-pay' ) ),
					'id'      => 'irp_payment_debug_log_enabled',
				),
				array(
					'type' => 'sectionend',
					'id'   => 'irp_general_setting',
				),
				array(
					'title' => '商家资料设定',
					'type'  => 'title',
					'id'    => 'irp_payment_api_settings',
				),
				array(
					'title' => '测试模式',
					'type'  => 'checkbox',
					'desc'  => '如果要使用测试模式请勾选,未勾选则为正式交易环境',
					'id'    => 'irp_payment_testmode_enabled',
				),
				array(
					'title' => '测试商家',
					'type'  => 'text',
					'desc'  => '请输入测试商家代号',
					'id'    => 'irp_payment_testmode_orgno',
				),
				array(
					'title' => '测试金钥',
					'type'  => 'text',
					'desc'  => '请输入测试金钥',
					'id'    => 'irp_payment_testmode_secret',
				),
				array(
					'title' => '正式商家',
					'type'  => 'text',
					'desc'  => '请输入正式商家代号',
					'id'    => 'irp_payment_orgno',
				),
				array(
					'title' => '正式金钥',
					'type'  => 'text',
					'desc'  => '请输入正式金钥',
					'id'    => 'irp_payment_secret',
				),
				array(
					'type' => 'sectionend',
					'id'   => 'irp_payment_api_settings',
				),
			);
			return $settings;
		}

		/**
		 * Render fields html.
		 */
		public function output() {
			global $current_section, $hide_save_button;
			$settings = $this->set_fields( $current_section );
			\WC_Admin_Settings::output_fields( $settings );
		}

		/**
		 * Save fields value.
		 */
		public function save() {
			global $current_section;
			$settings = $this->set_fields( $current_section );
			\WC_Admin_Settings::save_fields( $settings );
		}

	}

	return new Options();
}

add_filter( 'woocommerce_get_settings_pages', 'irp\set_options' );

要提供哪些设定栏位要根据串接金流商以及客户的需求而定,完成全域设定之後接下要处理的是订单以及使用者栏位,下一篇会介绍该如何新增自订栏位 metabox。

本文同步发表於:https://oberonlai.blog/tw/woocommerce-payment-options/


<<:  Dependency injection

>>:  Component 的 Component (不是递回)

Day 30:完赛了,下次一起再来参加铁人赛吧!

终於到第三十天了,从八月初开始预先撰写第一篇文章後,经过了许多困难与挑战(?)总算是撑到完赛了,还是...

【Laravel 】虚拟主机配置

一. 【文件】- host文件 【位址】- WINDOWS\system32\drivers\etc...

如何找到想要的工作2-稻穗问题

稻穗问题 夕阳西下,麦田沐浴在余晖的彩霞之中。片片的麦田在微风里泛着金浪,金浪的尾端旁有一道崎岖的小...

[Day28] 函式

在 Day21 - 物件的基础概念2 中有提到函式是物件的一个子型别,所以它本身就是一个物件,但函式...

[Day23] CH11:刘姥姥逛物件导向的世界——多型

今天要来接续昨天没介绍完的物件导向的第三个特性——多型。 多型(Polymorphism) 父类别可...