[原创中文翻译]symfony askeet24:第十三天,标签I。
October 6, 2007 – 5:45 am[欢迎转载,转载请注名出处http://symfony.net.cn。本文英文版权归symfony官方网站所有]
The QuestionTag class
问题标签类
There are several ways to implement tags. We chose to add a QuestionTag table with the following structure:
实现标签的方法有很多。按照下图所示结构增加QuestionTag数据表:
When a user tags a question, it creates a new record in the question_tag table, linked to both the user table and the question table. There are two versions of the tag recorded: The one entered by the user, and a normalized version (all lower case, without any special character) used for indexing.
当用户给问题标记标签时,在question_tag表创建新记录,连接到用户表和问题表。数据表里有两种形式的标签:用户自己输入的标签;规范化标签(全部小写,没有特殊字符)用来做索引。
Schema update
更新schema
As usual, adding a table to a symfony project is done by appending its Propel definition to the schema.xml file:
像往常一样,给项目加个表,改改schema.xml文件:
...
<table name="ask_question_tag" phpName="QuestionTag">
<column name="question_id" type="integer" primaryKey="true" />
<foreign-key foreignTable="ask_question">
<reference local="question_id" foreign="id" />
</foreign-key>
<column name="user_id" type="integer" primaryKey="true" />
<foreign-key foreignTable="ask_user">
<reference local="user_id" foreign="id" />
</foreign-key>
<column name="created_at" type="timestamp" />
<column name="tag" type="varchar" size="100" />
<column name="normalized_tag" type="varchar" size="100" primaryKey="true" />
<index name="normalized_tag_index">
<index-column name="normalized_tag" />
</index>
</table>
Rebuild the object model:
重建对象模型:
$ symfony propel-build-model
Custom class
自定义类
Add a new Tag.class.php in the askeet/lib/ directory with the following methods:
在askeet/lib/下增加新文件Tag.class.php,加几个方法:
<?php
class Tag
{
public static function normalize($tag)
{
$n_tag = strtolower($tag);
// remove all unwanted chars
$n_tag = preg_replace(’/[^a-zA-Z0-9]/’, ”, $n_tag);
return trim($n_tag);
}
public static function splitPhrase($phrase)
{
$tags = array();
$phrase = trim($phrase);
$words = preg_split(’/(”)/’, $phrase, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$delim = 0;
foreach ($words as $key => $word)
{
if ($word == ‘”‘)
{
$delim++;
continue;
}
if (($delim % 2 == 1) && $words[$key - 1] == ‘”‘)
{
$tags[] = trim($word);
}
else
{
$tags = array_merge($tags, preg_split(’/\s+/’, trim($word), -1, PREG_SPLIT_NO_EMPTY));
}
}
return $tags;
}
}
The first method returns a normalized tag, the second one takes a phrase as argument and returns an array of tags. These two methods will be of great use when manipulating tags.
第一个函数返回规范化标签(先都处理成小写,然后正则处理),第二个取得用户输入的短语($phrase)作为标签存到数组里。当操作标签时这两个方法有很大作用。
The interest of adding the class in the lib/ directory is that it will be loaded automatically and only when needed, without needing to require it. It’s called autoloading.
给lib/下增加类的好处在于如果需要载入时不需要写require代码。这叫自动载入。
Extend the model
扩展模型
In the new askeet/lib/model/QuestionTag.php, add the following method to set the normalized_tag when the tag is set:
在新的askeet/lib/model/QuestionTag.php里,增加些方法当标签被转化为规范化标签时使用:
public function setTag($v)
{
parent::setTag($v);
$this->setNormalizedTag(Tag::normalize($v));
}
The helper class that we just created is already of great use: It reduces the code of this method to only two lines.
刚才创建的helper类是很有用的:方法的代码被缩减到两行。
Add some test data
增加测试数据
Append a file to the askeet/data/fixtures/ directory with some tag test data in it:
给askeet/data/fixtures/增加些测试标签:
QuestionTag:
t1: { question_id: q1, user_id: fabien, tag: relatives }
t2: { question_id: q1, user_id: fabien, tag: girl }
t4: { question_id: q1, user_id: francois, tag: activities }
t6: { question_id: q2, user_id: francois, tag: 'real life' }
t5: { question_id: q2, user_id: fabien, tag: relatives }
t5: { question_id: q2, user_id: fabien, tag: present }
t6: { question_id: q2, user_id: francois, tag: 'real life' }
t7: { question_id: q3, user_id: francois, tag: blog }
t8: { question_id: q3, user_id: francois, tag: activities }
Make sure this file comes after the other files of the directory in the alphabetical order, so that the sfPropelData object can link these new records with the related records of the Question and User tables. You can now repopulate your database by calling:
确保标签按照字母顺序排列,所以sfPropelData对象用问题表和用户表的相关数据来链接新记录。现在重新载入数据:
$ php batch/load_data.php
We are now ready to work on tags in the actions. But first, let us extend the model for the Question class.
你现在可以在动作中使用标签了。但是首先,让我们扩展问题类的模型。
Display the tags of a question
显示问题的标签
Before adding anything to the controller layer, let’s add a new tag module so that things keep organized:
在给控制层加东西前,加个标签模块来保证规则:
$ symfony init-module frontend tag
Extend model
扩展模型
We will need to display the whole list of words tagged by all users for a given question. As the ability to retrieve the related tags should be a method of the Question class, we will extend it (in askeet/lib/model/Question.php). The trick here is to group double entries to avoid double tags (two identical tags should only appear once in the result). The new method has to return a tag array:
对于用户给某问题标出的标签,我们需要全部显示出来。问题类的方法能取出相关的标签,我们扩展一下(在askeet/lib/model/Question.php)。这里用的方法是准备两个输入来避免重复标签(两个一样的标签仅在结果显示一次)。新方法返回标签数组:
public function getTags()
{
$c = new Criteria();
$c->clearSelectColumns();
$c->addSelectColumn(QuestionTagPeer::NORMALIZED_TAG);
$c->add(QuestionTagPeer::QUESTION_ID, $this->getId());
$c->setDistinct();
$c->addAscendingOrderByColumn(QuestionTagPeer::NORMALIZED_TAG);
$tags = array();
$rs = QuestionTagPeer::doSelectRS($c);
while ($rs->next())
{
$tags[] = $rs->getString(1);
}
return $tags;
}
This time, as we need only one column (the normalized_tag), there is no point to ask Propel to return an array of Tag objects populated from the database (this process, by the way, is called hydrating). So we do a simple query that we parse into an array, which is much faster.
这次,我们需要一列(规范化标签),没有必要让propel去读数据库并返回标签数组(这个过程叫hydrating)。所以做个简单查询放到数组里,很快的。
Modify the view
修改视图
The question detail page should now display the list of tags for a given question. We will use the sidebar for that. As it has been built as a component slot during the seventh day, we can set a specific component for this bar in the question module only.
问题详细页面应该对给出的问题显示标签列表。我们将使用侧边拦。在第七天教材里被创建成了组件槽,我们在问题模块里为这个bar设置特殊的组件。
So in askeet/apps/frontend/modules/question/config/view.yml, add the following configuration:
在askeet/apps/frontend/modules/question/config/view.yml,增加些配置:
showSuccess:
components:
sidebar: [sidebar, question]
This component of the sidebar module is not yet created, but it is quite simple (in modules/sidebar/actions/components.class.php):
sidebar模块的组件还没创建,但很简单(module/sidebar/actions/components.class.php):
public function executeQuestion()
{
$this->question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter('stripped_title'));
}
The longest part to write is the fragment (modules/sidebar/templates/_question.php):
最需要写的是代码片断(module/sidebar/templates/_question.php):
<?php include_partial('sidebar/default') ?>
<h2>question tags</h2>
<ul id=”question_tags”>
<?php include_partial(’tag/question_tags’, array(’question’ => $question, ‘tags’ => $question->getTags())) ?>
</ul>
We choose to insert the list of tags as a fragment because it will be refreshed by an AJAX request a bit later.
我们选择插入一列标签作为代码片断,因为后面的AJAX请求会刷新它。
This partial has to be created in modules/tag/templates/_question_tags.php:
在modules/tag/templates/_queston_tags.php创建局部模板:
<?php foreach($tags as $tag): ?>
<li><?php echo link_to($tag, ‘@tag?tag=’.$tag, ‘rel=tag’) ?></li>
<?php endforeach; ?>
The rel=tag attribute is a MicroFormat. It is by no means compulsory, but as it costs nothing to add it here, we’ll let it stay.
rel=tag属性是MicroFormat。它决不是强制的,随便添加,我们让它保留。
Add the @tag routing rule in the routing.yml:
在routing.yml增加@tag路由规则:
tag:
url: /tag/:tag
param: { module: tag, action: show }
Test it
测试一下
Display the detail of the first question and look for the list of tags in the sidebar:
显示第一个问题的细节,看看sidebar的标签列表:
http://askeet/question/what-can-i-offer-to-my-step-mother
Display a short list of popular tags for a question
给流行问题显示标签短列表
The sidebar is a good place to show the whole list of tags for a question. But what about the tags displayed in the list of questions? For each question, we should only display a subset of tags. But which ones? We will choose the most popular ones, i.e. the tags than have been given to this question most often. We will probably have to encourage users to keep on tagging a question with existing tags to increase the popularity of relevant tags for this question. If all users don’t do that, maybe “moderators” will do it.
侧边拦是个显示整个问题标签列表的好地方。但是在问题列表里显示标签如何呢?对每个问题来说,我们只需要显示子标签。但是显示哪一个呢?我们选择最流行的那个标签,也就是说,这个问题最常用的标签。我们可以鼓励用户尽量使用已经存在的标签给问题加标签,来提高标签的人气。如果用户都不这么干,或许“moderators”会去做。
Extend the model
扩展模型
Anyway, this means that we have to add a ->getPopularTags() method to our Question object. But this time, the request to the database is not simple. Using Propel to do it would multiply the number of requests and take way too much time. Symfony allows you to use the power of SQL when it is the best solution, so we will just need a Creole connection to the database and execute a regular SQL query.
不论如何,我们给问题对象加个->getPopularTags()方法。但这次,发送给数据库的请求不简单。用propel实现会增加请求数量,浪费时间。如果使用SQL是最好的解决方法时, symfony允许你使用SQL,所以我们需要一个Creole连接数据库并执行一个规范sql查询。
This query should be something like:
查询将如下:
SELECT normalized_tag AS tag, COUNT(normalized_tag) AS count
FROM question_tag
WHERE question_id = $id
GROUP BY normalized_tag
ORDER BY count DESC
LIMIT $max
However, using the actual column and table names creates a dependency to the database and bypasses the data abstraction layer. If, in the future, you decide to rename a column or a table, this raw SQL query will not work anymore. That’s why the symfony version of the request doesn’t use the current names but the abstracted ones instead. It is slightly harder to read, but it is much easier to maintain.
然而,这里使用了真实的数据库字段名建立数据库和数据库抽象层的连接关系。如果在以后,你重命名数据库里一个字段,这个SQL查询就无法使用了。这就是为什么symfony没有使用当前名,而用了分离方法。理解起来有些困难,但做起来很简单。
public function getPopularTags($max = 5)
{
$tags = array();
$con = Propel::getConnection();
$query = ‘
SELECT %s AS tag, COUNT(%s) AS count
FROM %s
WHERE %s = ?
GROUP BY %s
ORDER BY count DESC
‘;
$query = sprintf($query,
QuestionTagPeer::NORMALIZED_TAG,
QuestionTagPeer::NORMALIZED_TAG,
QuestionTagPeer::TABLE_NAME,
QuestionTagPeer::QUESTION_ID,
QuestionTagPeer::NORMALIZED_TAG
);
$stmt = $con->prepareStatement($query);
$stmt->setInt(1, $this->getId());
$stmt->setLimit($max);
$rs = $stmt->executeQuery();
while ($rs->next())
{
$tags[$rs->getString(’tag’)] = $rs->getInt(’count’);
}
return $tags;
}
First, a connection to the database is opened in $con. The SQL query is built by replacing %s tokens in a string by the column and table names that come from the abstraction layer. A Statement object containing the query and a ResultSet object containing the result of the query are created. These are Creole objects, and their use is described in detail in the Creole documentation. The ->setInt() method of the Statement object replaces the first ? in the SQL query by the question id. The $max argument is used to limit the number of results returned with the ->setLimit() method.
首先,$con打开到数据库的连接;SQL查询用%s字符串替换字段名和表名;prepareStatement对象包含了查询和结果对象,也包括查询结果的创建;这就是Creole对象,使用方法细节见Creole手册;->setInt()方法替换第一个“?”为question id。$max参数用来返回方法->setLimit()的限制结果数量。
The method returns an associative array of normalized tags and popularity, ordered by descending popularity, with only one request to the database.
方法返回规范化标签和流行程度的联合数组,按流行人气排序。
Modify the view
修改view
Now we can add the list of tags for a question, which is formatted in a _list.php fragment in the modules/question/templates/ directory:
现在我们显示问题的标签列表,在_list.php代码碎片里,在modules/question/templates/下:
<?php use_helpers('Text', 'Date', 'Global', 'Question') ?>
<?php foreach($question_pager->getResults() as $question): ?>
<div class=”question”>
<div class=”interested_block” id=”block_<?php echo $question->getId() ?>”>
<?php include_partial(’question/interested_user’, array(’question’ => $question)) ?>
</div>
<h2><?php echo link_to($question->getTitle(), ‘@question?stripped_title=’.$question->getStrippedTitle()) ?></h2>
<div class=”question_body”>
<div>asked by <?php echo link_to($question->getUser(), ‘@user_profile?nickname=’.$question->getUser()->getNickname()) ?> on <?php echo format_date($question->getCreatedAt(), ‘f’) ?></div>
<?php echo truncate_text(strip_tags($question->getHtmlBody()), 200) ?>
</div>
tags: <?php echo tags_for_question($question) ?>
</div>
<?php endforeach; ?>
<div id=”question_pager”>
<?php echo pager_navigation($question_pager, $rule) ?>
</div>
Because we want to separate the tags by a + sign, and to avoid too much code in the template to deal with the limits, we write a tags_for_question() helper function in a new lib/helper/QuestionHelper.php helper library:
因为我们想把标签用“+”号区别开,而且要在在模板里避免写太多代码,我们用tags_for_question() helper方法,见lib/helper/QuestionHelper.php helper库:
function tags_for_question($question, $max = 5)
{
$tags = array();
foreach ($question->getPopularTags($max) as $tag => $count)
{
$tags[] = link_to($tag, ‘@tag?tag=’.$tag);
}
return implode(’ + ‘, $tags);
}
Test
测试
The list of questions now displays the popular tags for each question:
问题列表页面显示出了每一个问题的流行标签,用“+”号隔开:
Display the list of questions tagged with a word
用一个词做标签标注问题列表的问题
Each time we displayed a tag, we added a link to a @tag routing rule. This is supposed to link to a page that displays the popular questions tagged with a given tag. It is simple to write, so we won’t delay it anymore.
每当显示一个标签,我们加一个连接到@tag路由规则。然后连接到一个页面显示被流行标签标记过的问题。写起来很简单,抓紧。
The tag/show action
tag/show动作
Create a show action in the tag module:
在标签模块创建show动作:
public function executeShow()
{
$this->question_pager = QuestionPeer::getPopularByTag($this->getRequestParameter('tag'), $this->getRequestParameter('page'));
}
Extend the model
扩展模型
As usual, the code that deals with the model is placed in the model, this time in the QuestionPeer class since it returns a set of Question objects. We want the popular question by interested users, so this time, there is no need for a complex request. Propel can do it with a single ->doSelect() call:
像往常一样,代码放在模型里,这次QuestionPeer类返回一套Question对象。我们想让人气高的问题被用户被用户点击“感兴趣”,所以这个时候,没有什么综合需求了。propel用单独的->doSelect()实现:
public static function getPopularByTag($tag, $page)
{
$c = new Criteria();
$c->add(QuestionTagPeer::NORMALIZED_TAG, $tag);
$c->addDescendingOrderByColumn(QuestionPeer::INTERESTED_USERS);
$c->addJoin(QuestionTagPeer::QUESTION_ID, QuestionPeer::ID, Criteria::LEFT_JOIN);
$pager = new sfPropelPager(’Question’, sfConfig::get(’app_pager_homepage_max’));
$pager->setCriteria($c);
$pager->setPage($page);
$pager->init();
return $pager;
}
The method returns a pager of questions, ordered by popularity.
方法返回问题页面,按照人气排序。
Create the template
创建模板
The modules/tag/templates/showSuccess.php template is as simple as you expect it to be:
moduels/tag/templates/showSuccess.php模板和你想象的一样简单:
<h1>popular questions for tag "<?php echo $sf_params->get('tag') ?>"</h1>
<?php include_partial(’question/list’, array(’question_pager’ => $question_pager, ‘rule’ => ‘@tag?tag=.’$sf_params->get(tag))) ?>
Add the page parameter in the routing rule
在路由规则中加上页面参数
In the routing.yml, add a :page parameter with a default value in the @tag routing rule:
在routing.yml,用默认值给@tag路欧规则加上:page参数。
tag:
url: /tag/:tag/:page
param: { module: tag, action: show, page: 1 }
Test it
测试一下
Navigate to the activities tag page to see all the questions tagged with this word:
浏览页面看看吧,所有的问题都被标注了一个标签:



