Jobeetをやってみる 6日目

DoctrineとModelの使い方を学ぶ

Doctrineクエリオブジェト

Doctrine::create()でインスタンスを作成する


まずjobのリストを30日以上経過したら表示しないように変更する

<?php
// apps/frontend/modules/job/actions/actions.class.php
public function executeIndex(sfWebRequest $request)
{
  $q = Doctrine_Query::create()
    ->from('JobeetJob j')
    ->where('j.created_at > ?', date('Y-m-d H:i:s', strtotime('-30 days')));
  $this->jobeet_job_list = $q->execute();
}

Doctrineクエリオブジェクトで生成されたSQLデバッグ

実行されたクエリは log/frontend_dev.log にロギングされる
Doctrineのログは「sfDoctrineLogger」のタグになる

Feb 25 18:45:53 symfony [info] {sfDoctrineLogger} executeQuery : SELECT
j.id AS j__id, j.category_id AS j__category_id, j.type AS j__type,
j.company AS j__company, j.logo AS j__logo, j.url AS j__url,
j.position AS j__position, j.location AS j__location,
j.description AS j__description, j.how_to_apply AS j__how_to_apply,
j.token AS j__token, j.is_public AS j__is_public,
j.is_activated AS j__is_activated, j.email AS j__email,
j.expires_at AS j__expires_at, j.created_at AS j__created_at,
j.updated_at AS j__updated_at FROM jobeet_job j
WHERE j.created_at > ? - (2009-01-25 06:45:53 )

見やすいように適当に改行してるけど、実際は1行で書いてある。

オブジェクトのSerialization

このままだと作成してからの時間でjobが消えてしまうので、有効期限を設定できるようにする。

<?php
// lib/model/doctrine/JobeetJob.class.php
class JobeetJob extends BaseJobeetJob
{
  public function save(Doctrine_Connection $conn = null)
  {
    if ($this->isNew() && !$this->getExpiresAt()) {
      $now = $this->getCreatedAt() ? strtotime($this->getCreatedAt()) : time();
    }
    $this->setExpiresAt(date('Y-m-d H:i:s', strtotime('-30 days', $now)));
 
    return parent::save($conn);
  }
}

そしてリスト表示もexpires_atを見るように変更する

<?php
public function executeIndex(sfWebRequest $request)
{
  $q = Doctrine_Query::create()
    ->from('JobeetJob j')
    ->where('j.expires_at > ?', date('Y-m-d H:i:s'));
  $this->jobeet_job_list = $q->execute();
}

有効期限が過ぎたデータを作る

# data/fixtures/jobs.yml
JobeetJob:
  # other jobs

  expired_job:
    JobeetCategory: 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
    expires_at:     '2005-12-01 00:00:00'
    token:          job_expired
    email:          job@example.com
確認の前に

追加前のデータはJobeetの作成時に合わせてexpires_atが設定されているので、画面を確認する前に変更しておく
変更しないで確認すると、全て有効期限切れとなり、なにも表示されなくなる

# data/fixtures/jobs.yml
JobeetJob:
  job_sensio_labs:
    expires_at:   '2009-10-10'

  job_extreme_sensio:
    expires_at:   '2009-10-01'

app内での設定

とりあえず有効期限初期値がマジックナンバーになっているので、設定値化する

# apps/frontend/config/app.yml
all:
  active_days: 30

この設定値の取得方法は

<?php
sfConfig::get('app_active_days');

有効期限のデフォルト値を設定しているところを、この設定値を利用するように書き換える

<?php
// lib/model/doctrine/JobeetJob.class.php
public function save(Doctrine_Connection $conn = null)
{
  if ($this->isNew() && !$this->getExpiresAt()) {
    $now = $this->getCreatedAt() ? strtotime($this->getCreatedAt()) : time();
    $this->setExpiresAt(date('Y-m-d H:i:s', strtotime('-'.sfConfig::get('app_active_days').' days', $now)));
  }

  return parent::save($conn);
}

リファクタリング

Doctrine_QueryはAction(Controlレイヤ)で利用するべきではないので、Modelレイヤに処理を移動させる

<?php
// lib/model/doctrine/JobeetJobTable.class.php
class JobeetJobTable extends Doctrine_Table
{
  public function getActiveJobs()
  {
    $q = $this->createQuery('j')
      ->where('j.expires_at > ?', date('Y-m-d H:i:s'));

    return $q->execute();
  }
}

そしてActionの処理をモデルから呼び出すように変更

<?php
// apps/frontend/modules/job/actions/actions.class.php
public function executeIndex(sfWebRequest $request)
{
  $this->jobeet_job_list = Doctrine::getTable('JobeetJob')->getActiveJobs();
}

ついでに有効期限が切れそうな順でソートする*1

<?php
// lib/model/doctrine/JobeetJobTable.class.php
class JobeetJobTable extends Doctrine_Table
{
  public function getActiveJobs()
  {
    $q = $this->createQuery('j')
      ->where('j.expires_at > ?', date('Y-m-d H:i:s'))
      ->orderBy('j.expires_at ASC');

    return $q->execute();
  }
}

カテゴリ毎にjobを表示する

<?php
// lib/model/doctrine/JobeetCategoryTable.class.php
class JobeetCategoryTable extends Doctrine_Table
{
  public function getWithJobs()
  {
    $q = $this->createQuery('c')
      ->leftJoin('c.JobeetJobs j')
      ->where('j.expires_at > ?', date('Y-m-d H:i:s'));

    return $q->execute();
  }
}
<?php
// apps/frontend/modules/job/actions/actions.class.php
public function executeIndex(sfWebRequest $request)
{
  $this->categories = Doctrine::getTable('JobeetCategory')->getWithJobs();
}
<?php /* apps/frontend/modules/job/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>
<?php
// lib/model/doctrine/JobeetCategory.class.php
public function getActiveJobs()
{
  $q = Doctrine_Query::create()
    ->from('JobeetJob j')
    ->where('j.category_id = ?', $this->getId());
 
  return Doctrine::getTable('JobeetJob')->getActiveJobs($q);
}

JobeetJobTable::getActiveJobs()にDoctrine_Queryオブジェクトを渡すと、有効期限を参照する条件だけを加えるように変更する

<?php
// lib/model/doctrine/JobeetJobTable.class.php
public function getActiveJobs(Doctrine_Query $q = null)
{
  if (is_null($q))
  {
    $q = Doctrine_Query::create()
      ->from('JobeetJob j');
  }
 
  $q->andWhere('j.expires_at > ?', date('Y-m-d H:i:s'))
    ->addOrderBy('j.expires_at ASC');
 
  return $q->execute();
}

1ページに表示するjob数を設定する

データの取得数を制限するので、JobeetCategory::getActiveJobs()にLIMITを付けるように変更する

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

実際にテンプレートで使用するときに、表示件数を設定するので、マジックナンバーにならないように設定値化する

# apps/frontend/config/app.yml
all:
  active_days:         30
  max_job_on_homepage: 5

テンプレートに適用

<?php /* apps/frontend/modules/job/templates/indexSuccess.php at 14 */ ?>
<?php foreach ($category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage') as $i => $job): ?>

FixturesでPHPコードを利用する

表示個数の制限のデバッグのために、それぞれのカテゴリで11以上のデータが必要となる。
その為にはjobs.ymlにデータを記述しなくてはならないが、そんなに多くのデータを作成するのは骨が折れる

symfonyでは、YAMLファイル内でPHPコードが利用できるので、ループでその悩みを解決する
※制限として、「文末で使用する場合は "\n" を出力しないといけない」というのがある。

# data/fixtures/jobs.yml
<?php for ($i = 100; $i <= 130; $i++): ?>
  job_<?php echo $i ?>:
    JobeetCategory: 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
    expires_at:   '2009-10-31 00:00:00'
    token:        job_<?php echo "{$i}\n" ?>
    email:        job@example.com

<?php endfor; ?>

有効期限切れのjobの扱い

現状だと有効期限が切れたjobも見れてしまう
/frontend_dev.php/job/sensio-labs/paris-france/{$id}/web-developer-expired
※ serialが初期化された状態でdoctrine:data-loadをした場合、$idは恐らく3か4になる

見ることができないように、404ページへと強制的に遷移させたい
その条件処理をルーティングで実現する

# apps/frontend/config/routing.yml
job_show_user:
  url:     /job/:company_slug/:location_slug/:id/:position_slug
  class:   sfDoctrineRoute
  options:
    model: JobeetJob
    type: object
    method_for_query: retrieveActiveJob
  param:   { module: job, action: show }
  requirements:
    id: \d+
    sf_method: [get]

このメソッドはTableオブジェクトに実装する必要がある

<?php
// lib/model/doctrine/JobeetJobTable.class.php
class JobeetJobTable extends Doctrine_Table
{
  public function  retrieveActiveJob(Doctrine_Query $q)
  {
    $q->andWhere('a.expires_at > ?', date('Y-m-d H:i:s'));

    return $q->fetchOne();
  }
}

やってみて

本家の文章に結構誤植があったなーと
例えばdate('Y-m-d h:i:s')になってて、12時間しか有効期限が判別できなかったり



*1:本家だと逆順だけど、普通はこっちのほうがユーザビリティがあると思う