Caution: You are browsing the legacy symfony 1.x part of this website.

第七天:创建分类页

昨天我们扩展了许多symfony知识:Doctrine查询,导入数据,路由,调试和自定义配置。 今天我们将进行一些富有挑战性的工作。

我希望今天关于分类页面课程,会对你有更多的价值。

分类的路由

首先,我们要给分类页面定义一个友好的URL。添加路由规则到routing.yml文件开始部分:

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

tip

无论你何时想开始实现一个新功能,首先考虑URL、并建立关联都是一个好习惯。

因为slug不是category表的一个字段(模拟表中字段,也用getXXX读取),我们需要 在JobeetCategory类中添加虚拟存取器,使路由规则可以正常工作:

// lib/model/doctrine/JobeetCategory.class.php
public function getSlug()
{
  return Jobeet::slugify($this->getName());
}

The Category Link

现在,编辑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个job的分类才显示分类链接。链接中包含此分类下招聘信息总数。为了 让模板正常工作,我们需要在JobeetCategory类中添加countActiveJobs()方法:

// 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()方法调用了JcountActiveJobs()方法, 可是JobeetJobTable中并不存在这个方法,现在创建它:

// 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 . '.expires_at DESC');
 
    return $q;
  }
}

如你所见,我们对JobeetJobTable中代码进行了重构,引入共享方法addActiveJobsQuery(), 是代码更符合DRY (Don't Repeat Yourself)原则。

tip

一段代码第一次被重用,可能只复制一下就足够了。但当多个地方都需要这段代码,你就 应该重构代码,将代码段构造成可以被多次调用的共享函数或共享方法,象我们做过的那样。

countActiveJobs()中,我们放弃使用execute()统计记录总数,而是使用 更快的count()方法。

为实现这个小功能,我们对多个文件进行了修改。在每次添加代码时,我们都将其放在 正确的层中,并尽量让它能够重用(reusable)。这个过程中,我们也对一些已有的代码进行了重构(refactor)。 这就是开发symfony项目的典型流程。In the following screenshot we are showing 5 jobs to keep it short, you should see 10 (the max_jobs_on_homepage setting):

Homepage

创建分类模块

现在开始创建category模块:

$ php symfony generate:module frontend category

如果你已经创建了一个模块,那么你可能使用过doctrine:generate-module命令。 这个命令很好,但它生成代码中有90%,可能是我们用不到的。我们使用generate:module 命令,它生成一个空模块。

tip

为什么要创建一个category模块,而不是在job模块增加一个category动作?我们可以 这样做,但是因为分类页面的主体是分类而不是招聘信息,所以创建一个专用的category 模块更符合逻辑。

当我们访问分类页面,category路由规则必须找到与slug变量关联的分类。但是数据库中 并没有slug字段,我们也不能从slug中反推出分类的名称,所以没有办法确定与slug相 联系的分类。

更新数据库

我们需要给category表添加slug字段:

This slug column can be taken care of by a Doctrine behavior named Sluggable. We simply need to enable the behavior on our JobeetCategory model and it will take care of everything for you.

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

现在数据库中已经有了slug字段,你可以删除JobeetCategory中的getSlug()方法。

note

The setting of the slug column is taken care of automatically when you save a record. The slug is built using the value of the name field and set to the object.

使用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();
  }
}

note

Because we have removed the generated executeIndex() method, you can also remove the automatically generated indexSuccess.php template (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>

局部模板(Partial Templates)

请注意,我们从job模块的indexSuccess.php模板中复制了一段<table>代码。无论在哪 重复都不是一件好事,现在我们使用一个新技巧解决这个问题。当需要重用模板的一部分时, 你可以创建一个局部模板。局部模板是一段可以被几个模板共享的模板代码片段。局部模板 只不过是另一种模板,它的名字以下划线 (_)开头:

创建_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个参数用来给局部模板传递变量。

note

为什么不使用PHP内建的include()方法代替include_partial()?最主要的原因 是1include_partial()1辅助函数支持内建缓存。

用局部模板替换每个模板<table>部分:

// 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动作中,代替传递job对象到showSuccess局部模板,现在我们只 需要传送一个pager对象:

// 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();
}

tip

getParameter()方法的第2个参数为默认值。上面的动作里,如果请求的参数不存在, 那么getParameter()将返回默认值1

sfDoctrinePager构造函数以模型类和每页记录数作为参数,记录数在配置文件中设置:

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

从数据库中选取记录时,sfDoctrinePager::setQuery()方法以一个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="/legacy/images/first.png" alt="First page" />
    </a>
 
    <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getPreviousPage() ?>">
      <img src="/legacy/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="/legacy/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="/legacy/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方法列表:

  • getResults(): 为当前页返回Doctrine对象数组
  • getNbResults(): 返回记录总数
  • haveToPaginate(): 当页数超过一页,则返回true
  • getLinks(): 返回页面链接列表
  • getPage(): 返回当前页码
  • getPreviousPage(): 返回前一页页码
  • getNextPage(): 返回下一页页码
  • getLastPage(): 返回最后一页页码

Pagination

明天见

如果你昨天自己进行了开发,并觉得今天没有学到什么东西,说明你已经开始理解symfony 的基本原理。给symfony网站添加新功能的过程基本相似:考虑URL,创建动作,更新模块, 创建模板。如果你同时注意一些好多开发习惯,你将很快成为一名symfony大师。

明天开始Jobeet新的一周。为了表示庆祝,我们将谈论一个全新的主题:测试。