昨天我们扩展了许多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):
创建分类模块
现在开始创建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()
: 返回最后一页页码
明天见
如果你昨天自己进行了开发,并觉得今天没有学到什么东西,说明你已经开始理解symfony 的基本原理。给symfony网站添加新功能的过程基本相似:考虑URL,创建动作,更新模块, 创建模板。如果你同时注意一些好多开发习惯,你将很快成为一名symfony大师。
明天开始Jobeet新的一周。为了表示庆祝,我们将谈论一个全新的主题:测试。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.