NaN/NaN/NaN

WordPressで「お問い合わせ確認書をPDFで送りたい」「商品カタログをPDFでダウンロードさせたい」「WooCommerceの注文書を自動発行したい」という要件は非常によくあります。しかし、既存のプラグインやライブラリではなかなか思い通りにならないことも多いのが現実です。

この記事ではFUNBREW PDFのAPIをWordPressに統合する方法を、実践的なコード例とともに徹底解説します。wp_remote_post()によるAPI呼び出し、ショートコード実装、カスタム投稿タイプのPDF化、WooCommerceとの連携、REST APIエンドポイント追加、そしてセキュリティ対策(nonce・capability check)まで網羅します。

PHPでのAPI基本呼び出しは言語別クイックスタートを、エラーハンドリングはエラーハンドリングガイドを参照してください。

WordPressでPDF生成が必要なシーン

WordPressは単なるブログシステムではなく、ECサイト・業務システム・ポータルサイトとして幅広く使われています。PDF生成が必要になるシーンは多岐にわたります。

シーン 具体例
EC・WooCommerce 注文確認書、請求書、納品書の自動発行
会員サイト 会員証、修了証明書、資格証のダウンロード
業務・BtoB 見積書、契約書、報告書の生成
コンテンツ配布 商品カタログ、ホワイトペーパーのPDF化
フォーム連携 お問い合わせ・申込フォームの確認書発行

これらをWordPress単体で実現しようとするとき、多くの開発者はまずプラグインを探します。

既存プラグイン(TCPDF系等)の限界

WordPressのPDF関連プラグインにはいくつかの定番があります。WooCommerce PDF Invoices & Packing Slips、mPDF、dompdfベースのプラグインなどです。これらはサーバー上でPHPライブラリを使ってPDFを生成します。

しかし実際に使ってみると、以下のような課題にぶつかることがよくあります。

CSS・レイアウトの再現性

PHPベースのPDFライブラリはCSSのサポートが限定的です。flexboxgridは使えないことが多く、日本語フォントの表示も不安定になりがちです。こだわったデザインをHTMLで作っても、PDFにするとレイアウトが崩れる問題は頻繁に起こります。

// 典型的なmPDFの問題:CSSが正しく解釈されない
$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML('<div style="display:flex;">...</div>'); // flexboxが効かない

サーバーリソースの消費

TCPDFやmPDFはPHP上で動作するため、大きなPDFを生成するときや同時リクエストが多いときにサーバーのメモリ・CPUを大量に消費します。共有ホスティング環境では特に問題になります。

アップデートとメンテナンス

WordPressのバージョンアップ、PHPのバージョンアップのたびに互換性問題が発生するリスクがあります。プラグインが更新されなくなれば、脆弱性を抱えたまま運用せざるを得ない状況にもなります。

API方式のメリット

FUNBREW PDF APIのようなクラウドPDF生成APIを使う方式には、サーバー負荷・品質・メンテナンス性の面で大きなメリットがあります。

サーバー負荷ゼロ

PDF生成処理はFUNBREW PDFのサーバー側で実行されます。WordPressサーバーはAPIリクエストを送るだけなので、CPUもメモリも消費しません。トラフィックが急増してもWordPressサーバーには影響しません。

高品質なCSS対応

FUNBREW PDF APIはChromiumベースのレンダリングエンジンを使用しており、最新のCSSを完全にサポートしています。flexboxgrid、カスタムフォント、グラデーション、影など、HTMLで表現できるものはすべてPDFに反映されます。日本語フォントの対応についても日本語フォントガイドで詳しく解説しています。

シンプルな統合

FUNBREW PDF APIとのやりとりはHTTPリクエスト1本です。WordPressにはwp_remote_post()という便利な関数があり、外部HTTPリクエストを簡単に送れます。プラグインのインストールは不要で、コード数十行で統合できます。

メンテナンス不要

APIサーバーのメンテナンスはFUNBREW側が行います。WordPressのバージョンアップやPHPのバージョンアップの影響を受けません。

準備:APIキーの取得と設定

無料アカウントを作成し、ダッシュボードからAPIキーを発行します。

APIキーはwp-config.phpに定数として設定するのがWordPressらしい方法です。

// wp-config.php
define( 'FUNBREW_PDF_API_KEY', 'sk-your-api-key' );
define( 'FUNBREW_PDF_API_URL', 'https://api.pdf.funbrew.cloud/v1/pdf/from-html' );

または、プラグインのオプションページからユーザーが入力できるようにする方法も一般的です。その場合はget_option()で取得します。

// オプションページで保存した場合
$api_key = get_option( 'funbrew_pdf_api_key' );

APIキーの安全な管理方法はセキュリティガイドで詳しく解説しています。

wp_remote_post()でのAPI呼び出し

WordPressでの外部HTTPリクエストにはwp_remote_post()を使います。curlを直接使う必要はありません。wp_remote_post()はWordPressのHTTP APIラッパーで、cURL・fsockopen・streams等を環境に合わせて自動選択します。

/**
 * HTMLからPDFを生成する
 *
 * @param string $html     PDFに変換するHTML
 * @param array  $options  APIオプション(format, margin等)
 * @return string|WP_Error PDFバイナリ文字列 または WP_Error
 */
function funbrew_generate_pdf( $html, $options = array() ) {
    $api_key = defined( 'FUNBREW_PDF_API_KEY' ) ? FUNBREW_PDF_API_KEY : get_option( 'funbrew_pdf_api_key' );
    $api_url = defined( 'FUNBREW_PDF_API_URL' ) ? FUNBREW_PDF_API_URL : 'https://api.pdf.funbrew.cloud/v1/pdf/from-html';

    if ( empty( $api_key ) ) {
        return new WP_Error( 'missing_api_key', 'FUNBREW PDF APIキーが設定されていません。' );
    }

    $body = array_merge(
        array(
            'html'   => $html,
            'format' => 'A4',
            'engine' => 'quality',
        ),
        $options
    );

    $response = wp_remote_post(
        $api_url,
        array(
            'method'  => 'POST',
            'timeout' => 60,
            'headers' => array(
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ),
            'body'    => wp_json_encode( $body ),
        )
    );

    if ( is_wp_error( $response ) ) {
        return $response;
    }

    $status_code = wp_remote_retrieve_response_code( $response );

    if ( 200 !== $status_code ) {
        $body_text = wp_remote_retrieve_body( $response );
        return new WP_Error(
            'pdf_generation_failed',
            sprintf( 'PDF生成に失敗しました(ステータス: %d, メッセージ: %s)', $status_code, $body_text )
        );
    }

    return wp_remote_retrieve_body( $response );
}

この関数は成功時にPDFのバイナリ文字列を返し、失敗時にWP_Errorを返します。WordPress標準のエラーハンドリングパターンに従っているので、呼び出し側ではis_wp_error()でチェックするだけです。

PDFをブラウザに出力する

/**
 * PDFをブラウザに直接出力する
 *
 * @param string $pdf_data PDFバイナリ
 * @param string $filename ダウンロード時のファイル名
 */
function funbrew_output_pdf( $pdf_data, $filename = 'document.pdf' ) {
    // WordPress が出力バッファリングしている場合はクリア
    if ( ob_get_length() ) {
        ob_end_clean();
    }

    header( 'Content-Type: application/pdf' );
    header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
    header( 'Content-Length: ' . strlen( $pdf_data ) );
    header( 'Cache-Control: private, max-age=0, must-revalidate' );

    echo $pdf_data; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    exit;
}

ショートコード実装([funbrew_pdf])

ショートコードを使えば、投稿や固定ページにPDFダウンロードボタンを簡単に設置できます。

ショートコードの登録

/**
 * [funbrew_pdf] ショートコード
 *
 * 使い方: [funbrew_pdf post_id="123" label="PDFをダウンロード"]
 */
function funbrew_pdf_shortcode( $atts ) {
    $atts = shortcode_atts(
        array(
            'post_id' => get_the_ID(),
            'label'   => 'PDFをダウンロード',
            'format'  => 'A4',
        ),
        $atts,
        'funbrew_pdf'
    );

    $post_id = absint( $atts['post_id'] );
    $label   = esc_html( $atts['label'] );

    if ( ! $post_id ) {
        return '';
    }

    // nonceフィールドを埋め込んだダウンロードリンクを生成
    $nonce = wp_create_nonce( 'funbrew_pdf_download_' . $post_id );
    $url   = add_query_arg(
        array(
            'action'   => 'funbrew_pdf_download',
            'post_id'  => $post_id,
            'format'   => esc_attr( $atts['format'] ),
            '_wpnonce' => $nonce,
        ),
        admin_url( 'admin-ajax.php' )
    );

    return sprintf(
        '<a href="%s" class="funbrew-pdf-download-btn" target="_blank">%s</a>',
        esc_url( $url ),
        $label
    );
}
add_shortcode( 'funbrew_pdf', 'funbrew_pdf_shortcode' );

AJAXハンドラーの実装

/**
 * AJAXリクエストを受け取ってPDFを生成・出力する
 */
function funbrew_pdf_ajax_handler() {
    // 1. nonceの検証
    $post_id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : 0;
    if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'funbrew_pdf_download_' . $post_id ) ) {
        wp_die( 'セキュリティチェックに失敗しました。', 403 );
    }

    // 2. 投稿の取得
    $post = get_post( $post_id );
    if ( ! $post || 'publish' !== $post->post_status ) {
        wp_die( '投稿が見つかりません。', 404 );
    }

    // 3. HTMLの生成
    $format = isset( $_GET['format'] ) ? sanitize_text_field( wp_unslash( $_GET['format'] ) ) : 'A4';
    $html   = funbrew_build_post_html( $post );

    // 4. PDF生成
    $pdf_data = funbrew_generate_pdf( $html, array( 'format' => $format ) );

    if ( is_wp_error( $pdf_data ) ) {
        wp_die( esc_html( $pdf_data->get_error_message() ), 500 );
    }

    // 5. ブラウザへ出力
    $filename = sanitize_file_name( $post->post_name . '.pdf' );
    funbrew_output_pdf( $pdf_data, $filename );
}
// ログインユーザー向け
add_action( 'wp_ajax_funbrew_pdf_download', 'funbrew_pdf_ajax_handler' );
// 非ログインユーザー向け(公開投稿のPDF化を許可する場合)
add_action( 'wp_ajax_nopriv_funbrew_pdf_download', 'funbrew_pdf_ajax_handler' );

投稿コンテンツからHTMLを生成する

/**
 * 投稿オブジェクトからPDF用HTMLを生成する
 *
 * @param WP_Post $post 投稿オブジェクト
 * @return string HTML文字列
 */
function funbrew_build_post_html( $post ) {
    $title   = get_the_title( $post );
    $content = apply_filters( 'the_content', $post->post_content );
    $date    = get_the_date( 'Y年n月j日', $post );

    $html = '<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <style>
    @import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap");

    body {
      font-family: "Noto Sans JP", "Hiragino Sans", "Yu Gothic", sans-serif;
      font-size: 14px;
      line-height: 1.8;
      color: #1a1a1a;
      padding: 40px 48px;
      max-width: 800px;
      margin: 0 auto;
    }
    h1 {
      font-size: 22px;
      font-weight: 700;
      border-bottom: 3px solid #1a56db;
      padding-bottom: 12px;
      margin-bottom: 8px;
    }
    .meta {
      color: #6b7280;
      font-size: 12px;
      margin-bottom: 32px;
    }
    h2 { font-size: 18px; margin-top: 32px; border-left: 4px solid #1a56db; padding-left: 10px; }
    h3 { font-size: 15px; margin-top: 24px; }
    img { max-width: 100%; height: auto; }
    table { width: 100%; border-collapse: collapse; margin: 16px 0; }
    th, td { border: 1px solid #e5e7eb; padding: 8px 12px; }
    th { background: #f9fafb; font-weight: 600; }
    pre { background: #f4f4f5; padding: 16px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
    code { background: #f4f4f5; padding: 2px 5px; border-radius: 3px; font-size: 12px; }
    blockquote { border-left: 4px solid #e5e7eb; margin: 0; padding-left: 16px; color: #6b7280; }
    .footer {
      margin-top: 48px;
      padding-top: 16px;
      border-top: 1px solid #e5e7eb;
      font-size: 11px;
      color: #9ca3af;
      text-align: center;
    }
  </style>
</head>
<body>
  <h1>' . esc_html( $title ) . '</h1>
  <p class="meta">公開日: ' . esc_html( $date ) . '</p>
  <div class="content">' . $content . '</div>
  <div class="footer">Generated by FUNBREW PDF</div>
</body>
</html>';

    return $html;
}

ショートコードの使い方

投稿エディターやウィジェットに以下のように記述するだけで、PDFダウンロードボタンが表示されます。

[funbrew_pdf]
[funbrew_pdf label="この記事をPDFで保存"]
[funbrew_pdf post_id="123" label="カタログをダウンロード" format="A4"]

カスタム投稿タイプのPDF化

カスタム投稿タイプ(例:製品カタログ、案件管理)をPDF化する場合も、基本的な構成は同じです。ここでは「製品カタログ(product_catalog)」を例に解説します。

カスタム投稿タイプの登録

/**
 * 製品カタログ カスタム投稿タイプの登録
 */
function funbrew_register_product_catalog_cpt() {
    register_post_type(
        'product_catalog',
        array(
            'labels'       => array(
                'name'          => '製品カタログ',
                'singular_name' => '製品カタログ',
            ),
            'public'       => true,
            'has_archive'  => true,
            'supports'     => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
            'show_in_rest' => true,
        )
    );
}
add_action( 'init', 'funbrew_register_product_catalog_cpt' );

カスタムフィールドを含むPDFテンプレート

/**
 * 製品カタログ専用のHTMLテンプレートを生成する
 *
 * @param WP_Post $post 製品カタログ投稿
 * @return string HTML文字列
 */
function funbrew_build_catalog_html( $post ) {
    // カスタムフィールドの取得
    $product_code  = get_post_meta( $post->ID, 'product_code', true );
    $price         = get_post_meta( $post->ID, 'price', true );
    $specifications = get_post_meta( $post->ID, 'specifications', true ); // JSON文字列
    $specs         = $specifications ? json_decode( $specifications, true ) : array();

    $thumbnail_url = get_the_post_thumbnail_url( $post, 'large' );
    $content       = apply_filters( 'the_content', $post->post_content );
    $title         = get_the_title( $post );

    ob_start();
    ?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <style>
    @import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap");
    body {
      font-family: "Noto Sans JP", sans-serif;
      font-size: 13px;
      color: #1a1a1a;
      padding: 40px;
    }
    .header {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      border-bottom: 3px solid #1a56db;
      padding-bottom: 20px;
      margin-bottom: 28px;
    }
    .header-left h1 {
      font-size: 22px;
      margin: 0 0 4px;
    }
    .product-code {
      color: #6b7280;
      font-size: 12px;
    }
    .product-image {
      max-width: 200px;
      max-height: 150px;
      object-fit: contain;
    }
    .specs-table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 20px;
    }
    .specs-table th, .specs-table td {
      border: 1px solid #e5e7eb;
      padding: 8px 12px;
    }
    .specs-table th {
      background: #f9fafb;
      width: 35%;
      font-weight: 600;
    }
    .price-block {
      background: #eff6ff;
      border: 1px solid #bfdbfe;
      border-radius: 6px;
      padding: 16px 20px;
      margin: 24px 0;
      font-size: 20px;
      font-weight: 700;
      color: #1d4ed8;
    }
    .description { line-height: 1.8; margin-top: 24px; }
  </style>
</head>
<body>
  <div class="header">
    <div class="header-left">
      <h1><?php echo esc_html( $title ); ?></h1>
      <?php if ( $product_code ) : ?>
        <div class="product-code">製品コード: <?php echo esc_html( $product_code ); ?></div>
      <?php endif; ?>
    </div>
    <?php if ( $thumbnail_url ) : ?>
      <img src="<?php echo esc_url( $thumbnail_url ); ?>" alt="<?php echo esc_attr( $title ); ?>" class="product-image">
    <?php endif; ?>
  </div>

  <?php if ( $price ) : ?>
    <div class="price-block">
      参考価格: ¥<?php echo number_format( floatval( $price ) ); ?>(税別)
    </div>
  <?php endif; ?>

  <?php if ( ! empty( $specs ) ) : ?>
    <h2>仕様</h2>
    <table class="specs-table">
      <tbody>
        <?php foreach ( $specs as $key => $value ) : ?>
          <tr>
            <th><?php echo esc_html( $key ); ?></th>
            <td><?php echo esc_html( $value ); ?></td>
          </tr>
        <?php endforeach; ?>
      </tbody>
    </table>
  <?php endif; ?>

  <div class="description"><?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
</body>
</html>
    <?php
    return ob_get_clean();
}

管理画面にPDF出力ボタンを追加

/**
 * 製品カタログ編集画面にPDF出力ボタンを追加する
 */
function funbrew_add_catalog_pdf_metabox() {
    add_meta_box(
        'funbrew_pdf_metabox',
        'PDF出力',
        'funbrew_catalog_pdf_metabox_html',
        'product_catalog',
        'side',
        'high'
    );
}
add_action( 'add_meta_boxes', 'funbrew_add_catalog_pdf_metabox' );

function funbrew_catalog_pdf_metabox_html( $post ) {
    $nonce = wp_create_nonce( 'funbrew_pdf_download_' . $post->ID );
    $url   = add_query_arg(
        array(
            'action'   => 'funbrew_pdf_download',
            'post_id'  => $post->ID,
            '_wpnonce' => $nonce,
        ),
        admin_url( 'admin-ajax.php' )
    );
    echo '<p><a href="' . esc_url( $url ) . '" target="_blank" class="button button-primary">PDFをダウンロード</a></p>';
    echo '<p class="description">現在の保存内容でPDFを生成します。</p>';
}

WooCommerceとの連携(注文書PDF)

WooCommerceの注文データをPDFに変換する機能は、BtoB ECサイトで特に需要が高い機能です。注文完了後に自動でPDF請求書をメール添付する方法を解説します。

注文データからHTMLを生成

/**
 * WooCommerce注文からPDF用HTMLを生成する
 *
 * @param WC_Order $order WooCommerce注文オブジェクト
 * @return string HTML文字列
 */
function funbrew_build_order_pdf_html( $order ) {
    $order_id      = $order->get_id();
    $order_date    = wc_format_datetime( $order->get_date_created() );
    $billing_name  = $order->get_formatted_billing_full_name();
    $billing_addr  = $order->get_formatted_billing_address();
    $items         = $order->get_items();
    $subtotal      = $order->get_subtotal();
    $total_tax     = $order->get_total_tax();
    $total         = $order->get_total();
    $payment_method = $order->get_payment_method_title();

    ob_start();
    ?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <style>
    @import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap");
    body {
      font-family: "Noto Sans JP", sans-serif;
      font-size: 13px;
      color: #1a1a1a;
      padding: 48px;
    }
    .invoice-header {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      margin-bottom: 40px;
    }
    .invoice-title {
      font-size: 28px;
      font-weight: 700;
      color: #1a56db;
      margin: 0;
    }
    .invoice-meta { text-align: right; font-size: 12px; color: #6b7280; }
    .billing-address {
      background: #f9fafb;
      border: 1px solid #e5e7eb;
      border-radius: 6px;
      padding: 16px 20px;
      margin-bottom: 32px;
    }
    .billing-address strong { font-size: 15px; display: block; margin-bottom: 4px; }
    table {
      width: 100%;
      border-collapse: collapse;
      margin-bottom: 24px;
    }
    thead th {
      background: #1a56db;
      color: #fff;
      padding: 10px 12px;
      text-align: left;
      font-size: 12px;
    }
    tbody td {
      padding: 10px 12px;
      border-bottom: 1px solid #e5e7eb;
    }
    .text-right { text-align: right; }
    .totals-table {
      width: 280px;
      margin-left: auto;
      border-collapse: collapse;
    }
    .totals-table td {
      padding: 6px 12px;
      border-bottom: 1px solid #e5e7eb;
    }
    .totals-table .grand-total td {
      font-size: 16px;
      font-weight: 700;
      color: #1a56db;
      border-top: 2px solid #1a56db;
    }
    .footer {
      margin-top: 48px;
      font-size: 11px;
      color: #9ca3af;
      text-align: center;
      border-top: 1px solid #e5e7eb;
      padding-top: 16px;
    }
  </style>
</head>
<body>
  <div class="invoice-header">
    <h1 class="invoice-title">請求書</h1>
    <div class="invoice-meta">
      <p>注文番号: #<?php echo esc_html( $order_id ); ?></p>
      <p>注文日: <?php echo esc_html( $order_date ); ?></p>
      <p>お支払い方法: <?php echo esc_html( $payment_method ); ?></p>
    </div>
  </div>

  <div class="billing-address">
    <strong><?php echo esc_html( $billing_name ); ?> 様</strong>
    <?php echo wp_kses_post( nl2br( $billing_addr ) ); ?>
  </div>

  <table>
    <thead>
      <tr>
        <th>商品名</th>
        <th class="text-right">数量</th>
        <th class="text-right">単価</th>
        <th class="text-right">小計</th>
      </tr>
    </thead>
    <tbody>
      <?php foreach ( $items as $item ) : ?>
        <?php
        $product  = $item->get_product();
        $quantity = $item->get_quantity();
        $subtotal_item = $item->get_subtotal();
        $unit_price    = ( $quantity > 0 ) ? $subtotal_item / $quantity : 0;
        ?>
        <tr>
          <td><?php echo esc_html( $item->get_name() ); ?></td>
          <td class="text-right"><?php echo esc_html( $quantity ); ?></td>
          <td class="text-right">¥<?php echo number_format( $unit_price ); ?></td>
          <td class="text-right">¥<?php echo number_format( $subtotal_item ); ?></td>
        </tr>
      <?php endforeach; ?>
    </tbody>
  </table>

  <table class="totals-table">
    <tr>
      <td>小計</td>
      <td class="text-right">¥<?php echo number_format( $subtotal ); ?></td>
    </tr>
    <tr>
      <td>消費税</td>
      <td class="text-right">¥<?php echo number_format( $total_tax ); ?></td>
    </tr>
    <tr class="grand-total">
      <td>合計</td>
      <td class="text-right">¥<?php echo number_format( $total ); ?></td>
    </tr>
  </table>

  <div class="footer">このPDFはFUNBREW PDFによって生成されました。</div>
</body>
</html>
    <?php
    return ob_get_clean();
}

注文完了メールにPDFを添付する

/**
 * WooCommerce注文完了メールにPDF請求書を添付する
 *
 * @param array    $attachments 現在の添付ファイルパスの配列
 * @param string   $email_id    メールID
 * @param WC_Order $order       注文オブジェクト
 * @return array 添付ファイルパスの配列
 */
function funbrew_attach_invoice_to_order_email( $attachments, $email_id, $order ) {
    // 注文完了メールにのみ添付
    if ( 'customer_completed_order' !== $email_id ) {
        return $attachments;
    }

    if ( ! ( $order instanceof WC_Order ) ) {
        return $attachments;
    }

    $html     = funbrew_build_order_pdf_html( $order );
    $pdf_data = funbrew_generate_pdf( $html, array( 'format' => 'A4' ) );

    if ( is_wp_error( $pdf_data ) ) {
        // エラーをログに記録してメール送信は続行
        error_log( 'FUNBREW PDF Error: ' . $pdf_data->get_error_message() );
        return $attachments;
    }

    // 一時ファイルに保存(メール送信後に削除)
    $tmp_file = wp_tempnam( 'invoice-' . $order->get_id() . '.pdf' );
    file_put_contents( $tmp_file, $pdf_data ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents

    $attachments[] = $tmp_file;
    return $attachments;
}
add_filter( 'woocommerce_email_attachments', 'funbrew_attach_invoice_to_order_email', 10, 3 );

管理画面の注文一覧に「PDF出力」列を追加

/**
 * 注文一覧にPDF出力列を追加する
 */
function funbrew_add_orders_pdf_column( $columns ) {
    $new_columns = array();
    foreach ( $columns as $key => $value ) {
        $new_columns[ $key ] = $value;
        if ( 'order_total' === $key ) {
            $new_columns['pdf_invoice'] = 'PDF請求書';
        }
    }
    return $new_columns;
}
add_filter( 'manage_woocommerce_page_wc-orders_columns', 'funbrew_add_orders_pdf_column' );

/**
 * PDF出力列の内容を出力する
 */
function funbrew_orders_pdf_column_content( $column, $order ) {
    if ( 'pdf_invoice' !== $column ) {
        return;
    }
    $nonce = wp_create_nonce( 'funbrew_pdf_download_order_' . $order->get_id() );
    $url   = add_query_arg(
        array(
            'action'   => 'funbrew_order_pdf_download',
            'order_id' => $order->get_id(),
            '_wpnonce' => $nonce,
        ),
        admin_url( 'admin-ajax.php' )
    );
    echo '<a href="' . esc_url( $url ) . '" target="_blank" class="button">PDF</a>';
}
add_action( 'manage_woocommerce_page_wc-orders_custom_column', 'funbrew_orders_pdf_column_content', 10, 2 );

/**
 * 管理画面からの注文PDF生成AJAXハンドラー
 */
function funbrew_order_pdf_ajax_handler() {
    // capability check: shop_manager以上のみ許可
    if ( ! current_user_can( 'manage_woocommerce' ) ) {
        wp_die( '権限がありません。', 403 );
    }

    $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;

    if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'funbrew_pdf_download_order_' . $order_id ) ) {
        wp_die( 'セキュリティチェックに失敗しました。', 403 );
    }

    $order = wc_get_order( $order_id );
    if ( ! $order ) {
        wp_die( '注文が見つかりません。', 404 );
    }

    $html     = funbrew_build_order_pdf_html( $order );
    $pdf_data = funbrew_generate_pdf( $html );

    if ( is_wp_error( $pdf_data ) ) {
        wp_die( esc_html( $pdf_data->get_error_message() ), 500 );
    }

    funbrew_output_pdf( $pdf_data, 'order-' . $order_id . '.pdf' );
}
add_action( 'wp_ajax_funbrew_order_pdf_download', 'funbrew_order_pdf_ajax_handler' );

REST APIエンドポイントの追加

ヘッドレスWordPressや外部アプリとの連携には、REST APIエンドポイントを追加するのが最適です。

/**
 * PDF生成REST APIエンドポイントの登録
 */
function funbrew_register_pdf_rest_routes() {
    // 投稿のPDF生成(認証必要)
    register_rest_route(
        'funbrew/v1',
        '/pdf/post/(?P<id>\d+)',
        array(
            'methods'             => 'GET',
            'callback'            => 'funbrew_rest_generate_post_pdf',
            'permission_callback' => 'funbrew_rest_permission_check',
            'args'                => array(
                'id'     => array(
                    'required'          => true,
                    'type'              => 'integer',
                    'sanitize_callback' => 'absint',
                ),
                'format' => array(
                    'default'           => 'A4',
                    'type'              => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                    'enum'              => array( 'A4', 'A3', 'Letter', 'Legal' ),
                ),
            ),
        )
    );

    // カスタムHTMLからPDF生成(管理者のみ)
    register_rest_route(
        'funbrew/v1',
        '/pdf/html',
        array(
            'methods'             => 'POST',
            'callback'            => 'funbrew_rest_generate_html_pdf',
            'permission_callback' => function () {
                return current_user_can( 'manage_options' );
            },
            'args'                => array(
                'html'     => array(
                    'required' => true,
                    'type'     => 'string',
                ),
                'filename' => array(
                    'default' => 'document.pdf',
                    'type'    => 'string',
                ),
                'format'   => array(
                    'default' => 'A4',
                    'type'    => 'string',
                ),
            ),
        )
    );
}
add_action( 'rest_api_init', 'funbrew_register_pdf_rest_routes' );

/**
 * 投稿PDF生成のREST APIハンドラー
 *
 * @param WP_REST_Request $request
 * @return WP_REST_Response|WP_Error
 */
function funbrew_rest_generate_post_pdf( $request ) {
    $post_id = $request->get_param( 'id' );
    $format  = $request->get_param( 'format' );

    $post = get_post( $post_id );
    if ( ! $post || 'publish' !== $post->post_status ) {
        return new WP_Error( 'not_found', '投稿が見つかりません。', array( 'status' => 404 ) );
    }

    $html     = funbrew_build_post_html( $post );
    $pdf_data = funbrew_generate_pdf( $html, array( 'format' => $format ) );

    if ( is_wp_error( $pdf_data ) ) {
        return new WP_Error( 'pdf_failed', $pdf_data->get_error_message(), array( 'status' => 502 ) );
    }

    // PDFをbase64エンコードしてJSONで返す
    return rest_ensure_response(
        array(
            'post_id'  => $post_id,
            'filename' => $post->post_name . '.pdf',
            'pdf'      => base64_encode( $pdf_data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
            'size'     => strlen( $pdf_data ),
        )
    );
}

/**
 * カスタムHTMLのPDF生成RESEセT APIハンドラー
 */
function funbrew_rest_generate_html_pdf( $request ) {
    $html     = $request->get_param( 'html' );
    $filename = sanitize_file_name( $request->get_param( 'filename' ) );
    $format   = $request->get_param( 'format' );

    $pdf_data = funbrew_generate_pdf( $html, array( 'format' => $format ) );

    if ( is_wp_error( $pdf_data ) ) {
        return new WP_Error( 'pdf_failed', $pdf_data->get_error_message(), array( 'status' => 502 ) );
    }

    return rest_ensure_response(
        array(
            'filename' => $filename,
            'pdf'      => base64_encode( $pdf_data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
            'size'     => strlen( $pdf_data ),
        )
    );
}

/**
 * REST APIの認証チェック
 */
function funbrew_rest_permission_check() {
    // ログインユーザー、または公開コンテンツへのアクセスを許可
    if ( is_user_logged_in() ) {
        return true;
    }
    // 非ログインユーザーには公開投稿のみ許可
    return true; // 必要に応じて `return is_user_logged_in();` に変更
}

REST APIの使い方

# 投稿ID 123 のPDFを取得(認証あり)
curl -H "Authorization: Bearer YOUR_WP_JWT_TOKEN" \
  "https://your-site.com/wp-json/funbrew/v1/pdf/post/123?format=A4"

# カスタムHTMLからPDFを生成(管理者のみ)
curl -X POST \
  -H "Authorization: Bearer YOUR_WP_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>テスト</h1>","filename":"test.pdf"}' \
  "https://your-site.com/wp-json/funbrew/v1/pdf/html"

セキュリティ:nonce・capability check

WordPressプラグイン開発でのセキュリティは非常に重要です。PDF生成のような処理では特に注意が必要です。

なぜnonceが必要か

nonceはCSRF(クロスサイトリクエストフォージェリ)攻撃を防ぐためのトークンです。WordPressのnoneは「1時間有効なランダム文字列」で、セッションとアクションに紐づいています。nonce検証なしで外部からAJAXエンドポイントに直接リクエストできてしまうと、攻撃者がユーザーを騙してPDF生成させたり、大量リクエストでAPIキーを使い果たしたりする可能性があります。

// nonce生成(フォームやリンクに埋め込む)
$nonce = wp_create_nonce( 'funbrew_pdf_action_' . $post_id );

// nonce検証(AJAXハンドラーやフォーム処理の先頭で必ず行う)
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'funbrew_pdf_action_' . $post_id ) ) {
    wp_die( 'セキュリティチェックに失敗しました。', 403 );
}

capability checkの実装

WordPressのcapability(権限)システムを使って、誰がPDFを生成できるかを制御します。

// 管理者のみ
if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( '権限がありません。', 403 );
}

// 投稿の編集権限がある場合のみ
if ( ! current_user_can( 'edit_post', $post_id ) ) {
    wp_die( '権限がありません。', 403 );
}

// WooCommerceのショップマネージャー以上
if ( ! current_user_can( 'manage_woocommerce' ) ) {
    wp_die( '権限がありません。', 403 );
}

// 非ログインユーザーに公開投稿のPDFのみ許可する場合
$post = get_post( $post_id );
if ( 'publish' !== $post->post_status && ! current_user_can( 'read_private_posts' ) ) {
    wp_die( 'この投稿はまだ公開されていません。', 403 );
}

APIキーをオプションページで管理する

APIキーをコードにハードコーディングするのではなく、WordPressの管理画面で設定できるようにするのがベストプラクティスです。

/**
 * FUNBREW PDF 設定ページの登録
 */
function funbrew_register_settings_page() {
    add_options_page(
        'FUNBREW PDF 設定',
        'FUNBREW PDF',
        'manage_options',
        'funbrew-pdf-settings',
        'funbrew_settings_page_html'
    );
}
add_action( 'admin_menu', 'funbrew_register_settings_page' );

function funbrew_register_settings() {
    register_setting(
        'funbrew_pdf_settings',
        'funbrew_pdf_api_key',
        array(
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => '',
        )
    );
}
add_action( 'admin_init', 'funbrew_register_settings' );

function funbrew_settings_page_html() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    ?>
    <div class="wrap">
        <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
        <form method="post" action="options.php">
            <?php settings_fields( 'funbrew_pdf_settings' ); ?>
            <table class="form-table">
                <tr>
                    <th scope="row">
                        <label for="funbrew_pdf_api_key">APIキー</label>
                    </th>
                    <td>
                        <input
                            type="password"
                            id="funbrew_pdf_api_key"
                            name="funbrew_pdf_api_key"
                            value="<?php echo esc_attr( get_option( 'funbrew_pdf_api_key' ) ); ?>"
                            class="regular-text"
                        />
                        <p class="description">
                            <a href="https://pdf.funbrew.cloud/dashboard" target="_blank">ダッシュボード</a>からAPIキーを取得してください。
                        </p>
                    </td>
                </tr>
            </table>
            <?php submit_button(); ?>
        </form>
    </div>
    <?php
}

入力サニタイズのチェックリスト

処理 使用する関数
整数値の取得 absint( $_GET['post_id'] )
テキストのサニタイズ sanitize_text_field()
URLのサニタイズ esc_url_raw()
HTML出力のエスケープ esc_html(), esc_attr()
URLの出力 esc_url()
nonce検証 wp_verify_nonce()
ファイル名 sanitize_file_name()

詳細なセキュリティ対策についてはセキュリティガイドを参照してください。

プラグインとしてまとめる

これまでの関数をWordPressプラグインとしてパッケージングする場合の基本構成を示します。

wp-content/plugins/funbrew-pdf/
├── funbrew-pdf.php          # メインプラグインファイル
├── includes/
│   ├── class-pdf-generator.php    # PDF生成クラス
│   ├── class-shortcode.php        # ショートコード
│   ├── class-woocommerce.php      # WooCommerce連携
│   ├── class-rest-api.php         # REST API
│   └── class-admin-settings.php  # 管理画面設定
└── assets/
    └── css/
        └── download-btn.css
<?php
/**
 * Plugin Name: FUNBREW PDF Generator
 * Description: FUNBREW PDF APIを使ってWordPressのコンテンツをPDF化します。
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL v2 or later
 * Text Domain: funbrew-pdf
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // WordPress外からの直接アクセスを防ぐ
}

define( 'FUNBREW_PDF_PLUGIN_VERSION', '1.0.0' );
define( 'FUNBREW_PDF_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'FUNBREW_PDF_API_ENDPOINT', 'https://api.pdf.funbrew.cloud/v1/pdf/from-html' );

// クラスファイルの読み込み
require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-pdf-generator.php';
require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-shortcode.php';
require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-rest-api.php';
require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-admin-settings.php';

// WooCommerceが有効な場合のみ読み込む
if ( class_exists( 'WooCommerce' ) ) {
    require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-woocommerce.php';
}

パフォーマンスと本番運用のポイント

トランジェントキャッシュによる重複生成の防止

同じ投稿のPDFが頻繁に要求される場合は、WordPressのTransients APIでキャッシュを使うことでAPIコール数を削減できます。

/**
 * トランジェントキャッシュを使ったPDF生成
 *
 * @param WP_Post $post     投稿オブジェクト
 * @param bool    $force    キャッシュを無視して再生成するか
 * @return string|WP_Error
 */
function funbrew_get_cached_pdf( $post, $force = false ) {
    $cache_key = 'funbrew_pdf_' . $post->ID . '_' . $post->post_modified_gmt;

    if ( ! $force ) {
        $cached = get_transient( $cache_key );
        if ( false !== $cached ) {
            return $cached;
        }
    }

    $html     = funbrew_build_post_html( $post );
    $pdf_data = funbrew_generate_pdf( $html );

    if ( ! is_wp_error( $pdf_data ) ) {
        // 投稿が更新されるまで12時間キャッシュ
        set_transient( $cache_key, $pdf_data, 12 * HOUR_IN_SECONDS );
    }

    return $pdf_data;
}

/**
 * 投稿更新時にPDFキャッシュを削除する
 */
function funbrew_clear_pdf_cache_on_update( $post_id, $post ) {
    // 古いキャッシュキーを削除(全バリエーション)
    global $wpdb;
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
            '_transient_funbrew_pdf_' . $post_id . '_%'
        )
    );
}
add_action( 'save_post', 'funbrew_clear_pdf_cache_on_update', 10, 2 );

タイムアウトとリトライ

/**
 * リトライ付きPDF生成
 *
 * @param string $html      HTML
 * @param array  $options   オプション
 * @param int    $max_retry 最大リトライ回数
 * @return string|WP_Error
 */
function funbrew_generate_pdf_with_retry( $html, $options = array(), $max_retry = 3 ) {
    $last_error = null;

    for ( $attempt = 1; $attempt <= $max_retry; $attempt++ ) {
        $result = funbrew_generate_pdf( $html, $options );

        if ( ! is_wp_error( $result ) ) {
            return $result;
        }

        $last_error = $result;

        // 最終試行でなければ待機(指数バックオフ)
        if ( $attempt < $max_retry ) {
            sleep( $attempt ); // 1秒、2秒、... と待機
        }
    }

    return $last_error;
}

本番環境での包括的な運用ノウハウはプロダクション運用ガイドにまとめています。大量のPDFを一括生成する場合はPDF一括生成ガイドも参照してください。

まとめ

WordPressへのFUNBREW PDF API統合のポイントをまとめます。

  1. wp_remote_post()を使う — WordPressの標準HTTP APIで外部リクエストをシンプルに実装する
  2. nonceで認証する — AJAX・フォーム・AJAXエンドポイントにnonce検証を必ず実装する
  3. capability checkで権限を確認するcurrent_user_can()でユーザー権限を適切にチェックする
  4. WooCommerceと連携するwoocommerce_email_attachmentsフィルターで注文完了メールへのPDF添付が簡単にできる
  5. REST APIで外部連携する — ヘッドレスWordPressやネイティブアプリとの連携に活用する
  6. キャッシュで最適化する — Transients APIでAPIコール数を削減する

まずはPlaygroundで自分のHTMLテンプレートがどんなPDFになるか試してみてください。準備ができたらドキュメントでAPIの全機能を確認し、料金プランで最適なプランを選びましょう。請求書PDF自動化の詳細は請求書自動化ガイドもあわせてご覧ください。

関連記事

Powered by FUNBREW PDF