モノノフ日記

普通の日記です

Jobeet - 15日目: フィード

Day 15: Feeds (1_2) - Symfony

昨日、最初のあなたのsymfonyアプリケーションを開発し始めました。ぜひ続けてください。symfonyをより学ぶために、アプリケーションに新しい要素を追加してみたり、コミュニティで知識を共有したりしてみましょう。

今日は全く別のことについての話をしましょう。

もし仕事を探しているのなら、ポストされた新しい仕事をできるだけ早く通知して欲しいと思うでしょう。1時間ごとにWebサイトをチェックするのは非常に不便であるので、Jobeetユーザが最新の情報を読めるようにいくつかのフィードを追加していきます。

フォーマット

symfonyフレームワークはフォーマットとmime-typesをネイティブサポートします。同じモデルとコントローラがリクエストされたフォーマットに基づいて異なったテンプレートを持つことができる、という事を意味します。デフォルトのフォーマットはHTMLですが、symfonyは難しい設定なしでtxt, js, css, json, xml, rdf, atomといった他のフォーマットもサポートしています。

フォーマットはリクエストオブジェクトのsetRequestFormat()メソッドを使って設定できます。

<?php
$request->setRequestFormat('xml');

しかしほとんどの場合、フォーマットはURLに組み込まれています。このケースでは、特別なsf_format変数が該当するルートで使われているのなら、symfonyはその設定を使います。仕事リスト用のURLは下記のようになります。

http://jobeet.localhost/frontend_dev.php/job

このURLは下記と等しくなります。

http://jobeet.localhost/frontend_dev.php/job.html

sfPropelRouteCollectionクラスで生成されたルートは拡張可能でデフォルトのフォーマットがHTMLであるsf_formatを持つので、両URLは等しくなります。app:routesタスクを実行することで自身でチェックすることができます。

f:id:Kiske:20090428190555p:image

フィード

最新の仕事フィード

異なったフォーマットをサポートするのは異なったテンプレートを作るのと同じくらい簡単です。最新投稿の仕事用のAtomフィードを作るために indexSuccess.atom.phpテンプレートを作ります。

<?php
<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php -->
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Jobeet</title>
  <subtitle>Latest Jobs</subtitle>
  <link href="" rel="self"/>
  <link href=""/>
  <updated></updated>
  <author><name>Jobeet</name></author>
  <id>Unique Id</id>

  <entry>
    <title>Job title</title>
    <link href="" />
    <id>Unique id</id>
    <updated></updated>
     <summary>Job description</summary>
     <author><name>Company</name></author>
  </entry>
</feed>
テンプレート名

HTML はWebアプリケーションで使われる最も共通のフォーマットであるので、テンプレート名からはHTMLの文字を除きます。 indexSuccess.phpとindexSuccess.html.phpテンプレートの両方は等しくなり、symfonyは最初に見つけた方を使います。デフォルトテンプレートの末尾にはなぜSuccessがついているのでしょう?アクションは表示するテンプレートを指示するための値を返すことができます。もしアクションが何も返さないなら、下記コードと等しくなります。

<?php
return sfView::SUCCESS; // == 'Success'

もし接尾辞を変えたいのなら、何か他のもを返してください

<?php
return sfView::ERROR; // == 'Error'

return 'Foo';

前日に見たやり方だとテンプレート名はsetTemplate()メソッドを使うことでも変更できました。

<?php
$this->setTemplate('foo');

デフォルトではsymfonyはレイアウトを無効にしてHTMLフォーマットで無い場合でも指定されたフォーマットと一致するようContent-Type のレスポンスを変化させます。AtomフィードではsymfonyはContent-Typeを application/atom+xml; charset=utf-8. に変更します。

Jobeetのフッターではフィードへのリンクを更新します。

<?php
<!-- apps/frontend/templates/layout.php -->
<li class="feed">
  <a href="<?php echo url_for('@job?sf_format=atom') ?>">Full feed</a>
</li>

内部URIは変数として追加されるsf_format付きの仕事リストと同義です。
フィードをブラウザが自動で見つけるようにレイアウトのheadセクションに<link>タグをを追加します。

<?php
<!-- apps/frontend/templates/layout.php -->
<link rel="alternate" type="application/atom+xml" title="Latest Jobs" href="<?php echo url_for('@job?sf_format=atom', true) ?>" />

linkのhref属性には、url_for()ヘルパーの第2引数のおかげで絶対パスのURLが使われます。
Atomテンプレートヘッダーを下記コードに置き換えます。

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php -->
<title>Jobeet</title>
<subtitle>Latest Jobs</subtitle>
<link href="<?php echo url_for('@job?sf_format=atom', true) ?>" rel="self"/>
<link href="<?php echo url_for('@homepage', true) ?>"/>
<updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', JobeetJobPeer::getLatestPost()->getCreatedAt('U')) ?></updated> <author>
  <name>Jobeet</name>
</author>
<id><?php echo sha1(url_for('@job?sf_format=atom', true)) ?></id>

タイムスタンプとして日付を取得するため、getCreatedAt()の引数にUを使っていることに注目してください。最新の投稿の日付を取得するためにgetLatestPost()メソッドを作ります。

<?php
// lib/model/JobeetJobPeer.php
class JobeetJobPeer extends BaseJobeetJobPeer
{
  static public function getLatestPost()
  {
    $criteria = new Criteria();
    self::addActiveJobsCriteria($criteria);
 
    return JobeetJobPeer::doSelectOne($criteria);
  }
 
  // ...
}

フィードエントリは下記コードから生成されます。

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php -->
<?php use_helper('Text') ?>
<?php foreach ($categories as $category): ?>
  <?php foreach ($category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $job): ?>
    <entry>
      <title>
        <?php echo $job->getPosition() ?> (<?php echo $job->getLocation() ?>)
      </title>
      <link href="<?php echo url_for('job_show_user', $job, true) ?>" />
      <id><?php echo sha1($job->getId()) ?></id>
      <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $job->getCreatedAt('U')) ?></updated>
      <summary type="xhtml">
       <div xmlns="http://www.w3.org/1999/xhtml">
         <?php if ($job->getLogo()): ?>
           <div>
             <a href="<?php echo $job->getUrl() ?>">
               <img src="http://<?php echo $sf_request->getHost().'/uploads/jobs/'.$job->getLogo() ?>"
                 alt="<?php echo $job->getCompany() ?> logo" />
             </a>
           </div>
         <?php endif; ?>
 
         <div>
           <?php echo simple_format_text($job->getDescription()) ?>
         </div>
 
         <h4>How to apply?</h4>
 
         <p><?php echo $job->getHowToApply() ?></p>
       </div>
      </summary>
      <author>
        <name><?php echo $job->getCompany() ?></name>
      </author>
    </entry>
  <?php endforeach; ?>
<?php endforeach; ?>

リクエストオブジェクト($sf_request)のgetHost()メソッドは現在のホスト名を返します。ホスト名は会社ロゴへの絶対パスでのリンクを作るのに便利です。

f:id:Kiske:20090428191540p:image

フィードを作るとき、curlwgetのようなコマンドラインツールを使えばフィードの実際のコンテンツが見れるので簡単にデバッグできます。

カテゴリーフィード内の最新の仕事

Jobeetのゴールの1つはより目標とした仕事を利用者が見つけれるようにすることです。そのため、各カテゴリーごとのフィードを提供することが必要となります。

まず始めに異なったフォーマットのサポートを追加するため、category routeを更新しましょう。

// apps/frontend/config/routing.yml
category:
  url:     /category/:slug.:sf_format
  class:   sfPropelRoute
  param:   { module: category, action: show, sf_format: html }
  options: { model: JobeetCategory, type: object }
  requirements:
    sf_format: (?:html|atom)

この段階では、category routeはhtmlとatomフォーマットの両方を理解することができます。テンプレート内のカテゴリーフィードへのリンクを更新します。

<!-- apps/frontend/modules/job/templates/indexSuccess.php -->
<div class="feed">
  <a href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom')) ?>">Feed</a>
</div>
 
<!-- apps/frontend/modules/category/templates/showSuccess.php -->
<div class="feed">
  <a href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom')) ?>">Feed</a>
</div>

最後のステップはshowSuccess.atom.phpテンプレートを作ることです。しかしこのフィードは仕事のリストであるため、 _list.atom.phpパーシャルを作ることでフィードエントリを生成するためのコードのリファクタリングをします。HTMLフォーマットに関して言うと、パーシャルは固有のフォーマットです。

<!-- apps/frontend/job/templates/_list.atom.php -->
<?php use_helper('Text') ?>
 
<?php foreach ($jobs as $job): ?>
  <entry>
    <title><?php echo $job->getPosition() ?> (<?php echo $job->getLocation() ?>)</title>
    <link href="<?php echo url_for('job_show_user', $job, true) ?>" />
    <id><?php echo sha1($job->getId()) ?></id>
      <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $job->getCreatedAt('U')) ?></updated>
    <summary type="xhtml">
     <div xmlns="http://www.w3.org/1999/xhtml">
       <?php if ($job->getLogo()): ?>
         <div>
           <a href="<?php echo $job->getUrl() ?>">
             <img src="http://<?php echo $sf_request->getHost().'/uploads/jobs/'.$job->getLogo() ?>"
               alt="<?php echo $job->getCompany() ?> logo" />
           </a>
         </div>
       <?php endif; ?>
 
       <div>
         <?php echo simple_format_text($job->getDescription()) ?>
       </div>
 
       <h4>How to apply?</h4>
 
       <p><?php echo $job->getHowToApply() ?></p>
     </div>
    </summary>
    <author>
      <name><?php echo $job->getCompany() ?></name>
    </author>
  </entry>
<?php endforeach; ?>

仕事のフィードテンプレートを単純化するために_list.atom.phpパーシャルを使います。

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php -->
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Jobeet</title>
  <subtitle>Latest Jobs</subtitle>
  <link href="<?php echo url_for('@job?sf_format=atom', true) ?>" rel="self"/>
  <link href="<?php echo url_for('@homepage', true) ?>"/>
  <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', JobeetJobPeer::getLatestPost()->getCreatedAt('U')) ?></updated>
  <author>
    <name>Jobeet</name>
  </author>
  <id><?php echo sha1(url_for('@job?sf_format=atom', true)) ?></id>
 
<?php foreach ($categories as $category): ?>
  <?php include_partial('job/list', array('jobs' => $category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?>
<?php endforeach; ?>
</feed>

最終的に、showSuccess.atom.phpテンプレートを作ります。

<!-- apps/frontend/modules/category/templates/showSuccess.atom.php -->
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Jobeet (<?php echo $category ?>)</title>
  <subtitle>Latest Jobs</subtitle>
  <link href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom'), true) ?>" rel="self" />
  <link href="<?php echo url_for('category', array('sf_subject' => $category), true) ?>" />
  <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $category->getLatestPost()->getCreatedAt('U')) ?></updated>
  <author>
    <name>Jobeet</name>
  </author>
  <id><?php echo sha1(url_for('category', array('sf_subject' => $category), true)) ?></id>
 
  <?php include_partial('job/list', array('jobs' => $pager->getResults())) ?>
</feed>

メインの仕事フィードに関して言うと、カテゴリーごとの日付順の最新の仕事が必要となります。

<?php
// lib/model/JobeetCategory.php
class JobeetCategory extends BaseJobeetCategory
{
  public function getLatestPost()
  {
    $jobs = $this->getActiveJobs(1);
 
    return $jobs[0];
  }
 
  // ...
}

f:id:Kiske:20090428191722p:image

また明日

symfonyの多くの要素のうち、ネイティブフォーマットサポートを適用することで簡単にWebサイトにフィードを追加しました。
今日は仕事を探す機能を強化しました。明日は仕事を投稿する側への素晴らしいサービスの作り方を見ていきます。