Practical symfony

第七天:创建分类页

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

分类的路由

The Category Link

创建分类模块

更新数据库

局部模板(Partial Templates)

分页

明天见

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 for the 1.2 version - Propel edition - 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 (Propel edition)
Support symfony!
Buy this book
or donate.
Buy Practical symfony (Propel edition) from amazon.com

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

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

分类的路由

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

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

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

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

// lib/model/JobeetCategory.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/JobeetCategory.php
public function countActiveJobs()
{
  $criteria = new Criteria();
  $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
 
  return JobeetJobPeer::countActiveJobs($criteria);
}

我们可以看到countActiveJobs()方法调用了JobeetJobPeer::countActiveJobs()方法, 可是JobeetJobPeer中并不存在这个方法,现在创建它:

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

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

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

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

为实现这个小功能,我们对多个文件进行了修改。在每次添加代码时,我们都将其放在 正确的层中,并尽量让它能够重用(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

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

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

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

更新数据库

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

# config/schema.yml
propel:
  jobeet_category:
    id:           ~
    name:         { type: varchar(255), required: true }
    slug:         { type: varchar(255), required: true, index: unique }

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

由于每次改变分类名,都需要重新生成slug,所有我们需要覆盖setName()方法, 在每次保存分类名的同时保存相应的slug

// lib/model/JobeetCategory.php
public function setName($name)
{
  parent::setName($name);
 
  $this->setSlug(Jobeet::slugify($name));
}

使用propel:build-all-load更新数据库表,并载入数据:

$ php symfony propel:build-all-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();
  }
}

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

为什么不使用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条招聘信息。”

为分页显示Propel对象,symfony提供了一个专门的类: sfPropelPager。 在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 sfPropelPager(
    'JobeetJob',
    sfConfig::get('app_max_jobs_on_category')
  );
  $this->pager->setCriteria($this->category->getActiveJobsCriteria());
  $this->pager->setPage($request->getParameter('page', 1));
  $this->pager->init();
}

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

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

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

从数据库中选取记录时,sfPropelPager::setCriteria()方法以一个Criteria 对象为参数。现在,我们重构一小段模型代码:

添加getActiveJobsCriteria()方法:

// lib/model/JobeetCategory.php
public function getActiveJobsCriteria()
{
  $criteria = new Criteria();
  $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
 
  return JobeetJobPeer::addActiveJobsCriteria($criteria);
}

现在我们有了getActiveJobsCriteria()方法,可以重构JobeetCategory中其它方法 来调用它:

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

最后更新模板:

<!-- 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>

这里大部分代码用来处理指向其它页面的链接。下面是模板所使用的sfPropelPager方法列表:

Pagination

明天见

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

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

第八天:单元测试 »
« 第六天:更多模型知识

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.