バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

WordPressのWP_Queryループのネストをスッキリさせた話

はじめに

開発1部 マーケティングチームの西野です。

マーケティングチームではBUYSELL公式サイトなど当社で運営しているWebサイトの制作、更新を担当しております。

マーケティングチームで担当している運用フェーズにあるWebサイトはWordPressで制作しておりますが、カスタム投稿を使用している方も多くいると思います。

WordPressのカスタム投稿は、固定ページとは別に自分の作りたい情報を自由にカスタマイズして作成でき、とても便利な機能です。

カスタム投稿の情報を取得する時は、WP_Queryクラスを使う事が多いですが、この記事では、複雑なカスタム投稿の情報を取得した際に筆者自身がハマったWP_Queryクラスの注意点を紹介したいと思います。

背景

私たちのチームでは常に、膨大になりがちなコーディング作業を効率的に削減できるかを考えています。

取り組みの一環として、直に記述しているソースコードを、カスタム投稿を用いて自動的に更新・表示されるよう開発を進めています。

やりたかったこと

条件に合致する情報を都道府県別にリスト化します。下記画像のようなリストを、ソースコードにベタ書きするのではなく、WP_Queryを用いてHTML表示します。カスタム投稿をWP_Queryで取得しHTML表示をすれば、ノーコードで反映できるだけでなく、複数ページにわたって同様の内容を記載している場合でも、WordPressの管理画面上で、カスタム投稿を編集するだけで一括反映することが出来ます。

[HTMLの出力イメージ]

条件に合致する情報を持たない都道府県は、ソースコードに表示しないようにします。 例として、北海道、青森県、岩手県...と条件判別をした時に、青森県は条件に合致しないとします。

その場合、青森県は表示エリア自体を作成せず、北海道と岩手県のみ表示エリアを作成します。

既存の実装

1:条件が合致する都道府県を取得する

<?php
  //WP_Queryクラスの宣言
  $news_query = new WP_Query( $args );
  if ( $news_query->have_posts() ):
  //都道府県の日本語名
  global $pref_jpname;
  //都道府県の日本語名を格納する変数
  $locate_pref_jpname = "";
  //都道府県のアルファベット名を格納する変数
  $locate_pref_enname = "";

  //カスタム投稿のループ処理
  while ( $news_query->have_posts() ):
    $news_query->the_post();
    //カスタムフィールドの都道府県を変数で定義
    $address1a = get_field('post_address1');
    //$address1a を数値置換
    $address1a_int = (int)$address1a['value'];

    //条件に合致する場合、その都道府県名を変数$locate_pref_jpnameと
    //変数$locate_pref_ennameに代入する
    foreach($pref_jpname as $val_enname => $val_jpname) {
      if( $val_jpname == $address1a['label'] ) {
        $locate_pref_jpname = $val_jpname;
        $locate_pref_enname = $val_enname;
        break;
      }
    }
  endwhile; }
?>

2:合致した都道府県の表示エリアを作成

<!-- 1で変数に代入した都道府県の表示エリアを作成 -->
<div class="list-item <?php echo $locate_pref_enname; ?>">
  <div class="list-division-name"><?php echo $locate_pref_jpname; ?></div>
</div>

3:同じ都道府県に条件に合致する情報があるかチェックする

<?php
  $args2 = array(
    'post_type' => 'post',
    'meta_key' => 'post_display_order',
    'orderby'=> 'meta_value',
    'order'=> 'ASC',
    'posts_per_page' => -1,
    'meta_query' => [
      'relation' => 'OR',
      [
        'key' => 'post_closed',
        'value' => '0',
        'compare' => '='
      ],
      [
        'key' => 'post_closed',
        'compare' => 'NOT EXISTS'
      ]
    ],
  );
  $item_query = new WP_Query( $args2 );
  if ( $item_query->have_posts() ):

  while ( $item_query->have_posts() ): 
  $item_query->the_post();
    //取得するカスタム投稿の値
    $use_fields = [
      'post_name',
      'post_slag',
      'spost_label',
      'post_zip',
      ...
    ];
    
    //[1]同様、条件に合致する都道府県の値を取得する
    //条件に合致する情報の場合、その都道府県名を変数に代入する
    foreach($pref_jpname as $val_enname => $val_jpname) {
      if( $val_jpname == $address1['label'] ) {
        $locate_pref_jpname = $val_jpname;
        $locate_pref_enname = $val_enname;
        break;
      }
    }
  endwhile;
  endif;
?>

4:1と3の都道府県が合致している時に情報を出力

<?php
//外ループと内ループの都道府県が合致している時
if(($address1a['value'] == $address1['value']) && ($status != 1)): ?> 
  //情報をHTMLに出力・整形   
<?php endif; ?>

既存処理の問題点

余計なループ処理が多すぎました。

WP_Queryクラスを、条件に合致する都道府県の取得[1]で使用し、同じ都道府県の中で情報の取得[3]でも使用していた為、膨大なループリクエストが発生していました。さらに、データを整形するタイミングでループ処理を動かしていた事で、デバッグもしづらく可読性が低いコードになっていました。

結果的にWP_Queryを件数の二乗分ループを回していました....(もし100件あれば10000件)。

それによって負荷が高くなってしまい、パフォーマンス的にも問題がありそうという話は上がっていました。

リファクタリングした内容

カスタム投稿から取得する情報が複雑な場合は、WP_Queryで取得する情報を連想配列としてイメージします。自分が取得したい情報の全体を把握する事で、その後、どの情報を使用するかが明確になります。

<?php
Array = ['北海道' => ['en_name' => 'hokkaido', items => [item1, item2]],
         '岩手県' => ['en_name' => 'iwate', items => [item1, item2]],
         '東京都' => ['en_name' => 'tokyo', items => [item1, item2]]]
?>

取得する情報を整理したら、カスタム投稿のデータをWP_Queryを使って連想配列に格納します。

<?php
global $pref_jpname;

$grouped_items = [];

foreach($pref_jpname as $pref_enname => $group_name) {
  $grouped_items[$group_name] = [
    'pref_en' => $pref_enname,
    'items' => []
  ];
}

$args = [
  'post_type' => 'item_post',
  'meta_key' => 'post_display_order',
  'orderby' => 'meta_value',
  'order' => 'ASC',
  'posts_per_page' => -1,
  'meta_query' => [
    'relation' => 'OR',
    [
      'key' => 'store_post_closed',
      'value' => '0',
      'compare' => '='
    ],
    [
      'key' => 'post_closed',
      'compare' => 'NOT EXISTS'
    ]
  ],
];
$items = new WP_Query($args);
if ($items->have_posts()) {
  while ($items->have_posts()) {
    $items->the_post();
    $field_values = [];
    $use_fields = [
      'post_name',
      'post_slag',
      'post_address1',
      'post_info01',
      'post_info02',
      'post_info03',      
      ...
    ];
    foreach($use_fields as $use_field) {
      $field_values[$use_field] = get_field($use_field);
    }
    $pref_key = $field_values['post_address1']['label'];
    $grouped_items[$pref_key]['items'][] = $field_values;
  }
}
?>

カスタム投稿の情報を連想配列に格納したら、連想配列の中から必要な情報を抽出して、HTMLとして出力・整形するだけです。

<div class="item-list <?php echo $grouped_item['pref_en']; ?>">
  <div class="item-list-division-name"><?php echo $pref_jp; ?></div>
  <?php foreach ($grouped_item['items'] as $item): ?>
  <div id="item-<?php echo $item['post_slag']; ?>" style="display: none;">
    <div class="-body">
      <div class="item-body-data">
        <div class="item-body-name"><?php echo $item['post_info01']; ?></div>
        <dl class="item-body-detail">
          <dt class="item-body-detail-title">情報A</dt>
          <dd class="item-body-detail-data"><?php echo $item['post_info02']; ?></dd>
        </dl>
        <dl class="item-body-detail">
          <dt class="item-body-detail-title">情報B</dt>
          <dd class="item-body-detail-data"><?php echo $item['post_info03']; ?></dd>
        </dl>
      </div>
    </div>
  </div>
  <?php endforeach; ?>
</div>

修正する前に同じ都道府県の中で情報の取得はWP_Queryを使ってループしていました。その処理をforeach文を使用します。取得した情報、つまり連想配列だけループさせる事で、同じ情報を2回WP_Queryを使って取得する必要がなくなり、コード全体の可読性も上がりました。

修正後の実装

1:カスタム投稿のデータを全て連想配列に格納する

<?php
// このようなデータ構造を想定:
//['北海道' => ['pref_en' => 'hokkaido', items => [item1, item2]]]
$grouped_items = [];

foreach($pref_jpname as $pref_enname => $group_name) {
  $grouped_items[$group_name] = [
    'pref_en' => $pref_enname,
    'stores' => []
  ];
}
?>

2:取得するカスタム投稿の変数定義

<?php
$items = new WP_Query($args);
if ($items->have_posts()) {
  while ($items->have_posts()) {
    $items->the_post();
    $field_values = [];
    //取得するカスタム投稿の値
    $use_fields = [
      'post_name',
      'post_slag',
      'post_label',
      'post_address',
      'post_zip',
      ...
    ];
    foreach($use_fields as $use_field) {
      $field_values[$use_field] = get_field($use_field);
    }
    $pref_key = $field_values['post_address']['label'];
    $grouped_items[$pref_key]['items'][] = $field_values;
  }
}
?>

3:連想配列に合致する都道府県をHTMLに出力・整形

<div class="items_desc cf">
  <div class="item-list <?php echo $grouped_item['pref_en']; ?>">
    <div class="item-list-division-name"><?php echo $pref_jp; ?></div>
    <?php foreach ($grouped_item['items'] as $item): ?>
    <div id="item-<?php echo $item['post_slag']; ?>" style="display: none;">
      <div class="-body">
        <div class="item-body-data">
          <div class="item-body-name"><?php echo $item['post_info01']; ?></div>
          <dl class="item-body-detail">
            <dt class="item-body-detail-title">情報A</dt>
            <dd class="item-body-detail-data"><?php echo $item['post_info02']; ?></dd>
          </dl>
          <dl class="item-body-detail">
            <dt class="item-body-detail-title">情報B</dt>
            <dd class="item-body-detail-data"><?php echo $item['post_info03']; ?></dd>
          </dl>
        </div>
      </div>
    </div>
    <?php endforeach; ?>
  </div>
</div>

まとめ

取得する情報が多かったり複雑な場合、WP_Queryクラスを多用して情報を取得しようと考えてしまう方もいるかもしれません。

しかしWP_Queryクラスは便利な関数であると同時に、多用するとループリクエストが膨大になってしまったり、コード全体の可読性が下がる原因にもなり得ます。

筆者は今回、全体処理を

  • 取得する情報を連想配列に格納
  • 連想配列からHTMLに出力

の2段階に分けることで、コード量を大幅に減らす事ができ、可読性が上がりました。取得するデータ構造を整理してから、自分がどういう情報を取得したいのかを把握する。そうする事で、次のコードアクションが取りやすくなります。WordPressでカスタム投稿の情報を使う際の参考に少しでもなれば幸いです。

最後に、バイセルではWebコーダー(マーケティングエンジニア)を随時募集しております。
興味のある方はぜひ以下の採用サイトをご覧下さい。
https://herp.careers/v1/buyselltech/oz11uLWtdLNS