我们的第一个Symfory工程
现在我们试验一下Symfony。我们要在一个小时内构建一个全功能的网络程序。一个书店销售程序?可以!一个Weblog!这是一个好主意。让我们开始吧。
安装Symfony并且初始化工程
为了方便,我们将会使用Symfony沙盒。这是一个空的Symfony工程,在其中已经包含了所有所需要的库,并且完成了基本的配置。比起其他类型的安装,沙盒的最大优点就是我们可以立刻试验Symfony。
下载sf_sandbox.tgz,并且将其解压到我们的网络目录的根目录下。我们可以查看其中的readme文件,从而得以更为详细的信息。生成的文件结构如下所示:
www/
sf_sandbox/
apps/
frontend/
batch/
cache/
config/
data/
sql/
doc/
lib/
model/
log/
plugins/
test/
web/
css/
images/
js/
从这里我们可以看出sf_sandbox工程包含一个前端程序。我们可以通过请求下面的URL来测试沙盒:
http://localhost/sf_sandbox/web/index.php/
我们应会看到一个祝贺页面。
我们也可以将symfony安装一个定制的目录中,并且使用一个虚拟主机或是一个Alias设置我们的网络服务器。
初始化数据模型
所以Weblog应可以处理文章发布,并且我们可以进行评论。在sf_sandbox/config下创建一个schema.yml文件,并且将下面的数据模型粘贴到其中:
propel:
weblog_post:
_attributes: { phpName: Post }
id:
title: varchar(255)
excerpt: longvarchar
body: longvarchar
created_at:
weblog_comment:
_attributes: { phpName: Comment }
id:
post_id:
author: varchar(255)
email: varchar(255)
body: longvarchar
created_at:
这个配置文件使用YAML语法。这是一个非常简单的语言,可以通过缩进来描述类XML的树结构。而且,他具有比XML更快的读写速度。唯一的问题就是缩进是具有意义的,而且不允许使用Tab,所以记住要使用空格来进行缩进。我们将会在配置一章中了解更多的YAML以及symfony配置的内容。
这个概要描述了weblog所需要的两个表的结构。Post与Comment是要创建的相关类的名字。保存这个文件,打开一个命令行,进行sf_sandbox目录,并且执行下面的命令:
$ php symfony propel-build-model
注意:要保证我们在调用symfony命令时在我们工程的根目录下。
在sf_sandbox/lib/model/目录下创建了一些类。这些是对象相关映射的类,这允许我们在面向对象的代码中而不使用单一的SQL查询来访问相关的数据库。为了这个目的,Symfony使用Propel库。我们将这些对象称之为模型(model)。
现在我们执行下面的命令:
$ php symfony propel-build-sql
这会在sf_sandbox/data/sql/目录下创建一个lib.model.schema.sql文件。这个SQL查询可以用来使用相同的表结构来初始化一个数据库。我们可以使用命令行或是Web界面在MySQL中创建一个数据库。幸运的是symfony沙盒的配置使用一个简单的SQLite文件来进行工作,所以不需要数据库的初始化工作。默认情况下,sf_sandbox工程会使用sf_sandbox/data/目录中的sandbox.db进行工作。要构建基于SQL文件的表结构,可以输入下面的命令:
$ php symfony propel-insert-sql
注意:如果此时有警告并不用担心。insert-sql命令在添加我们的lib.model.schema.sql数据表之前会移除已存在的数据表,而在此时并没有需要移除的数据表。
(注:此时如果我们需要连接的是MySQL数据的话,我们做如下的一些修改:
1 修改config/databases.yml,其param:下的参数部分分别为:
phptype:指明我们要使用的数据库类型
hostspec:指明我们所要用的数据库所在的域名
database:指明我们要使用的数据库的名字
username:指明我们连接数据库所用的用户名
password:指明我们连接数据库所用的密码
2 修改config/propel.ini,所需要修改的部分如下:
propel.database:指明我们需要使用的数据库类型
propel.database.createUrl:我们需要使用的数据库所在的主机地址,其格式为mysql://username:password@domain/,其各部分分别为用户名,密码以及域名
propel.database.url:我们需要使用的数据库的地址,其格式为:mysql://username:password@domain/database,其各部分分别为用户名,密码,密码以及数据库的名字
)
创建程序框架
一个Blog的基本特征是可以创建,获取,更新以及删除发表及评论。因为我们是初次接触Symfony,所以我们不会从头开始创建Symfony代码,而是让Symfony创建一个我们可以使用并且在需要时可以修改的程序框架。Symfony可以解释数据模型来自动创建CRUD接口。
$ php symfony propel-generate-crud frontend post Post
$ php symfony propel-generate-crud frontend comment Comment
$ php symfony clear-cache
On *nix systems, you will have to change some rights:
$ chmod 777 data
$ chmod 777 data/sandbox.db
现在我们就拥有了两个可以用来处理Post与Comment类的模型(post,comment)。一个模型(module)通常代表一个页面或是具有相似目的的一组页面。我们的新模型位于sf_sandbox/apps/frontedn/modules/目录下,而他们可以通过下面的URL来进行访问:
http://localhost/sf_sandbox/web/frontend_dev.php/post
http://localhost/sf_sandbox/web/frontend_dev.php/comment
此时我们可以进行一些简单的测试。
修改布局
为了在两个新模型之间进行切换,weblog需要一些全局的浏览设置。
编辑全局模板sf_sandbox/apps/frontend/templates/layout.php,修改body内容如下:
<div id="container" style="width:600px;margin:0 auto;border:1px solid grey;padding:10px">
<div id="navigator" style="display:inline;float:right">
<ul>
<li><?php echo link_to('List of posts','post/list') ?></li>
<li><?php echo link_to('List of comments','comment/list') ?>
</ul>
</div>
<div id="title">
<h1><?php echo link_to('My First symfony project','@homepage') ?></h1>
</div>
<div id="content" style="clear:right">
<?php echo $sf_data->getRaw('sf_content') ?>
</div>
</div>
当我们编辑这个工程时,我们可以改变我们页面的标题。编辑程序(sf_sandbox/apps/frontend/config/view.yml)的视图配置文件,定位到显示title关键字的行,将其改为:
metas:
title: The best weblog ever
robots: index, follow
description: symfony project
keywords: symfony, project
language: en
主页面本身需要改变。他使用默认模块的默认模板,这个默认模块保存在框架中,但是却并不在我们的程序目录中。要覆盖他,我们需要创建一个自己的main模块:
$ php symfony init-module frontend main
默认情况下,index动作会显示一个默认的欢迎页面。要移除他,我们可以编辑sf_sandbox/apps/frontend/modules/main/actions/actions.class.php文件,并且移除executeIndex()方法的内容:
public function executeIndex()
{
}
编辑sf_sandbox/apps/frontend/modules/main/templates/indexSuccess.php文件来显示我们的欢迎页面:
<h1>Welcome to my swell weblog</h1>
<p>You are the <?php echo rand(1000,5000) ?>th visitor today.</p>
现在我们需要告诉Symfony当请求主页时需要执行哪个action。为了这个目的,我们可以编辑sf_sandbox/apps/frontend/config/routing.yml文件,并且改变homepage规则:
homepage:
url: /
param: { module: main, action: index }
我们可以通过再一次请求主页来查看结果:
http://localhost/sf_sandbox/web/frontend_dev.php/
从action向template传递数据
这样的速度很快,不是吗?现在我们要将评论模块与一个文章发布想混合,从而可以在一个发布的文章下面显示评论。
首先,我们要使得发布的评论为发布显示模板可用。在Symfony中这种逻辑类型是保存在actions中。编辑动作文件sf_sandbox/apps/frontend/modules/post/actions/actions.class.php,更改executeShow()方法,并且添加下面的最后四行:
public function executeShow()
{
$this->post = PostPeer::retrieveByPk($this->getRequestParameter('id'));
$this->forward404Unless($this->post);
$c = new Criteria();
$c->add(CommentPeer::POST_ID, $this->getRequestParameter('id'));
$c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
$this->comments = CommentPeer::doSelect($c);
}
Criteria与-Peer类是Propel类型关系映射的一部分。基本来说,这四行将会处理一个到Comment表的SQL查询,从而得到与当前发布相关的评论(通过URL参数id来指定)。动作中的$this->comments行将可以访问相应的模板中的一个$comments变量。现在修改发布显示模板sf_sandbox/apps/frontend/modules/post/templates/showSuccess.php,并且在最后添加下面的代码行:
...
<?php use_helper('Text', 'Date') ?>
<hr />
<?php if ($comments) : ?>
<p><?php echo count($comments) ?> comment<?php if (count($comments) > 1) : ?>s<?php endif; ?> to this post.</p>
<?php foreach ($comments as $comment): ?>
<p><em>posted by <?php echo $comment->getAuthor() ?> on <?php echo format_date($comment->getCreatedAt()) ?></em></p>
<div class="comment" style="margin-bottom:10px;">
<?php echo simple_format_text($comment->getBody()) ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
这个页面使用了Symfony提供的新的PHP函数(format_date(),simple_format_text()),并且调用'helpers',因为他们会为我们完成一些需要更多时间和代码的任务。为我们的第一个发布添加一条评论,然后检测我们的第一个发布,我们可以点击列表中前面的标号,或是直接点击列表链接本身:
http://localhost/sf_sandbox/web/frontend_dev.php/post/show?id=1
添加与另一个表相关的记录
当添加一条评论时,我们可以选择相应的发布id。这并不是用户友好的。让我们来改变这种行为,从而可以保证用户在添加一条评论之后可以返回到他正在查看的发布上。
首先,在modules/post/templates/showSuccess.php模板底部添加下面一行:
<?php echo link_to('Add a comment', 'comment/create?post_id='.$post->getId()) ?>
link_to() helper会创建一个指向评论模板中创建动作的一个超链接,所以我们可以直接由发布详细页面中添加一个评论。接下来,打开modules/comment/templates/editSuccess.php并且替换下面的代码行:
<tr>
<th>Post:</th>
<td><?php echo object_select_tag($comment, 'getPostId', array (
'related_class' => 'Post',
)) ?></td>
</tr>
为
<?php if ($sf_params->has('post_id')): ?>
<?php echo input_hidden_tag('post_id',$sf_params->get('post_id')) ?>
<?php else: ?>
<tr>
<th>Post*:</th>
<td><?php echo object_select_tag($comment, 'getPostId', array('related_class' => 'Post')) ?></td>
</tr>
<?php endif; ?>
comment/create页面中的表单指向一个comment/update动作,当提交成功时会重新定向到comment/show(这是生成的CRUD的默认动作)。对于weblog而言,在对于一个文章发布添加了一个评论之后,则会显示评论的详细内容。更好的解决是与评论同时显示发布文章的内容。所以打开modules/comment/actions/actions.class.php,并且找到executeUpdate()方法。注意动作中并没有定义created_at域:Symfony会知道当创建一个记录时一个名为created_at的域会设置为系统时间。动作的最后重定向必须进行修改来指向正确的动作。所以将其改为:
public function executeUpdate ()
{
if (!$this->getRequestParameter('id', 0))
{
$comment = new Comment();
}
else
{
$comment = CommentPeer::retrieveByPk($this->getRequestParameter('id'));
$this->forward404Unless($comment);
}
$comment->setId($this->getRequestParameter('id'));
$comment->setPostId($this->getRequestParameter('post_id'));
$comment->setAuthor($this->getRequestParameter('author'));
$comment->setEmail($this->getRequestParameter('email'));
$comment->setBody($this->getRequestParameter('body'));
$comment->save();
return $this->redirect('post/show?id='.$comment->getPostId());
}
现在用户可以为文章发布添加一个评论,并且在评论之后返回文章页面。我们希望一个weblog吗?现在我们已经有了一个weblog。
表单验证
访问者可以发布评论,但是如果他们所提交的表单没有任何内容时会怎么样呢?我们就会得到一个脏数据库。为了避免这样的情况,在sf_sandbox/apps/frontend/modules/comment/validate/目录下创建一个名为update.yml的文件,并且写入下面的内容:
methods:
post: [author, email, body]
get: [author, email, body]
fillin:
enabled: on
names:
author:
required: Yes
required_msg: The name field cannot be left blank
email:
required: No
validators: emailValidator
body:
required: Yes
required_msg: The text field cannot be left blank
emailValidator:
class: sfEmailValidator
param:
email_error: The email address is not valid.
注意,我们不应拷贝每一行开头的额外四个空格,因为这样YAML会解析抢购。这个文件的第一行必须为methods的m。
fillin动作会保证在验证失败的情况下会使用用户以前输入的数据来填充表单。名字声明部分为每一个表单的输入域设置验证规则。
在默认情况下,当检测到一个错误时控制器会将用户重定向到一个updateError.php模板。更好的方法是重新显示带有错误信息的表单。为了达到这样的目的,在modules/comment/actions/actions.class.php文件中添加一个handleErrorUpdate方法:
public function handleErrorUpdate()
{
$this->forward('comment', 'create');
}
现在就要完成了,再一次打开modules/comment/templates/editSuccess.php模板,并且在顶部添加下面的代码行:
<?php if ($sf_request->hasErrors()): ?>
<div id="errors" style="padding:10px;">
Please correct the following errors and resubmit:
<ul>
<?php foreach ($sf_request->getErrors() as $error): ?>
<li><?php echo $error ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
现在我们就有一个健壮的表单了。
改变URL形式
我们是否注意到了URL的形式?我们可以使得URL对于用户和搜索引擎更为的友好。对于文章发布而言,我们要使用post标题作为URL。
问题在于post标题可能包含特殊的字符,例如空格。如果我们只是进行转义,那么URL则会显示为丑陋的%20之类的内容,所以我们应扩展这个模块来添加一个新的方法,从而得到一个干净的,简洁的标题。要达到这样的目的,编辑sf_sandbox/lib/model/目录下的Post.php,并且添加下面的方法:
public function getStrippedTitle()
{
$result = strtolower($this->getTitle());
// strip all non word chars
$result = preg_replace('/\W/', ' ', $result);
// replace all white space sections with a dash
$result = preg_replace('/\ +/', '-', $result);
// trim dashes
$result = preg_replace('/\-$/', '', $result);
$result = preg_replace('/^\-/', '', $result);
return $result;
}
现在我们可以为这个post模块创建一个permalink动作。在modules/post/actions/actions.class.php添加下面的方法:
public function executePermalink()
{
$posts = PostPeer::doSelect(new Criteria());
$title = $this->getRequestParameter('title');
foreach ($posts as $post)
{
if ($post->getStrippedTitle() == $title)
{
break;
}
}
$this->forward404Unless($post);
$this->getRequest()->setParameter('id', $post->getId());
$this->forward('post', 'show');
}
post列表可以调用permalink动作。在modules/post/templates/listSuccess.php,删除id表头及相应的单元,并且将Ttitle的形式
<td><?php echo $post->getTitle() ?></td>
改为下面的形式:
<td><?php echo link_to($post->getTitle(), 'post/permalink?title='.$post->getStrippedTitle()) ?></td>
还剩下另外一步:编辑位于sf_sandbox/apps/frontend/config/目录下的routing.yml文件,并且在顶部添加下面的规则:
下面我们可以浏览我们的程序并且查看URL的形式。
前端清理
当然,如果这是一个weblog,那么每一个人都可以发表。而这不是我们所希望的样子,对吧?好吧,让我们对于我们的模板进行一些小的清理。
在模板modules/post/templates/showSuccess.php中,通过移除下面的行来清除edit链接:
<?php echo link_to('edit', 'post/edit?id='.$post->getId()) ?>
对于modules/post/templates/listSuccess.php模板也执行同样的操作:
<?php echo link_to('create', 'post/create') ?>
我们同时也需要从modules/post/actions/actions.class.php文件中移除下面的方法:
* executeCreate
* executeEdit
* executeUpdate
* executeDelete
好了,现在读者再也不可以发表了。
生成后端
对于我们来说要发表文章,我们可以通过在命令行输入下面的命令来生成一个后端程序:
$ php symfony init-app backend
$ php symfony propel-init-admin backend post Post
$ php symfony propel-init-admin backend comment Comment
此时,我们使用admin生成器。也基本的CURD生成器相比,他提供了更多的特性与自定义。
正如我们在前端程序所做的,编辑layout(apps/backend/templates/layout.php)来添加全局浏览:
<div id="navigation">
<ul style="list-style:none;">
<li><?php echo link_to('Manage posts', 'post/list') ?></li>
<li><?php echo link_to('Manage comments', 'comment/list') ?></li>
</ul>
</div>
<div id="content">
<?php echo $sf_data->getRaw('sf_content') ?>
</div>
在开发环境下,我们可以通过调用下面的URL来访问我们的后台程序:
http://localhost/sf_sandbox/web/backend_dev.php/post
生成的admin的最大优点就是我们可以通过编辑一个配置文件很容易的来进行定制。将backend/modules/post/config/generator.yml改为:
generator:
class: sfPropelAdminGenerator
param:
model_class: Post
theme: default
fields:
title: { name: Title }
excerpt: { name: Exerpt }
body: { name: Body }
nb_comments: { name: Comments }
created_at: { name: Creation date }
list:
title: Post list
layout: tabular
display: [=title, excerpt, nb_comments, created_at]
object_actions:
_edit: ~
_delete: ~
max_per_page: 5
filters: [title, created_at]
edit:
title: Post detail
fields:
title: { type: input_tag, params: size=53 }
excerpt: { type: textarea_tag, params: size=50x2 }
body: { type: textarea_tag, params: size=50x10 }
created_at: { type: input_date_tag, params: rich=on }
注意在已存在的Post表的列中,admin将会查找nb_comments。但是此时并没有相关的方法,但是很容易在sf_sandbox/lib/model/Post.php中添加:
public function getNbComments()
{
return count($this->getComments());
}
现在我们可以刷新页面来查看改变。
限制后台访问
后台可以为任何人访问。我们需要添加访问限制。在apps/backend/modules/post/config/目录下添加一个security.yml文件,其内容为:
all:
is_secure: on
对于comment模块也要重复同样的操作。现在我们不可以访问这些模块,除非我们已经进行登陆。
但是登陆现在并不存在。好吧,我们可以很容易的添加一个。首先,创建一个安全模块框架:
$ php symfony init-module backend security
这个新的模块将用来处理登陆表单与请求。编辑apps/backend/modules/security/templates/indexSuccess.php来创建登陆表单:
<h2>Authentication</h2>
<?php if ($sf_request->hasErrors()): ?>
Identification failed - please try again
<?php endif; ?>
<?php echo form_tag('security/login') ?>
<label for="login">login:</label>
<?php echo input_tag('login', $sf_params->get('login')) ?>
<label for="password">password:</label>
<?php echo input_password_tag('password') ?>
<?php echo submit_tag('submit', 'class=default') ?>
</form>
在安全模块中添加为表单所调用的登陆动作(apps/backend/modules/security/actions.class.php):
public function executeLogin()
{
if ($this->getRequestParameter('login') == 'admin' && $this->getRequestParameter('password') == 'password')
{
$this->getUser()->setAuthenticated(true);
return $this->redirect('main/index');
}
else
{
$this->getRequest()->setError('login', 'incorrect entry');
return $this->forward('security', 'index');
}
}
作为主模块,在index动作中移除默认代码:
public function executeIndex()
{
}
最后一件事情就是要将安全模块作为默认模块来处理登陆动作。为了完成这样的任务,打开apps/backend/config/setting.yml配置文件,并且添加下面的代码行:
all:
.actions:
login_module: security
login_action: index
这样当我们试着访问文章发布管理时,我们必须输入登陆名与密码。
结论
好了,现在我们学习的时间已经到了。我们完成了我们的练习。现在我们可以在生产环境下使用这些程序。
frontend: http://localhost/sf_sandbox/web/index.php/
backend: http://localhost/sf_sandbox/web/backend.php/
此时,如果我们遇到错误,也许是因为我们在cacahe中放了一些动作后改变了模块造成的(在开发环境下,cacahe并没有激活)。要清除cacahe,我们可以简单的输入下面的命令:
$ php symfony cc