Practical symfony

7日目: カテゴリページで遊ぶ

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


About

You are currently reading "Practical symfony" which is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.

Jobeet Links

Master symfony

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Books on symfony

Learn more about symfony with the official guides.
books.sensiolabs.com

L'audit Qualité par SensioLabs

200 points de contrôle de votre applicatif web.
audit.sensiolabs.com

Chapter Content

カテゴリのルート

カテゴリのリンク

求人のcategoryモジュールの作成

データベースを更新する

パーシャル

リストのパジネーション

また明日

symfony training
Be trained by symfony experts
Feb 21: Köln (Getting Started with Symfony2 - English)
Feb 27: Köln (Mastering Symfony2 - English)
Mar 05: Köln (Web Development with Symfony2 - Deutsch)
Mar 05: Montreal (Web Development with Symfony2 - English)
Mar 05: Montreal (Getting Started with Symfony2 - English)
and more...

Search


powered by google
You are currently browsing "Practical symfony" in Japanese for the 1.2 version - Doctrine edition - Switch to version: - Switch to ORM: - Switch to language:
Creative Commons License This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.
This version of symfony is not maintained anymore.
If some of your projects still use this version, consider upgrading as soon as possible.
Practical symfony (Doctrine edition)
Support symfony!
Buy this book
or donate.
Buy Practical symfony (Doctrine edition) from amazon.com

昨日はたくさんの異なる領域: Doctrineでクエリを行う、フィクスチャ、ルーティング、デバッグとカスタムの設定などsymfonyの知識を広げました。 今日は少しチャレンジして終わります。

あなたがJobeetのカテゴリページで取り組んでくださることを期待しています。 今日のチュートリアルはさらに大切になります。

準備はいいですか?実現可能な実装について語りましょう。

カテゴリのルート

最初に、カテゴリページに対してプリティURLを定義するためにルートを定義する必要があります。 ルーティングファイルの冒頭で次の内容を追加します:

# apps/frontend/config/routing.yml
category:
  url:      /category/:slug
  class:    ~sfDoctrineRoute~
  param:    { module: category, action: show }
  options:  { model: JobeetCategory, type: object }

新しい機能を実装し始めるとき、最初にURLを考えて関連ルートを作るのはよい習慣です。 デフォルトのルーティングルールを削除するときにこれは必須です。

ルートは関連オブジェクトからの任意のカラムをパラメーターとして使うことができます。 オブジェクトクラスで定義された関連アクセサーが存在する場合、ルートは他の値も使用できます。 slugパラメーターは対応するcategoryテーブルのカラムを持たないので、ルートを動作させるためにJobeetCategoryのバーチャルアクセサーを追加する必要があります:

// 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を越える場合のみにリンクを表示します。 リンクは表示されない求人件数を含みます。 このテンプレートを動作させるために、JobeetCategorycountActiveJobs()メソッドを追加する必要があります:

// lib/model/doctrine/JobeetCategory.class.php
public function countActiveJobs()
{
  $q = Doctrine_Query::create()
    ->from('JobeetJob j')
    ->where('j.category_id = ?', $this->getId());
 
  return Doctrine::getTable('JobeetJob')->countActiveJobs($q);
}

countActiveJobs()メソッドはJobeetJobTableにまだ存在しないcountActiveJobs()メソッドを使用します。 JobeetJobTable.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()メソッドにおいて、execute()を使って結果数をカウントする代わりに、はるかに速いcount()メソッドを使いました。

このシンプルな機能のために、たくさんのファイルを変更しました。 コードを追加するたびに、アプリケーションの正しいレイヤーに設置しようとしました。 コードを再利用できるようにすることにも取り組みました。 このプロセスにおいて、既存のコードをリファクタリングすることも行いました。 これはsymfonyプロジェクトに取り組む際の典型的なワークフローです。 次のスクリーンショットでは短くするために5件の求人を表示しており、10件を見ることになります(max_jobs_on_homepage設定):

ホームページ

求人のcategoryモジュールの作成

categoryモジュールを作りましょう:

$ php symfony generate:module frontend category

モジュールを作ってあるのであれば、おそらくdoctrine:generate-moduleを使ったことでしょう。 これはよいのですが生成コードの90%は不要なので、空のモジュールを作成するgenerate:moduleを使いました。

jobモジュールにcategoryアクションを追加しないのはなぜでしょうか? できますが、カテゴリページのメインの対象はカテゴリなので、専用のcategoryモジュールを作るのは自然に思われます。

カテゴリページにアクセスする際に、categoryルートはslugリクエスト変数に関連するカテゴリを見つけなければなりません。 スラッグはデータベースに保存されないのとスラッグからカテゴリの名前を推測できないので、スラッグに関連するカテゴリを見つける方法がありません。

データベースを更新する

categoryテーブル用にslugカラムを追加する必要があります:

このslugカラムはDoctrineのSluggableビヘイビアによって考慮されます。 JobeetCategoryモデルのビヘイビアを有効にすればすべてが考慮されます。

# config/doctrine/schema.yml
JobeetCategory:
  actAs:
    Timestampable: ~
    Sluggable:
      fields: [name]
  columns:
    name:
      type: string(255)
      notnull:  true

slugは本当のカラムなので、JobeetCategoryからgetSlug()メソッドを削除する必要があります。

レコードを保存する際にslugカラムの設定は自動的に考慮されます。 nameフィールドの値を使ってスラッグはビルドされオブジェクトに設定されます。

データベースのテーブルを更新するにはdoctrine:build-all-reloadタスクを使い、データベースにフィクスチャを投入します:

$ php symfony doctrine:build-all-reload --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件の求人でページ分割される"

Doctrineオブジェクトのリストをページ分割するために、symfonyは専用のクラス: sfDoctrinePagerを提供します。 categoryアクションにおいて、showSuccessテンプレートにjobオブジェクトを渡す代わりに、ページャーを渡します:

// apps/frontend/modules/category/actions/actions.class.php
public function executeShow(sfWebRequest $request)
{
  $this->category = $this->getRoute()->getObject();
 
  $this->pager = new sfDoctrinePager(
    'JobeetJob',
    sfConfig::get('app_max_jobs_on_category')
  );
  $this->pager->setQuery($this->category->getActiveJobsQuery());
  $this->pager->setPage($request->getParameter('page', 1));
  $this->pager->init();
}

sfRequest::getParameter()メソッドは2番目の引数でデフォルトの値を受け取ります。 上記のアクションにおいて、pageリクエストパラメーターが存在しない場合、getParameter()1を返します。

sfDoctrinePagerコンストラクターはモデルクラスとページごとに返すアイテムの最大個数を受け取ります。 後者の値を設定ファイルに追加します:

# apps/frontend/config/app.yml
all:
  active_days:          30
  max_jobs_on_homepage: 10
  max_jobs_on_category: 20

sfDoctrinePager::setQuery()メソッドはデータベースからアイテムをselectする際に使うDoctrine_Queryオブジェクトを受け取ります。

getActiveJobsQuery()メソッドを追加します:

// lib/model/doctrine/JobeetCategory.class.php
public function getActiveJobsQuery()
{
  $q = Doctrine_Query::create()
    ->from('JobeetJob j')
    ->where('j.category_id = ?', $this->getId());
 
  return Doctrine::getTable('JobeetJob')->addActiveJobsQuery($q);
}

getActiveJobsQuery()メソッドを定義したので、このメソッドを使うようにするためにJobeetCategoryメソッドをリファクタリングできます:

// 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" />
    </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 $pager->getNbResults() ?></strong> jobs in this category
 
  <?php if ($pager->haveToPaginate()): ?>
    - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager->getLastPage() ?></strong>
  <?php endif; ?>
</div>

たいていのコードでは他のページへのリンクが扱われます。 このテンプレートで使われるsfDoctrinePagerメソッドのリストは次のとおりです:

ページ分割

また明日

昨日独自の実装に取り組んだのであれば今日はあまり学ばなかったと感じるでしょう。 これはsymfonyの哲学に慣れつつあることを意味します。 symfonyのWebサイトに新しい機能を追加するプロセスは常に同じです: URLを考え、アクションを作り、モデルを更新し、テンプレートを書きます。 そして、よい開発習慣を複数の事例に適用できるのであれば、早くsymfonyマスターになれます。

明日はJobeetの新しい週の始まりです。 祝うために、真新しいトピック: テストを語ります。

8日目: ユニットテスト »
« 6日目: モデルの詳細

Questions & Feedback

If you find a typo or an error, please register and open a ticket.

If you need support or have a technical question, please post to the official user mailing-list.