第三天-深入MVC体系结构

 在第二天的学习中,我们了解了如何基于一个关系数据模型构建一个对象模型,并且为这些对象中的一个对象生成了一个程序框架。顺便说一下,在前一天所生成的程序代码可以在askeet的SVN仓库得到:

http://svn.askeet.com/

我们在第三天的目标是要为这个网站定义一个漂亮的结构布局,将问题列表作为默认主页,显示对一个问题感兴趣的用户数量,以及为了进行数据测试由样本文本文件移居数据库。所需要做的内容并不是很多,但是有许多内容需要阅读和理解。

为了阅读这一节教程,我们需要熟悉Symfony一书中所解释的工程,程序,模块以及动作等概念。

MVC模型

今天是我们第一次进入MVC体系结构的世界。这意味着什么呢?简单来说就是生成一个页面的代码依据其特性位于不同的文件中。

如果代码独立于页面专注于数据的操作,他应位于Model中(大多数情况下在askeet/lib/model/目录中)。如果他专注于最终的显示,他应位于View中。在Symfony中,视图层依据于模型(例如askeet/apps/frontend/modules/question/templates/中)以及配置文件。最后,将所有这些联系在一起,并将网站逻辑转换为PHP代码的程序代码位于Controller,而在Symfony中,一个指定页面的控制器被称之为动作。我们可以在Symfony一书的MVC implementation in symfony一章中了解更多关于这个模型的内容。

然而今天我们的程序只是做一些小的修改,我们将会操作许多不同的文件。不要感到麻烦,因为文件的组织与不同层的代码的分离很快就会变得明显并且有用。

改变布局

在设计模式中,一个动作所调用的模板内容集成到一个全局的模板或是布局中。换句话说,布局包含所有接口的不变部分,他修饰动作的结果。打开默认的布局(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" />
 
</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>

我们试着尽可能保持标记的可理解,并且将所有的样式移到CSS样式表中。我们并不会在这里描述样式文件,因为CSS语法并不是这个教程的目的。我们可以在SVN仓库中下载这些样式文件。

我们创建了两个样式文件(main.css,layout.css)。将他们拷贝到我们的askeet/web/css/目录下,并且编辑我们的frontend/config/view.yml文件来改变自动载入的样式文件:

stylesheets:    [main, layout]

此时的布局仍是轻量级的,我们将会在后来重新构建。模板中重要的一点就是<head>部分,这是自动生在的,而sf_content变量包含动作的结果。

我们可以通过请求主页来检测修改是否可以正确显示--这一次是在开发环境中:
http://askeet/frontend_dev.php/

关于环境的说明

如果我们想要了解http://askeet/frontend_dev.php/与http://askeet/之间的区别,那么我们需要看一下Symfony一书的配置一节。现在我们只需要知道他们指向同一个程序,但是却在不同的环境中。一个环境是一个唯一的配置,此时的框架的特性可以依据需要来决是否激活。

在这种情况下,/frontend_dev.php/ URL指向开发环境,此时在每一次请求时都会解析整个配置,HTML缓存未激活,而调试工具都可用(包括窗口右上角的半透明的工具条)。而 / URL--等同于/index.php/--指向生产环境,此时配置是"编译"的,而为了加速页面的传输禁用了调试工具条。

这两个PHP脚本--frontend_dev.php与index.php--称之为前端控制器,所有到程序的请求都是由他们来处理的。我们可以在askeet/web/目录下找到他们。事实上,index.php应命名为frontend_dev.php,但是因为frontend是我们创建的第一个程序,Symfony推测我们也许希望其作为默认程序,所以将其重命名为index.php,从而我们可以在仅请求/来在生产环境中查看我们的程序。如果我们希望了解更多的关于通常的MVC模型中前端控制器与控制器层,我们可以查看Symfony一书的控制器一章。

一个良好的规则就是我们在开发环境中进行浏览,直到对所有的特性感到满意,然后切换到生产环境来检测其速度与良好的URL。

记住,当我们添加新类或是改变配置文件后,要在生产环境中查看结果,应要先清除缓存。

重新定义主页

现在,当我们请求新网站的主页时,他显示一个'恭喜'页面。一个更好的主意是显示问题列表。要这样做,打开frontend程序的routing配置文件(askeet/apps/frontend/config/routing.yml),定位到homepage:部分,将其改变:

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

在开发环境中刷新页面(http://askeet/frontend_dev.php),现在就会显示问题列表了。

如果我们是一个很好奇的人,我们也许会查找包含’恭喜'信息的页面。而我们会很奇怪并不会在askeet目录下找到这个文件。事实上,默认的default/index动作的模板是在Symfony的数据目录中定义的,而且是独立于工程的。如是我们希望覆盖他,我们可以在我们自己的目录中创建一个默认的模块。

路由系统所提供的功能我们将会在后面详细讨论,但是如果我们感兴趣,我们可以阅读Symfony一书的rounting一章。

定义测试数据

主页的列表显示仍然显得很空,除非我们添加我们自己的问题。当我们开发一个程序时,有一些测试数据是一个好主意。手动输入测试数据是相当痛苦的,这也就是为什么Symfony可以使用文件来移居数据库的原因。

我们将在askeet/data/fixtures/目录下创建一个测试数据文件(这个目录需要创建)。使用下面的内容来创建一个名为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 }

首先,也许我们会认出这里的YAML语法。如果我们还不熟悉Symfony,那么也许我们还不知道在框架中YAML格式是配置文件最喜欢的格式。这并不是唯一的--如果我们还可以使用XML或是.ini文件,可以很容易的添加一个配置处理器允许Symfony读取他们。如果我们有时间和耐心,我们可以读一下Symfony一书中的实际配置一章中的关于YAML和Symfony配置的更多内容。而现在,如果我们并不熟悉YAML语法,那么我们需要立即开始,因为这个教程大量用了YAML语法格式。

好了,现在回到测试数据文件。他定义了对象实体,并以内部名称来标识。这个标签对于链接相关对象而不必定义id是十分有用的。例如,创建的第一个对象为User类,并且标识为fabien。第一个问题标识为q1。这就很容易通过指定一个相关对象标签来创建一个类的对象:

Interest:
  i1:
    user_id: fabien
    question_id: q1

前面所给定的数据文件使用了短YAML语法来描述这些内容。我们可以在Symfony一书的数据文件一章了解到更多的关于数据迁移的内容。

我们并不需要为created_at与updated_at列定义值,因为在默认情况下Symfony知道如何来填充这些域。

创建一个批文件来迁移数据

下一步就是实际的迁移数据库,而我们希望用一个可以在命令进行调用的PHP脚本来完成这些工作--一个批处理。

批处理框架

用下面的内容在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();
 
?>

这个脚本并没有做任何事情,或者说是几乎没有做任何事情:他定义了一个要进行配置的路径,程序以及环境,装入这个配置,并且初始化数据管理器。但是这已经很多了:这就意味着下面所编写的代码将会利用自动装入的类,自动连接到Propel对象以及Symfony根类。

如果我们已经测试了Symfony的前端控制器(例如askeet/web/index.php),我们就会发现这些代码十分的熟悉。这是因为正如批请求一样,每一个web请求需要访问同样的对象与配置。

数据导入

现在已经准备好了批处理的框架,要在使其来完成一些事情了。批处理需要完成:
1 读取YAML文件
2 创建Propel对象实例
3 在所链接的数据库的数据表中创建相关的记录

这听起来很复杂,但是在Symfony中,由于sfPropelData对象,我们只需要两行代码就可以完成这些工作。在askeet/batch/load_data.php脚本最后的?>前添加下面的代码:
data = new sfPropelData();
$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');

这就完成了。创建了一个sfPropelData对象,并且通知其将指定目录下的所有数据--我们的fixtures目录--装入到databases.yml配置文件中所定义的数据库中。

这里的DIRECTORY_SEPARATOR常量用来兼容Windows与*nix平台。

启动批处理

最后,我们可以检测这些代码是否值得我们这样的论述,在命令行输入下面的命令:

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

通过刷新开发中的主页我们可以检测数据库中的这些修改:
http://askeet/frontend_dev.php

数据已成功装入。

默认情况下,sfPropelData对象会在装入新的数据之前删除我们所有的数据。我们也可以在当前数据之后添加:

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

在模块中访问数据

当请求question模块的list动作时所显示的页面是executeList()(可以在askeet/frontend/modules/question/actions/action.class.php动作文件中找到)方法传递到askeet/apps/frontend/modules/question/templates/listSuccess.php模板中的结果。这是基于Symfony一书中控制器一章中所解释的名字转换。让我们看一下所执行的代码:

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; ?>

一步一步的,其所做的工作如下:

1 动作请求满足一个空标准(criteria)的Question表的记录
2 记录列表放在一个数组中($questions)传递到模板
3 模板在动作所传递过来的问题中循环
4 模板显示记录中每一列的值

->getId(),->getTitle(),->getBody()等方法是在Symfony的propel-build-model命令调用中生成的,用来获取id,title,body等数据域的值。这些是标准的获取方法,由在域名字前添加get前缀进行格式化,而Propel也提供了标准的设置方法,以set为前缀。Propel文档描述了为每一个类所创建的访问方法。

而我们也许会感到迷惑的是QuestionPeer::doSelect(new Criteria())调用,他也是一个标准的Propel请求。Propel文档会详细的对其进行解释。

如果我们并不能完全理解上面所编写的代码也不担心,几天后就会变得清晰了。

修改question/list模板

现在数据已经包含了对问题感兴趣的记录,这样就应该很容易的得到对一个问题感兴趣的用户。如是我们看一下askeet/lib/model/om/目录下由Propel所生成的BaseQuestion.php类,我们就会注意到有一个->getInterests()方法。Propel会检测到在Interest数据表定义中的question_id外键,从而推测以一个问题会有一些感兴趣的用户。这样就使其很容易通过修改listSuccess.php(askeet/frontend/modules/question/templates/)模板来显示我们所希望的内容。在这个处理中,我们会移除那些丑陋的表格,并且替换为我们漂亮的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; ?>

我们在这里可以看到与原始的listSuccess.php相同的foreach循环。link_to()与truncate_text()函数是由Symfony所提供的模板帮助器。第一创建了一个指向相同模块的另一个动作的超链接,而第二个将问题内容截短为200个字符。link_to()帮助器是自动装入的,但是我们要使用truncate_text()必须声明使用帮助器的Text组。

现在我们可以刷新主而来测试我们的新模板:
http://askeet/frontend_dev.php/

感兴趣的用户数在每一个问题的附近显示。要得到上面的显示,下载main.css样式文件,并将其放置在askeet/web/css/目录中。

清理

propel-generate-crud命令会创建一些我们并不会需要的动作与模板。现在是清除他们的时候了。

在askeet/apps/frontend/modules/question/actions/actions.class.php中要移除的动作:
    * executeIndex
    * executeEdit
    * executeUpdate
    * executeCreate
    * executeDelete

在askeet/apps/frontend/modules/question/templates/目录中要移除的模板:
editSuccess.php

明天见

今天是我们深入Model-View-Controller世界的伟大的一步:通过操作Propel对象模型的布局,模板,动作与对象,我们访问了一个MVC结构程序中所有层次。如果我们并不能完全理解这些层之间的桥梁也不必担心:这会一点点清晰起来的。

今天打开许多文件,如果我们想要了解在一个工程中文件是如何组织的,可以查看Symfony一书的文件结构一章。

明天将会是另一个伟大的一天:我们将会修改视图,设置一个更为复杂的路由规则,修改模块,深入数据操作以及在表之间进行链接。
posted @ 2008-02-23 09:48  jlins  阅读(268)  评论(0编辑  收藏  举报