見出し画像

ACFの投稿オブジェクトで紐づけられた側の投稿に、紐づけた側の投稿を表示

こんにちは。LINICAエンジニアの橋本です。
気づいたら前回の投稿から結構日が経ってしまってました。

なにやらタイトルがややこしいですが・・今回もWordPressのお話です。
ACFの投稿オブジェクトを使って、他のカスタム投稿の内容を表示する、というのはよくやることだと思いますが、今回はその反対をやってみました。
例えば、

  • ニュース

  • 製品

という2つのカスタム投稿タイプがある場合に、ニュース側に投稿オブジェクトを追加し、ニュースごとに関連する製品を紐づけられるようにします。
この実装は非常に楽にできます。

今回はこの逆をやります。つまり、ニュースに製品が紐づけられたら、製品側でも紐づいたニュース記事を自動的に表示する、という実装です。

今回は、投稿オブジェクトで選べる投稿が一つの場合と、複数選択が可能な場合の2パターンをご紹介します。(クラシックエディタでの制作です。)

投稿オブジェクトの作成

せっかくなので、投稿オブジェクトを作成するところからはじめます。

フィールドグループを新規追加します。

フィールドタイプ:「投稿オブジェクト」
フィールドラベル:関連製品
フィールド名  :related_product
投稿タイプでフィルター:製品

としました。

複数選択はひとまず選択せずにおきます。
ニュースにフィールドグループを紐付け、変更内容を保存します。

ニュースを新規作成すると、このように関連製品が選べるようになるので、関連する製品を選び、更新します。
(カテゴリーは事前にACFで追加しておいたものです。)

次に、single-news.phpを編集します。
以下のようになりました。

<main>
  <div class="l-container p-post">
    <h1 class="c-title">ニュース</h1>
    <h2 class="c-title --md"><?php the_title(); ?></h2>
    <div class="p-post__date-and-cat">
      <p class="c-date"><?php echo the_date(); ?></p>
      <p class="c-tag">
        <?php
        $terms = get_the_terms($post->ID, 'news-cat');
        if(!empty($terms)) {
          foreach($terms as $term) {
            echo '<a href="' . esc_url(get_term_link($term)) . '">' . $term->name . '</a>';
          }
        }
        ?>
      </p>
    </div>
    <div>
      <div class="p-wysiwyg">
        <?php the_content(); ?>
      </div>
      <?php if ( $products_card = get_field( 'related_product' ) ) :
        $product_id = $products_card->ID;
        ?>
        <div class="p-post__card-area">
          <h2 class="c-title --sm">関連製品</h2>
          <div class="p-post__card-group">
            <article class="p-card">
              <a href="<?php echo get_the_permalink($product_id); ?>">
                <div class="p-card__img-area">
                  <img src="<?php echo get_the_post_thumbnail_url($product_id); ?>" alt="">
                </div>
              </a>
              <h3 class="p-card__title c-title"><?php echo get_the_title($product_id); ?></h3>
              <div class="p-card__text-area">
                <?php echo get_the_excerpt($product_id); ?>
              </div>
            </article>
          </div>
        </div>
      <?php endif; ?>
    </div>
  </div>
</main>

重要なのは関連製品の部分のコードです。抜き出すと以下の部分です。

<?php if ( $products_card = get_field( 'related_product' ) ) :
  $product_id = $products_card->ID;
  ?>
  <div class="p-post__card-area">
    <h2 class="c-title --sm">関連製品</h2>
    <div class="p-post__card-group">
      <article class="p-card">
        <a href="<?php echo get_the_permalink($product_id); ?>">
          <div class="p-card__img-area">
            <img src="<?php echo get_the_post_thumbnail_url($product_id); ?>" alt="">
          </div>
        </a>
        <h3 class="p-card__title c-title"><?php echo get_the_title($product_id); ?></h3>
        <div class="p-card__text-area">
          <?php echo get_the_excerpt($product_id); ?>
        </div>
      </article>
    </div>
  </div>
<?php endif; ?>

関連製品(related_product)に登録がある場合のみ、このエリアが表示されます。投稿のIDを$product_idという変数に格納し、その後、タイトルやサムネの表示に使っています。

上記のコードでこのような表示になったかと思います。(同じ見た目にするにはもちろんCSSを書く必要があります。)

ここまでは、通常よくやる実装かと思います。

逆をやってみる(単数選択編)

では、次に製品側でニュースの情報を表示できるように実装します。

<main>
  <div class="l-container p-post">
    <h1 class="c-title">製品情報</h1>
    <h2 class="c-title --md"><?php the_title(); ?></h2>
    <div class="p-post__date-and-cat">
      <p class="c-date"><?php echo get_the_date(); ?></p>
      <ul class="c-tag">
        <?php
        $terms = get_the_terms($post->ID, 'products-cat');
        if(!empty($terms)) {
          foreach($terms as $term):
            echo '<li><a href="' . esc_url(get_term_link($term)) . '">' . $term->name . '</a></li>';
          endforeach;
        }
        ?>
      </ul>
    </div>
    <div>
      <img src="<?php echo get_the_post_thumbnail_url(); ?>" alt="">
      <div class="p-wysiwyg">
        <?php the_content(); ?>
      </div>
    </div>
  </div>
</main>

製品詳細のページは、こんな感じです。
単にタイトルや投稿日、カテゴリ、サムネとテキスト(the_content())が表示されています。
当然ながら、製品Aが紐づけられているニュース記事(「製品紹介に製品Aを追加しました。」)は表示されていません。

the_content()を囲っているdivの下に、以下のように追加します。

<?php
  $args = array(
    'post_type' => 'news',
    'post_status' => 'publish',
    'posts_per_page' => -1,
    'meta_query' => array(
      array(
        'key' => 'related_product',
        'value' => $post_id,
        'compare' => 'like',
      ),
    ),
  );
  $query = new WP_Query($args);
  if ($query->have_posts()): ?>
    <h2 class="c-title --sm">この製品に関連するニュース</h2>
    <?php
      while ( $query -> have_posts() ): $query -> the_post();
    ?>
  <div>
    <div class="p-products__card-area">
      <article class="p-products-card">
        <a href="<?php echo get_the_permalink(); ?>">
          <p class="c-date"><?php echo the_date(); ?></p>
          <h3 class="p-products-card__title"><?php echo get_the_title(); ?></h3>
        </a>
      </article>
    </div>
  </div>
  <?php endwhile; endif; ?>

重要なのは以下の部分です。

$args = array(
  'post_type' => 'news',
  'post_status' => 'publish',
  'posts_per_page' => -1,
  'meta_query' => array(
    array(
      'key' => 'related_product',
      'value' => $post_id,
      'compare' => 'like',
    ),
  ),
);
$query = new WP_Query($args);

この部分で、カスタム投稿タイプ「ニュース(news)」の投稿の中で、「関連製品(related_product)」のIDが現在表示している投稿IDと一致するものを、$argsに入れています。

この記述により、上記のように製品側にもニュースが表示されるようになりました。

逆をやってみる(複数選択編)

次に、紐づけるオブジェクトが複数の場合です。
ACFの投稿オブジェクトを複数選択に変更しておきます。

ニュースの該当のページを見てみると、エラーになりました。
これは複数選択にしたことで、表示方法が変わるためです。

$products_card = get_field( 'related_product' );
var_dump($products_card);

とすると、以下のように表示されました。
複数選択にしたことで、配列に格納されたようです。

というわけで、配列の中をループで回しましょう。
関連製品の部分を以下のように書き換えます。

<?php if ( $products_cards = get_field( 'related_product' ) ) :
  $products_cards = get_field( 'related_product' );
  ?>
  <div class="p-post__card-area">
    <h2 class="c-title --sm">関連製品</h2>
    <div class="p-post__card-group">
      <?php foreach ($products_cards as $products_card) { 
          $product_id = $products_card->ID;
       ?>
      <article class="p-card">
        <a href="<?php echo get_the_permalink($product_id); ?>">
          <div class="p-card__img-area">
            <img src="<?php echo get_the_post_thumbnail_url($product_id); ?>" alt="">
          </div>
        </a>
        <h3 class="p-card__title c-title"><?php echo get_the_title($product_id); ?></h3>
        <div class="p-card__text-area">
          <?php echo get_the_excerpt($product_id); ?>
        </div>
      </article>
      <?php } ?>
    </div>
  </div>
<?php endif; ?>

foreachで、$products_cardsの配列をループさせ、製品の投稿IDを取得、表示しています。
これで、ニュース詳細は先ほどの表示に戻りました。

次に、製品側です。
実は先ほどのコードのままでも表示は崩れていないのですが、meta_queryのvalue部分を以下のように変更する必要があります。

'value' => serialize(strval($post->ID)),

理由は、複数選択になったことにより、related_productのデータがシリアライズされたことにあります。
単数選択の場合は、以下のような値だったデータが、

複数選択にすると、以下のように変わりました。(DBのwp_postmetaテーブルを見ると確認できます。)

このような構造になると、単純に

'value' => $post->ID,

としていると、
例えば$post->IDが5のとき、この文字列から5を探すことになるので、5文字のID(10000)が登録されていた場合も、s:5:"10000"となり、一致してしまいます。

そのため、value側もシリアライズして、絞り込む必要があります。
シリアライズすることで、valueも

s:2:"12";

のようになるので、文字数に関係なく、IDを探しにいけるというわけです。

meta_queryでの絞り込みといまだに仲良く慣れていないので、ここまでくるのに結構な時間を溶かしましたが、とりあえず、
投稿オブジェクトは複数選択の場合、シリアライズされて保存されている
というのを覚えて帰っていただければ幸いです。

もし機会があったら、試してみてください。