2026年5月19日

2026年5月19日

Paid Memberships Proのエラーを解決する方法

はじめに

Paid Memberships Proで決済完了後にコンテンツへのアクセスが付与されない・会員登録フォームを送信しても確認メールが届かない・会員レベルをアップグレードしたが旧レベルのコンテンツにもアクセスできてしまう・サブスクリプションの更新が失敗してもアカウントが自動キャンセルされないといった問題は、チェックアウトフック・メール設定・レベル変更時のキャッシュが原因です。

症状・原因

  • Stripeで決済成功のメールが届くが会員ページに「アクセスが制限されています」と表示される
  • 登録フォーム送信直後は会員になれるが翌日にはアカウントが無効になっている
  • 無料トライアルから有料プランに移行したが旧コンテンツの制限が外れない
  • PayPalでサブスクリプションをキャンセルしてもWordPress側のステータスが更新されない

解決手順

ステップ1:Paid Memberships Proの状態を確認する

# PMPro設定確認
wp eval "
if (defined('PMPRO_VERSION')) {
    echo 'PMPro version: ' . PMPRO_VERSION . PHP_EOL;
}

// 会員レベル一覧を確認
\$levels = pmpro_getAllLevels(false, true);
echo 'Total levels: ' . count(\$levels) . PHP_EOL;

foreach (\$levels as \$level) {
    printf('  #%d: %s | Price: %s | Trial: %s days' . PHP_EOL,
        \$level->id,
        \$level->name,
        \$level->billing_amount,
        \$level->trial_limit ?? 0
    );
}

// アクティブな会員を確認
global \$wpdb;
\$active = \$wpdb->get_var(
    \"SELECT COUNT(*) FROM {\$wpdb->pmpro_memberships_users}
     WHERE status = 'active'\"
);
echo 'Active members: ' . \$active . PHP_EOL;

// 最近の注文を確認
\$orders = \$wpdb->get_results(
    \"SELECT id, user_id, membership_id, status, total, timestamp
     FROM {\$wpdb->pmpro_membership_orders}
     ORDER BY id DESC LIMIT 10\"
);
echo 'Recent orders:' . PHP_EOL;
foreach (\$orders as \$o) {
    printf('  #%d: user #%d | level %d | %s | %s | %s' . PHP_EOL,
        \$o->id, \$o->user_id, \$o->membership_id, \$o->status, \$o->total, \$o->timestamp);
}
"

ステップ2:チェックアウト後の処理をカスタマイズする

// functions.php: PMProチェックアウト処理

// ① チェックアウト完了後のカスタム処理
add_action('pmpro_after_checkout', function(int $user_id, object $order): void {
    $level_id = $order->membership_id;

    // 会員登録時刻を記録
    update_user_meta($user_id, '_pmpro_joined_at', current_time('mysql'));
    update_user_meta($user_id, '_pmpro_level_id', $level_id);

    // ウェルカムメールを送信
    $user  = get_userdata($user_id);
    $level = pmpro_getLevel($level_id);

    wp_mail(
        $user->user_email,
        sprintf('[%s] 会員登録ありがとうございます', get_bloginfo('name')),
        sprintf(
            "こんにちは %s さん、\n\n「%s」プランへの登録が完了しました。\n\n会員ページへ: %s",
            $user->display_name,
            $level->name,
            pmpro_url('member')
        )
    );

    error_log(sprintf('[PMPro] User #%d joined level #%d', $user_id, $level_id));
}, 10, 2);

// ② チェックアウトフォームのカスタムフィールド追加
add_action('pmpro_checkout_after_username', function(): void {
    $company = sanitize_text_field($_POST['pmpro_company'] ?? '');
    ?>
    <div class="pmpro_checkout-field">
        <label for="pmpro_company">会社名</label>
        <input type="text" id="pmpro_company" name="pmpro_company"
               value="<?php echo esc_attr($company); ?>"/>
    </div>
    <?php
});

add_action('pmpro_checkout_after_tos', function(object $order): void {
    $company = sanitize_text_field($_POST['pmpro_company'] ?? '');
    if (!empty($company)) {
        update_user_meta($order->user_id, 'company', $company);
    }
});

ステップ3:会員レベルの変更処理を修正する

// functions.php: レベル変更処理

// ① 会員レベル変更時の処理
add_action('pmpro_after_change_membership_level', function(int $level_id, int $user_id, int $old_level_id): void {
    // 旧レベルのキャッシュをクリア
    wp_cache_delete($user_id, 'user_meta');
    clean_user_cache($user_id);

    $new_level = pmpro_getLevel($level_id);
    $old_level = $old_level_id ? pmpro_getLevel($old_level_id) : null;

    error_log(sprintf('[PMPro] User #%d: level %s -> %s',
        $user_id,
        $old_level ? $old_level->name : 'none',
        $new_level ? $new_level->name : 'cancelled'
    ));

    // レベルに応じたWPロールを設定
    $user = new WP_User($user_id);
    if ($level_id === 0) {
        $user->remove_role('pmpro_member');
        $user->add_role('subscriber');
    } else {
        $user->remove_role('subscriber');
        $user->add_role('pmpro_member');
    }
}, 10, 3);

// ② コンテンツアクセス制御をカスタマイズ
add_filter('pmpro_has_membership_access_filter', function(array $hasaccess, WP_Post $post, WP_User $user, array $levels): array {
    // 管理者は常にアクセス可能
    if (user_can($user, 'manage_options')) {
        return [true, $user, $levels];
    }

    // 期限切れから24時間はグレースピリオドとして許可
    $expired_at = get_user_meta($user->ID, '_pmpro_expired_at', true);
    if ($expired_at && (time() - strtotime($expired_at)) < DAY_IN_SECONDS) {
        return [true, $user, $levels];
    }

    return $hasaccess;
}, 10, 4);

// ③ 会員期限切れ時の処理
add_action('pmpro_membership_post_membership_expiry', function(int $user_id, object $level): void {
    update_user_meta($user_id, '_pmpro_expired_at', current_time('mysql'));

    $user = get_userdata($user_id);
    wp_mail(
        $user->user_email,
        sprintf('[%s] 会員期限が切れました', get_bloginfo('name')),
        sprintf("こんにちは %s さん、\n\n「%s」プランの有効期限が切れました。\n\n更新はこちら: %s",
            $user->display_name,
            $level->name,
            pmpro_url('checkout') . '?level=' . $level->id
        )
    );
}, 10, 2);

ステップ4:PayPal/Stripe連携を修正する

# 決済ゲートウェイとサブスクリプションの確認
wp eval "
// 現在の決済ゲートウェイ設定を確認
\$gateway = get_option('pmpro_gateway', 'paypal');
echo 'Gateway: ' . \$gateway . PHP_EOL;

// サブスクリプション未更新の会員を確認
global \$wpdb;
\$expiring = \$wpdb->get_results(
    \"SELECT mu.user_id, mu.membership_id, mu.enddate, u.user_email
     FROM {\$wpdb->pmpro_memberships_users} mu
     JOIN {\$wpdb->users} u ON mu.user_id = u.ID
     WHERE mu.status = 'active'
     AND mu.enddate IS NOT NULL
     AND mu.enddate < DATE_ADD(NOW(), INTERVAL 7 DAY)
     AND mu.enddate > NOW()
     LIMIT 20\"
);

echo '7日以内に期限切れの会員: ' . count(\$expiring) . PHP_EOL;
foreach (\$expiring as \$member) {
    printf('  User #%d (%s): level %d, ends %s' . PHP_EOL,
        \$member->user_id, \$member->user_email,
        \$member->membership_id, \$member->enddate);
}

// 失敗した決済を確認
\$failed = \$wpdb->get_results(
    \"SELECT id, user_id, membership_id, status, gateway, timestamp
     FROM {\$wpdb->pmpro_membership_orders}
     WHERE status IN ('error', 'review')
     ORDER BY timestamp DESC LIMIT 10\"
);
echo 'Failed orders: ' . count(\$failed) . PHP_EOL;
foreach (\$failed as \$o) {
    printf('  #%d: user #%d | %s | %s | %s' . PHP_EOL,
        \$o->id, \$o->user_id, \$o->status, \$o->gateway, \$o->timestamp);
}
"

ステップ5:会員リマインダーと更新通知を設定する

// functions.php: リマインダー設定

// ① 更新リマインダーメールをカスタマイズ
add_filter('pmpro_email_body', function(string $body, object $email): string {
    if ($email->template !== 'membership_expiring') return $body;

    // 更新URLを追加
    $user     = get_userdata($email->data['user_id']);
    $level_id = $email->data['membership_id'];

    $renew_url = pmpro_url('checkout') . '?level=' . $level_id;
    $body     .= sprintf(
        "\n\n今すぐ更新する: %s\n\n更新すると%d%%割引が適用されます。",
        $renew_url,
        10
    );

    return $body;
}, 10, 2);

// ② サブスクリプション更新失敗時の処理
add_action('pmpro_subscription_payment_failed', function(object $order): void {
    $user  = get_userdata($order->user_id);
    $level = pmpro_getLevel($order->membership_id);

    // 管理者に通知
    wp_mail(
        get_option('admin_email'),
        sprintf('[PMPro] 決済失敗: %s (Level: %s)', $user->user_email, $level->name),
        sprintf("User #%d の決済が失敗しました。\n\nOrder: #%d\nGateway: %s\nError: %s",
            $order->user_id, $order->id, $order->gateway, $order->error ?? 'unknown')
    );

    update_user_meta($order->user_id, '_pmpro_payment_failed_at', current_time('mysql'));
});

// ③ IPN・Webhookのデバッグログを有効化
add_action('pmpro_ipn_log', function(string $message): void {
    error_log('[PMPro IPN] ' . $message);
});

注意事項

  • Paid Memberships ProとStripe/PayPalの連携が上手くいかない場合は「Memberships → ゲートウェイ設定」でWebhook URLが正しく設定されているか確認してください。本番環境ではhttps://のURLが必要で、ローカル環境のURLでは決済通知を受信できません
  • 会員レベル変更後にコンテンツアクセスが即座に反映されない場合はWordPressのオブジェクトキャッシュ(Redis/Memcached)が古い権限情報をキャッシュしている可能性があります。clean_user_cache()を呼び出してキャッシュをクリアしてください
  • 無料トライアルから有料移行の際に旧コンテンツにアクセスできてしまう問題はpmpro_has_membership_access_filterフィルターが複数のプラグインで設定されている場合に起きやすいです。優先度を確認してください

まとめ

Paid Memberships Pro修復は①pmpro_getAllLevels()で会員レベル確認・pmpro_membership_ordersテーブルで注文ステータスを確認、②pmpro_after_checkoutフックでウェルカムメール送信・会員登録時刻を記録、③pmpro_after_change_membership_levelフックでキャッシュクリア・WPロール更新・pmpro_has_membership_access_filterでグレースピリオド設定、④決済ゲートウェイ設定確認・期限切れ直前会員の特定・失敗注文の確認、⑤pmpro_email_bodyフィルターで更新リマインダーカスタマイズ・pmpro_subscription_payment_failedフックで失敗通知する手順で解決します。

お気軽にご相談ください

お見積りへ お問い合わせへ