昨日はすばらしい日でした。 プリティURLの作り方とたくさんの事を自動で行うsymfonyフレームワークの使い方を学習しました。
今日は、あちらこちらのコードを調整してJobeetのWebサイトを強化します。 作業の中で、このチュートリアルの最初の5日の間に紹介したすべての機能を詳しく学びます。
PropelのCriteriaオブジェクト
2日目の必要要件を下記に示します:
"Jobeetサイトにユーザーが訪れたとき、利用可能な求人情報の一覧が見れる"
しかし現在のところ、利用可能かどうかは関係なく全ての求人情報が表示されます:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = JobeetJobPeer::doSelect(new Criteria()); } // ... }
アクティブな求人は30日前以内に投稿されたものです。
doSelect()
メソッドは実行するデータベースへのリクエストを記述するCriteria
オブジェクトを受け取ります。
上記のコードでは、空のCriteria
が渡されます。
このことはすべてのレコードがデータベースから読み取られることを意味します。
利用可能な求人情報だけが選択されるように変更してみましょう:
public function executeIndex(sfWebRequest $request) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CREATED_AT, time() - 86400 * 30, Criteria::GREATER_THAN); $this->jobeet_job_list = JobeetJobPeer::doSelect($criteria); }
Criteria::add()
メソッドは生成されるSQLにWHERE
句を追加します。
ここでは30日よりも古くない求人情報のみを選択するために基準を制限します。
add()
メソッドはたくさんの異なる比較演算子を受け取ります;
次のものがよく使われます:
Criteria::EQUAL
Criteria::NOT_EQUAL
Criteria::GREATER_THAN
,Criteria::GREATER_EQUAL
Criteria::LESS_THAN
,Criteria::LESS_EQUAL
Criteria::LIKE
,Criteria::NOT_LIKE
Criteria::CUSTOM
Criteria::IN
,Criteria::NOT_IN
Criteria::ISNULL
,Criteria::ISNOTNULL
Criteria::CURRENT_DATE
,Criteria::CURRENT_TIME
,Criteria::CURRENT_TIMESTAMP
Propelで生成されるSQLのデバッグ
手書きでSQLを記述しないので、Propelは異なるデータベース間の違いを管理して、3日目に選んだデータベースエンジンに最適化されたSQLステートメントを生成します。
しかし時に、Propelが生成したSQLステートメントを見ることは非常に役に立ちます。
たとえば、期待通りに動かないCriteriaをデバッグするときなどです。
dev
環境ではlog/
ディレクトリ内にこれらのクエリのログをとります。
アプリケーションと環境の全ての組み合わせのログファイルがあります。
探しているのはfrontend_dev.log
という名前のファイルです:
# log/frontend_dev.log Dec 6 15:47:12 symfony [debug] {sfPropelLogger} exec: SET NAMES 'utf8' Dec 6 15:47:12 symfony [debug] {sfPropelLogger} prepare: SELECT jobeet_job.ID, jobeet_job.CATEGORY_ID, jobeet_job.TYPE, jobeet_job.COMPANY, jobeet_job.LOGO, jobeet_job.URL, jobeet_job.POSITION, jobeet_job.LOCATION, jobeet_job.DESCRIPTION, jobeet_job.HOW_TO_APPLY, jobeet_job.TOKEN, jobeet_job.IS_PUBLIC, jobeet_job.CREATED_AT, jobeet_job.UPDATED_AT FROM `jobeet_job` WHERE jobeet_job.CREATED_AT>:p1 Dec 6 15:47:12 symfony [debug] {sfPropelLogger} Binding '2008-11-06 15:47:12' at position :p1 w/ PDO type PDO::PARAM_STR
created_at
カラムに対してwhere句を生成したことがご自身でわかります(WHERE jobeet_job.CREATED_AT > :p1
)。
note
クエリの:p1
文字列はPropelがプリペアードステートメントを生成することを示します。
:p1
の実際の値(上記の例では'2008-11-06 15:47:12
')はクエリ実行の間に渡されデータベースによって適切にエスケープされます。
プリペアードステートメントの利用によってSQLインジェクション攻撃への露出が劇的に減ります。
このことはよいことです。 しかし変更点のテストのため毎回ブラウザー、IDE、ログファイルの間を行ったりきたりするのは少し面倒です。 symfonyのWebデバッグツールバーのおかげで、必要な全ての情報がブラウザー上で快適に利用できます:
オブジェクトのシリアライズ
上記のコードが動作するとしても、2日目からの要件が考慮されていないので完璧とはほど遠い状態です:
"ユーザーは求人広告の有効期間を30日延長するために戻ることができる"
しかし上記のコードはcreated_at
の値に依存するのと、このカラムは作成時の日付を保存するので上記の要件を満たすことができません。
しかし3日目に記述したデータベーススキーマを覚えているのであれば、expires_at
カラムも定義しました。
フィクスチャデータに設定されていないので現在このデータの値は常に空です。
しかしjobが作成されるとき、これを現在の日付の後の30日に自動的に設定できます。
Propelオブジェクトがデータベースにシリアライズされる前に自動的に何かを行う必要があるときに、モデルクラスのsave()
メソッドをオーバーライドできます:
// lib/model/JobeetJob.php class JobeetJob extends BaseJobeetJob { public function save(PropelPDO $con = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? $this->getCreatedAt('U') : time(); $this->setExpiresAt($now + 86400 * 30); } return parent::save($con); } // ... }
isNew()
メソッドはオブジェクトがまだデータベースにシリアライズされていないときはtrue
を返し、それ以外はfalse
を返します。
では、アクティブな求人情報を選択するためにcreated_at
カラムの代わりにexpires_at
カラムを利用するアクションを変更してみましょう:
public function executeIndex(sfWebRequest $request) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $this->jobeet_job_list = JobeetJobPeer::doSelect($criteria); }
将来、expires_at
の日付で選択した仕事のみを対象とするためにクエリを制限します。
フィクスチャのさらなる使い方
Jobeetのホームページをブラウザーでリフレッシュしても数日前に投稿されたデータベース内の求人内容は変更されません。 フィクスチャにすでに期限切れした求人情報を追加してみましょう:
# data/fixtures/020_jobs.yml JobeetJob: # other jobs expired_job: category_id: programming company: Sensio Labs position: Web Developer location: Paris, France description: | Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: Send your resume to lorem.ipsum [at] dolor.sit is_public: true is_activated: true created_at: 2005-12-01 token: job_expired email: job@example.com
note
インデントを壊さないようにするためにフィクスチャファイルにコードをコピー&ペーストする際には気をつけてください。>expired_job
の前には2つのスペースだけしか置かなければなりません。
フィクスチャファイルに追加した求人を見ればわかるように、Propelによって自動的に満たされる場合でもcreated_at
カラムの値を定義できます。
定義された値はデフォルト値を上書きします。
フィクスチャをリロードして古い求人が表示されないことを確認するためにブラウザーをリフレッシュします:
$ php symfony propel:data-load
created_at
の値に基づき、exprires_at
カラムがsave()
メソッドによって自動的に満たされることを確認するために次のクエリを実行することもできます:
SELECT `position`, `created_at`, `expires_at` FROM `jobeet_job`;
カスタムコンフィギュレーション
JobeetJob::save()
メソッドにおいて、期限切れしている求人に関して日数を決め打ちしました。
30日を設定可能にするほうがよいです。
symfonyフレームワークはアプリケーション固有の設定用に組み込みの設定ファイル、app.yml
ファイルを提供します。
このYAMLファイルは任意の設定を格納できます:
# apps/frontend/config/app.yml all: active_days: 30
アプリケーションではこれらの設定はグローバルなsfConfig
クラスを通して利用可能です:
sfConfig::get('app_active_days')
後から見るようにsfConfig
クラスもsymfonyの設定へアクセス方法を提供するので、app_
がプレフィックスとしてつけられます。
新しい設定をコードに適用してみましょう:
public function save(PropelPDO $con = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? $this->getCreatedAt('U') : time(); $this->setExpiresAt($now + 86400 * sfConfig::get('app_active_days')); } return parent::save($con); }
app.yml
ファイルはアプリケーションのグローバル設定を集めるよい方法です。
最後に、プロジェクト規模での設定が必要な場合、symfonyプロジェクトルートのconfig
フォルダーで新しいapp.yml
ファイルを作るだけです。
リファクタリング
書いたコードが動作してはいますが、まだ完全に正しいものではありません。問題はどこにあるのでしょうか?
Criteria
のコードはアクションに所属せず(Controllerレイヤー)、Modelレイヤーに所属します。
MVCモデルにおいて、モデルはすべてのビジネスロジックを定義し、Controllerはデータを読み取るモデルのみを呼び出します。
コードは求人のコレクションを返すので、コードをJobeetJobPeer
クラスに移動させてgetActiveJobs()
メソッドを作りましょう:
// lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function getActiveJobs() { $criteria = new Criteria(); $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN); return self::doSelect($criteria); } }
これでアクションではアクティブな求人データを取得するために新しいメソッドを使うことができます。
public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = JobeetJobPeer::getActiveJobs(); }
このリファクタリングには以前のコードよりいくつかの利点があります。:
- 利用可能な仕事を取得するロジックはModelにある
- Controllerのコードはより読みやすくなる
getActionJobs()
メソッドは再利用できる(たとえば別のアクションで使う)- Modelコードでユニットテストができる
expires_at
カラムで求人をソートしてみましょう:
static public function getActiveJobs() { $criteria = new Criteria(); $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->addDescendingOrderByColumn(self::EXPIRES_AT); return self::doSelect($criteria); }
addDescendingOrderByColumn()
メソッドは生成されたSQLにORDER BY
句を追加します(addAscendingOrderByColumn()
も存在します)。
ホームページでのカテゴリ表示
2日目の必要要件を下記に示します:
"求人はカテゴリでまずソートされ、その次に投稿日時でソートされる(新しいものが最初に)"
これまで、求人のカテゴリについては考慮していませんでした。 必要要件からはホームページでカテゴリに基づいて表示しなければなりません。 まず最初に少なくとも1つの利用可能な求人から全てのカテゴリを取得することが必要です。
JobeetCategoryPeer
クラスを開きgetWithJobs()
メソッドを追加します:
// lib/model/JobeetCategoryPeer.php class JobeetCategoryPeer extends BaseJobeetCategoryPeer { static public function getWithJobs() { $criteria = new Criteria(); $criteria->addJoin(self::ID, JobeetJobPeer::CATEGORY_ID); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->setDistinct(); return self::doSelect($criteria); } }
Criteria::addJoin()
メソッドは生成されたSQLにJOIN
句を追加します。
デフォルトでは、JOINの条件がWHERE
句に追加されます。
3番目の引数を追加すればJOIN演算子を変更することもできます
(Criteria::LEFT_JOIN
、Criteria::RIGHT_JOIN
、とCriteria::INNER_JOIN
)。
indexアクションを適宜変更します:
// apps/frontend/modules/job/actions/actions.class.php public function executeIndex(sfWebRequest $request) { $this->categories = JobeetCategoryPeer::getWithJobs(); }
テンプレートでは、全てのカテゴリを渡すように反復し、利用可能な求人を表示する必要があります:
// apps/frontend/modules/job/templates/indexSuccess.php <?php use_stylesheet('jobs.css') ?> <div id="jobs"> <?php foreach ($categories as $category): ?> <div class="category_<?php echo Jobeet::slugify($category->getName()) ?>"> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <table class="jobs"> <?php foreach ($category->getActiveJobs() as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table> </div> <?php endforeach; ?> </div>
note
テンプレート内でカテゴリ名を表示するには、echo $category
を使うことができます。
変だと思いませんか?
$category
はオブジェクトなのに、どうやってecho
はカテゴリ名を表示するのでしょうか?
答えは3日目で全てのModelクラスに対して定義した__toString()
マジックメソッドです。
これを動かすには、カテゴリオブジェクトにアクティブな求人を返すJobeetCategory
クラスにgetActiveJobs()
メソッドを追加する必要があります:
// lib/model/JobeetCategory.php public function getActiveJobs() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); return JobeetJobPeer::getActiveJobs($criteria); }
add()
の呼び出しにおいて、Criteria::EQUAL
がデフォルト値なので3番目の引数を省略しました。
JobeetCategory::getActiveJobs()
メソッドは渡されたカテゴリ用にアクティブな求人を読み取るためにJobeetJobPeer::getActiveJobs()
メソッドを使います。
JobeetJobPeer::getActiveJobs()
を呼び出すとき、カテゴリを提供することで条件をさらに制限したい場合を考えます。
カテゴリオブジェクトを渡す代わりに、Criteria
オブジェクトを渡すことに決めました。
これが一般的な条件をカプセル化するための最良の方法だからです。
getActiveJobs()
は独自の基準でCriteria
引数をマージする必要があります。
Criteria
はオブジェクトなので、これはとてもシンプルです:
// lib/model/JobeetJobPeer.php static public function getActiveJobs(Criteria $criteria = null) { if (is_null($criteria)) { $criteria = new Criteria(); } $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->addDescendingOrderByColumn(self::EXPIRES_AT); return self::doSelect($criteria); }
結果の制限
ホームページの仕事リストの中に実装すべき1つの要件がまだあります:
"各カテゴリごとに最新の10件を表示し、得られたカテゴリに関する全ての求人リストへのリンクを表示する"
これらは単にgetActiveJobs()
メソッドに追加するだけで十分です:
// lib/model/JobeetCategory.php public function getActiveJobs($max = 10) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); $criteria->setLimit($max); return JobeetJobPeer::getActiveJobs($criteria); }
適切なLIMIT
句はModelの中で決め打ちされていますが、この値を設定可能にすることはよいことです。
app.yml
にセットした求人の最大件数をテンプレートに渡すように変更します:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <?php foreach ($category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $i => $job): ?>
加えて、app.yml
に新しい設定を追加します。
all: active_days: 30 max_jobs_on_homepage: 10
動的なフィクスチャ
max_jobs_on_homepage
に1より低い値がセットされなければ、違いはわからないでしょう。
フィクスチャを使ってたくさんの求人を追加することが必要です。
そこで、手作業で既存の求人を10〜12回ほどコピペできますがもっとよい方法があります。
たとえフィクスチャファイルであっても重複することは悪です。
symfonyのYAMLファイルはファイルを解析する直前に評価されるPHPコードを格納できます。
020_jobs.yml
フィクスチャファイルを編集して末端に次のコードを追加します:
# Starts at the beginning of the line (no whitespace before) <?php for ($i = 100; $i <= 130; $i++): ?> job_<?php echo $i ?>: category_id: programming company: Company <?php echo $i."\n" ?> position: Web Developer location: Paris, France description: Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: | Send your resume to lorem.ipsum [at] company_<?php echo $i ?>.sit is_public: true is_activated: true token: job_<?php echo $i."\n" ?> email: job@example.com <?php endfor; ?>
YAMLパーサーはインデントが崩れていると動作しない点に注意してください。 YAMLファイルにPHPコードを追加する場合の簡単なティップスを下記に示すので覚えておいてください:
<?php ?>
文は常に行の先頭か値に組み込まれている必要がある- もし
<?php ?>
文が行末にあるなら、改行するために("\n")が必要となる
propel:data-load
タスクでフィクスチャファイルをリロードしてホームページでProgramming
カテゴリに関して10
件の求人だけが表示されるか見ます。
次のスクリーンショットにおいて、画像を小さくするために求人の最大数を5
件に変更しました:
求人ページの保護
求人が期限切れになるとき、URLを知っているとしても、もはやアクセスできないようにすべきです。
期限切れの求人用のURLを試してください(id
を実際のデータベースのid
に置き換えてください - SELECT id, token FROM jobeet_job WHERE expires_at < NOW()
):
/frontend_dev.php/job/sensio-labs/paris-france/ID/web-developer-expired
求人ページを表示する代わりに、ユーザーを404ページに転送させる必要があります。
しかしルートが自動的に求人ページを取得するのでこれをどうやればよいでしょうか?
デフォルトでは、sfPropelRoute
は標準のdoSelectOne()
メソッドを使います。
オブジェクトを読み取るには、ルート設定のmethod_for_criteria
オプションを提供することで変更できます:
# apps/frontend/config/routing.yml job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: model: JobeetJob type: object method_for_criteria: doSelectActive param: { module: job, action: show } requirements: id: \d+ sf_method: [GET]
doSelectActive()
メソッドはルートによってビルドされたCriteria
オブジェクトを受け取ります:
// lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function doSelectActive(Criteria $criteria) { $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); return self::doSelectOne($criteria); } // ... }
これで、無効になった求人ページにアクセスすると404エラーページに転送されるようになっているでしょう。
カテゴリページへのリンク
今度はホームページにカテゴリページへのリンクを追加してカテゴリページを作りましょう。
しかし、少しお待ちください。 予定時間はまだ過ぎていないのであまり作業をしてきませんでした。 あなたには十分な時間と自分自身でこれをすべて実装するための知識があります! 練習してみましょう。 明日我々の実装を確認します。
また明日
ローカルなJobeetプロジェクトで実装に取り組んでください。 symfony公式サイトで利用できるオンラインのAPIドキュメントと すべてのドキュメントを遊んでみてください。
我々の実装と共に明日またお会いしましょう。
Good luck!
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.