tips

welcartの商品を在庫や価格で絞り込み&並べ替えする。

welcart sort product

アーカイブページのループに在庫や価格でのソート・絞り込みを反映させる。

在庫がないのに商品が表示されているのは楽●の空売りみたいで鬱陶しい。商品数が多ければなおのこと。ところが、通常のqueryにwelcartの在庫を 反映させることはできません。一旦ループから離脱して再ループさせれば擬似的に実現できるが、ページ送りやレイアウト崩れの可能性を考えるとテンプレート ごとに細かな対応が必要で面倒臭い。

で、困った時のウェルカスタムさんで探したところ「queryに在庫情報を組み込む現実的な解決法」が掲載されていた。ここによく纏められているように、在庫状態ではなく在庫数で判別しているので縛りがあること、また、複数SKUを持つ商品の場合の処理など、ウェルカートのSKUがシリアライズされているので、価格でのソートなども含めてクエリでの絞り込みの実現は難しそうだ。

ということで、現状はカスタムフィールドにデータを持たせるのが唯一の解決策と思われたので、最終的に以下のようにカスタマイズすることにしました。

各商品記事のカスタムフィールドに必要な情報を書き込む処理

ソートすることを目的にするなら、独立したカスタムフィールドにクエリから参照しやすように数値で登録されていることがベストでしょう。

まずは、以下のようにsave_postにフックしてにソート用のカスタムフィールドを書き込むことにしました。ついでに usces_action_reg_orderdataという受注確定後に処理されるフックがあったので、それにも引っ掛けることにする。 usces_have_skus()はループ内で複数回使うとだめだったりと、扱いが難しいので、get_skusでpostIDからskuのデータ をすべて取得、直接在庫の有無を呼び出す方法を採用、skuを複数展開されていても大丈夫なように、ステータスを一旦配列に入れてから最小の値だけ取り出 すことにします。これで、在庫がなくなったら反映されるという基本要件を満たしてくれます。ちなみに最後のcurrent_screenへのフックはというと、商品マスターの「最新の情報に更新」ボタンを押すとすべての商品データのカスタムフィールドを設定してくれる、という我ながらなんという親切設計!!

/*
* WELCART PRODUCT SORT
* Plugin URI: //web.contempo.jp/weblog/tips/p5438
* Author: Mizuho Ogino 
* Version: 1.0
* License: //www.gnu.org/licenses/gpl.html GPL v2 or later
*/

add_action( 'save_post', 'usces_update_postmeta_for_sorting', 10 );
function usces_update_postmeta_for_sorting( $post_id ){ // 各カスタムフィールドの更新
    if ( !get_post_meta( $post_id, '_itemCode', true ) ) return;
    global $usces;
    $_itemStock = $_itemPrice = array();
    $skus = $usces->get_skus( $post_id );
    if ( $skus ): foreach ( $skus as $sku ): 
        $_itemStock[] = (int)$sku['stock'];
        if ((int)$sku['stock'] == 4 ) continue; // 廃盤を回避
        $_itemPrice[] = (int)$sku['price']; 
    endforeach; endif;
    if ( $_itemStock ) {
        update_post_meta( $post_id, '_itemStock', min( $_itemStock ) ); // 0:在庫あり 1:在庫僅少 2:売り切れ 3:入荷待ち 4:廃盤 の中で一番低いステータス
    }
    if ( $_itemPrice ) {
        update_post_meta( $post_id, '_itemPriceMin', min( $_itemPrice ) ); // SKU中の一番安い設定価格
        update_post_meta( $post_id, '_itemPriceMax', max( $_itemPrice ) ); // SKU中の一番高い設定価格
    }

    global $wpdb;
    $sold = 0;
    $datestr = substr(get_date_from_gmt(gmdate('Y-m-d H:i:s', time())), 0, 10);
    $yearstr = substr($datestr, 0, 4);
    $monthstr = substr($datestr, 5, 2);
    $daystr = substr($datestr, 8, 2);
    $order_table_name = $wpdb->prefix . "usces_order";
    $order_date = date('Y-m-d H:i:s', mktime(0, 0, 0, (int)$monthstr, ((int)$daystr-30), (int)$yearstr)); // 30日分のDBを検索
    $query = "SELECT order_cart FROM {$order_table_name} WHERE order_date >= '{$order_date}'";
    $dbres = $wpdb->get_col($query);
    if( $dbres ): foreach( (array)$dbres as $carts ):
        $rows = unserialize($carts);
        foreach( (array)$rows as $carts ){
            if ( $post_id == $carts['post_id'] ) $sold = $sold + $carts['quantity'];
        }
    endforeach; endif;
    update_post_meta( $post_id, '_itemPopular', $sold ); // 売上数を登録

    return $post_id;
}

add_action( 'usces_action_reg_orderdata', 'usces_action_reg_orderdata_update_postmeta' );
function usces_action_reg_orderdata_update_postmeta( $args ){ // 在庫に変動があればカスタムフィールドを更新
    extract( $args );
    foreach( $cart as $cartrow ){
        $post_id = $cartrow['post_id'];
        usces_update_postmeta_for_sorting( $post_id );
    }
}

add_action( 'current_screen', 'usces_add_actions_to_admin_current_screen' );
function usces_add_actions_to_admin_current_screen( $current_screen ){ // 「最新の情報に更新」ボタンですべてのカスタムフィールドを更新
    if( isset($_GET['page']) && 'usces_itemedit' == $_GET['page'] && isset($_REQUEST['refresh']) ){
        $get_posts = get_posts( array( 'meta_key' => '_itemCode', 'post_type' => 'post', 'numberposts' => -1 ) );
        if( $get_posts ): foreach ( $get_posts as $val ) : 
            usces_update_postmeta_for_sorting( $val->ID );
        endforeach; endif; 
    }
}

welcartにはget_bestseller_idsという関数があったので、当初は「人気順」もダイレクトにベストセラーの順位を登録するように設計してみました。しかしショップサイトの問題点として、「価格」や「在庫」が絶対的な情報であるのに対して、商品の「人気順」が相対的な数値ということがあります。つまり1点売り上げがあるとすべての順位に変動が生じてしまうので、一々すべての商品のカスタムフィールドを更新しなくてはならなくなり、延いてはユーザーサイドの表示速度に影響が出る可能性もあります。wp_cronなどを用いて定期更新にする手も考えましたが、それでも二段階更新になることには変わりないので、人気順の代わりにbestseller_idsも参照している1ヶ月の「売上数」をカウントしてカスタムフィールドに登録することにしました。これならば、売上があった際に該当商品を変更するだけで済みます。(とはいえ、在庫数の変動や記事の更新時にカウントされているので、しばらく売上がない商品は更新されないので完全とは言えない。厳密さを求める向きは手動反映かcronの設置をどうぞ。)

アーカイブページ内での商品一覧のソート処理

ループ全般にフックしてくれるpre_get_postという便利な関数を使用します。$_GET[‘instock’]で在庫の有無を、$_GET[‘sort’]で並び順を変更するという仕様。デフォルトで在庫がないものは通常ループ内で非表示に設定してあります。

つまり、セレクトボックスなどで、カテゴリページのリンクに以下の様に絞り込み用のクエリを設定してやれば、ループが自動的に読み込まれるわけです。例としては以下のようになります。

価格の安い順:
//www.example.com/category/item?sort=price_desc

価格の高い順:
//www.example.com/category/item?sort=price_asc

価格高い順、かつ在庫のない商品もすべて表示:
//www.example.com/category/item?sort=price_asc&instock=false

 

add_action( 'pre_get_posts', 'usces_custom_sort_item', 10, 1 );
function usces_custom_sort_item( $query ) {
    if ( is_admin() || !$query->is_main_query() ) return;
    if ( $query->is_search ) {
        set_query_var('post_type', 'post');
    }
    if ( $query->is_category() || $query->is_tag() ) {
        $args = array();
        $sort = isset($_GET['sort']) && is_string($_GET['sort']) ? $_GET['sort'] : '';
        if ( $sort ){
            if ( $sort == 'price_asc' ) { //価格の安い順 'price_asc'の値は任意に変更可
                $query->set( 'meta_key', '_itemPriceMin' );
                $query->set( 'orderby', array( 'meta_value_num' => 'ASC', 'date' => 'DESC' ) );
            } elseif ( $sort == 'price_desc' ) { //価格の高い順 'price_desc'の値は任意に変更可
                $query->set( 'meta_key', '_itemPriceMax' );
                $query->set( 'orderby', array( 'meta_value_num' => 'DESC', 'date' => 'DESC' ) );
            } elseif ( $sort == 'popular' ) { //人気順(一定期間の売上数の多い順) 'popular'の値は任意に変更可
                $query->set( 'meta_key', '_itemPopular' );
                $query->set( 'orderby', array( 'meta_value_num' => 'DESC', 'date' => 'DESC' ) );
            }
        }
        $instock = isset($_GET['instock']) && is_string($_GET['instock']) ? $_GET['instock'] : '';
        if ( $instock !== 'false' ) { //在庫の有無で抽出 'false'の値は任意に変更可
            $query->set( 
                'meta_query' , array(
                    'relation' => 'OR',
                    array(
                        'key' => '_itemStock',
                        'value' => array( 0, 1 ),
                        'compare' => 'IN',
                    ),
                    array(
                        'key' => '_itemStock',
                        'compare' => 'NOT EXISTS',
                    ),
                    $args
                )
            );
        } elseif ($args ) {
            $query->set( $args );
        }
    }
}

add_filter( 'paginate_links', 'usces_append_query_string', 10 );
add_filter( 'term_link', 'usces_append_query_string', 10 );
function usces_append_query_string( $url ) {// get_term_link // paginate_links のクエリ書き換え
    if ( isset( $_GET['instock'] ) ) $url = add_query_arg( 'instock', $_GET['instock'], $url );
    if ( isset( $_GET['sort'] ) ) $url = add_query_arg( 'sort', $_GET['sort'], $url );
    return $url;
}

言わずもがなですが、在庫がない場合 if ( $instock !== ‘false’ ) のところを if ( $instock === ‘true’ ) とかにしてやれば、trueが設定されている時に「在庫あり」のみを表示(在庫なしは表示しない)というように反転できます。

paginate_linksはその名の通りページネーションやカテゴリのリンク出力にフックしてくれるフィルターで、ページを送ったらソートが解除されると困るので設定してあります。term_linkはカテゴリを選択していってもクエリを継続させます。絞り込み系のサイト構成に向いています。入らなければコメントアウトしましょう。

 

リンクの実際の出力方法など

並べ替えボタンの出力例も書いておきます。

<?php
    $current_url = (empty($_SERVER["HTTPS"]) ? "http://" : "https://") . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
    $sort = isset($_GET['sort']) && is_string($_GET['sort']) ? $_GET['sort'] : '';
    foreach ( array( 'date'=>'新着順','price_asc'=>'安い順','price_desc'=>'高い順','popular'=>'人気順' ) as $key => $val ):
        $sort_option[ $key ] = "\t\t\t" .'<a class="sort-order'. ( $sort == $key ? ' selected': '' ).'" value="'.$key.'" href="'.( $key == 'date' ? remove_query_arg( 'sort', $current_url ) : add_query_arg( 'sort', $key, $current_url ) ).'" data-target="'.$key.'">' .$val. '</a>'. "\n";
    endforeach;
    $instock = isset($_GET['instock']) && is_string($_GET['instock']) ? $_GET['instock'] : '';
    foreach ( array( 'true'=>'在庫あり','false'=>'すべて表示' ) as $key => $val ):
        $sort_option[ $key ] = "\t\t\t" .'<a class="sort-order'. ( $sort == $key ? ' selected': '' ).'" value="'.$key.'" href="'.( $key == 'date' ? remove_query_arg( 'sort', $current_url ) : add_query_arg( 'sort', $key, $current_url ) ).'" data-target="'.$key.'">' .$val. '</a>'. "\n";
    endforeach;
?>

<select class="sort-group">
    <option value="date" data-url="<?php echo remove_query_arg( 'sort', $current_url ); ?>"<?php echo ( !$sort ? ' selected': '' ); ?>>新着順</option>
    <option value="price_asc" data-url="<?php echo add_query_arg( 'sort', 'price_asc', $current_url ); ?>"<?php echo ( $sort == 'price_asc' ? ' selected': '' ); ?>>価格の安い順</option>
    <option value="price_desc" data-url="<?php echo add_query_arg( 'sort', 'price_desc', $current_url ); ?>"<?php echo ( $sort == 'price_desc' ? ' selected': '' ); ?>>価格の高い順</option>
    <option value="popular" data-url="<?php echo add_query_arg( 'sort', 'popular', $current_url ); ?>"<?php echo ( $sort == 'popular' ? ' selected': '' ); ?>>人気順</option>
</select>
<select class="instock-group">
    <option value="true" data-url="<?php echo remove_query_arg( 'instock', $current_url ); ?>">在庫あり</option>
    <option value="false" data-url="<?php echo add_query_arg( 'instock', 'true', $current_url ); ?>">すべて表示</option>
</select>
<script type="text/javascript" charset="utf-8">
    jQuery( 'select.sort-group, select.instock-group' ).change(function() {
        window.location = jQuery( this ).find( 'option:selected' ).data( 'url' );
    });
</script>

Your Comment

コードの記述は<pre>または<code>タグで括って下さい。自動的にエスケープされます。

 

右の文字を入力して下さい captcha