原文:http://www.symfony-project.com/askeet/5
昨天的Symfony
在第四天,我们运用了重构概念把一系列的源码移到与其相关联的文档。你学习了怎样修改模型,许多常用函数由此从动作移到模型。
开发已经明晰,但应用程序仍缺乏功能。是时间建立与用户的互动了。除了超链接之外,HTML的互动之基就是表格了。
今天的任务是制作登陆表单和为问题分页。 这将是一个快速开发,但是它可以让你从昨天恢复过来。
测试资料中以含有用户的信息,但我们的程序还不能识别任何一个用户。就让我们为每页添加一个登录的超链接。打开共享模板askeet/apps/frontend/templates/layout.php,在link_to about前加入:
<li><?php echo link_to('sign in', 'user/login') ?></li>
注意:目前的模板把以上的超链接放置于调试工具栏的后面。点击“Sf”图标显示它。
是时间创建user模块了。question模块被在第二天建立起来,但是这次我们将只让symfony创建模块框架,而把其余代码留给我们自己来写。输入以下创建指令:
$ symfony init-module frontend user
注意:新的模块含有默认动作index和模板indexSucess.php并不是我们所需要的,所以删除它们。
打开askeet/apps/frontend/modules/user/actions/action.class.php, 加入以下登录动作:
public function executeLogin() { $this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer()); return sfView::SUCCESS; }
以上的动作把反向链接(referrer)保存为请求的一项属性。保存后此属性就能被相应的模板访问。我们把反向链接置于<hidden> tag中,当表单成功提交后,动作就能把页面转到反向链接。
return sfView::SUCCESS 把动作的结果传递到loginSuccess.php模板。由于返回的是SUCCESS,动作调出loginSuccess.php这一模板。注意login是动作的名称。
在更深入讲解动作之前,我们先看一看模板。
许多网上的信息交换是通过表单来达到的。Symfony提供了一套表单建立和管理的助手。
在In the askeet/apps/frontend/modules/user/templates/目录下,建立模板loginSuccess.php:
<?php echo form_tag('user/login') ?> <fieldset> <div class="form-row"> <label for="nickname">nickname:</label> <?php echo input_tag('nickname', $sf_params->get('nickname')) ?> </div> <div class="form-row"> <label for="password">password:</label> <?php echo input_password_tag('password') ?> </div> </fieldset> <?php echo input_hidden_tag('referer', $sf_request->getAttribute('referer')) ?> <?php echo submit_tag('sign in') ?> </form>
以上模板引用了表单助手(form helpers)。这些助手可使表单撰写自动化。form_tag()助手建立一个默认动作为POST的表单,且动作指向指定的参数。input_tag()助手返回一个<input>tag其id属性的值为助手函数的第一个参数;而<input>的默认值设定为函数的第二个参数。更多关于表单助手请阅读相关章节。
到此的最关键点是以上表单提交与显示的动作是相同的(form_tag()的参数)。所以,让我们回到原来的动作。
把我们刚写的login动作覆盖为以下代码:
public function executeLogin() { if ($this->getRequest()->getMethod() != sfRequest::POST) { // display the form $this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer()); } else { // handle the form submission $nickname = $this->getRequestParameter('nickname'); $c = new Criteria(); $c->add(UserPeer::NICKNAME, $nickname); $user = UserPeer::doSelectOne($c); // nickname exists? if ($user) { // password is OK? if (true) { $this->getUser()->setAuthenticated(true); $this->getUser()->addCredential('subscriber'); $this->getUser()->setAttribute('subscriber_id', $user->getId(), 'subscriber'); $this->getUser()->setAttribute('nickname', $user->getNickname(), 'subscriber'); // redirect to last page return $this->redirect($this->getRequestParameter('referer', '@homepage')); } } } }
login动作将用于显示和处理登陆表单。因此,它需要知道是被呼出的环境(context)。如果login动作不是在POST模式下呼出的,它则是由链接呼出。假如login动作在POST模式下被访问,则说明了它是由表单提交呼出的。后者需要进行表单处理。
login动作从请求中获取nickname参数,然后检查它是否以存在于数据库的User资料表中。
在不久的将来我们会建立密码控制的机制。目前的动作只是简单地把用户的nickname和id属性储存于session中。referer被预存于<hidden> tag 中,最终用户被转向到referer指定的地址。如果referer没有预存的话,用户则被转向到主页(@homepage)。
注意以上的范例中有两种不同的属性:request属性和session属性。request属性($this→getRequest()→setAttribute())只存在于请求范围,一旦被转向它将被销毁。而session属性($this→getUser()→setAttribute())被保存直道用户期间(session)结束。要了解更多关于属性,请阅读Symfony book parameter holder这章。
用户可以登录askeet网站是一件非常好的事,但是他们不会仅仅是为了好玩才这样做的。创建登录动作的目的在于管理用户权限:登录的用户能够发帖,声明对一个问题的兴趣, 评论一个问题,而其他非登录用户则只能够浏览。
验证一用户,你需要调用sfUser里的setAuthenticated()函数。除了鉴定,sfUser提供了凭证管理机制(→addCredential())去细化用户访问权限控制。更多关于凭证。
以下代码鉴定用户,添加“subscriber”权限。
$this->getContext()->getUser()->setAuthenticated(true); $this->getContext()->getUser()->addCredential('subscriber');
当nickname被识别后, 不但用户的数据被放入会话(session)属性中,用户同样被给于权限去访问受到保护的网站内容。在明天,我们将会看到如何限制验证后的用户访问网站内容。
注意→setAttribute()函数的最后一个参数 “subscriber” 定义命名空间(namespace),其优点在于让我们储存相同名字的属性到不同的命名空间。另一优点是可让我们用一个名令快速清除命名空间里的所用属性:
public function executeLogout() { $this->getUser()->setAuthenticated(false); $this->getUser()->clearCredentials(); $this->getUser()->getAttributeHolder()->removeNamespace('subscriber'); $this->redirect('@homepage'); }
使运用命名空间可以使我们不用一个接一个的删除这两个属性。 这样少了一行代码, 要想着这变懒!
原来的模板显示 “login” 链接即使用户已经登录。就做一下更正吧。打开askeet/apps/frontend/templates/layout.php,把我们今天最初加到那段代码改为:
<?php if ($sf_user->isAuthenticated()): ?> <li><?php echo link_to('sign out', 'user/logout') ?></li> <li><?php echo link_to($sf_user->getAttribute('nickname', '', 'subscriber').' profile', 'user/profile') ?></li> <?php else: ?> <li><?php echo link_to('sign in/register', 'user/login') ?></li> <?php endif ?>
是时候测试了:点击 sign in, 输入有效用户名(anonymous),登录。如果登录后 “sign in”变成 “sign out”,表示程序正确运行。最后点击 “sign out”看看 “sign in”是否真确显示。
更多关于操作对话属性的内容session的说明。
当数以千计的symfony爱好者涌向askeet网站时, 首页显示的问题数量汇编的越来越长。为了避免缓慢的相应和过多滚屏,我们必须使用分页技术。
Symfony提供了sfPropelPager来达到分页的目的。sfPropelPager封装了对数据库的请求,从而只返回显示页所需的数据。打个比方说,假如一分页器初始设定显示10个档案,sfPropelPager将会把数据库查询限制为10档案,利用偏差实现分页(SQL里的LIMIT和OFFSET语句)。
教程第三天question模块的list动作如下:
public function executeList () { $this->questions = QuestionPeer::doSelect(new Criteria()); }
修改此动作,目标是让模板访问到sfPropelPager对象,同时让问题由被关注的数目(number of interest)排列:
public function executeList () { $pager = new sfPropelPager('Question', 2); $c = new Criteria(); $c->addDescendingOrderByColumn(QuestionPeer::INTERESTED_USERS); $pager->setCriteria($c); $pager->setPage($this->getRequestParameter('page', 1)); $pager->setPeerMethod('doSelectJoinUser'); $pager->init(); $this->question_pager = $pager; }
sfPropelPager在初始化时,设定了每页档案数量,以上范例为2档每页。→setPage()指定了显示页,至于哪页被显示则由请求的参数决定。举个例说,假如请求参数是2,表示第二页,sfPropelPager将返回档案3-5。这个请求参数的默认值是1,因此1-2档将返回如果此参数空缺。
译著:我认为如果请求参数是2,返回的应是3-4
更多关于pager的说明
在通常情况下,把常数置于配置文件是比较好的做法。就以以上的范例来说,每页档案数可存放到配置文件。把以上有 new sfPropelPager 这一行改为:
$pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));
打开文件 askeet/apps/frontend/config/app.yml,加入:
all:
pager:
homepage_max: 2
pager关键词被用来做命名空间, 这就是为什么它出现在参数名称中的原因。更多关于命名自定义参数的配置和规则请看配置文件的说明
打开 listSuccess.php 模板,将
<?php foreach($questions as $question): ?>
修改为
<?php foreach($question_pager->getResults() as $question): ?>
我们的网页将显示分页好的页面。
目前的模板需植入网页导航栏 - 让用户能够前翻页和后翻页。在模板的最后加入以下代码:
<div id="question_pager"> <?php if ($question_pager->haveToPaginate()): ?> <?php echo link_to('«', 'question/list?page=1') ?> <?php echo link_to('<', 'question/list?page='.$question_pager->getPreviousPage()) ?> <?php foreach ($question_pager->getLinks() as $page): ?> <?php echo link_to_unless($page == $question_pager->getPage(), $page, 'question/list?page='.$page) ?> <?php echo ($page != $question_pager->getCurrentMaxLink()) ? '-' : '' ?> <?php endforeach; ?> <?php echo link_to('>', 'question/list?page='.$question_pager->getNextPage()) ?> <?php echo link_to('»', 'question/list?page='.$question_pager->getLastPage()) ?> <?php endif; ?> </div>
这段代码利用了很多sfPropelPager对象的方法。 在→haveToPaginate()中,只有当要显示的结果超过了每页所允许的数量才返回真值。而→getPreviousPage(), →getNextPage() and →getLastPage()的作用和他们的名字是一样的。→getLinks()提供了有多少个页需要被显示的数组。→getCurrentMaxLink()返回了最后一个页码。
这个例子展示了一个简单的symfony链接助手(Helper): link_to_unless()。在给定的第一个参数是假的情况下,他将输出一个正常的link_to助手。如果为真,他将只显示被包含在一个简单的<span>里面的文本而不显示链接。
你测试分页了吗?应该这样做。修改还没有结束除非你亲眼验证了它是可以工作的。为了做测试,需要打开我们在第三天所建立的测试数据文件, 然后添加一些问题使页面导航出现。重新载入数据,然后浏览主页。 瞧,它出现了。
默认情况下的URL像这样:
http://askeet/frontend_dev.php/question/list/page/XX
路由规则可让我们建立这样的URL:
http://askeet/frontend_dev.php/index/XX
打开文件apps/frontend/config/routing.yml,在顶部加入:
popular_questions:
url: /index/:page
param: { module: question, action: list }
既然我们已经在编辑这个文件了,那就再为login添加一个路由规则:
login:
url: /login
param: { module: user, action: login }
question/list 动作执行的代码与模型有密切的联系,因此我们将把这段代码移到模型。 把原来的question/list 动作改为:
public function executeList () { $this->question_pager = QuestionPeer::getHomepagePager($this->getRequestParameter('page', 1)); }
打开模型 QuestionPeer.php (位于lib/model文件夹),加入以下函数:
public static function getHomepagePager($page) { $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max')); $c = new Criteria(); $c->addDescendingOrderByColumn(self::INTERESTED_USERS); $pager->setCriteria($c); $pager->setPage($page); $pager->setPeerMethod('doSelectJoinUser'); $pager->init(); return $pager; }
同样的概念实用于我们昨天写的 question/show 动作。用Propel对象来检索问题的行为应该属于模型,所以把 question/show 动作改为:
public function executeShow() { $this->question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter('stripped_title')); $this->forward404Unless($this->question); }
QuestionPeer.php 随之也需修改:
public static function getQuestionFromTitle($title) { $c = new Criteria(); $c->add(QuestionPeer::STRIPPED_TITLE, $title); return self::doSelectOne($c); }
question/templates/listSuccess.php 模板显示问题的那一部分代码将用于其他地方,因此我们将把这部分置于片断(fragment)内。把 listSuccess.php 的内容改为以下:
<h1>popular questions</h1> <?php echo include_partial('list', array('question_pager' => $question_pager)) ?>
在相同的目录下(question/templates/),新建文件 _list.php。 _list.php是一个片断,其内容如下:
<?php use_helpers('Text', 'Question') ?> <?php foreach($question_pager->getResults() as $question): ?> <div class="interested_block"> <?php echo include_partial('interested_user', array('question' => $question)) ?> </div> <h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2> <div class="question_body"> <?php echo truncate_text($question->getBody(), 200) ?> </div> <?php endforeach ?> <div id="question_pager"> <?php if ($question_pager->haveToPaginate()): ?> <?php echo question_pager_link('«', 1) ?> <?php echo question_pager_link('<', $question_pager->getPreviousPage()) ?> <?php foreach ($question_pager->getLinks() as $page): ?> <?php echo link_to_unless($page == $question_pager->getPage(), $page, 'question/list?page='.$page) ?> <?php echo ($page != $question_pager->getCurrentMaxLink()) ? '-' : '' ?> <?php endforeach ?> <?php echo question_pager_link('>', $question_pager->getNextPage()) ?> <?php echo question_pager_link('»', $question_pager->getLastPage()) ?> <?php endif ?> </div>
登录表格和分页器在当今几乎所有的网络应用程序中都有使用。今天,你看到了用symfony开发他们是相当容易的。再一次,我们用重构来结束了一天的工作。在不需要设计一个大的图纸下,重构是我们一点一点开发应用程序所必须付出的代价。
明天,我们将继续我们登录处理的工作去限制注册用户访问网站的一些内容。 并且我们将实现以一些表格验证去避免不正确的数据递交。