外部ブログサービスの記事をWordPressのカスタム投稿タイプに登録する

公式サイトはWordPressで、日常の出来事などを外部ブログサービス(Lekumo)で運用しているとあるサイト。最近公式サイトの投稿(最新情報)以外にもカスタム投稿タイプを追加して投稿を登録するようになり、サイトホームに情報を表示したいのですがなかなかスペースが取れません。そこで、サイトホームに「Topics」エリアを設け、公式サイトの投稿とカスタム投稿タイプ、そして外部ブログサービスの記事をまとめて新しい順に表示できないかと考えました。(全てWordPressで管理できると良いのですが、引き継いだサイトなので…)

とはいえ、普通に考えると外部ブログサービスの記事データはfetch_feedでRSSを取得して表示するしかないと思われます。そこで、cronで定期的にRSSを取得してカスタム投稿タイプに登録してしまおう、と考えました。そうすればWP_Queryでまとめて取得できるようになります。

この半年弱、PowerCMS X案件でPHPに真剣に向き合ってきたので、PHPを書くことが楽しくなりました。WordPressのカスタム投稿タイプに登録するコードも1時間ほどで書くことができました。

要点

RSS(atom.xml)はPHPのDOMDocumentクラスでパースします。entry要素をループ処理すれば最新の記事データを得ることができます。

$atom = file_get_contents( $url );
$doc = new DOMDocument();
$doc->loadXML( $atom );
$entries = $doc->getElementsByTagName( 'entry' );
foreach ( $entries as $entry ) {
    $title = $entry->getElementsByTagName( 'title' )[0]->nodeValue;
    $entry_unique_id = $entry->getElementsByTagName( 'id' )[0]->nodeValue;
    $published = new DateTime( $entry->getElementsByTagName( 'published' )[0]->nodeValue );
    $result[] = (object) array(
        'title'           => $title,
        'entry_unique_id' => $entry_unique_id,
        'published'       => $published,
    );
}

RSSから取得した記事が既にカスタム投稿タイプに登録されているか否かのチェックを行います。カスタムフィールドentry_unique_idにRSSの各記事のid要素値を格納する仕様にしたので、entry_unique_idを検索します。

$args = array(
    'post_type'  => 'lekumo',
    'meta_key'   => 'entry_unique_id',
    'meta_value' => $entry_unique_id
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
    return true;
}
return false;

RSSの記事がまだカスタム投稿タイプに登録されていない場合は登録を行います。update_post_metaを使用したけれどadd_post_metaで良かったかも。カスタムタクソノミーの指定やカスタムフィールドへの値の登録も容易でした。$entry->titleみたいに書きたくなるのはPowerCMS Xの作法の影響です。

$post_data = array(
    'post_type'   => 'lekumo',
    'post_status' => 'publish',
    'post_title'  => $entry->title,
    'post_date'   => $entry->published->format( 'Y-m-d H:i:s' ),
    'tax_input'   => array( 'lekumo_content' => $taxonomy_id )
);
$post_id = wp_insert_post( $post_data );
if ( $post_id ) {
    update_post_meta( $post_id, 'entry_unique_id', $entry->entry_unique_id );
    return true;
}

完成したコード

余談ですが「WEB+DB PRESS Vol.115」を読んでから型宣言を書くようになりました。簡単ですがドキュメントもしっかり残す習慣をつけました。

<?php
require_once( './wp/wp-load.php' );

class RegisterLekumoContents
{
    /**
     * エントリーが登録されているか否かのチェック
     *
     * @param string $entry_unique_id atom.xmlのfeed>entry>tagの値
     *
     * @return object|bool 投稿に存在する場合は投稿データ、存在しない場合はfalse
     */
    private function exist_post( string $entry_unique_id ) {
        $args = array(
            'post_type'   => 'lekumo',
            'post_status' => array( 'any', 'trash' ),
            'meta_key'    => 'entry_unique_id',
            'meta_value'  => $entry_unique_id
        );
        $query = new WP_Query( $args );

        if ( $query->have_posts() ) {
            $query->the_post();
            $updated = get_post_meta( get_the_ID(), 'date_updated', true );
            try {
                return (object) array(
                    'post_id'      => get_the_ID(),
                    'date_updated' => new DateTime( $updated ),
                );
            } catch ( Exception $e ) {
                trigger_error( '日付処理に失敗しました', E_USER_ERROR );
            }
        }

        return false;
    }

    /**
     * エントリーの登録・更新
     *
     * @param object $entry エントリーのデータ
     * @param int $taxonomy_id 投稿に設定するタクソノミーのID
     * @param int|null $post_id 投稿ID
     *
     * @return bool 登録の成否
     */
    private function register_post( object $entry, int $taxonomy_id, int $post_id = null ): bool {
        $post_data = array(
            'ID'          => $post_id,
            'post_type'   => 'lekumo',
            'post_status' => 'publish',
            'post_title'  => $entry->title,
            'post_date'   => $entry->published->format( 'Y-m-d H:i:s' ),
            'tax_input'   => array( 'lekumo_content' => $taxonomy_id )
        );
        $post_id = wp_insert_post( $post_data );

        if ( $post_id ) {
            update_post_meta( $post_id, 'entry_unique_id', $entry->entry_unique_id );
            update_post_meta( $post_id, 'date_updated', $entry->updated->format( 'Y-m-d H:i:s' ) );
            update_post_meta( $post_id, 'permalink', $entry->permalink );
            return true;
        }

        return false;
    }

    /**
     * atom.xmlのパース
     *
     * @param string $url atom.xmlのURL
     *
     * @return array
     */
    private function parse_atom( string $url ): array {
        $result = [];
        $atom = file_get_contents( $url );
        if ( $atom === false ) {
            trigger_error( 'Atomが取得できませんでした', E_USER_ERROR );
        }

        $doc = new DOMDocument();
        $doc->loadXML( $atom );
        $entries = $doc->getElementsByTagName( 'entry' );
        foreach ( $entries as $entry ) {
            $title = $entry->getElementsByTagName( 'title' )[0]->nodeValue;
            $entry_unique_id = $entry->getElementsByTagName( 'id' )[0]->nodeValue;
            try {
                $published = new DateTime( $entry->getElementsByTagName( 'published' )[0]->nodeValue );
                $updated = new DateTime( $entry->getElementsByTagName( 'updated' )[0]->nodeValue );
            } catch ( Exception $e ) {
                trigger_error( '日付処理に失敗しました', E_USER_ERROR );
            }
            $permalink = $entry->getElementsByTagName( 'link' )[0]->getAttribute( 'href' );
            $result[] = (object) array(
                'title'           => $title,
                'entry_unique_id' => $entry_unique_id,
                'published'       => $published,
                'updated'         => $updated,
                'permalink'       => $permalink,
            );
        }

        return $result;
    }

    public function run( string $url, int $taxonomy_id ) {
        $entries = $this->parse_atom( $url );
        foreach ( $entries as $entry ) {
            $post_data = $this->exist_post( $entry->entry_unique_id );
            if ( ! $post_data ) {
                $this->register_post( $entry, $taxonomy_id );
                echo "Entry \"{$entry->title}\" registered.", PHP_EOL;
            } else {
                $post_updated = $post_data->date_updated->format( 'YmdHis' );
                $entry_updated = $entry->updated->format( 'YmdHis' );
                if ( $post_updated < $entry_updated ) {
                    $this->register_post( $entry, $taxonomy_id, $post_data->post_id );
                    echo "Entry \"{$entry->title}\" updated.", PHP_EOL;
                }
            }
        }
    }
}

$registerLekumoContents = new RegisterLekumoContents();
$registerLekumoContents->run( 'https://example.com/blog_a/atom.xml' , 3 );
$registerLekumoContents->run( 'https://example.com/blog_b/atom.xml' , 4 );

今後

サイトリニューアルする場合は外部ブログサービスを使い続けるのか、情報をどう分類して格納していくのか、権限を細かく設定できるCMSを使えるようにするのか、等を検討する必要があるかと考えています。