php

【プラグイン不要】WooCommerceでカスタムフィールド値を使い「タイムセール機能」を自作する

前談:WooCommerceにはセール機能はちゃんとあるから、それが使えればそれ使えば良い

まず、あらかじめ言っておくと、WooCommerce(以下、WC)のデフォルト機能で、「セール機能」は存在する。
「セール価格・期間を設定すると、その期間内だけセール価格で商品を販売する」という、一般的なセール機能だ。


今回の対応は、「セール価格」よりも安い金額で「タイムセール」をしたいという場合の対応。

ただ、今回実現したかったのは、
・一定期間(たとえば1時間だけ限定)、通常セールの金額より安い金額で販売できる機能をつける
ことだった。

今回のクライアントの運用として、
・「WCデフォルトの標準価格」に「メーカー希望小売価格」
・「WCデフォルトのセール価格」に「サイトでの販売価格」
を入力し、販売するという運用を実施していたため、デフォルトの「セール機能」はすでに使われてしまっていた。
そのため、デフォルト機能とは別に追加で、カスタムフィールド「タイムセール価格」「タイムセール開始日」「タイムセール終了日」を準備して、そこに入力された期間はカスタムフィールドの価格で販売できる、cronイベントスケジューリング機能を実装した。

カスタムフィールドを使う方法となるので、カスタムフィールドは自作なり「Advanced Custom Fields (ACF)」なりで、用意されたし。

下記で想定しているカスタムフィールドは、
「_タイムセール開始日」「_タイムセール終了日」。
入力ルールは、「20230418_10:00」とする。(「_」や「:」といった記号も必須とする)
(時間指定がなく、日付が変わった瞬間に公開・非公開を変えたい場合は、「_10:00」は不要)


目次

  1.  結論
  2. 【コード解説】作業① タイムセール開始のスケジューリング機能の記述
  3. 【コード解説】作業② タイムセール開始時間になったら発火するアクションの記述
  4. 【コード解説】作業③ タイムセール終了のスケジューリング機能の記述
  5. 【コード解説】作業④ タイムセール終了時間になったら発火するアクションの記述

結論:functions.phpに下記を記述

// セール開始するスケジュールを設定
add_action('woocommerce_new_product', 'on_product_sale_start', 11, 1);
add_action('woocommerce_update_product', 'on_product_sale_start', 11, 1);
function on_product_sale_start($product_id)
{
 // タイムセール処理発火時に本アクションを実行しないようにする
 if(!doing_action('my_save_to_sale_start')){

  // タイムセール開始日に日付が入ってたら
  if (get_post_meta($product_id, '_タイムセール開始日', true)) {
   // WCデフォルトのセール価格があったらそれを一時的にメタフィールド「_default_sale_price」として保存
   if(!empty(get_post_meta($product_id, '_sale_price'))){
    $default_sale_price = get_post_meta($product_id, '_sale_price',true);
    update_post_meta($product_id, '_default_sale_price', $default_sale_price);
   };
  
   //_タイムセール開始日 の値をとってくる
   $event_date = get_post_meta($product_id, '_タイムセール開始日', true);
   // 「_タイムセール開始日」の文字列を日にちと時間に分割
   $event_date1 = substr($event_date, 0, 8);
   $event_date2 = substr($event_date, 9);
   if (empty($event_date2)) {
    // 時間に入力なければ00:00にする
    $event_date2 = '00:00';
   };
   //「_タイムセール開始日」の文字列をDateTimeオブジェクトに変換
   $date = new DateTime($event_date1 . ' ' . $event_date2 . ':00 JST');
   //タイムスタンプに変換
   $time_stamp = date_timestamp_get($date);

   //すでにスケジュールされているものがあれば削除
   wp_clear_scheduled_hook('my_save_to_sale_start', array($product_id));

   //タイムスタンプに応じて my_save_to_sale_start を実施するスケジュールを設定
   wp_schedule_single_event($time_stamp, 'my_save_to_sale_start', array($product_id));
  } else {
   // 値をブランクにしたらスケジュールを削除
   update_post_meta($product_id, '_タイムセール金額', '');
   wp_clear_scheduled_hook('my_save_to_sale_start', array($product_id));
  }
 }
};

//my_save_to_sale_start のフックに my_update_to_sale_start 関数を設定
add_action('my_save_to_sale_start', 'my_update_to_sale_start');
function my_update_to_sale_start($product_id)
{
 $product = wc_get_product($product_id);
 $sale_price = array();
 $sale_price[0] = get_post_meta($product_id, '_タイムセール金額', true);

 if ( !is_wp_error( $product ) ) {
  if( !empty($sale_price) ){
   $product->set_sale_price( $sale_price[0] );
  }
  $product->save();
 }
 // タイムセール開始日とタイムセール金額のフィールドを空にする
 update_post_meta($product_id, '_タイムセール開始日', '');
 update_post_meta($product_id, '_タイムセール金額', '');
};




// セール終了するスケジュールを設定
add_action('woocommerce_new_product', 'on_product_sale_end', 11, 1);
add_action('woocommerce_update_product', 'on_product_sale_end', 11, 1);
function on_product_sale_end($product_id)
{
 // タイムセール開始日に日付が入ってたら
 if (get_post_meta($product_id, '_タイムセール終了日', true)) {
  //_タイムセール終了日 の値をとってくる
  $event_date = get_post_meta($product_id, '_タイムセール終了日', true);
  // 文字列を日にちと時間に分割
  $event_date1 = substr($event_date, 0, 8);
  $event_date2 = substr($event_date, 9);
  if (empty($event_date2)) {
   // 時間に入力なければ00:00にする
   $event_date2 = '00:00';
  };
  //DateTimeオブジェクトに変換
  $date = new DateTime($event_date1 . ' ' . $event_date2 . ':00 JST');
  //タイムスタンプに変換
  $time_stamp = date_timestamp_get($date);
  //すでにスケジュールされているものがあれば削除
  wp_clear_scheduled_hook('my_save_to_sale_end', array($product_id));
  //タイムスタンプに応じて my_save_to_sale_end を実施するスケジュールを設定
  wp_schedule_single_event($time_stamp, 'my_save_to_sale_end', array($product_id));
 } else {
  // 値をブランクにしたらスケジュールを削除
  wp_clear_scheduled_hook('my_save_to_sale_end', array($product_id));
 }
};


//my_save_to_sale_end フックに my_update_to_sale_end 関数を設定
add_action('my_save_to_sale_end', 'my_update_to_sale_end');
function my_update_to_sale_end($product_id)
{
 //もし元々のセール金額が設定されていたら、セール金額を元の金額に戻す
 if(!empty(get_post_meta($product_id, '_default_sale_price',true))){
  $product = wc_get_product($product_id);
  $sale_price = array();
  $sale_price[0] = get_post_meta($product_id, '_default_sale_price', true);

  if ( !is_wp_error( $product ) ) {
   if( !empty($sale_price) ){
    $product->set_sale_price( $sale_price[0] );
   }
   $product->save();
  }
 }else{
  // 元々セール金額が空だった場合、セール金額をブランクにする
  $product = wc_get_product($product_id);
  $sale_price = array();
  $sale_price[0] = '';

  if ( !is_wp_error( $product ) ) {
   $product->set_sale_price( $sale_price[0] );
   $product->save();
  }
 };
 // タイムセール終了日のフィールドを空にする
 update_post_meta($product_id, '_タイムセール終了日', '');
};

ざっくり流れを言うと、

  1. 【タイムセール開始のスケジューリング機能の記述】商品登録or更新された時のアクション「on_product_sale_start」を定義。ここで、カスタムフィールド値を使って、タイムスタンプで実行される「my_save_to_sale_start」をスケジュール
  2. 【時間になったら発火するアクションの記述】「my_save_to_sale_start」で実行するアクション「my_update_to_sale_start」を定義。スケジュールされた時間に、セール価格が変わるアクションが発火
  3. 【タイムセール終了のスケジューリング機能の記述】(1.の「終了」版なので、1.と内容はほぼ一緒)
  4. 【時間になったら発火するアクションの記述】(2.の「終了」版なので、1.と内容はほぼ一緒)

としている。
個別にポイント解説。以下は開発者orコードオタク向け解説。


作業① タイムセール開始のスケジューリング機能の記述

// セール開始するスケジュールを設定
add_action('woocommerce_new_product', 'on_product_sale_start', 11, 1);
add_action('woocommerce_update_product', 'on_product_sale_start', 11, 1);
function on_product_sale_start($product_id)
{
 // タイムセール処理発火時に本アクションを実行しないようにする
 if(!doing_action('my_save_to_sale_start')){

  // タイムセール開始日に日付が入ってたら
  if (get_post_meta($product_id, '_タイムセール開始日', true)) {
   // WCデフォルトのセール価格があったらそれを一時的にメタフィールド「_default_sale_price」として保存
   if(!empty(get_post_meta($product_id, '_sale_price'))){
    $default_sale_price = get_post_meta($product_id, '_sale_price',true);
    update_post_meta($product_id, '_default_sale_price', $default_sale_price);
   };
  
   //_タイムセール開始日 の値をとってくる
   $event_date = get_post_meta($product_id, '_タイムセール開始日', true);
   // 「_タイムセール開始日」の文字列を日にちと時間に分割
   $event_date1 = substr($event_date, 0, 8);
   $event_date2 = substr($event_date, 9);
   if (empty($event_date2)) {
    // 時間に入力なければ00:00にする
    $event_date2 = '00:00';
   };
   //「_タイムセール開始日」の文字列をDateTimeオブジェクトに変換
   $date = new DateTime($event_date1 . ' ' . $event_date2 . ':00 JST');
   //タイムスタンプに変換
   $time_stamp = date_timestamp_get($date);

   //すでにスケジュールされているものがあれば削除
   wp_clear_scheduled_hook('my_save_to_sale_start', array($product_id));

   //タイムスタンプに応じて my_save_to_sale_start を実施するスケジュールを設定
   wp_schedule_single_event($time_stamp, 'my_save_to_sale_start', array($product_id));
  } else {
   // 値をブランクにしたらスケジュールを削除
   update_post_meta($product_id, '_タイムセール金額', '');
   wp_clear_scheduled_hook('my_save_to_sale_start', array($product_id));
  }
 }
};

まず、add_action関数を使って、'woocommerce_new_product''woocommerce_update_product'フックに、'on_product_sale_start'関数を割り当てている。これにより、製品が新規作成されたり、更新されたりするたびに、'on_product_sale_start'関数が実行される。

'on_product_sale_start'関数は、引数として製品ID($product_id)を受け取る。関数の最初のif文では、現在実行中のアクションが'my_save_to_sale_start'でない場合にのみ、処理が行われる。(ここの'my_save_to_sale_start' は、次の工程で定義する、「時間になったら発火するアクション」のこと。この記述をしないとタイムセール中の価格がうまく反映されない)

次に、製品のタイムセール開始日(_タイムセール開始日)が設定されている場合、以下の処理が実行される。

      1. WooCommerceデフォルトのセール価格('_sale_price')が設定されている場合、その値を一時的にデフォルトセール金額('_default_sale_price')として保存する。
      2. タイムセール開始日(_タイムセール開始日)の値を取得し、日付と時間に分割する。時間が設定されていない場合、00:00とする。
      3. 日付と時間を組み合わせてDateTimeオブジェクトを作成し、タイムスタンプに変換する。
      4. すでにスケジュールされているタイムセール開始イベント('my_save_to_sale_start')があれば削除し、新たにタイムスタンプに応じてイベントをスケジュールする。

タイムセール開始日が設定されていない場合、タイムセール金額(_タイムセール金額)を空にし、スケジュールされているタイムセール開始イベントを削除する。


作業② タイムセール開始時間になったら発火するアクションの記述

//my_save_to_sale_start フックに my_update_to_sale_start 関数を設定
add_action('my_save_to_sale_start', 'my_update_to_sale_start');
function my_update_to_sale_start($product_id)
{
 $product = wc_get_product($product_id);
 $sale_price = array();
 $sale_price[0] = get_post_meta($product_id, '_タイムセール金額', true);

 if ( !is_wp_error( $product ) ) {
  if( !empty($sale_price) ){
   $product->set_sale_price( $sale_price[0] );
  }
  $product->save();
 }
 // タイムセール開始日とタイムセール金額のフィールドを空にする
 update_post_meta($product_id, '_タイムセール開始日', '');
 update_post_meta($product_id, '_タイムセール金額', '');
};

まず、add_action関数を使って、'my_save_to_sale_start'フックに、'my_update_to_sale_start'関数を割り当てている。これにより、タイムセールが開始されるタイミングで'my_update_to_sale_start'関数が実行される。

'my_update_to_sale_start'関数は、引数として製品ID($product_id)を受け取る。関数ではまず、wc_get_product関数を使って、製品IDから製品オブジェクト($product_id)を取得する。次に、タイムセール金額('_タイムセール開始日')を取得して、$sale_price配列に格納する。

製品オブジェクトが正常である場合、以下の処理が実行される。

      1. タイムセール金額が空でない場合、製品のセール価格をタイムセール金額に設定する。
      2. 製品オブジェクトの変更を保存する。

最後に、タイムセール開始日('_タイムセール開始日')とタイムセール金額('_タイムセール金額')のフィールドを空にする。

このコードにより、タイムセールが開始されたタイミングで、製品のセール価格がタイムセール金額に更新される。そして、タイムセール開始日とタイムセール金額のフィールドは空にされる。


作業③ タイムセール終了のスケジューリング機能の記述

// セール終了するスケジュールを設定
add_action('woocommerce_new_product', 'on_product_sale_end', 11, 1);
add_action('woocommerce_update_product', 'on_product_sale_end', 11, 1);
function on_product_sale_end($product_id)
{
 // タイムセール開始日に日付が入ってたら
 if (get_post_meta($product_id, '_タイムセール終了日', true)) {
  //_タイムセール終了日 の値をとってくる
  $event_date = get_post_meta($product_id, '_タイムセール終了日', true);
  // 文字列を日にちと時間に分割
  $event_date1 = substr($event_date, 0, 8);
  $event_date2 = substr($event_date, 9);
  if (empty($event_date2)) {
   // 時間に入力なければ00:00にする
   $event_date2 = '00:00';
  };
  //DateTimeオブジェクトに変換
  $date = new DateTime($event_date1 . ' ' . $event_date2 . ':00 JST');
  //タイムスタンプに変換
  $time_stamp = date_timestamp_get($date);
  //すでにスケジュールされているものがあれば削除
  wp_clear_scheduled_hook('my_save_to_sale_end', array($product_id));
  //タイムスタンプに応じて my_save_to_sale_end を実施するスケジュールを設定
  wp_schedule_single_event($time_stamp, 'my_save_to_sale_end', array($product_id));
 } else {
  // 値をブランクにしたらスケジュールを削除
  wp_clear_scheduled_hook('my_save_to_sale_end', array($product_id));
 }
};

まず、add_action関数を使って、'woocommerce_new_product'および'woocommerce_update_product'フックに、'on_product_sale_end'関数を割り当てている。これにより、新しい製品が作成されたり、既存の製品が更新されたりしたときに、'on_product_sale_end'関数が実行される。

'on_product_sale_end'関数は、引数として製品ID($product_id)を受け取る。関数ではまず、タイムセール終了日('_タイムセール終了日')が設定されているかどうかを確認する。

タイムセール終了日が設定されている場合、以下の処理が実行される。

      1. タイムセール終了日の値を取得し、日付と時間に分割する。
      2. 時間が設定されていない場合は、00:00に設定する。
      3. タイムセール終了日をDateTimeオブジェクトに変換し、タイムスタンプに変換する。
      4. すでに設定されているスケジュールがあれば、それを削除する。
      5. タイムスタンプに応じて、'my_save_to_sale_end'アクションを実行するスケジュールを設定する。

タイムセール終了日が設定されていない場合、既存のスケジュールを削除する。

このコードにより、タイムセール終了日が設定されている場合、タイムセールが終了するタイミングで’my_save_to_sale_end’アクションが実行されるようになる。


作業④ タイムセール終了時間になったら発火するアクションの記述

//my_save_to_sale_end フックに my_update_to_sale_end 関数を設定
add_action('my_save_to_sale_end', 'my_update_to_sale_end');
function my_update_to_sale_end($product_id)
{
 //もし元々のセール金額が設定されていたら、セール金額を元の金額に戻す
 if(!empty(get_post_meta($product_id, '_default_sale_price',true))){
  $product = wc_get_product($product_id);
  $sale_price = array();
  $sale_price[0] = get_post_meta($product_id, '_default_sale_price', true);

  if ( !is_wp_error( $product ) ) {
   if( !empty($sale_price) ){
    $product->set_sale_price( $sale_price[0] );
   }
   $product->save();
  }
 }else{
  // 元々セール金額が空だった場合、セール金額をブランクにする
  $product = wc_get_product($product_id);
  $sale_price = array();
  $sale_price[0] = '';

  if ( !is_wp_error( $product ) ) {
   $product->set_sale_price( $sale_price[0] );
   $product->save();
  }
 };
 // タイムセール終了日のフィールドを空にする
 update_post_meta($product_id, '_タイムセール終了日', '');
};

まず、add_action関数を使って、'my_save_to_sale_end'フックに'my_update_to_sale_end'関数を割り当てている。これにより、タイムセールが終了するタイミングで'my_update_to_sale_end'関数が実行される。

'my_update_to_sale_end'関数は、引数として製品ID($product_id)を受け取る。関数ではまず、元々のセール金額('_default_sale_price')が設定されているかを確認する。もし設定されていた場合、その金額を再度セール金額に設定して製品情報を保存する。これにより、タイムセールが終了した後も、元々のセール金額が適用される。

もし、元々のセール金額が空だった場合、セール金額をブランクに設定して製品情報を保存する。これにより、タイムセールが終了した後、製品は通常価格で販売されるようになる。

最後に、タイムセール終了日('_タイムセール終了日')のフィールドを空にする。これにより、タイムセールが終了したことを示す。


大変だったこと

今回少し手間取ったのが、WooCommerceのアクションフックの利用だ。
WooCommerceのアクションフックの文献が英語ばっかりで調べるの大変。しかも書いてる内容がすげえざっくりしてることも多い。日本人でWooCommerceがっつり弄っている人少ない!だから日本語文献少ない!いやんなっちゃった。

が、WooCommerceは機能が膨大な分、WPのアクションフックだけの利用では正常に動かないことが多い。
WordPressサイト制作者がWooCommerceでつまづくポイントはここだと思う。


上記機能利用する際の注意

カスタムフィールド「_タイムセール開始日」「_タイムセール終了日」に過去日付を入れてはならない。入れたら更新した途端に処理が走ってしまうので、公開する予定じゃなかった時間に公開にされてしまう可能性が出るので、気をつけたし。


プラグイン入れればすぐできるかもだけど、WooCommerce関連プラグインは有料が多い

今回作った機能、実際はプラグインとして存在はしているのだ。しかし、まともに使えるのは有料プラグインばっかり。そして制約多し。
できるだけ、自分の知識をフル活用して自作するのが、良いかもしれない。それで良いものができたら、日本人向けのプラグイン化して、汎用性高く、かつ日本ユーザーに優しいものが作れたらベストだ。

TOP