WooCommerce

WooCommerce创建Shipping方法详解(2022)

如何在WooCommerce中创建类似Flat Rate那样的配送方式呢?答案是使用Shipping Method API,本文将代码简化,介绍一下在WooCommerce中创建配送选项的过程,基于WooCommerce 6.1.1。

注册并创建运费选项的过程

  1. 定义运费类,要定义运费的名称、价格、计算运费的方式等属性和方法。
  2. 向WooCommerce注册这个运费类。

用代码来描述这个过程,如下:

add_action( 'woocommerce_shipping_init', 'define_your_shipping_method' );
add_filter( 'woocommerce_shipping_methods', 'add_your_shipping_method' );

function define_your_shipping_method(){
  class Your_Shipping_Method extends WC_Shipping_Method {
    // 定义运费名称、费用、计算运费方法等等的代码
  }
}

function add_your_shipping_method( $methods ) {
  $methods['your_shipping_method'] = 'Your_Shipping_Method'; 
  return $methods;
}

如何定义一个运费类

class Your_Shipping_Method extends WC_Shipping_Method {
  /**
   * Constructor.
   *
   * @param int $instance_id Shipping method instance.
   */
  public function __construct( $instance_id = 0 ) {
    $this->id                 = 'your_shipping_method';
    $this->instance_id        = absint( $instance_id );
    $this->method_title       = '我的运费名称';
    $this->method_description = '我的运费描述';

    // 让运费支持shipping zones,而不是单独显示出来
    $this->supports           = array(
      'shipping-zones',
      'instance-settings',
      'instance-settings-modal',
    );

    $this->init();

    add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );

  }

  /**
   * Init user set variables.
   */
  public function init() {

    // 定义选项字段
    $this->instance_form_fields = array(
      'title'         => array(
        'title'       => __( 'Title', 'woocommerce' ),
        'type'        => 'text',
        'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
        'default'     => $this->method_title,
        'desc_tip'    => true,
      ),
      'cost'       => array(
        'title'             => __( 'Cost', 'woocommerce' ),
        'type'              => 'text',
        'description'       => '请输入价格',
        'default'           => '0',
        'desc_tip'          => true,
        'sanitize_callback' => array( $this, 'sanitize_cost' ),
      ),
    );

    // 获取用户的选择
    $this->title = $this->get_option( 'title' );
    $this->cost  = $this->get_option( 'cost' );
  }


  /**
   * Calculate the shipping costs.
   *
   * @param array $package Package of items from cart.
   */
  public function calculate_shipping( $package = array() ) {
    $rate = array(
      'id'      => $this->get_rate_id(),
      'label'   => $this->get_option( 'title' ),
      'cost'    => $this->get_option( 'cost' ),
      'package' => $package,
    );

    if( $rate['cost'] > 0 ){
      $this->add_rate( $rate );
    }

    do_action( 'woocommerce_' . $this->id . '_shipping_add_rate', $this, $rate );

  }

  /**
   * Sanitize the cost field.
   *
   */
  public function sanitize_cost( $value ) {
    $value = is_null( $value ) ? '' : $value;
    $value = wp_kses_post( trim( wp_unslash( $value ) ) );
    $value = str_replace( array( get_woocommerce_currency_symbol(), html_entity_decode( get_woocommerce_currency_symbol() ) ), '', $value );
   
    return $value;
  }
}

简单讲解一下这段代码:

  1. 首先在类的构造函数里定义运费的名称、描述、唯一ID等基本属性。
  2. 在init()函数里定义运费的选项,这些选项的值会被保存到wp_options表里,再获取用户填写的值。
  3. 在calculate_shipping()函数里实现运费计算的逻辑。
  4. sanitize_cost()是价格字段的过滤函数,可有可无。

完整代码

放到插件或主题的functions.php中均可。

add_action( 'woocommerce_shipping_init', 'define_your_shipping_method' );
add_filter( 'woocommerce_shipping_methods', 'add_your_shipping_method' );

function define_your_shipping_method(){
  
  class Your_Shipping_Method extends WC_Shipping_Method {
    /**
     * Constructor.
     *
     * @param int $instance_id Shipping method instance.
     */
    public function __construct( $instance_id = 0 ) {
      $this->id                 = 'your_shipping_method';
      $this->instance_id        = absint( $instance_id );
      $this->method_title       = '我的运费名称';
      $this->method_description = '我的运费描述';

      // 让运费支持shipping zones,而不是单独显示出来
      $this->supports           = array(
        'shipping-zones',
        'instance-settings',
        'instance-settings-modal',
      );

      $this->init();

      add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );

    }

    /**
     * Init user set variables.
     */
    public function init() {

      // 定义选项字段
      $this->instance_form_fields = array(
        'title'         => array(
          'title'       => __( 'Title', 'woocommerce' ),
          'type'        => 'text',
          'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
          'default'     => $this->method_title,
          'desc_tip'    => true,
        ),
        'cost'       => array(
          'title'             => __( 'Cost', 'woocommerce' ),
          'type'              => 'text',
          'description'       => '请输入价格',
          'default'           => '0',
          'desc_tip'          => true,
          'sanitize_callback' => array( $this, 'sanitize_cost' ),
        ),
      );

      // 获取用户的选择
      $this->title = $this->get_option( 'title' );
      $this->cost  = $this->get_option( 'cost' );
    }


    /**
     * Calculate the shipping costs.
     *
     * @param array $package Package of items from cart.
     */
    public function calculate_shipping( $package = array() ) {
      $rate = array(
        'id'      => $this->get_rate_id(),
        'label'   => $this->get_option( 'title' ),
        'cost'    => $this->get_option( 'cost' ),
        'package' => $package,
      );

      if( $rate['cost'] > 0 ){
        $this->add_rate( $rate );
      }

      do_action( 'woocommerce_' . $this->id . '_shipping_add_rate', $this, $rate );

    }

    /**
     * Sanitize the cost field.
     *
     */
    public function sanitize_cost( $value ) {
      $value = is_null( $value ) ? '' : $value;
      $value = wp_kses_post( trim( wp_unslash( $value ) ) );
      $value = str_replace( array( get_woocommerce_currency_symbol(), html_entity_decode( get_woocommerce_currency_symbol() ) ), '', $value );
     
      return $value;
    }
  }
}

function add_your_shipping_method( $methods ) {
  $methods['your_shipping_method'] = 'Your_Shipping_Method'; 
  return $methods;
}

效果展示

添加WooCommerce自定义运费
添加自定义运费方法
编辑WooCommerce自定义运费
编辑自定义运费选项
在结算页面显示自定义运费
运费价格大于0,就会在结算页面显示出来

计算运费时获取配送地址

calculate_shipping( $package = array() )函数里的$package包含所有信息,例如购物车里的产品数量、价格以及用户填写的配送地址等等。

如果用户填写了配送信息,$package['destination']会包含这些信息,它是一个数组,结构如下:

 Array
(
    [country] => 'GB'
    [state] => 'Lincolnshire'
    [postcode] => 'LN12 1PB'
    [city] => 'Mablethorpe'
    [address] => 'Mill Rd'
    [address_1] => 'Mill Rd'
    [address_2] => 
)

全局模式或instance模式

上面介绍的方式是是instance模式,就是可以针对不同的shipping zone创建多个实例。如果想改成全局模式,需要修改两个地方。

首先,去掉下面的代码。

// 让运费支持shipping zones,而不是单独显示出来
$this->supports = array(
    'shipping-zones',
    'instance-settings',
    'instance-settings-modal',
);

其次,初始化选项时要使用

$this->form_fields

instance的方式是使用$this->instance_form_fields

参考代码

add_rate()的使用方法

array(
	'id'             => $this->get_rate_id(), // ID for the rate. If not passed, this id:instance default will be used.
	'label'          => '', // Label for the rate.
	'cost'           => '0', // Amount or array of costs (per item shipping).
	'taxes'          => '', // Pass taxes, or leave empty to have it calculated for you, or 'false' to disable calculations.
	'calc_tax'       => 'per_order', // Calc tax per_order or per_item. Per item needs an array of costs.
	'meta_data'      => array(), // Array of misc meta data to store along with this rate - key value pairs.
	'package'        => false, // Package array this rate was generated for @since 2.6.0.
	'price_decimals' => wc_get_price_decimals(),
)

创建表单字段的方法

源代码位于woocommerce/includes/abstracts/abstract-wc-settings-api.php,下面列举了类型(例如type=’text’)和对应的参数。

text | price | decimal | password | color | textarea | checkbox

$defaults  = array(
	'title'             => '',
	'disabled'          => false,
	'class'             => '',
	'css'               => '',
	'placeholder'       => '',
	'type'              => 'text',
	'desc_tip'          => false,
	'description'       => '',
	'custom_attributes' => array(),
);

select

$defaults  = array(
	'title'             => '',
	'disabled'          => false,
	'class'             => '',
	'css'               => '',
	'placeholder'       => '',
	'type'              => 'select',
	'desc_tip'          => false,
	'description'       => '',
	'custom_attributes' => array(),
	'options'           => array(),
);

multiselect

$defaults  = array(
	'title'             => '',
	'disabled'          => false,
	'class'             => '',
	'css'               => '',
	'placeholder'       => '',
	'type'              => 'multiselect',
	'desc_tip'          => false,
	'description'       => '',
	'custom_attributes' => array(),
	'select_buttons'    => false,
	'options'           => array(),
);

title

$defaults  = array(
	'title' => '',
	'class' => '',
);

扩展表单字段的方法

如果上面的字段类型不能满足需求,需要扩展一下怎么办?可以通过修改 Your_Shipping_Method 类来实现。

比如,创建一个类型为paragraph的字段,功能就是显示一段话,首先给Your_Shipping_Method添加一个新的方法,如下:

public function generate_paragraph_html( $key, $data ){
  $field_key = $this->get_field_key( $key );
  $defaults  = array(
    'title'             => '',
    'content' => '',
  );

  $data = wp_parse_args( $data, $defaults );

  ob_start();
  ?>
  <tr valign="top">
    <th scope="row" class="titledesc">
      <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
    </th>
    <td class="forminp">
      <p><?php echo wp_kses_post( $data['content'] ); ?> </p>
    </td>
  </tr>
  <?php
  return ob_get_clean();
}

然后这样定义一下字段

$this->instance_form_fields = array(
  // Other options...
  'custom_field'       => array(
    'title'   => __( '扩展字段', 'woocommerce' ),
    'type'    => 'paragraph',
    'content' => '这里随便写点东西'
  ),
);

效果如下图所示:

按照运费价格从低到高排序

add_filter( 'woocommerce_package_rates' , 'sola_sort_shipping_methods', 10, 2 );
   
function sola_sort_shipping_methods( $rates, $package ) {

   	if( $rates && is_array($rates) ){
   		uasort( $rates, function ( $a, $b ) { 
   		    if ( $a == $b ) return 0;
   		    return ( $a->cost < $b->cost ) ? -1 : 1; 
   		} );
   	}  
    
    return $rates;
         
}

刷新shipping rates缓存

运费不会重复计算,如果没有更新购物车内容、配送地址或后台的配送选项,则显示上一次缓存的结果。要刷新缓存,可以用以下操作:

  • 更新购物车的产品或产品数量
  • 更换配送地址
  • 在后台保存一下配送设置
  • 在后台woocommerce设置里的shipping options下打开debug mode

代码刷新缓存的方法如下:

WC_Cache_Helper::get_transient_version( 'shipping', true );

或者用下面这个函数,顺便解释了原理。

function purge_shipping_cache(){
    if (!class_exists('WC_Cache_Helper') || !method_exists('WC_Cache_Helper', 'get_transient_version')) {

        global $wpdb;

        $transients = $wpdb->get_col("
            SELECT SUBSTR(option_name, LENGTH('_transient_') + 1)
            FROM `{$wpdb->options}`
            WHERE option_name LIKE '_transient_wc_ship_%'
        ");

        foreach ($transients as $transient) {
            delete_transient($transient);
        }

        return;
    }

    WC_Cache_Helper::get_transient_version('shipping', true);
}

那么shipping缓存的原理是什么呢?简单的说,运费结果保存在woocommerce session里,session里记录了一个package_hash,每次显示shipping时,当根据购物车内容和配送地址计算出来的hash与session里的保存的不一样时,就要重算运费。

$package_hash = 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );

上面是package_hash的计算方法,$package_to_hash是根据购物车内容和配送地址得出的值,后面的shipping version则是我们用代码刷新的根据。它根据时间生成,可以理解为shipping的版本号,保存在WordPress瞬态缓存里面,只要刷新了版本号,就算购物车和地址都没变,shipping也会重新计算。

这个缓存刷新方法来自https://www.tollmanz.com/invalidation-schemes/

购物车的shipping calculator在没有填地址时就显示shipping费用

这很正常,没填地址时会显示与配送地址无关的shipping选项,比如Flat Rate这种,计算的费用可能只跟重量有关。但这种逻辑可能有些问题,用户会奇怪我连地址都没写怎么可能知道运费呢,去美国和去中国一个费用?为了避免这个问题,可以修改成填写国家后再显示运费,方法如下:

function sola_hide_shipping_when_no_country(){
    $country = WC()->cart->get_customer()->get_shipping_country();
    return $country ? true : false;
}
add_filter( 'woocommerce_cart_ready_to_calc_shipping', 'sola_hide_shipping_when_no_country' );

woocommerce显示shipping calculator的函数是位于cart-totals.phpwc_cart_totals_shipping_html(),此函数调用前会有一个判断:

if ( WC()->cart->needs_shipping() && WC()->cart->show_shipping() )

代码中使用的filter就定义在WC()->cart->show_shipping()方法中。

另外,可以在woocommerce设置的shipping options选项卡下打开“Hide shipping costs until an address is entered”这一项,这时用户必须在shipping calculator里填写完整的地址(国家、城市、具体地址)后才会显示运费。

3条评论