15日目: Web サービス
Jobeet のフィード追加に関して、求職者はリアルタイムで新しい求人情報が知らされます。
一方では、求人を投稿するとき、できる限りもっとも注目されたいと願います。たくさんの小さな Web サイトで求人情報が配信される場合、適任者を見つける機会が増えます。これが~ロングテール~の力です。今日開発する ~Web サービス~のおかげでアフィリエイトは自身の Web サイトに投稿された最新の求人情報を公開できます。
~アフィリエイト~
2日目の要件です:
「ストーリー F7: アフィリエイトは現在アクティブな求人リストを検索する」
フィクスチャ
アフィリエイトのために新しい~フィクスチャ~ファイルを作りましょう:
[yml]
symfony:
url: http://www.symfony-project.org/
email: fabien.potencier@example.org
is_active: false
token: symfony
s
を追加した配列を定義することで簡単に実現できます。
フィクスチャファイルにおいて、テスト作業を簡略化するために~トークン~は決め打ちされていますが、実際のユーザーがアカウントを申し込むとき、トークンの生成が必要になります:
return parent::save($con);
}
// ...
}
return parent::save($conn);
}
// ...
}
データをリロードできます:
$ php symfony propel:data-load
求人情報の Web サービス
新しいリソースを作成するとき、最初に ~URL~ を定義するのはよい習慣です:
[yml]
# apps/frontend/config/routing.yml
api_jobs:
url: /api/:token/jobs.:sf_format
class: sfPropelRoute
param: { module: api, action: list }
options: { model: JobeetJob, type: list, method: getForToken }
requirements:
sf_format: (?:xml|json|yaml)
このルートに関して、特殊変数の ~sf_format
~ は URL で終わり、有効な値は xml
、json
もしくは yaml
です。
アクションがルートに関連するオブジェクトのコレクションを読み取るときに、getForToken()
メソッドが呼び出されます。アフィリエイトがアクティベートされることを確認する必要があるとき、ルートのデフォルトのふるまいをオーバーライドする必要があります:
return $affiliate->getActiveJobs();
}
// ...
}
return $affiliate->getActiveJobs();
}
// ...
}
トークンがデータベースに存在しない場合、sfError404Exception
の例外が投げられます。この例外クラスは自動的に~404|404エラー~
レスポンスに変換されます。これがモデルクラスから404
ページを生成するためのもっともシンプルな方法です。
getForToken()
メソッドは今新しく作る2つのメソッドを使います。
最初に、トークンを渡されたアフィリエイトを取得するために getByToken()
メソッドを作らなければなりません:
[php]
// lib/model/JobeetAffiliatePeer.php
class JobeetAffiliatePeer extends BaseJobeetAffiliatePeer
{
static public function getByToken($token)
{
$criteria = new Criteria();
$criteria->add(self::TOKEN, $token);
return self::doSelectOne($criteria);
}
}
それから、getActiveJobs()
メソッドはアフィリエイトによって選択されたカテゴリ用の現在アクティブな求人リストを返します:
getForToken()
メソッドは getActiveJobs()
という名前の新しいメソッドを使い現在アクティブな求人リストを返します:
$criteria = new Criteria();
$criteria->add(JobeetJobPeer::CATEGORY_ID, $categories, Criteria::IN);
JobeetJobPeer::addActiveJobsCriteria($criteria);
return JobeetJobPeer::doSelect($criteria);
}
// ...
}
$q = Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q);
return $q->execute();
}
// ...
}
最後のステップは api
アクションとテンプレートを作ることです。generate:module
タスクでモジュールをブートストラップします:
$ php symfony generate:module frontend api
NOTE デフォルトの
index
アクションを使わないので、アクションクラスからこれを除外し、関連するindexSucess.php
テンプレートを除外します。
アクション
すべてのフォーマットは同じ list
アクションを共有します:
[php]
// apps/frontend/modules/api/actions/actions.class.php
public function executeList(sfWebRequest $request)
{
$this->jobs = array();
foreach ($this->getRoute()->getObjects() as $job)
{
$this->jobs[$this->generateUrl('job_show_user', $job, true)] =
➥ $job->asArray($request->getHost());
}
}
テンプレートに JobeetJob
オブジェクトの配列を渡す代わりに、文字列の配列を渡します。同じアクションに対して3つの異なるテンプレートがあるので、値を処理するロジックは JobeetJob::asArray()
メソッドで取り除かれました:
[php]
// ...
}
xml
フォーマット
テンプレートを作れば xml
フォーマットを簡単にサポートできます:
[php]
<!-- apps/frontend/modules/api/templates/listSuccess.xml.php -->
<?xml version="1.0" encoding="utf-8"?>
<jobs>
<?php foreach ($jobs as $url => $job): ?>
<job url="<?php echo $url ?>">
<?php foreach ($job as $key => $value): ?>
<<?php echo $key ?>><?php echo $value ?></<?php echo $key ?>>
<?php endforeach ?>
</job>
<?php endforeach ?>
</jobs>
json
フォーマット
JSON フォーマットのサポートも同じです:
[php]
<!-- apps/frontend/modules/api/templates/listSuccess.json.php -->
[
<?php $nb = count($jobs); $i = 0; foreach ($jobs as $url => $job): ++$i ?>
{
"url": "<?php echo $url ?>",
<?php $nb1 = count($job); $j = 0; foreach ($job as $key => $value): ++$j ?>
"<?php echo $key ?>": <?php echo json_encode($value).($nb1 == $j ? '' : ',') ?>
<?php endforeach ?>
}<?php echo $nb == $i ? '' : ',' ?>
<?php endforeach ?>
]
~yaml
フォーマット|フォーマット (作成)~
組み込みのフォーマットに関して、Content-Type を変更する、レイアウトを無効にするなど、 symfony はバックグランドで同じコンフィギュレーションを変更 します。
YAML フォーマットは組み込みのリクエストフォーマットのリストにないので、レスポンスの Content-Type は変更可能でレイアウトはアクションでは無効です:
[php]
class apiActions extends sfActions
{
public function executeList(sfWebRequest $request)
{
$this->jobs = array();
foreach ($this->getRoute()->getObjects() as $job)
{
$this->jobs[$this->generateUrl('job_show_user', $job, true)] =
➥ $job->asArray($request->getHost());
}
switch ($request->getRequestFormat())
{
case 'yaml':
$this->setLayout(false);
$this->getResponse()->setContentType('text/yaml');
break;
}
}
}
アクションにおいて、setLayout()
メソッドはデフォルトの~レイアウト|レイアウト (無効)~を変更もしくは false
にセットされているときにそれを無効にします。
YAML 用のテンプレートは次の内容を読み込みます:
[php]
<!-- apps/frontend/modules/api/templates/listSuccess.yaml.php -->
<?php foreach ($jobs as $url => $job): ?>
-
url: <?php echo $url ?>
<?php foreach ($job as $key => $value): ?>
<?php echo $key ?>: <?php echo sfYaml::dump($value) ?>
<?php endforeach ?>
<?php endforeach ?>
有効ではないトークンで Web サービスを呼び出すとき、XML フォーマットの404ページ、JSON フォーマットの404ページを用意します。しかし YAML フォーマットに関して、symfony は何をレンダリングすればよいのかわかりません。
フォーマットを作成するとき、~カスタムエラーテンプレート~を作らなければなりません。テンプレートは404ページと他のすべての例外に使われます。
~例外|例外処理~は運用環境と開発環境で異なるので、2つのファイルが必要です (デバッグに config/error/exception.yaml.php
、運用環境に config/error/error.yaml.php
):
[php]
// config/error/exception.yaml.php
<?php echo sfYaml::dump(array(
'error' => array(
'code' => $code,
'message' => $message,
'debug' => array(
'name' => $name,
'message' => $message,
'traces' => $traces,
),
)), 4) ?>
// config/error/error.yaml.php
<?php echo sfYaml::dump(array(
'error' => array(
'code' => $code,
'message' => $message,
))) ?>
試す前に、YAML フォーマット用のレイアウトを作らなければなりません:
[php]
// apps/frontend/templates/layout.yaml.php
<?php echo $sf_content ?>
TIP 組み込みのテンプレートの404エラーと~例外|例外処理~をオーバーライドするのは簡単で、
config/error/
ディレクトリにファイルを作ります。
~Web サービスのテスト|テスト (Web サービス)~
Web サービスをテストするには、アフィリエイトフィクスチャを data/fixtures/
から test/fixtures/
ディレクトリにコピーして自動生成された apiActionsTest.php
ファイルの内容を次のものに置き換えます:
[php]
// test/functional/frontend/apiActionsTest.php
include(dirname(__FILE__).'/../../bootstrap/functional.php');
$browser = new JobeetTestFunctional(new sfBrowser());
$browser->loadData();
$browser->
info('1 - Web service security')->
info(' 1.1 - A token is needed to access the service')->
get('/api/foo/jobs.xml')->
with('response')->isStatusCode(404)->
info(' 1.2 - An inactive account cannot access the web service')->
get('/api/symfony/jobs.xml')->
with('response')->isStatusCode(404)->
info('2 - The jobs returned are limited to the categories configured for the affiliate')->
get('/api/sensio_labs/jobs.xml')->
with('request')->isFormat('xml')->
with('response')->begin()->
isValid()->
checkElement('job', 32)->
end()->
info('3 - The web service supports the JSON format')->
get('/api/sensio_labs/jobs.json')->
with('request')->isFormat('json')->
with('response')->matches('/"category"\: "Programming"/')->
info('4 - The web service supports the YAML format')->
get('/api/sensio_labs/jobs.yaml')->
with('response')->begin()->
isHeader('content-type', 'text/yaml; charset=utf-8')->
matches('/category\: Programming/')->
end()
;
このテストにおいて、2つの新しいメソッドに注目します:
isValid()
: XML レスポンスが妥当であるかどうかチェックしますisFormat()
: これはリクエストのフォーマットをテストしますmatches()
: HTML ではないフォーマットに対して、レスポンスが引数として渡される正規表現を検証します
アフィリエイトアプリケーションのフォーム
Web サービスを提供する準備ができたので、アフィリエイト用のアカウント作成フォームを作りましょう。再度新しい機能をアプリケーションに追加することで古典的なプロセスを説明します。
ルーティング
ご想像のとおり、最初に作るのは~ルート~です:
[yml]
# apps/frontend/config/routing.yml
affiliate:
class: sfPropelRouteCollection
options:
model: JobeetAffiliate
actions: [new, create]
object_actions: { wait: get }
これは新しい設定オプション: actions
をもつ古典的な ##ORM## コレクションルートです。ルートによって定義されるデフォルトの7つのアクションすべてが不要なので、actions
オプションは new
と create
アクションのみにマッチするようにルートに指示します。追加の wait
ルートはまもなくアフィリエイトになる人にアカウントに関するフィードバックをします。
ブートストラップする
2番目のステップはモジュールの生成です:
$ php symfony propel:generate-module frontend affiliate JobeetAffiliate --non-verbose-templates
テンプレート
propel:generate-module
タスクは古典的な7つのアクションとそれらに対応する~テンプレート~を生成します。templates/
ディレクトリにおいて、_form.php
と newSuccess.php
以外のすべてのファイルを削除します。維持するファイルに関して、これらの内容を次のものに置き換えます:
[php]
<!-- apps/frontend/modules/affiliate/templates/newSuccess.php -->
<?php use_stylesheet('job.css') ?>
<h1>Become an Affiliate</h1>
<?php include_partial('form', array('form' => $form)) ?>
<!-- apps/frontend/modules/affiliate/templates/_form.php -->
<?php include_stylesheets_for_form($form) ?>
<?php include_javascripts_for_form($form) ?>
<?php echo form_tag_for($form, 'affiliate') ?>
<table id="job_form">
<tfoot>
<tr>
<td colspan="2">
<input type="submit" value="Submit" />
</td>
</tr>
</tfoot>
<tbody>
<?php echo $form ?>
</tbody>
</table>
</form>
waitSuccess.php
テンプレートを作ります:
[php]
<!-- apps/frontend/modules/affiliate/templates/waitSuccess.php -->
<h1>Your affiliate account has been created</h1>
<div style="padding: 20px">
Thank you!
You will receive an email with your affiliate token
as soon as your account will be activated.
</div>
最後に、affiliate
モジュールを指し示すようにフッターのリンクを変更します:
[php]
// apps/frontend/templates/layout.php
<li class="last">
<a href="<?php echo url_for('affiliate_new') ?>">Become an affiliate</a>
</li>
アクション
ここで繰り返しますが、作成フォームのみを使うので、actions.class.php
ファイルを開き executeNew()
、executeCreate()
と processForm()
以外のすべてのメソッドを削除します。
processForm()
アクションに関して、リダイレクト URL を wait
アクションに変更します:
[php]
// apps/frontend/modules/affiliate/actions/actions.class.php
$this->redirect($this->generateUrl('affiliate_wait', $jobeet_affiliate));
wait
アクションはシンプルなのでテンプレートに何も渡さずに済みます:
[php]
// apps/frontend/modules/affiliate/actions/actions.class.php
public function executeWait(sfWebRequest $request)
{
}
アフィリエイトはトークンを選択したり、アカウントを即座にアクティベートできません。~フォーム~をカスタマイズするために JobeetAffiliateForm
ファイルを開きます:
[php]
$this->validatorSchema['jobeet_category_affiliate_list']->setOption('required', true);
$this->validatorSchema['jobeet_categories_list']->setOption('required', true);
$this->widgetSchema['url']->setLabel('Your website URL');
$this->widgetSchema['url']->setAttribute('size', 50);
$this->widgetSchema['email']->setAttribute('size', 50);
$this->validatorSchema['email'] = new sfValidatorEmail(array('required' => true));
}
}
新しい sfForm::useFields()
メソッドによって、保ちたいフィールドのホワイトリストを指定することができます。言及されていないフィールドはすべてフォームから削除されます。
フォームフレームワークは他のカラムのように~多対多のリレーションシップ|多対多のリレーションシップ (フォーム)~をサポートします。
デフォルトでは、sfWidgetFormChoice
ウィジェットのおかげでこのようなリレーションシップはドロップダウンボックスのようなレンダラです。10日目で見たように、expanded
オプションを指定してレンダリングされたタグを変更しました。
メールと URL は input タグのデフォルトサイズよりも長くなりがちなので、デフォルトの HTML 属性は setAttribute()
メソッドを使用して設定できます。
テスト
最後のステップは新しい機能に対して~機能テスト|フォーム (テスト)~を書くことです。
affiliate
モジュール用の生成テストを次のコードで置き換えます:
[php]
// test/functional/frontend/affiliateActionsTest.php
include(dirname(__FILE__).'/../../bootstrap/functional.php');
$browser = new JobeetTestFunctional(new sfBrowser());
$browser->loadData();
$browser->
info('1 - An affiliate can create an account')->
get('/affiliate/new')->
click('Submit', array('jobeet_affiliate' => array(
'url' => 'http://www.example.com/',
'email' => 'foo@example.com',
info('2 - An affiliate must at least select one category')->
get('/affiliate/new')->
click('Submit', array('jobeet_affiliate' => array(
'url' => 'http://www.example.com/',
'email' => 'foo@example.com',
)))->
JobeetTestFunctional
クラスのなかで新しい getProgrammingCategory()
メソッドを作りました:
[php]
// lib/test/JobeetTestFunctional.class.php
class JobeetTestFunctional extends sfTestFunctional
{
public function getProgrammingCategory()
{
$criteria = new Criteria();
$criteria->add(JobeetCategoryPeer::SLUG, 'programming');
return JobeetCategoryPeer::doSelectOne($criteria);
}
// ...
}
しかし getMostRecentProgrammingJob()
メソッドのなかにこのコードがあるので、コードを~リファクタリング~して JobeetCategoryPeer
で getForSlug()
メソッドを作りましょう:
[php]
// lib/model/JobeetCategoryPeer.php
static public function getForSlug($slug)
{
$criteria = new Criteria();
$criteria->add(self::SLUG, $slug);
return self::doSelectOne($criteria);
}
JobeetTestFunctional
のこのコードの2つの存在を置き換えます。
アフィリエイトのバックエンド
~バックエンド~に関して、管理者によってアフィリエイトをアクティベートするために affiliate
モジュールを作成しなければなりません:
$ php symfony propel:generate-admin backend JobeetAffiliate --module=affiliate
新しく作成されたモジュールにアクセスするには、アクティベートされるアフィリエイトの人数を伴うメインメニューにリンクを追加します:
[php]
<!-- apps/backend/templates/layout.php -->
<li>
return self::doCount($criteria);
}
return $q->count();
}
// ...
}
バックエンドで必要なアクションのみがアカウントをアクティベートするもしくはアクティベートを解除するので、インターフェイスを少し簡略化するために config
セクションにおいて、デフォルトのジェネレータを変更し list ビューからアカウントを直接アクティベートするリンクを追加します:
[yml]
# apps/backend/modules/affiliate/config/generator.yml
config:
fields:
is_active: { label: Active? }
list:
title: Affiliate Management
display: [is_active, url, email, token]
sort: [is_active]
object_actions:
activate: ~
deactivate: ~
batch_actions:
activate: ~
deactivate: ~
actions: {}
filter:
display: [url, email, is_active]
管理者をより生産的にするために、アクティベートされるアフィリエイトのみを表示するようにデフォルトのフィルタを変更します:
[php]
// apps/backend/modules/affiliate/lib/affiliateGeneratorConfiguration.class.php
class affiliateGeneratorConfiguration extends BaseAffiliateGeneratorConfiguration
{
public function getFilterDefaults()
{
return array('is_active' => '0');
}
}
書く必要があるのは activate
、deactivate
アクションに対応するコードだけです:
[php]
// apps/backend/modules/affiliate/actions/actions.class.php
class affiliateActions extends autoAffiliateActions
{
public function executeListActivate()
{
$this->getRoute()->getObject()->activate();
$this->redirect('jobeet_affiliate');
}
public function executeListDeactivate()
{
$this->getRoute()->getObject()->deactivate();
$this->redirect('jobeet_affiliate');
}
public function executeBatchActivate(sfWebRequest $request)
{
$affiliates = $q->execute();
foreach ($affiliates as $affiliate)
{
$affiliate->activate();
}
$this->redirect('jobeet_affiliate');
}
public function executeBatchDeactivate(sfWebRequest $request)
{
$affiliates = $q->execute();
foreach ($affiliates as $affiliate)
{
$affiliate->deactivate();
}
$this->redirect('jobeet_affiliate');
}
}
return $this->save();
}
public function deactivate()
{
$this->setIsActive(false);
return $this->save();
}
// ...
}
~Eメール~を送信する
アフィリエイトのアカウントが管理者によってのみ変更されるとき、メールは購読を確認にトークンを渡すためにアフィリエイトに送信されます。
PHP には Swift Mailer、Zend_Mail と ezcMail のようなメールを送信するためのよいライブラリがたくさんあります。来たる日に他の ~Zend Framework~ ライブラリを使うので、メールを送信するのに ~Zend_Mail
~ を使いましょう。
Zend Framework のインストールと設定
Zend Mail ~ライブラリ|サードパーティのライブラリ~は Zend Framework の一部です。Zend Framework のすべては必要ないので symfony フレームワーク自身と並行して必要なパーツだけを lib/vendor/
ディレクトリにインストールします。
最初に、
Zend Framework をダウンロードして lib/vendor/Zend/
ディレクトリがあるようにファイルを展開します。
NOTE 次の説明内容は Zend Framework 1.8.0 でテストしました。
次のファイルとディレクトリ以外のすべてを削除してディレクトリをクリーンナップします:
Exception.php
Loader/
Loader.php
Mail/
Mail.php
Mime/
Mime.php
Search/
NOTE Eメール送信のために
Search/
ディレクトリは必要ありませんが明日のチュートリアルで必要です。
それから、Zend オートローダを登録するシンプルな方法を提供するために次のコードを ProjectConfiguration
クラスに追加します:
[php]
// config/ProjectConfiguration.class.php
class ProjectConfiguration extends sfProjectConfiguration
{
static protected $zendLoaded = false;
static public function registerZend()
{
if (self::$zendLoaded)
{
return;
}
set_include_path(sfConfig::get('sf_lib_dir').'/vendor'.PATH_SEPARATOR.get_include_path());
require_once sfConfig::get('sf_lib_dir').'/vendor/Zend/Loader.php';
Zend_Loader_Autoloader::getInstance();
self::$zendLoaded = true;
}
// ...
}
また明日
symfony の ~REST~ アーキテクチャのおかげで、プロジェクト用にWebサービスを実装するのはとても簡単です。しかしながら、今日は読み込みだけの Web サービス用のコードを書いたので、Web サービスの読み書き機能を実装するための symfony の知識は十分にあります。
プロジェクトに新しい機能を追加するプロセスに慣れ親しんでいるので、フロントエンドと対応するバックエンドでアフィリエイトアカウント作成フォームの実装は本当に簡単でした。
2日目の要件を覚えているなら次のとおりです:
"アフィリエイトは返される求人の件数を制限することおよび、カテゴリを指定することでクエリを絞りこむこともできる。"
この機能の実装は簡単なので今夜やってみましょう。
アフィリエイトのアカウントが管理者によって有効にされるときは、アフィリエイトの購読を確認しトークンを渡すためにメールがアフィリエイトに送信されます。メールの送信は明日お話しするトピックです。
ORM