LaravelでRSSの配信してPubSubHubbubでPush、Twitterで通知する

Laravel5でRSS1.0とsitemap.xmlを配信してみます。

[Response::xml macro を作ったよ - localdisk](http://localdisk.hatenablog.com/entry/2014/01/27/Response%3A%3Axml_macro_%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%88)を参考にすると、名前空間と属性を使う必要のないRSS2.0と名前空間をルートに指定するだけでいいAtomであれば簡単に作成できるはずなので、わざわざRSS1.0にしなくても素直にAtomを使っておけばいいとおもう。

* [hbsnow-sandbox/blog-laravel5](https://github.com/hbsnow-sandbox/blog-laravel5)


## マイグレーション

```ps1
php artisan migrate:make articles_setup_table
```

公開日と`create_at`が等価なので公開日にあたるキーは作成しません。

```php
public function up()
{
    Schema::create('articles', function($table)
    {
        $table->increments('id')->unsigned();
        $table->string('title', 256);
        $table->string('slug', 128)->unique;
        $table->string('state', 16);
        $table->timestamps();
    });
}
```

RSS出力のために必要なものだけの最小の構成にしようと思ったんだけど、コントローラの一部メソッドの書き換えが面倒なので影響のない範囲でのギリギリの構成になっています。

`state`は記事の公開状態で、例えばあとでログインユーザのみに公開するような設定とかを追加したいときに困るので`boolean`ではありません。

```ps1
php artisan migrate
```

これでテーブルの作成が終了しました。

## コントローラ

```php
<?php namespace App\Http\Controllers\Blog;

use App\Http\Controllers\Controller;
use App\Eloquent\Blog\Article;

class RssController extends Controller {

    private $dom;

    /**
     * コンストラクタ
     *
     * @return void
     */
    public function __construct()
    {
        $root = 'ルートのURL';
        $rss = '出力するRSSのURL';
        $data = <<<XML
<rdf:RDF xml:lang="ja"
 xmlns="http://purl.org/rss/1.0/"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xmlns:atom="http://www.w3.org/2005/Atom"
 xmlns:dc="http://purl.org/dc/elements/1.1/">
    <channel rdf:about="{$rss}">
        <title>4uing</title>
        <link>{$root}</link>
        <atom:link href="{$rss}" rel="self" type="application/rss+xml" />
        <atom:link rel="hub" href="http://pubsubhubbub.appspot.com" />
        <description>プログラムやウェブ制作についての情報を配信しています。</description>
        <items><rdf:Seq/></items>
    </channel>
</rdf:RDF>
XML;

        $dom = new \DOMDocument;
        $dom->loadXML($data);
        $dom->encoding = 'UTF-8';
        $this->dom = $dom;
    }

    /**
     * RSSの表示
     *
     * @return Response
     */
    public function index()
    {
        return response($this->getXml(), 200)->header('Content-Type', 'application/xml');
    }

    /**
     * XMLの生成
     *
     * @return string
     */
    public function getXml()
    {
        // add Article
        $this->addArticle();

        return $this->dom->saveXML();
    }

    /**
     * 公開済みのブログの記事を追加
     *
     * @return DOMDocument
     */
    public function addArticle()
    {
        $articles = Article::where('state', 'public')->get();

        foreach($articles as $article)
        {
            $this->add($article);
        }

        return $this->dom;
    }

    /**
     * RSSにデータを追加する
     *
     * @param Article $article
     * @return DOMDocument
     */
    public function add($article)
    {
        $root = $this->dom->documentElement;
        $rdf_ns = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
        $channel = $root->getElementsByTagNameNS($rdf_ns, 'Seq')->item(0);

        $loc = route('blog.show', $article->slug);

        $li = $this->dom->createElementNS($rdf_ns, 'rdf:li');
        $li->setAttributeNS($rdf_ns, 'rdf:resource', $loc);
        $channel->appendChild($li);

        $item = $this->dom->createElement('item');
        $item->setAttributeNS($rdf_ns, 'rdf:about', $loc);
        $root->appendChild($item);

        $title = $this->dom->createElement('title');
        $title->appendChild($this->dom->createTextNode($article->title));
        $item->appendChild($title);

        $link = $this->dom->createElement('link');
        $link->appendChild($this->dom->createTextNode($loc));
        $item->appendChild($link);

        $date = $this->dom->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:date');
        $date->appendChild($this->dom->createTextNode($article->created_at->format('Y-m-d')));
        $item->appendChild($date);

        return $this->dom;
    }
}
```

## PubSubHubbub

せっかくなので[PubSubHubbub](http://pubsubhubbub.appspot.com/)でGoogleに更新情報をPushします。composerからインストールできるよさそうなライブラリがなかったので自分で作りました。

* [pshb](https://github.com/hbsnow/pshb)

`require`に`"hbsnow/pshb": "~1.0.1"`を追加して、`composer update`でインストールするだけです。

```php
/**
 * PSHB
 *
 * @return string
 */
private function pshb()
{
    $message = '';

    if (App::env('APP_ENV') !== 'local') {
        $pshb = new Publisher('http://pubsubhubbub.appspot.com/');
        $rss = route('blog.rss');

        if (! $pshb->update($rss)) {
            $message = 'PubSubHubBubのPostに失敗しました。';
        }
    }

    return $message;
}
```

こんな感じで使います。

## sitemap.xml

ついでに`sitemap.xml`も作りました。

```php
<?php namespace App\Http\Controllers;

use App\Eloquent\Blog\Article;

class SitemapController extends Controller {

    private $dom;
    private $main_contents;

    /**
     * コンストラクタ
     *
     * @return void
     */
    public function __construct()
    {
        $dom = new \DOMDocument;
        $dom->loadXML('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>');
        $dom->encoding = 'UTF-8';
        $this->dom = $dom;

        $this->main_contents = [
            'コンテンツURLの配列'
        ];
    }

    /**
     * サイトマップの表示
     *
     * @return Response
     */
    public function index()
    {
        return response($this->getXml(), 200)->header('Content-Type', 'application/xml');
    }

    /**
     * XMLの生成
     *
     * @return string
     */
    public function getXml()
    {
        // add Contents
        foreach ($this->main_contents as $value) {
            call_user_func_array(array($this, 'add'), $value);
        }

        // add Article
        $this->addArticle();

        return $this->dom->saveXML();
    }

    /**
     * 公開済みのブログの記事を追加
     *
     * @return DOMDocument
     */
    public function addArticle()
    {
        $articles = Article::where('state', 'public')->get();

        foreach($articles as $article)
        {
            $this->add('記事のURL');
        }

        return $this->dom;
    }

    /**
     * サイトマップにデータを追加する
     *
     * @param string $loc
     * @param string $changefreq
     * @return DOMDocument
     */
    public function add($loc, $changefreq = null)
    {
        $root = $this->dom->documentElement;

        // urlset > url
        $url_elem = $this->dom->createElement('url');
        $root->appendChild($url_elem);

        // url > loc
        $loc_elem = $this->dom->createElement('loc');
        $loc_elem->appendChild($this->dom->createTextNode($loc));
        $url_elem->appendChild($loc_elem);

        // url > changefreq
        if($changefreq !== null)
        {
            $changefreq_elem = $this->dom->createElement('changefreq');
            $changefreq_elem->appendChild($this->dom->createTextNode($changefreq));
            $url_elem->appendChild($changefreq_elem);
        }

        return $this->dom;
    }
}
```

処理の内容はRSSとほとんど同じです。

## Twitterで更新を通知する

Twitterでの更新の通知はRSSを使って[dlvr.it](https://dlvr.it/)や[Twibble.io](https://twibble.io/)のようなWebサービスをおこなえますが、サービス内容に変更があるかもしれないという不安はあるのでWebサービスに頼らずに作成しました。

[thujohn/twitter](https://github.com/thujohn/twitter)を使うのでcomposerでインストールし、ドキュメントの指示通り`config/app.php`に必要項目を追記して、追記後にコンフィグを生成します。

```ps1
php artisan vendor:publish
```

出力された設定ファイル`ttwitter.php`に[Twitter Apps](https://apps.twitter.com/)で作成した情報を入力する。確認のついでにTwitter側のパーミッション設定もRead and Writeに変更しておきます。

```php
/**
 * Tweet
 *
 * @param string $title
 * @param string $url
 * @return void
 */
private function tweet($title, $url)
{
    $status = '"' . $title . ' | ここにサイトタイトル" ' . $url;
    Twitter::postTweet(['status' => $status, 'format' => 'json']);
}
```

自分の使っているメソッドはこんな感じです。

## 参照文献

* [A Guide to Using Eloquent ORM in Laravel ♥ Scotch](http://scotch.io/tutorials/php/a-guide-to-using-eloquent-orm-in-laravel)
* [sitemaps.org - プロトコル](http://www.sitemaps.org/ja/protocol.html)