Jobeetのフィード追加に関して、求職者はリアルタイムで新しい求人情報が知らされます。
一方では、求人を投稿するとき、できる限りもっとも注目されたいと願います。 たくさんの小さなWebサイトで求人情報が配信される場合、適任者を見つける機会が増えます。 これがロングテールの力です。 今日開発するWebサービスのおかげでアフィリエイトは自身のWebサイトに投稿された最新の求人情報を公開できます。
アフィリエイト
2日目の要件です:
"ストーリー F7: アフィリエイトは現在アクティブな求人リストを読み取る"
フィクスチャ
アフィリエイト用に新しいフィクスチャファイルを作りましょう:
# data/fixtures/030_affiliates.yml JobeetAffiliate: sensio_labs: url: http://www.sensio-labs.com/ email: fabien.potencier@example.com is_active: true token: sensio_labs jobeet_category_affiliates: [programming] symfony: url: / email: fabien.potencier@example.org is_active: false token: symfony jobeet_category_affiliates: [design, programming]
多対多のリレーションの真ん中のテーブル用のレコード作成は真ん中のテーブルのキーにs
を追加した配列を定義することで簡単に実現できます。
配列の内容はフィクスチャファイルで定義されたオブジェクトの名前です。
異なるファイルからオブジェクトをリンクできますが、まず名前を定義しなければなりません。
フィクスチャファイルにおいて、テスト作業を簡略化するためにトークンは決め打ちされていますが、 実際のユーザーがアカウントを申し込むとき、トークンの生成が必要になります:
// lib/model/JobeetAffiliate.php class JobeetAffiliate extends BaseJobeetAffiliate { public function save(PropelPDO $con = null) { if (!$this->getToken()) { $this->setToken(sha1($this->getEmail().rand(11111, 99999))); } return parent::save($con); } // ... }
データをリロードできます:
$ php symfony propel:data-load
求人情報のWebサービス
新しいリソースを作成するとき、最初にURLを定義するのはよい習慣です:
# 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()
メソッドが呼び出されます。
アフィリエイトがアクティベートされることを確認する必要があるとき、ルートのデフォルトのふるまいをオーバーライドする必要があります:
// lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function getForToken(array $parameters) { $affiliate = JobeetAffiliatePeer::getByToken($parameters['token']); if (!$affiliate || !$affiliate->getIsActive()) { throw new sfError404Exception(sprintf('Affiliate with token "%s" does not exist or is not activated.', $parameters['token'])); } return $affiliate->getActiveJobs(); } // ... }
トークンがデータベースに存在しない場合、sfError404Exception
の例外が投げられます。
この例外クラスは自動的に~404|404エラー~
レスポンスに変換されます。
これがモデルクラスから404
ページを生成するためのもっともシンプルな方法です。
getForToken()
メソッドは今新しく作る2つのメソッドを使います。
最初に、トークンを与えられたアフィリエイトを取得するためにgetByToken()
メソッドを作らなければなりません:
// 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()
メソッドはアフィリエイトによって選択されたカテゴリ用の現在アクティブな求人リストを返します:
// lib/model/JobeetAffiliate.php class JobeetAffiliate extends BaseJobeetAffiliate { public function getActiveJobs() { $cas = $this->getJobeetCategoryAffiliates(); $categories = array(); foreach ($cas as $ca) { $categories[] = $ca->getCategoryId(); } $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $categories, Criteria::IN); JobeetJobPeer::addActiveJobsCriteria($criteria); return JobeetJobPeer::doSelect($criteria); } // ... }
最後のステップはapi
アクションとテンプレートを作ることです。
generate:module
タスクでモジュールをブートストラップします:
$ php symfony generate:module frontend api
note
デフォルトのindex
アクションを使わないので、アクションクラスからこれを除外し、関連するindexSucess.php
テンプレートを除外します。
アクション
すべてのフォーマットは同じlist
アクションを共有します:
// 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()
メソッドで取り除かれました:
// lib/model/JobeetJob.php class JobeetJob extends BaseJobeetJob { public function asArray($host) { return array( 'category' => $this->getJobeetCategory()->getName(), 'type' => $this->getType(), 'company' => $this->getCompany(), 'logo' => $this->getLogo() ? 'http://'.$host.'/uploads/jobs/'.$this->getLogo() : null, 'url' => $this->getUrl(), 'position' => $this->getPosition(), 'location' => $this->getLocation(), 'description' => $this->getDescription(), 'how_to_apply' => $this->getHowToApply(), 'expires_at' => $this->getCreatedAt('c'), ); } // ... }
xml
フォーマット
テンプレートを作ればxml
フォーマットを簡単にサポートできます:
<!-- 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フォーマットのサポートも同じです:
<!-- 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は変更可能でレイアウトはアクションでは無効です:
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用のテンプレートを次の内容を読み込みます:
<!-- 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
):
// 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フォーマット用のレイアウトを作らなければなりません:
// apps/frontend/templates/layout.yaml.php <?php echo $sf_content ?>
tip
組み込みのテンプレートの404エラーと例外をオーバーライドするのは簡単でconfig/error/
ディレクトリにファイルを作ります。
Webサービスのテスト
Webサービスをテストするには、アフィリエイトフィクスチャをdata/fixtures/
からtest/fixtures/
ディレクトリにコピーして自動生成されたapiActionsTest.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')->checkElement('job', 32)-> info('3 - The web service supports the JSON format')-> get('/api/sensio_labs/jobs.json')-> with('request')->isFormat('json')-> with('response')->contains('"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')-> contains('category: Programming')-> end() ;
このテストにおいて、2つの新しいメソッドに注目します:
isFormat()
: これはリクエストのフォーマットをテストしますcontains()
: 非HTMLフォーマットに関して、レスポンスが テキストの期待されたスニペットを含むのかチェックします
アフィリエイトアプリケーションのフォーム
Webサービスを提供する準備ができたので、アフィリエイト用のアカウント作成フォームを作りましょう。 再度新しい機能をアプリケーションに追加することで古典的なプロセスを説明します。
ルーティング
ご想像の通り、ルートは最初に作るものです:
# apps/frontend/config/routing.yml affiliate: class: sfPropelRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: get }
これは新しい設定オプション: actions
を持つ古典的なPropelコレクションルートです。
ルートによって定義されるデフォルトの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
以外のすべてのファイルを削除します。
維持するファイルに関して、これらの内容を次のものに置き換えます:
<!-- 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
テンプレートを作ります:
<!-- 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
モジュールを指し示すようにフッターのリンクを変更します:
// 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
アクションに変更します:
// apps/frontend/modules/affiliate/actions/actions.class.php $this->redirect($this->generateUrl('affiliate_wait', $jobeet_affiliate));
wait
アクションはシンプルなのでテンプレートに何も渡さずに済みます:
// apps/frontend/modules/affiliate/actions/actions.class.php public function executeWait() { }
アフィリエイトはトークンを選択したり、アカウントを即座にアクティベートできません。
フォームをカスタマイズするためにJobeetAffiliateForm
ファイルを開きます:
// lib/form/doctrine/JobeetAffiliateForm.class.php class JobeetAffiliateForm extends BaseJobeetAffiliateForm { public function configure() { unset($this['is_active'], $this['token'], $this['created_at']); $this->widgetSchema['jobeet_category_affiliate_list']->setOption('expanded', true); $this->widgetSchema['jobeet_category_affiliate_list']->setLabel('Categories'); $this->validatorSchema['jobeet_category_affiliate_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)); } }
フォームフレームワークは他のカラムのように多対多のリレーションをサポートします。
デフォルトでは、sfWidgetFormChoice
ウィジェットのおかげでこのようなリレーションはドロップダウンボックスのようなレンダラーです。
10日目で見たように、expanded
オプションを指定してレンダリングされたタグを変更しました。
EメールとURLはinputタグのデフォルトサイズよりも長くなりがちなので、デフォルトのHTML属性はsetAttribute()
メソッドを使用して設定できます。
テスト
最後のステップは新しい機能に対して機能テストを書くことです。
affiliate
モジュール用の生成テストを次のコードで置き換えます:
// 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', 'jobeet_category_affiliate_list' => array($browser->getProgrammingCategory()->getId()), )))-> isRedirected()-> followRedirect()-> with('response')->checkElement('#content h1', 'Your affiliate account has been created')-> 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', )))-> with('form')->isError('jobeet_category_affiliate_list') ;
チェックボックスの選択をシミュレートするには、チェックする識別子の配列を渡します。
タスクを簡略化するために、JobeetTestFunctional
クラスで新しいgetProgrammingCategory()
メソッドが作成されました:
// lib/model/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()
メソッドを作りましょう:
// 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
新しく作成されたモジュールにアクセスするには、アクティベートされるアフィリエイトの人数を伴うメインメニューにリンクを追加します:
<!-- apps/backend/templates/layout.php --> <li> <a href="<?php echo url_for('@jobeet_affiliate') ?>"> Affiliates - <strong><?php echo JobeetAffiliatePeer::countToBeActivated() ?></strong> </a> </li> // lib/model/JobeetAffiliatePeer.php class JobeetAffiliatePeer extends BaseJobeetAffiliatePeer { static public function countToBeActivated() { $criteria = new Criteria(); $criteria->add(self::IS_ACTIVE, 0); return self::doCount($criteria); } // ... }
バックエンドで必要なアクションのみがアカウントをアクティベートするもしくはアクティベートを解除するので、インターフェイスを少し簡略化するためにconfig
セクションのデフォルトのジェネレーターを変更しlistビューからアカウントを直接アクティベートするリンクを追加します:
# 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]
管理者をより生産的にするために、アクティベートされるアフィリエイトのみを表示するようにデフォルトのフィルターを変更します:
// apps/backend/modules/affiliate/lib/affiliateGeneratorConfiguration.class.php class affiliateGeneratorConfiguration extends BaseAffiliateGeneratorConfiguration { public function getFilterDefaults() { return array('is_active' => '0'); } }
書く必要があるのはactivate
、deactivate
アクションに対するコードだけです:
// 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 = JobeetAffiliatePeer::retrieveByPks($request->getParameter('ids')); foreach ($affiliates as $affiliate) { $affiliate->activate(); } $this->redirect('@jobeet_affiliate'); } public function executeBatchDeactivate(sfWebRequest $request) { $affiliates = JobeetAffiliatePeer::retrieveByPks($request->getParameter('ids')); foreach ($affiliates as $affiliate) { $affiliate->deactivate(); } $this->redirect('@jobeet_affiliate'); } } // lib/model/JobeetAffiliate.php class JobeetAffiliate extends BaseJobeetAffiliate { public function activate() { $this->setIsActive(true); return $this->save(); } public function deactivate() { $this->setIsActive(false); return $this->save(); } // ... }
Eメールを送信する
アフィリエイトのアカウントが管理者によってのみ変更されるとき、Eメールは購読を確認にトークンを渡すためにアフィリエイトに送信されます。
PHPにはSwiftMailer、Zend_MailとezcMailのようなEメールを送信するためのよいライブラリがたくさんあります。
来たる日に他のZend Frameworkライブラリを使うので、Eメールを送信するのに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
クラスに追加します:
// 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; } // ... }
Eメールを送信する
管理者がアフィリエイトをバリデートする際にEメールを送信できるようにactivate
アクションを編集します:
// apps/backend/modules/affiliate/actions/actions.class.php class affiliateActions extends autoAffiliateActions { public function executeListActivate() { $affiliate = $this->getRoute()->getObject(); $affiliate->activate(); // アフィリエイトにEメールを送信する ProjectConfiguration::registerZend(); $mail = new Zend_Mail(); $mail->setBodyText(<<<EOF Your Jobeet affiliate account has been activated. Your token is {$affiliate->getToken()}. The Jobeet Bot. EOF ); $mail->setFrom('jobeet@example.com', 'Jobeet Bot'); $mail->addTo($affiliate->getEmail()); $mail->setSubject('Jobeet affiliate token'); $mail->send(); $this->redirect('@jobeet_affiliate'); } // ... }
動作するコードに関して、jobeet@example.com
を本当のEメールアドレスに変更する必要があります。
note
Zend_Mail
ライブラリの全チュートリアルはZend Frameworkの公式サイトで読めます。
また明日
symfonyのRESTアーキテクチャのおかげで、プロジェクト用にWebサービスを実装するのはとても簡単です。 しかしながら、今日は読み込みだけのWebサービス用のコードを書いたので、Webサービスの読み書き機能を実装するためのsymfonyの知識は十分にあります。
プロジェクトに新しい機能を追加するプロセスに慣れ親しんでいるので、フロントエンドと対応するバックエンドでアフィリエイトアカウント作成フォームの実装は本当に簡単でした。
2日目の要件を覚えているなら次のとおりです:
"アフィリエイトは返される求人の件数を制限することおよび、カテゴリを指定することでクエリを絞りこむこともできる。"
この機能の実装は簡単なので今夜やってみましょう。
明日は、JobeetのWebサイトに欠けている最後の機能である検索エンジンを実装します。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.