[原创中文翻译]symfony askeet24:第十八天,过滤器。
October 8, 2007 – 8:24 pm[欢迎转载,转载请注名出处http://symfony.net.cn。本文英文版权归symfony官方网站所有]
We saw yesterday how to make the askeet service available through an XML API. Today’s program will focus on filters, and we will illustrate their use with the creation of sub domains to askeet. For instance, ‘php.askeet.com’ will display only PHP tagged questions, and any new question posted in this domain will be tagged with ‘php’. Let’s call this new feature ‘askeet universe’ and develop it right away.
昨天我们讲解了怎样通过XML API来让askeet服务可用。今天的程序关注过滤器, 我们通过建立askeet二级域名来讲解它的作用。例如,php.askeet.com只显示被标记为“PHP”的问题,在这里发布的问题都会被标记为“PHP”。现在就让我们来开发它。
Configurable feature
可配制的特性
First, this new feature has to be optional. Askeet is supposed to be a piece of software that you can install on any configuration, and you might not want to allow subdomains in, say, an enterprise Intranet.
首先,新的特点可以选配。askeet是一个可以按照配置安装,可以不允许有二级域名,企业级别的软件。
So we will add a new parameter in the application configuration. To enable the universe feature, it must be set to on. To add a custom parameter, open the askeet/apps/frontend/config/app.yml file and add:
我们给程序配置写上新参数。启用universe特性,它必须被设置为on。加入自定义参数,打开askeet/apps/frontend/config/app.yml文件并加入:
all:
.global:
universe: on
This parameter is now available to all the actions of your application. To get its value, use the sfConfig::get(’app_universe’) call.
参数现在适用于所有的动作。为了获得初始值,使用sfConfig::get(“app_universe”)。
You will find more about custom settings in the configuration chapter of the symfony book.
你会发现更多的自定义配置内容,去看看symfony book的configuration章节。
Create a filter
创建过滤器
A filter is a piece of code executed before every action. That’s what we need to inspect the host name prior to all actions, in search for a tag name in the domain.
过滤器是在每个动作执行前执行的代码。这是我们需要的,来检查所有动作涉及的主机名,根据标签名搜索域名中是否包含。
Filters have to be declared in a special configuration file to be executed, the askeet/apps/frontend/config/filters.yml file. This file is created by default when you initiate an application, and it is empty. Open it and add in:
过滤器被写在配置文件里执行,见askeet/apps/frontend/config/filters.yml文件。当你创建程序时此文件就默认创建了,里面是空的。打开加几行代码:
myTagFilter:
class: myTagFilter
This declares a new myTagFilter filter. We will create a myTagFilter.class.php class file in the askeet/apps/frontend/lib/ directory to make it available to the whole frontend application:
这里定义了新的myTagFilter过滤器。我们在askeet/apps/frontend/lib/下创建myTagFilter.class.php类文件来让整个前台程序都能用:
<?php
class myTagFilter extends sfFilter
{
public function execute ($filterChain)
{
// execute this filter only once
if (sfConfig::get(’app_universe’) && $this->isFirstCall())
{
// do things
}
// execute next filter
$filterChain->execute();
}
}
?>
This is the general structure of a filter. If the app_universe parameter is not set to on, the filter doesn’t execute. As we want the filter to be executed only once per request (although there may be more than one action per request, because we use forwards), we check the ->isFirstCall() method. It is true only the first time the filter is executed in a given request.
这是过滤器的基本结构。如果app_universe参数没有打开,过滤器不执行。我们想让过滤器针对每个请求执行一次(因为我们用了forwards写法,所以每个请求都会用到不止一个动作),我们用->isFirstCall()方法。当发出请求时只有第一次过滤器执行。
One word about the filterChain object: All the steps of the execution of a request (configuration, front controller, action, view) are a chain of filters. A custom filter just comes very early in this chain (before the execution of an action), and it must not break the execution of the other steps of the chain filter. That’s why a custom filter must always end up with $filterChain->execute();.
用一句话来总结filterChain对象:所有请求执行步骤最后都在过滤器上(配置,前台控制,动作,视图)。自定义过滤器很早执行(在动作执行前),而且不能打断其他过滤器的执行。这就是为什么自定义过滤器必须以$filterChain->execute()结束;
The sfFilter class has an initialize() method, executed when the filter object is created. You can override it in your custom filter if you need to deal with filter parameters in your own way.
sfFilter类有initialize()方法,当filter对象创建时执行。如果你想自己处理参数过滤,你可以在你自定义的过滤器里重写。
Get a permanent tag from the domain name
从域名所包含的字母里获得标签
We want to inspect the host name to check if it contains a sub domain that might be a tag. Tags like ‘www’ or ‘askeet’ must be ignored. In addition, we want to be able to modify the rule of sub domains to ignore, for instance if we use load balancing techniques with alternative domain names such as ‘www1′, ‘www2′, etc. This is why we decided to put the rule of universes to ignore (a regular expression) in a parameter of the filters.yml configuration file:
检查主机名看看是不是存在包含标签的子域名,像“www”或者“askeet”之类的标签必须被忽略。进一步说,我们想修改子域名规则来忽略这类标签,例如如果我们用载入平衡技术处理例如www1、www2的标签,等等。我们决定把universe的规则省略(用正则表达式),把参数写在filters.yml配置文件中:
myTagFilter:
class: myTagFilter
param:
host_exclude_regex: /^(www|askeet)/
Now it is time to have a look at the content of the execute() action of the filter (replacing the // do things comment):
现在看看过滤器执行的execute()动作:
// is there a tag in the hostname?
$hostname = $this->getContext()->getRequest()->getHost();
if (!preg_match($this->getParameter('host_exclude_regex'), $hostname) && $pos = strpos($hostname, '.'))
{
$tag = Tag::normalize(substr($hostname, 0, $pos));
// add a permanent tag custom configuration parameter
sfConfig::set(’app_permanent_tag’, $tag);
// add a custom stylesheet
$this->getContext()->getResponse()->addStylesheet($tag);
}
The filter looks for a possible permanent tag in the URI. If one is found, it is added as a custom parameter, and a custom stylesheet is added to the view. So, for instance:
过滤器在URL里寻找可能用到的参数标签。如果找到一个,就设置成自定义参数,自定义样式加到视图层。例如:
// calling this URI to display the PHP universe
http://php.askeet.com
// will create a constant
sfConfig::set(’app_permanent_tag’, ‘php’);
// and include a custom stylesheet in the view
<link rel=”stylesheet” type=”text/css” media=”screen” href=”/css/php.css” mce_href=”/css/php.css” />
As the execution of a custom filter happens very early in the filter chain, and even earlier than the view configuration parsing, the custom stylesheet will appear in the output HTML file before the other style sheets. So if you have to override style settings of the main askeet site in a custom stylesheet, these settings need to be declared !important.
自定义过滤器在过滤操作的早期执行,比视图层语法分析执行还要早,自定义样式会早于其他样式在输出的HTML里显示。所以如果你不得不重写自定义样式表,这些配置的声明就很重要!
Model modifications
修改模型
We now need to modify the actions and model methods that should take the permanent tag into account. As we like to keep the model logic inside the Model layer, and because refactoring becomes really necessary, we take advantage of the permanent tag modifications to take the Propel requests out of the actions, and put them in the model. If you take a look at the list of modifications for today’s release in the askeet trac, you will see that a few new model methods were created, and that the actions call these methods instead of doing doSelect() by themselves:
我们现在需要修改动作和模型方法,把标签参数放进帐户。我们想把模型逻辑放到模型层上,重新修饰变得很有必要,我们使用参数标签修改把propel 请求分离出动作,把它们放到模型里。如果你看一看askeet trac上今天的修改列表,你会看到几个新模型方法创建了,动作调用这些方法代替使用自身的doSelect():
Answer->getRecent() Question->getPopularAnswers() QuestionPeer::getPopular() QuestionPeer::getRecent() QuestionTagPeer::getForUserLike()Filter lists according to the permanent tag
根据标签显示过滤器列表
When a list of questions, tags, or answers are displayed in an askeet universe, all the requests must take into account a new search parameter. In symfony, search parameters are calls to the ->add() method of the Criteria object.
当在askeet上显示问题,标签,或者答案列表时,所有的请求都被送到了新搜索参数里。在symfony里,搜索参数被Criteria对象的->add()函数调用。
So add the following method to the QuestionPeer and AnswerPeer classes:
给QuestionPeer和AnswerPeer类增加几个函数:
private static function addPermanentTagToCriteria($criteria)
{
if (sfConfig::get(’app_permanent_tag’))
{
$criteria->addJoin(self::ID, QuestionTagPeer::QUESTION_ID, Criteria::LEFT_JOIN);
$criteria->add(QuestionTagPeer::NORMALIZED_TAG, sfConfig::get(’app_permanent_tag’));
$criteria->setDistinct();
}
return $criteria;
}
We now need to look for all the model methods that return a list that must be filtered in a universe, and add to the Criteria definition the following line:
现在我们看看所有模型返回都在universe里过滤的列表,增加后面几行到Criteria:
$c = self::addPermanentTagToCriteria($c);
For instance, the QuestionPeer::getHomepagePager() has to be modified to look like:
例如,修改QuestionPeer::getHomepagePager():
public static function getHomepagePager($page)
{
$pager = new sfPropelPager(’Question’, sfConfig::get(’app_pager_homepage_max’));
$c = new Criteria();
$c->addDescendingOrderByColumn(self::INTERESTED_USERS);
// add this line
$c = self::addPermanentTagToCriteria($c);
$pager->setCriteria($c);
$pager->setPage($page);
$pager->setPeerMethod(’doSelectJoinUser’);
$pager->init();
return $pager;
}
The same modification must be repeated quite a few times, in the following methods:
同样的修改必须重复几次,看下面:
QuestionPeer::getHomepagePager()
QuestionPeer::getPopular()
QuestionPeer::getPopular()
QuestionPeer::getRecentPager()
QuestionPeer::getRecent()
AnswerPeer::getPager()
AnswerPeer::getRecentPager()
AnswerPeer::getRecent()
For complex requests not using the Criteria object, we need to add the permanent tag as a WHERE statement in the SQL code. Check how we did it for the QuestionTagPeer::getPopularTags() and QuestionTagPeer::getPopularTagsFor() methods in the askeet trac or in the SVN repository.
对复杂请求不要用Criteria对象,我们现在在SQL查询代码中把标签当作WHERE条件。去askeet trac或者在SVN里看看我们对QuestionTagPeer::getPopularTagsFor()函数怎么做的。
Lists of tags for a question or a user
给答案或者用户显示标签列表
All the questions of the ‘PHP’ universe are tagged with ‘php’. But if a user is browsing questions in the ‘PHP’ universe, the ‘php’ tag must not be displayed in the list of tags since it is implied. When outputting a list of tags for a question or a user in a universe, the permanent tag must be omitted. This can be done easily by bypassing it in loops, as for instance in the Question->getTags() method:
所有关于PHP的问题都被加上了PHP标签。但如果用户在PHP范围内浏览问题,我们做得含蓄点,PHP标签就不要显示在标签列表了。所以在一定范围内输出问题或者用户的标签列表,永久标签必须省略掉。实现方式很简单,在循环里绕过就成,例如在Question->getTags()方法中:
public function getTags()
{
$c = new Criteria();
$c->add(QuestionTagPeer::QUESTION_ID, $this->getId());
$c->addGroupByColumn(QuestionTagPeer::NORMALIZED_TAG);
$c->setDistinct();
$c->addAscendingOrderByColumn(QuestionTagPeer::NORMALIZED_TAG);
$tags = array();
foreach (QuestionTagPeer::doSelect($c) as $tag)
{
if (sfConfig::get(’app_permanent_tag’) == $tag)
{
continue;
}
$tags[] = $tag->getNormalizedTag();
}
return $tags;
}
The same kind of technique is to be used in the following methods:
在后面的方法中使用了同样的技术:
Question->getTags()
Question->getPopularTags()
User->getTagsFor()
User->getPopularTags()
Append the permanent tag to new questions
附加永久标签到新问题
When a question is created in an askeet universe, it must be tagged with the permanent tag in addition to the tags entered by the user. As a reminder, in the question/add method, the Question->addTagsForUser() method is called:
当在askeet某二级域名下里创建问题时,提交的问题被自动加上永久标签。提醒一下,在question/add方法里,Question->addTagsForUser()方法被调用:
$question->addTagsForUser($this->getRequestParameter('tag'), $sf_user->getId());
…where the tag request parameters contains the tags entered by the user, separated by blanks (we called this a ‘phrase’). So we will just append the permanent tag to the phrase in the first line of the addTagsForUser method:
… …永久标签里包含用户输入的标签,用空格格开(我们叫这个为“短语”)。我们将附加永久标签赋值给“短语”,在addTagsForUser方法的第一行:
public function addTagsForUser($phrase, $userId)
{
// split phrase into individual tags
$tags = Tag::splitPhrase($phrase.(sfConfig::get('app_permanent_tag') ? ' '.sfConfig::get('app_permanent_tag') : ''));
// add tags
foreach ($tags as $tag)
{
$questionTag = new QuestionTag();
$questionTag->setQuestionId($this->getId());
$questionTag->setUserId($userId);
$questionTag->setTag($tag);
$questionTag->save();
}
}
That’s it: if the user hasn’t already included the permanent tag, it is added to the list of tags to be given to the new question.
这就是了:如果用户没有使用永久标签,那就自动给他加上。
Server configuration
服务器配置
In order to make the new domains available, you have to modify your web server configuration.
为了让新二级域名工作正常,你需要修改你的web服务器配置。
In local, i.e. if you don’t control the DNS to the askeet site, add a new host for each new universe that you want to add (in the /etc/hosts file in a Linux system, or in the C:\WINDOWS\system32\drivers\etc\hosts file in a Windows system):
在本地,也就是说,如果你无法控制给askeet的DNS服务器,那么如此增加你想要得二级域名(Linux下见/etc/hosts,如果在windows系统下,在c:\windows\system32\drivers\etc\hosts文件):
127.0.0.1 php.askeet
127.0.0.1 senseoflife.askeet
127.0.0.1 women.askeet
You need administrator rights to do this.
做这些需要管理员权限。
In all cases, you have to add a server alias in your virtual host configuration (in the httpd.conf Apache file):
你还需要加服务器别名在apache主机配置文件里(httpd.conf apache文件里):
<VirtualHost *:80>
ServerName askeet
ServerAlias *.askeet
DocumentRoot "/home/sfprojects/askeet/web"
DirectoryIndex index.php
Alias /sf /usr/local/lib/php/data/symfony/web/sf
<Directory “/home/sfprojects/askeet/web”>
AllowOverride All
</Directory>
</VirtualHost>
After restarting the web server, you can test one of the universes by requesting, for instance:
重新启动web服务器,这样测试即可: