WordPress の「最新の投稿n件」を「外部サイトで」表示させるために「JSON化してFTPで置く」という力技

# 事の発端
いま扱ってる案件において、
サイトA : WordPress で構築されたブログ系サイト
サイトB : 外部 EC サービス
という構成をとっている状況で、
「サイトA で書いた記事の最新5件を、サイトB のトップページにも表示したい」
という要望があった。

まぁ普通に考えたら サイトB になんか jQuery 書いて、
サイトAの該当ページからDOMまるっともらえばいいじゃん的なところだけど、
この案件で扱ってるのがちょっとした著名人の商材で、
以前テレビで取り扱われたときには大量のアクセスが発生し、
GoogleAnalytics のリアルタイムレポートで最大4,000ユーザを叩き出したので、
単純に「サイトB から サイトA の DOM 要素引っ張る」だと、
アクセスのバースト時に無駄な負荷をかけることになるから、それは避けたい。

できることなら サイトA の記事データをどこかにキャッシュしたい、
だが サイトB のECサービスは自作CGI等を動かすことができない、
じゃあどうする? と考えたところで達した結論。

① サイトA での記事更新時に最新記事一覧を JSON 化、サイトB へ FTP で送る
② サイトB では自サーバ上の JSON ファイルを参照しトップページに記事一覧を表示

という対応を取ることにした。

# WordPress側の対応

やるべきことは、
①-① 管理画面の記事更新時になんかしら関数を実行するよう仕込む
①-② その関数の中で最新5件を取得
①-③ 取得した情報をこねくりまわして JSON 化
①-④ JSON 化したキャッシュデータをECサービス側へ FTP で送信
というもの。

今回も現場の若い子向けにできるだけ処理を細かく分けてコメント書いたので、
基本的には読めばわかるようになってるはずなのでまるっと貼っちゃう。

```wp-config.php
// ###############################################
// 最新の投稿をECサービスに出すためのキャッシュファイルFTP設定
// ###############################################
define( "POSTCACHE_TYPE",		"post,news" );
define( "POSTCACHE_SIZE",		5 );
define( "POSTCACHE_FTP_HOST",	FTPサーバのFQDN );
define( "POSTCACHE_FTP_USER",	FTPユーザ名 );
define( "POSTCACHE_FTP_PASS",	FTPパスワード );
define( "POSTCACHE_FTP_PASV",	true );
define( "POSTCACHE_FTP_TSL",	true );
define( "POSTCACHE_FTP_DIR",	"js/post-loader/data" );
```
といった感じで設定値を定義してあげて、

```functions.php
/*----------------------------------------
投稿更新時にJSONをECサービスへFTP
----------------------------------------*/
function doSyncPostCache( $intPostID ) {
	// wp-config.php で定義したカンマ区切りを配列に分解して保持
	$arrPostCacheType			= preg_split( "/\,/", POSTCACHE_TYPE );

	// トリガーになった投稿を取得
	$objTrigger					= get_post( $intPostID );

	// 投稿タイプ取得
	$strPostCacheType			= $objTrigger->post_type;

	// 処理対象の投稿タイプ以外は無視
	if ( !in_array( $strPostCacheType, $arrPostCacheType ) ) {
		return false;
	}

	// wp-config.php で定義した件数を保持
	$intPostCacheSize			= POSTCACHE_SIZE;

	// 投稿一覧取得のパラメータ準備
	$arrPostParam				= [
		"sort_order"			=> "DESC",
		"sort_column"			=> "post_date",
		"hierachical"			=> 0,
		"exclude"				=> "",
		"include"				=> "",
		"meta_key"				=> "",
		"meta_value"			=> "",
		"authors"				=> "",
		"child_of"				=> 0,
		"parent"				=> -1,
		"exclude_tree"			=> "",
		"numberposts"			=> $intPostCacheSize,
		"offset"				=> 0,
		"post_type"				=> $strPostCacheType,
		"post_status"			=> "publish"
	];

	// キャッシュ更新の必要を判定する変数
	$flgPostCache				= false;

	// 対象タイプの投稿を取得
	$arrPost					= get_posts( $arrPostParam );

	// キャッシュ更新の必要を判定
	// ※ 更新したトリガー記事が対象に含まれてない = キャッシュ更新不要と判断
	foreach ( $arrPost as $objPost ) {
		if ( $objPost->ID == $objTrigger->ID ) {
			$flgPostCache		= true;
		}
	}

	// 更新不要なら終了
	if ( !$flgPostCache ) {
		return false;
	}

	// キャッシュファイル保存先ディレクトリ
	$strPostCacheDir			= get_template_directory() . "/json";

	// キャッシュファイル名設定
	$strPostCacheFile			= "{$strPostCacheType}.json";

	// サムネイルの URL から実ファイルの Path に変換する際に使う正規表現用
	$strHomeURL					= preg_quote( get_home_url() . "/", "/" );

	// キャッシュデータ初期化
	$objPostCache				= (object)[
		"strType"				=> $strPostCacheType,		// 投稿タイプ
		"strDateTimeCache"		=> date( "Y-m-d h:i:s" ),	// キャッシュ生成日時
		"arrPost"				=> [],						// 投稿の入れ物
	];

	// 対象タイプの投稿からキャッシュの中身を生成
	foreach ( $arrPost as $objPost ) {
		// 投稿オブジェクトを初期化
		$objPostCacheData		= (object)[
			"intID"				=> $objPost->ID,												// ID
			"strDateTimePub"	=> $objPost->post_date,											// 公開日時
			"strTitle"			=> apply_filters( "the_title",		$objPost->post_title ),		// 投稿タイトル
			"strContent"		=> apply_filters( "the_content",	$objPost->post_content ),	// 投稿本文
			"strURL"			=> get_permalink( $objPost, false ),							// 投稿のURL
			"binThumbnail"		=> ""															// アイキャッチの入れ物
		];

		// アイキャッチを取得
		if ( has_post_thumbnail( $objPost ) ) {
			// 画像IDを取得
			$intThumbnailID		= get_post_thumbnail_id( $objPost->ID );

			// 画像オブジェクトを取得
			$objThumbnail		= get_post( $intThumbnailID );

			// 画像情報配列を取得
			$arrThumbnailInfo	= wp_get_attachment_image_src( $intThumbnailID );

			// URL情報から実ファイルまでの Path を取得
			$strThumbnailPath	= preg_replace( "/$strHomeURL/", ABSPATH, $arrThumbnailInfo[0] );

			// 画像ファイルの中身をBase64エンコードしつつ投稿オブジェクトに保持
			$objPostCacheData->binThumbnail	= "data:{$objThumbnail->post_mime_type};base64," . base64_encode( file_get_contents( $strThumbnailPath ) );
		}

		// 投稿オブジェクトを配列に追加
		$objPostCache->arrPost[]= $objPostCacheData;
	}

	// キャッシュディレクトリがなかったら作成
	if ( !file_exists( $strPostCacheDir ) ) {
		mkdir( $strPostCacheDir );
	}

	// キャッシュファイルにキャッシュデータをJSONで保存
	file_put_contents( "{$strPostCacheDir}/{$strPostCacheFile}", json_encode( $objPostCache ) );

	// キャッシュファイルの保存に失敗してたら終了
	if ( !file_exists( "{$strPostCacheDir}/{$strPostCacheFile}" ) || filesize( "{$strPostCacheDir}/{$strPostCacheFile}" ) == 0 ) {
		return false;
	}

	// FTP接続開始
	if ( POSTCACHE_FTP_TSL ) {
		$objFTP					= ftp_ssl_connect( POSTCACHE_FTP_HOST );
	} else {
		$objFTP					= ftp_connect( POSTCACHE_FTP_HOST );
	}

	// FTPログイン
	$flgResultLogin				= ftp_login(
									$objFTP,
									POSTCACHE_FTP_USER,
									POSTCACHE_FTP_PASS
								);

	if ( $flgResultLogin ) {
		// PASVモードに設定
		if ( POSTCACHE_FTP_PASV ) {
			ftp_pasv( $objFTP, true );
		}
	
		// リモートディレクトリ移動
		foreach ( preg_split( "/\,/", POSTCACHE_FTP_DIR ) as $strDir ) {
			ftp_chdir( $objFTP, $strDir );
		}
	
		// ファイル送信
		$objResultTransfer		= ftp_put(
									$objFTP,
									$strPostCacheFile,
									"{$strPostCacheDir}/{$strPostCacheFile}",
									FTP_BINARY,
									false
								);
	}

	// FTP接続終了
	ftp_close( $objFTP );

	// おつかれさまでした
	return true;
}

// 投稿保存時のアクションに追加
add_action( "save_post", "doSyncPostCache" );
```
てな具合に実際の処理を記述した。


get_posts() に使えるパラメータとしてはこちらを参考にした。
[テンプレートタグ/get　posts - WordPress Codex 日本語版](https://wpdocs.osdn.jp/%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%BF%E3%82%B0/get_posts)

# ちょっとした工夫の解説
## 投稿タイプの判定
WordPress の管理画面で記事を保存すると内部的にいろんな処理が走るんだが、
今回は「最新5件のキャッシュ化」が目的なので、
新規も更新も含めてフックできる **save_post** を使うことにした。
その場合、プレビューや画像アップロード等でも反応してしまうので、
目的の更新でのみ処理を続行するようにした。

## キャッシュ対象の判定
また、目的の更新だったとしても、最新5件が影響を受けないような
昔の記事の編集とかだった場合には、キャッシュは更新する必要がないので、
「いま更新したのが最新5件に含まれているかどうか」も判定している。

## アイキャッチ画像をBase64で埋め込む
キャッシュ化と FTP 送信の目的自体が「元サイトへの負荷をかけない」ことなので、
サイトB で表示するときのアイキャッチ画像もキャッシュに含めるようにしている。
JSON ファイルと一緒に画像ファイルを FTP で送る手もあるけど、
そうすると最新5件が差し替わったときに古くていらなくなった画像の削除もしなきゃだし、
そのへんがめんどくさかったので Base64 化してタグに埋め込むことにした。
実際に表示する際は、

```
<img src="data:image/jpeg;base64,Base64エンコードした画像ファイルの中身">
```
といった具合に書いてあげれば画像が表示される。

## TSL によるセキュアな FTP 接続
今回は送信先の仕様が TLS 接続必須だったので、 **ftp_ssl_connect()**を使っている。

# おわりに
サイトB で表示するにあたっては、JSON を ajax で取得して〜というだけなのでここでは割愛。
ただ、今回の案件、リアルな接続先ECサービスが MakeShop だったので、
FTP でファイルを置いたサーバと jQuery を動かすページのサーバが別なため、
クロスオリジン的に JSON データを ajax で取得できない状況だったので、
ここに書いたものからさらにちょっと加工して、
「JSON データを元に連想配列を代入する JavaScript ファイル」としてキャッシュ生成している。
外部ECサービスでは各社いろいろな制限事項があるので、
こういった力技でも問題解決できるならまぁよし、というケースがちょこちょこありますよ。