7日目: カテゴリページで遊ぶ
昨日はたくさんの異なる領域: ##ORM## でクエリを行う、フィクスチャ、ルーティング、デバッグとカスタムの設定など symfony の知識を広げました。今日は少しチャレンジして終わります。
あなたが Jobeet のカテゴリページで取り組んでくださることを期待しています。今日のチュートリアルはさらに大切になります。
準備はいいですか?実現可能な実装について語りましょう。
カテゴリのルート
最初に、カテゴリページに対してプリティ URL を定義するためにルートを定義する必要があります。ルーティングファイルの冒頭で次の内容を追加します:
#新しい機能を実装し始めるとき、最初に ~URL~ を考えて関連~ルート~を作るのはよい習慣です。デフォルトのルーティングルールを削除するとき、このルートは必須です。
ルートは関連オブジェクトからの任意のカラムをパラメータとして使うことができます。オブジェクトクラスで定義される関連アクセサが存在する場合、ルートはほかの値も利用できます。slug
パラメータは対応する category
テーブルのカラムをもたないので、ルートが機能するようにするために JobeetCategory
のバーチャルアクセサを追加する必要があります:
// lib/model/JobeetCategory.php // lib/model/doctrine/JobeetCategory.class.php public function getSlug() { return Jobeet::slugify($this->getName()); }
カテゴリのリンク
リンクをカテゴリページに追加するために job
モジュールの indexSuccess.php
テンプレートを編集します:
<!-- some HTML code --> <h1> <?php echo link_to($category, 'category', $category) ?> </h1> <!-- some HTML code --> </table> <?php if (($count = $category->countActiveJobs() - ➥ sfConfig::get('app_max_jobs_on_homepage')) > 0): ?> <div class="more_jobs"> and <?php echo link_to($count, 'category', $category) ?> more... </div> <?php endif; ?> </div> <?php endforeach; ?> </div>
現在のカテゴリで表示する求人件数が10を越える場合のみにリンクを表示します。リンクは表示されない求人件数を含みます。このテンプレートを動作させるために、countActiveJobs()
メソッドを JobeetCategory
に 追加する必要があります:
[php] // lib/model/JobeetCategory.php public function countActiveJobs() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
return JobeetJobPeer::countActiveJobs($criteria);
}
countActiveJobs()
メソッドは JobeetJobPeer
にはまだ存在しない countActiveJobs()
メソッドを利用します。JobeetJobPeer.php
ファイルの内容を次のコードで置き換えます: [php] // lib/model/doctrine/JobeetCategory.class.php public function countActiveJobs() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId());
return Doctrine_Core::getTable('JobeetJob')->countActiveJobs($q);
}
countActiveJobs()
メソッドは JobeetJobTable
にまだ存在しない countActiveJobs()
メソッドを利用します。JobeetJobTable.php
ファイルの内容を次のコードで置き換えます:
[php] // lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function getActiveJobs(Criteria $criteria = null) { return self::doSelect(self::addActiveJobsCriteria($criteria)); }
static public function countActiveJobs(Criteria $criteria = null)
{
return self::doCount(self::addActiveJobsCriteria($criteria));
}
static public function addActiveJobsCriteria(Criteria $criteria = null)
{
if (is_null($criteria))
{
$criteria = new Criteria();
}
$criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN);
$criteria->addDescendingOrderByColumn(self::CREATED_AT);
return $criteria;
}
static public function doSelectActive(Criteria $criteria)
{
return self::doSelectOne(self::addActiveJobsCriteria($criteria));
}
}
ご覧のとおり、コードをより ~DRY~ (Don't Repeat Yourself) にするために、新しく共有される addActiveJobsCriteria()
メソッドを導入して JobeetJobPeer
のコード全体をリファクタリングしました。
[php] // lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function retrieveActiveJob(Doctrine_Query $q) { return $this->addActiveJobsQuery($q)->fetchOne(); }
public function getActiveJobs(Doctrine_Query $q = null)
{
return $this->addActiveJobsQuery($q)->execute();
}
public function countActiveJobs(Doctrine_Query $q = null)
{
return $this->addActiveJobsQuery($q)->count();
}
public function addActiveJobsQuery(Doctrine_Query $q = null)
{
if (is_null($q))
{
$q = Doctrine_Query::create()
->from('JobeetJob j');
}
$alias = $q->getRootAlias();
$q->andWhere($alias . '.expires_at > ?', date('Y-m-d H:i:s', time()))
->addOrderBy($alias . '.created_at DESC');
return $q;
}
}
ご覧の通り、コードをより ~DRY~ (Don't Repeat Yourself) にするために、新しく共有する addActiveJobsQuery()
メソッドを導入して JobeetJobTable
のコード全体をリファクタリングしました。
最初にコードのピースが再利用されるとき、コードのコピーで間に合うかもしれません。しかしそれをほかの場所で使う場合、今しがた作業したように、共用の関数もしくはメソッドへのすべての利用をリファクタリングする必要があります。
countActiveJobs()
メソッドにおいて、doSelect()
を使って結果数をカウントする代わりに、ずっと速い doCount()
メソッドを使いました。 countActiveJobs()
メソッドにおいて、execute()
を使って結果数をカウントする代わりに、はるかに速い count()
メソッドを使いました。
このシンプルな機能のために、たくさんのファイルを変更しました。コードを追加するたびに、アプリケーションの正しいレイヤーに設置しようとしました。コードを再利用できるようにすることにも取り組みました。このプロセスにおいて、既存のコードをリファクタリングすることも行いました。これは symfony プロジェクトに取り組む際の典型的なワークフローです。次のスクリーンショットでは短くするために5件の求人を表示しており、10件を見ることになります (max_jobs_on_homepage
設定):
求人の category モジュールの作成
category
~モジュール~を作りましょう:
$ php symfony generate:module frontend category
モジュールを作ってあるのであれば、おそらく propel:generate-module
を使ったことでしょう。これはよいのですが、生成コードの90%は不要なので、空のモジュールを作る generate:module
を使いました。
category
アクションをjob
モジュールに 追加しないのはなぜでしょうか?できますが、カテゴリページのメインの対象はカテゴリなので、専用のcategory
モジュールを作るのは自然に思われるからです。
カテゴリページにアクセスする際に、category
ルートは slug
リクエスト変数に関連するカテゴリを見つけなければなりません。~スラッグ~はデータベースに保存されないのとスラッグからカテゴリの名前を推測できないので、スラッグに関連するカテゴリを見つける方法がありません。
データベースを更新する
category
テーブルのために slug
カラムを追加する必要があります:
[yml] # config/schema.yml propel: jobeet_category: id: ~ name: { type: varchar(255), required: true } slug: { type: varchar(255), required: true, index: unique } この slug
カラムは Doctrine の Sluggable
ビヘイビアによって考慮されます。JobeetCategory
モデルのビヘイビアを有効にすればすべてが考慮されます。
---
# config/doctrine/schema.yml
JobeetCategory:
actAs:
Timestampable:
Sluggable:
fields: [name]
columns:
name:
type: string(255)
notnull: true
slug
は本当のカラムなので、JobeetCategory
から getSlug()
メソッドを削除する必要があります。
category
の名前が代わるたびに、slug
を算出して変更する必要があります。setName()
メソッドをオーバーライドしてみましょう:
// lib/model/JobeetCategory.php public function setName($name) { parent::setName($name); $this->setSlug(Jobeet::slugify($name)); }
レコードを保存する際に slug カラムの設定は自動的に考慮されます。
name
フィールドの値を使ってスラッグはビルドされオブジェクトに設定されます。
データベースのテーブルを更新するには propel:build --all --and-load
タスクを使い、データベースにフィクスチャを投入します:
$ php symfony propel:build --all --and-load --no-confirmation
executeShow()
メソッドを作るための場所が手に入りました。次のコードで category
アクションファイルの内容を置き換えます:
// apps/frontend/modules/category/actions/actions.class.php class categoryActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->category = $this->getRoute()->getObject(); } }
生成された
executeIndex()
メソッドを削除したので、indexSuccess.php
テンプレートも自動的に削除できます (apps/frontend/modules/category/templates/indexSuccess.php
)。
最後のステップは showSuccess.php
テンプレートを作ることです:
// apps/frontend/modules/category/templates/showSuccess.php <?php use_stylesheet('jobs.css') ?> <?php slot('title', sprintf('Jobs in the %s category', $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>
~パーシャル|パーシャルテンプレート~
job の indexSuccess.php
テンプレートから求人リストを作成する <table>
タグをコピー&ペーストしたことに注目してください。これはよいことではありません。新しいトリックを学びましょう。テンプレートの一部を再利用する必要があるとき、~パーシャル|パーシャルテンプレート~ (partial) を作る必要があります。パーシャルは複数のテンプレートの間で共有できる~テンプレート~コードスニペットです。アンダースコア (_
) で始まる別の種類のテンプレートにすぎません。
_list.php
ファイルを作ります:
// apps/frontend/modules/job/templates/_list.php <table class="jobs"> <?php foreach ($jobs 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>
~include_partial()
~ ヘルパーを利用することでパーシャルをインクルードできます:
<?php include_partial('job/list', array('jobs' => $jobs)) ?>
include_partial()
の最初の引数はパーシャルの名前です (モジュールの名前、/
と先頭の _
がないパーシャルの名前で構成)。第2引数はパーシャルに渡される変数の配列です。
include_partial()
ヘルパーの代わりに PHP 組み込みのinclude()
関数を使わないのはなぜでしょうか?2つの主な違いはinclude_partial()
ヘルパーの組み込みのキャッシュサポートです。
両方からの HTML コードの <table>
を include_partial()
の呼び出しに置き換えます:
// in apps/frontend/modules/job/templates/indexSuccess.php <?php include_partial('job/list', array('jobs' => $category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?> // in apps/frontend/modules/category/templates/showSuccess.php <?php include_partial('job/list', array('jobs' => $category->getActiveJobs())) ?>
リストの~ページ送り~
2日目の要件より:
「リストはページごとに20件の求人でページ分割される」
ORM## オブジェクトのリストをページ分割するために、symfony は専用のクラス: sfPropelPager
を提供します。category
アクションにおいて、showSuccess
テンプレートに job オブジェクトを渡す代わりに、ページャを渡します:
// apps/frontend/modules/category/actions/actions.class.php public function executeShow(sfWebRequest $request) { $this->category = $this->getRoute()->getObject(); $this->pager = new sfPropelPager( 'JobeetJob', sfConfig::get('app_max_jobs_on_category') );
$this->pager->setCriteria($this->category->getActiveJobsCriteria()); $this->pager->setQuery($this->category->getActiveJobsQuery()); $this->pager->setPage($request->getParameter('page', 1)); $this->pager->init(); }
sfRequest::getParameter()
メソッドはデフォルトの値を第2引数として受け取ります。上記のアクションにおいて、page
リクエストパラメータが存在しない場合、getParameter()
は1
を返します。
sfPropelPager
のコンストラクタはモデルクラスとページごとに返すアイテムの最大個数を受け取ります。後者の値を設定ファイルに追加します:
---
# apps/frontend/config/app.yml
all:
active_days: 30
max_jobs_on_homepage: 10
max_jobs_on_category: 20
sfPropelPager::setCriteria()
メソッドはデータベースからアイテムを SELECT する際に使う Criteria
オブジェクトを受け取ります。 sfDoctrinePager::setQuery()
メソッドはデータベースからアイテムを SELECT する際に使う Doctrine_Query
オブジェクトを受け取ります。
getActiveJobsCriteria()
メソッドを追加します: getActiveJobsQuery()
メソッドを追加します:
[php] // lib/model/JobeetCategory.php public function getActiveJobsCriteria() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
return JobeetJobPeer::addActiveJobsCriteria($criteria);
}
[php] // lib/model/doctrine/JobeetCategory.class.php public function getActiveJobsQuery() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId());
return Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q);
}
getActiveJobsCriteria()
メソッドを定義したので、このメソッドを使うようにするために、JobeetCategory
メソッドをリファクタリングできます: getActiveJobsQuery()
メソッドを定義したので、このメソッドを使うようにするために、JobeetCategory
メソッドをリファクタリングできます:
[php] // lib/model/JobeetCategory.php public function getActiveJobs($max = 10) { $criteria = $this->getActiveJobsCriteria(); $criteria->setLimit($max);
return JobeetJobPeer::doSelect($criteria);
}
public function countActiveJobs()
{
$criteria = $this->getActiveJobsCriteria();
return JobeetJobPeer::doCount($criteria);
}
[php] // lib/model/doctrine/JobeetCategory.class.php public function getActiveJobs($max = 10) { $q = $this->getActiveJobsQuery() ->limit($max);
return $q->execute();
}
public function countActiveJobs()
{
return $this->getActiveJobsQuery()->count();
}
最後に、テンプレートを更新しましょう:
<!-- apps/frontend/modules/category/templates/showSuccess.php --> <?php use_stylesheet('jobs.css') ?> <?php slot('title', sprintf('Jobs in the %s category', $category->getName())) ?> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <?php include_partial('job/list', array('jobs' => $pager->getResults())) ?> <?php if ($pager->haveToPaginate()): ?> <div class="pagination"> <a href="<?php echo url_for('category', $category) ?>?page=1"> <img src="/images/first.png" alt="First page" title="First page" /> </a> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getPreviousPage() ?>"> <img src="/images/previous.png" alt="Previous page" title="Previous page" /> </a> <?php foreach ($pager->getLinks() as $page): ?> <?php if ($page == $pager->getPage()): ?> <?php echo $page ?> <?php else: ?> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $page ?>"><?php echo $page ?></a> <?php endif; ?> <?php endforeach; ?> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getNextPage() ?>"> <img src="/images/next.png" alt="Next page" title="Next page" /> </a> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getLastPage() ?>"> <img src="/images/last.png" alt="Last page" title="Last page" /> </a> </div> <?php endif; ?> <div class="pagination_desc"> <strong><?php echo count($pager) ?></strong> jobs in this category <?php if ($pager->haveToPaginate()): ?> - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager->getLastPage() ?></strong> <?php endif; ?> </div>
たいていのコードではほかのページへのリンクが扱われます。このテンプレートで使われる sfPropelPager
メソッドのリストは次のとおりです:
getResults()
: 現在のページ用の ##ORM## オブジェクトを返すgetNbResults()
: 結果の合計数を返すhaveToPaginate()
: 複数のページがある場合はtrue
を返すgetLinks()
: 表示するページリンクのリストを返すgetPage()
: 現在のページ番号を返すgetPreviousPage()
: 前のページ番号を返すgetNextPage()
: 次のページ番号を返すgetLastPage()
: 最後のページ番号を返す
sfPropelPager
は Iterator
と Countable
インターフェイスも実装するので、結果の数を得るのに getNbResults
メソッドの代わりに count()
関数を使うことができます。
また明日
昨日独自の実装に取り組んだのであれば今日はあまり学ばなかったと感じるでしょう。これは symfony の哲学に慣れつつあることを意味します。symfony の Web サイトに新しい機能を追加するプロセスは常に同じです: URL を考え、アクションを作り、モデルを更新し、テンプレートを書きます。そして、よい開発習慣を複数の事例に適用できるのであれば、早く symfony マスターになれます。
明日は Jobeet の新しい週の始まりです。祝うために、真新しいトピック: テストを語ります。
ORM
インデックス
Document Index
関連ページリスト
Related Pages
- 1日目: プロジェクトを始める
- 2日目: プロジェクト
- 3日目: ~データモデル~
- 4日目: Controller と View
- 5日目: ルーティング
- 6日目: モデルの詳細
- 7日目: カテゴリページで遊ぶ
- 8日目: ユニットテスト
- 9日: 機能テスト
- 10日目: フォーム
- 11日目: フォームをテストする
- 12日目: アドミンジェネレータ
- 13日目: ユーザー
- 14日目: フィード
- 15日目: Web サービス
- 16日目: ~メーラー~
- 17日目: 検索
- 18日目: ~AJAX~
- 19日目: 国際化とローカライゼーション
- 20日目: プラグイン
- 21日目: キャッシュ
- 22日目: デプロイ
- 23日目: 別の視点から symfony を見る
- Appendix A - License
- 謝辞
日本語ドキュメント
Japanese Documents
- 2011/01/18 Chapter 17 - Extending Symfony
- 2011/01/18 The generator.yml Configuration File
- 2011/01/18 Les tâches
- 2011/01/18 Emails
- 2010/11/26 blogチュートリアル(8) ビューの作成
リリース情報
Release Information
- 2.0 : 2.0.15(2011/05/30)
Symfony2日本語ドキュメント - 1.4 : 1.4.18(2012/05/30)
Changelog