[原创中文翻译]symfony askeet24:第三天,深入MVC模式。

September 25, 2007 – 12:56 am

[欢迎转载,转载请注名出处http://symfony.net.cn。本文英文版权归symfony官方网站所有]
The MVC model

MVC模型

(译者:MVC模式部分的解释,个人认为好好翻译一下,加深理解,琢磨透了也就那么简单的东西而已。)

Today will be the first dive in the world of the MVC architecture. What does this mean? Simply that the code used to generate one page is located in various files according to its nature.
今天第一次深入到MVC模式。这是什么呢?简单说是靠许多文件来生成一个页面。

If the code concerns data manipulation independent from a page, it should be located in the Model (most of the time in askeet/lib/model/). If it concerns the final presentation, it should be located in the View; in symfony, the view layer relies on templates (for instance in askeet/apps/frontend/modules/question/templates/) and configuration files. Eventually, the code dedicated to tie all this together and to translate the site logic into good old PHP is located in the Controller, and in symfony the controller for a specific page is called an action (look for actions in askeet/apps/frontend/modules/question/actions/). You can read more about this model in the MVC implementation in symfony chapter of the symfony book.

如果代码不在页面上操作数据库的数据,那么就是靠的Model(多数情况下见askeet/lib/model/)实现。如果涉及到最后显示数据,靠的是View;symfony的view层靠的是模板(templates)(例如:askeet/apps/frontend/modules/question/templates/)和配置文件。最终,代码在controller层专注于集合所有并把站点功能逻辑用PHP程序表达出来,controller层体现的页面在symfony里被叫做action(例如askeet/apps/frontend/modules/question/actions/)。在symfony book里有更详细介绍。

While our applications view will only change slightly today, we will manipulate a lot of different files. Don’t panic though, since the organization of files and the separation of the code in various layers will soon become evident and very useful.

今天我们的view程序将只有轻微变化,操作很多不同文件。别慌,文件和代码在layer将会变得明了和有用。

Change the layout

改变布局

In application of the decorator design pattern, the content of the template called by an action is integrated into a global template, or layout. In other words, the layout contains all the invariable parts of the interface, it “decorates” the result of actions. Open the default layout (located in askeet/apps/frontend/templates/layout.php) and change it to the following:

在设计程序模式中,action调用模板内容整合成一个全局模板,或者是布局。换句话说,布局包含界面所有的部分,它装饰了action的结果。打开默认布局(askeet/apps/frontend/templates/layout.php)更改代码如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd“>
<html xmlns=”http://www.w3.org/1999/xhtml” xml:lang=”en” lang=”en”>
<head>

<?php echo include_http_metas() ?>

<?php echo include_metas() ?>

<?php echo include_title() ?>

<link rel=”shortcut icon” href=”/favicon.ico” mce_href=”/favicon.ico” />

</head>
<body>
<div id=”header”>
<ul>
<li><?php echo link_to(’about’, ‘@homepage’) ?></li>
</ul>
<h1><?php echo link_to(image_tag(’askeet_logo.gif’, ‘alt=askeet’), ‘@homepage’) ?></h1>
</div>
<div id=”content”>
<div id=”content_main”>
<?php echo $sf_data->getRaw(’sf_content’) ?>
<div class=”verticalalign”></div>
</div>
<div id=”content_bar”>
<!– Nothing for the moment –>
<div class=”verticalalign”></div>
</div>
</div>
</body>
</html>

We tried to keep the markup as semantic as possible, and to move all the styling into the CSS stylesheets. These stylesheets won’t be described here, since CSS syntax is not the purpose of this tutorial. They are available for download though, in the SVN repository.

我们尽量让语义简单,所有的样式都依赖CSS。样式表的知识不在这里描述,CSS语法不是我们要介绍的。需要的CSS可以从SVN下载。

We created two stylesheets (main.css and layout.css). Copy them into your askeet/web/css/ directory and edit your frontend/config/view.yml to change the autoloaded stylesheets:

创建两个CSS文件(main.css和layout.css)。拷贝到askeet/web/css/下修改frontend/config/view.yml载入样式表:

stylesheets: [main, layout]

[译者:由于没有安装SVN,我从askeet trac上下载了两个样式表,而且标明的原始版本的。下载到本地一看,如果写成stylesheets: [main],访问http://askeet/frontend_dev.php/question样式表能起作用,照上面写样式表就不起作用,不过好在样式表可以调,这里初步估计是layout.css的问题。等基本做完此教程,我再细细研究研究这个地方吧,全当这里留了个书签。也请已经找到原因的朋友不吝赐教。谢谢!]

[译者(2007年10月2日上午11:35):也是突发奇想,安装了数据抓包软件,抓抓包看看,到底是什么问题导致样式表出问题。先这么写[main,layout]结果抓包一看,请求的是main,layout.css,废话嘛不是,哪里有这样的样式表!改成[main],正确,请求的是main.css样式表,改成[layout]请求的是layout.css样式表,也正确。那么现在的问题是,为什么askeet24教程上会这么写呢?莫非是教材错了?]

[译者(2007年10月2日上午11:40):问题终于找到解决方案了!我仔细查看了一下apps\frontend\config\下的view.yml文件,原来该这么写[main, layout],注意,逗号的后面、layout的前面该有个空格!这样抓包一看,main.css和layout.css都正确了!新东西新玩法啊!YAML的语法确实该找个时间好好看看了,否则很多细节的错误都很难找到解决方法。呵呵,又自学到了一招!]

This layout is still lightweight for the moment, it will be rebuilt later (in about a week). The important things in this template are the <head> part, which is mostly generated, and the sf_content variable, which contains the result of the actions.

这时的布局仍然很简单,后面会重建(大概7天以后)。重要的是template的<head>部分,大部分内容都是生成的。sf_content变量,包含了动作(action)的结果。

Check that the modifications display correctly by requesting the home page - this time in the development environment:

通过首页检查修改是否正确——开发环境中进行。

http://askeet/frontend_dev.php/

wwwsymfony-projectcom_updated-layout_congratulations_new.gif

A few words about environments

说说环境

If you wonder what the difference between http://askeet/frontend_dev.php/ and http://askeet/ is, you should probably have a look at the configuration chapter of the symfony book. For now, you just need to know that they point to the same application, but in different environments. An environment is a unique configuration, where some features of the framework can be activated or deactivated as required.

如果想知道http://askeet/frontend_dev.php/http://askeet/的区别,在symfony book里有答案。现在,你只需要知道他们指向同一个程序,只是在不同环境下而已。一个环境有独立的设置,根据需求激活或者撤消一些框架中的特性。

In this case, the /frontend_dev.php/ URL points to the development environment, where the whole configuration is parsed at each request, the HTML cache is deactivated, and the debug tools are all available (including a semi-transparent toolbar located at the top right corner of the window). The / URL - equivalent to /index.php/ - points to the production environment, where the configuration is “compiled” and the debug tools deactivated to speed up the delivery of pages.

例如,/frontend_dev.php/url指明了开发环境,所有的相关信息都可以列出,HTML缓存被撤消,debug工具条被启用(包括右上角的semi-transparent工具条)。/——等同于/index.php/——代表产品发布环境,配置已经被编译过了而且工具条被撤消以加快页面传输。

These two PHP scripts - frontend_dev.php and index.php - are called front controllers, and all the requests to the application are handled by them. You can find them in the askeet/web/ directory. As a matter of fact, the index.php file should be named frontend_prod.php, but as frontend is the first application that you created, symfony deduced that you probably wanted it to be the default application and renamed it to index.php, so that you can see your application in the production environment by just requesting /. If you want to learn more about the front controllers and the Controller layer of the MVC model in general, refer to the controller chapter in the symfony book.

frontend.php和index.php这两个php脚本文件,被称为front controller,所有程序的需求都靠他们传递。在askeet/web/下可以找到。事实上,index.php可以被命名为frontend_prod.php,但是作为前台你创建的第一个程序。symfony推断你想把这个作为默认的程序而把他命名为index.php,所以你可以通过/(就是什么都不输入)来直接访问发布模式(index.php)。如果你想知道更多,看一下symfony book。

A good rule of thumb is to navigate in the development environment until you are satisfied with the feature you are working on, then switch to the production environment to check its speed and “nice” URLs.

好的方法是一直在开发环境中调试直到你觉得满意为止,满意了再用发布模式察看速度和“好看的”网址。

Remember to always clear the cache when you add some classes or when you change some configuration files to see the result in the production environment.

记得当你增加类或改变配置文件时清缓存,然后在去看发布模式里的结果。

Redefine the default homepage

重新定义默认主页

For now, if you request the home page of the new website, it shows a ‘Congratulations’ page. A better idea would be to show the list of questions (referenced in these documents as question/list and translated as: the list action of the question module). To do this, open the routing configuration file of the frontend application, found in askeet/apps/frontend/config/routing.yml and locate the homepage: section. Change it to:

现在,如果你打开主页,会显示:“Congratulations”页面。最好能显示questions列表(正如文档中提到的,question/list和被转换成:question模块的action列表)。为了做好这个,打开前台路由配置文件,askeet/apps/frontend/config/routing.yml找到homepage:部分。改为:

homepage:

url: /
param: { module: question, action: list }

Refresh the home page in the development environment (http://askeet/frontend_dev.php/); it now displays the list of questions.

在开发模式中刷新主页,(http://askeet/frontend_dev.php/);现在显示question列表了。

If you are a curious person, you might have looked for this page containing the ‘Congratulations’ message. And you might be surprised not to find it in your askeet directory. As a matter of fact, the template for the default/index action is defined in the symfony data directory and is independent from the project. If you want to override it, you can still create a default module in your own project.

如果你好奇心很强,你可以找一下“congratulations”信息。你会很惊奇发现在你的askeet目录下找不到了。事实上,default/index action的template定义在symfony的data下,而不依赖此项目。如果你想推翻它,你可以在自己的项目里建一个新的默认模块。

The possibilities offered by the routing system will be detailed in the near future, but if you are interested, you can read the routing chapter of the symfony book.

很快路由系统会变得很复杂,但是如果你感兴趣,请阅读symfony book。

Define test data

定义测试数据

(译者:这部分看不看都无所谓,不过比较适合学习symfony的脚本批处理功能。大网站一般都能用上。)

The list displayed by the home page will remain quite empty, unless you add your own questions. When you develop an application, it is a good idea to have some test data at your disposal. Entering test data by hand (either via the CRUD interface of directly in the database) can be a real pain, that’s why symfony can use text files to populate databases.

首页的显示信息很空,除非你写几个问题进去。开发中,有些测试数据会很方便。手动输入(要么通过CRUD要么直接输入数据库)会很麻烦,symfony可以用test files操作数据库。

We’ll create a test data file in the askeet/data/fixtures/ directory (this directory has to be created). Create a file called test_data.yml with the following content:

我们在askeet/data/fixtures/下创建新test data file(你需要手动创建目录)。建一个文件test_data.yml如下:

User:
anonymous:
nickname: anonymous
first_name: Anonymous
last_name: Coward

fabien:
nickname: fabpot
first_name: Fabien
last_name: Potencier

francois:
nickname: francoisz
first_name: François
last_name: Zaninotto

Question:

q1:
title: What shall I do tonight with my girlfriend?
user_id: fabien
body: |
We shall meet in front of the Dunkin’Donuts before dinner,
and I haven’t the slightest idea of what I can do with her.
She’s not interested in programming, space opera movies nor insects.
She’s kinda cute, so I really need to find something
that will keep her to my side for another evening.

q2:
title: What can I offer to my step mother?
user_id: anonymous
body: |
My stepmother has everything a stepmother is usually offered
(watch, vacuum cleaner, earrings, del.icio.us account).
Her birthday comes next week, I am broke, and I know that
if I don’t offer her something sweet, my girlfriend
won’t look at me in the eyes for another month.

q3:
title: How can I generate traffic to my blog?
user_id: francois
body: |
I have a very swell blog that talks
about my class and mates and pets and favorite movies.

Interest:

i1: { user_id: fabien, question_id: q1 }
i2: { user_id: francois, question_id: q1 }
i3: { user_id: francois, question_id: q2 }
i4: { user_id: fabien, question_id: q2 }

First of all, you may recognize YAML here. If you are not familiar with symfony, you might not know that the YAML format is the favorite format for configuration files in the framework. It is not exclusive - if you are attached to XML or .ini files, it is very easy to add a configuration handler to allow symfony to read them. If you have time and patience, read more about YAML and the symfony configuration files in the configuration in practice chapter of the symfony book. As of now, if you are not familiar with the YAML syntax, you should get started right away, since this tutorial will use it extensively.

首先,你要认识YAML。如果你不熟悉symfony,你不会知道YAML格式是框架中最好的配置文件定义格式。如果你用XML或者.ini文件,它也不是独立的,增加配置内容让symfony读懂是很简单的。如果你有时间并且足够耐心,多读读symfony book中关于YAML和symfony配置文件的章节。现在,如果你不熟悉YAML语法,你可以现在开始,用用就会了。

Ok, back to the test data file. It defines instances of objects, labeled with an internal name. This label is of great use to link related objects without having to define ids (which are often auto-incremented and can not be set). For instance, the first object created is of class User, and is labeled fabien. The first Question is labeled q1. This makes it easy to create an object of class Interest by mentioning the related object labels:

好了,回到test data file。它定义了对象例子,使用标签。这标签连接关系对象,没有定义ID(很多是自动增加的而且不能改变)。例如,第一个对象是用户类。第一个问题被标签q1。使用标签很方便。

Interest:
i1:
user_id: fabien
question_id: q1

The data file given previously uses the short YAML syntax to say the same thing. You can find more about data population files in the data files chapter of the symfony book.

前面给的data文件用了简单的YAML语法,表达的一个意思。在symfony book里有详细描述。

There is no need to define values for the created_at and updated_at columns, since symfony knows how to fill them by default.

没有必要定义created_at和updated_at字段,symfony会自动给你填上当前系统时间。

Create a batch to populate the database

创建批处理导入数据

The next step is to actually populate the database, and we wish to do that with a PHP script that can be called with a command line - a batch.

下一步是导入数据,我们希望用PHP脚本实现,我们叫他批处理。

Batch skeleton

批处理骨架

Create a file called load_data.php in the askeet/batch/ directory with the following content:

在askeet/batch/下建立一个文件load_data.php:

<?php
define('SF_ROOT_DIR', realpath(dirname(__FILE__).'/..'));
define('SF_APP', 'frontend');
define('SF_ENVIRONMENT', 'dev');
define('SF_DEBUG', true);
require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');
// initialize database manager
$databaseManager = new sfDatabaseManager();
$databaseManager->initialize();
?>

This script does nothing, or close to nothing: it defines a path, an application and an environment to get to a configuration, loads that configuration, and initializes the database manager. But that’ already a lot: that means that all the code written below will take advantage of the auto-loading of classes, automatic connection to Propel objects, and the symfony root classes.

这脚本啥都不干,也和任何东西都没任何关系:定义path,application和environment来获得配置文件,载入配置,初始化database manager。但这已经很多了:下面的代码会实现auto-loading类,自动连接propel对象和symfony root类。

If you have examined symfony’s front controllers (like askeet/web/index.php), you might find this code extremely familiar. That’s because every web request requires access to the same objects and configuration, as a batch request does.

如果你已经检查了symfony的前台controller(例如askeet/web/index.php),你会发现这些代码很相似。因为每一个web程序的请求都是从这里通过的。

Data import

数据导入

Now that the frame of the batch is ready, it is time to make it do something. The batch has to:

现在批处理的框架有了。批处理可以做:

1.read the YAML file

1.读YAML文件。

2.Create instances of Propel objects

2.创建项目例子。

3.Create the related records in the tables of the linked database

3.创建关联的数据记录。

This might sound complicated, but in symfony, you can do that with two lines of code, thanks to the sfPropelData object. Just add the following code before the final ?> in the askeet/batch/load_data.php script:

听起来挺复杂,但symfony让你两行代码搞定。幸亏有sfPropelData对象。在?>前加两行代码,askeet/batch/load_data.php。

$data = new sfPropelData();
$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');

That’s all. A sfPropelData object is created, and told to load all the data of a specific directory - our fixtures directory - into the database defined in the databases.yml configuration file.

好了,sfPropelData被创建了,fixtures目录被载入了数据库所有数据,内容定义在databases.yml文件中。

The DIRECTORY_SEPARATOR constant is used here to be compatible with Windows and unix platforms.

DIRECTORY_SEPARATOR被用在这里兼容Windows和Unix平台。

Launch the batch

载入批处理

At last, you can check if these few lines of code were worth the hassle. type in the command line:

最后,让我们看看这几行代码是否有价值,在命令行里输入:

$ cd /home/sfprojects/askeet/batch
$ php load_data.php

Check the modifications in the database by refreshing the development home page again:

刷新首页:

http://askeet/frontend_dev.php

Hooray, the data is there.

万岁,数据在这里了。

wwwsymfony-projectcom_loaded-data_fixtures.gif

By default, the sfPropelData object deletes all your data before loading the new ones. You can also append to the current data:

默认sfPropelData载入新数据前会删除你以前所有的数据。你可以保留当前数据:

$data = new sfPropelData();
$data->setDeleteCurrentData(false);
$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');

Accessing the data in the model

在模型里访问数据

The page displayed when requesting the list action of the question module is the result of the executeList() method (found in the askeet/apps/frontend/modules/question/actions/action.class.php action file) passed to the askeet/apps/frontend/modules/question/templates/listSuccess.php template. This is based on a naming convention that is explained in the controller chapter of the symfony book. Let’s have a look at the code that is executed:

当对question模块发出请求时,此页显示executeList()的结果(askeet/apps/frontend/modules/question/actions/action.class.php),并发送结果到askeet/apps/frontend/modules/question/templates/listSuccess.php模板。symfony book里有解释。看看代码:

actions.class.php里:

public function executeList ()
{
$this->questions = QuestionPeer::doSelect(new Criteria());
}

listSuccess.php里:

...
<?php foreach ($questions as $question): ?>
<tr>
<td><?php echo link_to($question->getId(), 'question/show?id='.$question->getId()) ?></td>
<td><?php echo $question->getTitle() ?></td>
<td><?php echo $question->getBody() ?></td>
<td><?php echo $question->getCreatedAt() ?></td>
<td><?php echo $question->getUpdatedAt() ?></td>
</tr>
<?php endforeach; ?>

Step-by-step, here is what it does:

一步一步看看是怎么工作的:

1.The action requires the records of the Question table that satisfy an empty criteria - i.e. all the questions

1.action按照空标准从Question表里调出数据——就是说,所有数据都调出来。

2.This list of records is put in an array ($questions) that is passed to the template

2.数据被放到数组,$questions=array(),$question传递到模板。

3.The template iterates over all the questions passed by the action

3.模板迭代计算(foreach)出所有的从action传过来的数据。

4.The templates shows the value of the columns of each record

4.模板显示每条数据的值。

The ->getId(), ->getTitle(), ->getBody(), etc. methods were created during the symfony propel-build-model command call (do you remember yesterday ?) to retrieve the value of the id, title, body, etc. fields. These are standard getters, formed by adding the prefix get to the camelCased field name - and Propel also provides standard setters, prefixed with set. The Propel documentation describes the accessors created for each class.

这些方法->getId()、->getTitle()、->getBody()等等,在你输入symfony propel-build-model时就创建了(还记得昨天我们都做了什么吗?),这些方法就是为了查询获得id、title、body。这叫获得标准,格式为在字段名前面加get前缀——propel也给插入提供了标准,在前面加set前缀。propel文档描述了为class创建存储。

[译者:symfony propel定义方法的方式很简单,如果是查询数据操作(select),就在字段名前加get,例如,想获取title字段内容,用getTitle()方法;如果是插入或者更新title字段内容,用setTitle()方法;如果是删除,用delete()方法。这些方法都可以在askeet/lib/model/om下对应文件找到。]

As for the mysterious QuestionPeer::doSelect(new Criteria()) call, it is also a standard Propel request. The Propel documentation will explain it thoroughly.

神秘的QuestionPeer::doSelect(new Criteria())代码,这也是标准propel请求方式。propel文档里有详细解释。

[译者:看到这里如果你很轻松就明白了,那么恭喜你!看到symfony propel的强大了吧,基本的web网站的功能都可以通过一条命令实现,只不过在我们眼里不再是熟悉的sql语句和mysql_fetch_array()代码。如果想学好symfony,那么就需要适应这样的封装代码。不过你也可以选择不用propel生成数据操作对象,改成自己写,一样可以的。在action里把这个

$this->questions = QuestionPeer::doSelect(new Criteria());

改成


$db = new mysqldbclass;
/*******************************************
这里使用mysqldbclass类
askeet/lib/mysql.db.class.php
你也可以不用,直接写mysql_query($sql)
********************************************/
$sql="select id,title,body,created_at,update_at from ask_question";
$this->list_result = $db->query($sql);

是一样的,无非模板中foreach循环$list_result数组代替循环$questions数组就是。
]

Don’t worry if you don’t understand all the code written above, it will become clearer in a few days.

不要因为你还不明白上面所有代码而烦恼,后面几天会变很简单。

Modify the question/list template

修改question/list模板

Now that the database contains interests for questions, it should be easy to get the number of interested users for one question. If you have a look at the BaseQuestion.php class generated by Propel in the askeet/lib/model/om/ directory, you will notice a ->getInterests() method. Propel saw the question_id foreign key in the Interest table definition, and deduced that a question has several interests. This makes it very easy to display what we want by modifying the listSuccess.php template, located in askeet/apps/frontend/modules/question/templates/. In the process, we’ll remove the ugly tables and replace them with nice divs:

现在数据库为问题(question)包含了interests,统计一个问题有多少用户感兴趣就变得很容易了。如果看一下askeet/lib/model/om/下的BaseQuestion.php(propel生成的),你会很快发现->getInterests()方法。propel看到我们在Interest表中把question_id作为外键,推论出一个question会有几个interest。那么我们修改askeet/apps/frontend/modules/question/templates/下listSuccess.php模板来显示我们需要的就很容易了。让我们删除丑陋的表格,换成漂亮的div:

<?php use_helper('Text') ?>

<h1>popular questions</h1>
<?php foreach($questions as $question): ?>
<div class=”question”>
<div class=”interested_block”>
<div class=”interested_mark” id=”interested_in_<?php echo $question->getId() ?>”>
<?php echo count($question->getInterests()) ?>
</div>
</div>
<h2><?php echo link_to($question->getTitle(), ‘question/show?id=’.$question->getId()) ?></h2>
<div class=”question_body”>
<?php echo truncate_text($question->getBody(), 200) ?>
</div>
</div>
<?php endforeach; ?>

You recognize here the same foreach loop as in the original listSuccess.php. The link_to() and the truncate_text() functions are template helpers provided by symfony. The first one creates a hyperlink to another action of the same module, and the second one truncates the body of the question to 200 characters. The link_to() helper is auto-loaded, but you have to declare the use of the Text group of helpers to use truncate_text().

你会认出这里listSuccess.php中使用了和原来listSuccess.php中一样的foreach循环。link_to()和truncate_text()是symfony提供的template helper。第一个创建超级链接到同名module的其他action,第二个把question截断在200个字符内。link_to() helper是可以自动载入的,但你在使用前需要先声明use_helper。

Come on, try on your new template by refreshing the development homepage again.

来吧,看看新的效果。

http://askeet/frontend_dev.php/

wwwsymfony-projectcom_better-question-list_question_list_day3.gif

The number of interested users appears correctly close to each question. To get the presentation of the above capture, download the main.css stylesheet and put it in your askeet/web/css/ directory.

感兴趣的用户(interested users)数量显示在每一个question旁边。想得到上面的效果,下载main.css样式表文件到askeet/web/css/。

Cleanup

清理工作

The propel-generate-crud command created some actions and templates that will not be needed. It’s time to remove them.

propel-generate-crud命令创建了一些action和template,有些是不需要的。到时候清理一下了。

Actions to remove in askeet/apps/frontend/modules/question/actions/actions.class.php:

删除askeet/apps/frontend/modules/question/actions.class.php中:

executeIndex
executeEdit
executeUpdate
executeCreate
executeDelete

Template to remove in askeet/apps/frontend/modules/question/templates/:

删除askeet/apps/frontend/modules/question/templates/下

editSuccess.php

[译者:在这里不推荐删除这些actions和templates,留着可以对比学习一下。askeet集合了symfony的所有精华,学习起来并不是很轻松的事情。大量的封装很容易把你弄晕,所以建议备份所有代码,对比着学习。]

Post a Comment