• コーディング

【WordPress】データベースのデータを取得して動的に投稿ページを生成する

  • #WordPress

忘れないうちに備忘録残しておこうと久々の投稿です。

今回、WordPressを使ったサイト制作をすることになり、
その際の要件に動的にページを生成するということが入ってきました。
データベースに登録している情報を引っ張ってきて、自動でページを生成するということですね。
例えば、データベースのテーブルに設定されているページ件数が10件だったら、自動で10件表示させるようなイメージですね。

ChatGPTやCodeCopilot使ったり、英語のページ漁ったり。。。
なかなかピンポイントで解決策載ってるページもなく、AI使ってもすぐに解決に至らず、数日、時間を費やしてました。。。

今後、再現性高くやるためにも、備忘録に残しておこうと思った次第です。

同じような課題にぶちあったっている人の参考になればと思います。

では本題に入りましょう。

対応の流れ

  1. プラグインのインストール
  2. プラグインの設定
  3. データベースにデータ作成(今回はLocalを使用しているので、そこからアクセス出来るデータベースに作成)
  4. 該当ファイルにphpを記述

今回使ったファイル

  • functions.php
  • index.php
  • single-fruit-post.php(カスタム投稿ページ)※ファイル名はルールがあるので、注意
  • archive-fruit-post.php(アーカイブページ)※ファイル名はルールがあるので、注意

前提条件

  • 動的に生成したカスタム投稿ページの公開ステータスは下書きモード
  • 動的に生成したカスタム投稿ページは管理画面で編集出来るようにする
  • 一度、動的に生成したページは自動で削除や公開ステータスを変更されないようにする。そこは手動対応が前提。
  • 今回はローカル環境での検証。サーバーアップロードして検証はしていません。
  • ローカル環境での作業は便利なLocalというツールを使ってます。使い方については割愛しますので、公式サイトや紹介サイトを参考にしてください。

完成イメージ

あくまで機能実装の紹介のため、レイアウトは整えていません。

トップページ

アーカイブページ

 

カスタム投稿ページ

1.プラグインのインストール

今回使用したプラグインはCustom Post Type UIです。
カスタムで作れる投稿タイプとタクソノミーを使用するので、このプラグインを選びました。
まずはインストールして有効化しましょう!

2.プラグインの設定

それではインストールしたCustom Post Type UIでカスタム投稿タイプとカスタムタクソノミーを登録します。
今回はカスタム投稿タイプスラッグを「fruit-post」、カスタムタクソノミースラッグを三つ、「fruit」、「price」、「area」と設定しました。

以下、詳細な設定です。

カスタム投稿タイプ

基本設定
投稿タイプスラッグfruit-post
複数形のラベル果物ニュース(※これは任意の名前で可。)
単数形のラベル果物ニュース(※複数形と同じで問題ない)
追加ラベル
ここは全件、空欄で問題なし
設定
公開True
一般公開クエリー可True
UI を表示True
ナビゲーションメニューに表示True
ユーザーと一緒に削除False
REST API で表示True
REST API ベーススラッグ空欄
REST API コントローラークラス空欄
REST API 名前空間空欄
アーカイブありTrue(※ここは必ずTrueにする
アーカイブに使うURLを設定するとその名称が表示される。
空欄にすると今回の設定では「http://sample.local/fruit-post/?fruit%5B%5D=orange」となる。例えば、hogeと設定すると「http://sample.local/hoge/?fruit%5B%5D=orange」となる
検索から除外False
権限タイプpost
階層False
エクスポート可能False
リライトTrue
カスタムクエリー変数スラッグ空欄
メニューの位置空欄
メニューに表示True
メニューアイコン
空欄
メタボックスコールバック
空欄
サポート
任意でチェック
カスタム「サポート」
空欄
タクソノミーカスタムタクソノミーで作成した「果物」「金額」「エリア」をチェック

カスタムタクソノミー

タクソノミーとは「分類」というイメージ。
カスタム投稿タイプ「fruit-post」で使うためのタクソノミーをここで作っていく。
例として、「fruit」という名前で作成する方法を記載するので、
同様のやり方で「price」、「area」も作ってください。

基本設定
タクソノミースラッグfruit
複数形のラベル果物(※これは任意の名前で可。)
単数形のラベル果物(※複数形と同じで問題ない)
利用する投稿タイプ果物ニュースにチェック
追加ラベル
ここは全件、空欄で問題なし
公開True
公開クエリー可True
階層True
UI を表示True
メニューに表示するTrue
ナビゲーションメニューに表示True
クエリー変数True
カスタムクエリー変数文字列空欄
リライトTrue
カスタムリライトスラッグ空欄
フロントでのリライトTrue
階層リライトFalse
管理画面でカラムを表示True
REST API で表示True
REST API ベーススラッグ空欄
REST API コントローラークラス空欄
REST API 名前空間空欄
タグクラウドへ表示します。False
クイック編集 / 一括編集パネルに表示。False
並び替えFalse
メタボックスコールバック空欄
デフォルトのターム空欄

ファイル名について

カスタム投稿ページとアーカイブページのファイル名は決まりがある。
登録済の一覧画面に記載があるので、ここを参考にするとよい。

データベースにデータ作成

実際には既存のデータベースからデータを引っ張ってくるという形になるが、
公開は動作検証のため、手動でデータ登録を行った。

私はLocalを使っているので、参考イメージを以下に掲載。
赤枠のところをクリックするとデータベースが開ける。

これが登録後のイメージ。

テーブル名は「wp_sample_fruits」と登録。

カラム名は「page_id」「fruit_name」「price」「area」と登録。

列名長さ
page_idint自動で11と入る
fruit_namevarchar任意で20と設定
pricevarchar任意で20と設定
areavarchar任意で20と設定

page_idfruit_namepricearea
1apple100aomori
2strawberry150saga
3strawberry120ehime
4grapes150yamanashi
5apple150aomori
6orange100ehime

該当ファイルにphpを記述

全てにファイルは使用するテーマの同一階層に格納してください。

functions.php

/**
 * カスタム投稿ページの動的生成
 */
function generate_dynamic_posts() {
    global $wpdb;

    // カスタム投稿タイプが存在するかを確認
    if (!post_type_exists('fruit-post')) {
        return;
    }

    // タクソノミーが存在するかを確認
    if (!taxonomy_exists('fruit') || !taxonomy_exists('price') || !taxonomy_exists('area')) {
        return;
    }

    // データベースから値を取得
    $results = $wpdb->get_results("SELECT page_id, fruit_name, price, area FROM wp_sample_fruits");

    foreach ($results as $row) {
        $post_title = 'Container Post ' . $row->page_id;
        $post_content = '果物: ' . $row->fruit_name . ', 金額: ' . $row->price . ', エリア: ' . $row->area;
        $post_slug = 'page-' . $row->page_id; // スラッグを page_id に設定

        // 投稿が存在するかをチェック
        $post_exists = get_page_by_path($post_slug, OBJECT, 'fruit-post');
        if ($post_exists) {
            continue; // スラッグが既に存在する場合はスキップ
        }

        // 新規投稿の作成
        $post_id = wp_insert_post(array(
            'post_title' => $post_title,
            'post_content' => $post_content,
            'post_status' => 'draft', // 初期状態を下書きに設定。公開済:publish
            'post_type' => 'fruit-post',
            'post_name' => $post_slug // スラッグを指定
        ));

        if (!is_wp_error($post_id)) {
            // タクソノミーを設定
            wp_set_object_terms($post_id, $row->fruit_name, 'fruit');
            wp_set_object_terms($post_id, $row->price, 'price');
            wp_set_object_terms($post_id, $row->area, 'area');
        } else {
            error_log('Failed to create post: ' . $post_id->get_error_message());
        }
    }
}
add_action('init', 'generate_dynamic_posts');
/**
 * 絞り込み検索
 */
function custom_search_query($query) {
    // クエリが検索結果ページに対するものであり、且つ管理画面でない場合
    if ($query->is_search && !is_admin()) {
        $tax_query = array('relation' => 'AND'); // タクソノミークエリ内の全ての条件を満たす場合

        // 果物
        if (!empty($_GET['fruit_name'])) {
            $tax_query[] = array(
                'taxonomy' => 'fruit', // 指定のタクソノミーに対してクエリを実行
                'field' => 'slug', // スラッグで識別
                'terms' => $_GET['fruit_name'], // 指定した'fruit_name'の値に一致するタクソノミーを識別
                'operator' => 'IN', // 'terms'に含まれるいずれかの値に一致
            );
        }

        // 金額
        if (!empty($_GET['price'])) {
            $tax_query[] = array(
                'taxonomy' => 'price', // 指定のタクソノミーに対してクエリを実行
                'field' => 'slug', // スラッグで識別
                'terms' => $_GET['price'], // 指定した'price'の値に一致するタクソノミーを識別
                'operator' => 'IN', // 'terms'に含まれるいずれかの値に一致
            );
        }

        // エリア
        if (!empty($_GET['area'])) {
            $tax_query[] = array(
                'taxonomy' => 'area', // 指定のタクソノミーに対してクエリを実行
                'field' => 'slug', // スラッグで識別
                'terms' => $_GET['area'], // 指定した'area'の値に一致するタクソノミーを識別
                'operator' => 'IN', // 'terms'に含まれるいずれかの値に一致
            );
        }

        $query->set('tax_query', $tax_query);
    }
}
add_action('pre_get_posts', 'custom_search_query');

index.php

<form method="get" action="<?php echo esc_url(get_post_type_archive_link('fruit-post')); ?>">
  <p style="background-color: #f3f3f3; font-weight: bold; padding: 3px 8px;">
  果物で探す
  </p>
  <?php
    // 取得するタクソノミーが「fruit(果物)」の場合
    $terms = get_terms(array(
      'taxonomy' => 'fruit',
      'hide_empty' => false, // 投稿がない場合でも表示
    ));
    if (!empty($terms) && !is_wp_error($terms)) {
      echo '<ul style="margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 24px;">';
      foreach ($terms as $term) {
        echo '<li style="list-style: none;">';
        echo '<label>';
        echo '<input type="checkbox" name="fruit[]" value="' . esc_attr($term->slug) . '"> ';
        echo esc_html($term->name);
        echo '</label>';
        echo '</li>';
      }
      echo '</ul>';
    } else {
      echo '<p>カスタムタクソノミーが見つかりませんでした。</p>';
    }
  ?>

  <p style="background-color: #f3f3f3; font-weight: bold; padding: 3px 8px;">
  金額で探す
  </p>
  <?php
    // 取得するタクソノミーが「price(金額)」の場合
    $terms = get_terms(array(
      'taxonomy' => 'price',
      'hide_empty' => false, // 投稿がない場合でも表示
    ));

    if (!empty($terms) && !is_wp_error($terms)) {
      echo '<ul style="margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 24px;">';
      foreach ($terms as $term) {
        echo '<li style="list-style: none;">';
        echo '<label>';
        echo '<input type="checkbox" name="price[]" value="' . esc_attr($term->slug) . '"> ';
        echo esc_html($term->name);
        echo '</label>';
        echo '</li>';
      }
      echo '</ul>';
    } else {
      echo '<p>カスタムタクソノミーが見つかりませんでした。</p>';
    }
  ?>

  <p style="background-color: #f3f3f3; font-weight: bold; padding: 3px 8px;">
  エリアで探す
  </p>
  <?php
    // 取得するタクソノミーが「area(エリア)」の場合
    $terms = get_terms(array(
      'taxonomy' => 'area',
      'hide_empty' => false, // false:投稿がない場合でも表示
    ));

    if (!empty($terms) && !is_wp_error($terms)) {
      echo '<ul style="margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 24px;">';
      foreach ($terms as $term) {
        echo '<li style="list-style: none;">';
        echo '<label>';
        echo '<input type="radio" name="area" value="' . esc_attr($term->slug) . '"> '; // 配列ではなく単一値
        echo esc_html($term->name);
        echo '</label>';
        echo '</li>';
      }
      echo '</ul>';
    } else {
      echo '<p>カスタムタクソノミーが見つかりませんでした。</p>';
    }
  ?>
  <button type="submit">検索</button>
</form>

single-fruit-post.php

<?php
get_header();

if (is_singular('fruit-post')) {
  global $wpdb;

  $post_id = get_the_ID(); // 現在の投稿IDを取得
  $post_slug = get_post_field('post_name', $post_id); // 投稿スラッグを取得

  // デバッグ情報の表示
  echo '<p>Post ID: ' . esc_html($post_id) . '</p>';
  echo '<p>Post Slug: ' . esc_html($post_slug) . '</p>';

  // スラッグから数値部分を抽出
  if (preg_match('/(\d+)/', $post_slug, $matches)) {
    $page_id = intval($matches[1]);

    // データベースから値を取得して表示する
    $result = $wpdb->get_row($wpdb->prepare("SELECT * FROM wp_sample_fruits WHERE page_id = %d", $page_id));

    if ($result) {
      echo '<h2>ページID ' . esc_html($result->page_id) . '</h2>';
      echo '<p>果物: ' . esc_html($result->fruit_name) . '</p>';
      echo '<p>金額: ' . esc_html($result->price) . '</p>';
      echo '<p>エリア: ' . esc_html($result->area) . '</p>';
    } else {
      echo '<p>No data found for this page. Page ID: ' . esc_html($page_id) . '</p>';
    }
  } else {
    echo '<p>Invalid slug format. Post Slug: ' . esc_html($post_slug) . '</p>';
  }
} else {
  echo 'ページIDが取得できません';
}

get_footer();
?>

archive-fruit-post.php

<?php
  get_header();

  // クエリをカスタマイズ
  $args = array(
    'post_type' => 'fruit-post',
  );

  $tax_query = array();

  if (!empty($_GET['fruit'])) {
    $tax_query[] = array(
      'taxonomy' => 'fruit',
      'field' => 'slug',
      'terms' => array_map('sanitize_text_field', $_GET['fruit']),
    );
  }

  if (!empty($_GET['price'])) {
    $tax_query[] = array(
      'taxonomy' => 'price',
      'field' => 'slug',
      'terms' => array_map('sanitize_text_field', $_GET['price']),
    );
  }

  if (!empty($_GET['area'])) {
    $tax_query[] = array(
      'taxonomy' => 'area',
      'field' => 'slug',
      'terms' => sanitize_text_field($_GET['area']), // 単一値としてサニタイズ
    );
  }

  if ($tax_query) {
    $args['tax_query'] = $tax_query;
  }

  $query = new WP_Query($args);

  if ($query->have_posts()) :
    echo '<ul>'; // ULタグの開始
    while ($query->have_posts()) : $query->the_post();
      // データベースから値を取得
      global $wpdb;
      $post_id = get_the_ID();
      $post_slug = get_post_field('post_name', $post_id);

      // スラッグから数値部分を抽出
      if (preg_match('/(\d+)/', $post_slug, $matches)) {
        $page_id = intval($matches[1]);
      } else {
        $page_id = 0;
      }

      // データベースから値を取得して表示する
      $result = $wpdb->get_row($wpdb->prepare("SELECT fruit_name, price, area FROM wp_sample_fruits WHERE page_id = %d", $page_id));

      echo '<li>'; // LIタグの開始
      echo '<a href="' . get_permalink() . '">' . get_the_title() . '</a>';

      // データベースの値を表示
      if ($result) {
        echo '<p>果物: ' . esc_html($result->fruit_name) . '</p>';
        echo '<p>金額: ' . esc_html($result->price) . '</p>';
        echo '<p>エリア: ' . esc_html($result->area) . '</p>';
      }

      echo '</li>'; // LIタグの終了
    endwhile;
    echo '</ul>'; // ULタグの終了
    wp_reset_postdata();
  else :
    echo '<p>No posts found</p>';
  endif;

  get_footer();
?>

終わりに

自分でまとめてみて、かなり長くなってしまったが、いかがでしたでしょうか?

これまでは管理画面で投稿記事を作成して、投稿ページを表示するということしかやったことがなかったので、
データベースから値を取得して、動的にページ生成することは初めての経験だった。

必死に調べてその時は理解出来ても、時間が経つにつれて忘れてしまうので、今回はその為の備忘録でした。

  • 松浦 一彦
  • 約11年間、事業者会社、制作会社でWEBデザイン、WEBディレクション、GA/GTMを使用したアクセス解析などを経験。
    2022年3月よりフリーランスとして独立。
    現在は業務委託として複数の企業のプロジェクトに参加する傍ら、受託案件を受けたり、自身のブログを運用しています。

    • スキルUI/UX改善、サイト分析、WEBサイト&LPデザイン、コーディング、ディレクション、動画編集、WordPress構築
    • 言語HTML、CSS、Javascript/jQuery、PHP
    • ツールPhotoshop、illustrator、Premiere、After Effect、XD、Figma、Googleアナリティクス

関連記事