昨天,我们研究了symfony是如何通过抽象化数据库引擎间的差别,将关联元素转化为面向 对象的类,来简化数据库管理的;我们使用Doctrine描述数据库结构、生成数据表,并给 数据库添加了一些初始数据。
今天,我们将完成昨天生成的工作模块(job
moudle)。这个模块已经包括Jobeet需要的
所有代码:
- 显示所有工作的列表页
- 创建新工作的页面
- 更新工作的页面
- 删除工作的页面
代码已经有了,我们要修改模板,让它符合我们的设计。
MVC体系
如果你没有用过框架,你可能习惯于将php代码与HTML代码混编在同一个php文件中。这些 php文件可能都包含配置代码、业务逻辑、SQL语句和HTML代码。
你可能使用过模板引擎从HTML代码中分离出业务逻辑,也可能使用过数据抽象层从业务逻辑 中分离出关系模型。但是大多时候,这只会让你产生大量的代码。开发速度提高,但不久, 你就发现代码越来越难维护,因为除了你之外没有人能读懂这些代码是如何工作的。
对于这些问题,有一个非常好的解决办法——[MVC 设计模式] (http://en.wikipedia.org/wiki/Model-view-controller) ,这是现在最普遍的使用的 组织代码的方式。简单来说,MVC模式定义了一种最自然的组织代码的方式。这种模式将 代码分离为3个层:
模型层(Model layer)定义业务逻辑(数据库属于这个层)。上节课中你已经知道, symfony将与模型层关联的所有类和文件都放在
lib/model
目录下。视图层(View)是与用户交互的层(模板引擎是这个层的一部分)。在symfony中, 视图层主要由PHP模板组成。这些文件存储在不同的
templates/
目录中,今天稍后会讲到。控制层(Controller)是一段代码,它从模型层读取数据传送给视图层,由视图层显示 到客户端。当我们第1天安装symfony时,我们看到所有的请求都由前端控制器(
index.php
andfrontend_dev.php
)管理。这些前端控制器(front controllers)将实际工作交由动作 (actions)处理。象我们昨天看到的一样,这些动作被合理的组织到模块(modules)中。
今天,我将按第2天给出的布局修改主页和工作页,并让它们能动态显示内容。这样的话, 我们要做很多事,你可以从其中了解 symfony目录结构,及各层代码分离的方法。
布局(layout)
首先,如果你仔细研究了网页的设计图,你会发现每个页面都有很多相同的地方。 这表示我们必须在每个页面添加一些相同的代码。如你所知,代码重复是非常糟糕的, 所以我们首先要解决这个问题。
我们通常办法是:将重复代码(如头部代码和底部代码)分别写入单独的页面,然后 再让需要显示这些内容的页面去调用这些单独的页面。下面是个简单的图示:
这种办法看起来不错,大多数时候我们也是这么做的。但是,这个办法存在一个缺陷, 就是头部和底部两个独立的文件中会含有没有闭合的HTML标签。 为了让一切看上去很 完美,我们使用了另外一种更好方式来解决这个问题—— 装饰设计模式 (Decorator design pattern) (Decorator design pattern):就是使用全局模板(layout ),来装饰展示内容的 模板(译注:内容模板如同画,全局模板如同框,装饰模式就是用画框来装饰画。反过 来说,一个画框又可以装饰不同的画):
默认的全局模板(layout)放在layout.php
文件中,可以在
apps/frontend/templates/
目录下找到它。这个目录中的通常存放全局模板文件,
程序(app)中每个模块(module)都可以使用。
用下面代码替换layout.php
中的代码:
<!-- 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> <title>Jobeet - Your best job board</title> <link rel="shortcut icon" href="/favicon.ico" /> <?php include_javascripts() ?> <?php include_stylesheets() ?> </head> <body> <div id="container"> <div id="header"> <div class="content"> <h1><a href="/job"> <img src="/legacy/images/logo.jpg" alt="Jobeet Job Board" /> </a></h1> <div id="sub_header"> <div class="post"> <h2>Ask for people</h2> <div> <a href="/job/new">Post a Job</a> </div> </div> <div class="search"> <h2>Ask for a job</h2> <form action="" method="get"> <input type="text" name="keywords" id="search_keywords" /> <input type="submit" value="search" /> <div class="help"> Enter some keywords (city, country, position, ...) </div> </form> </div> </div> </div> </div> <div id="content"> <?php if ($sf_user->hasFlash('notice')): ?> <div class="flash_notice"> <?php echo $sf_user->getFlash('notice') ?> </div> <?php endif; ?> <?php if ($sf_user->hasFlash('error')): ?> <div class="flash_error"> <?php echo $sf_user->getFlash('error') ?> </div> <?php endif; ?> <div class="content"> <?php echo $sf_content ?> </div> </div> <div id="footer"> <div class="content"> <span class="symfony"> <img src="/legacy/images/jobeet-mini.png" /> powered by <a href="/"> <img src="/legacy/images/symfony.gif" alt="symfony framework" /> </a> </span> <ul> <li><a href="">About Jobeet</a></li> <li class="feed"><a href="">Full feed</a></li> <li><a href="">Jobeet API</a></li> <li class="last"><a href="">Affiliates</a></li> </ul> </div> </div> </div> </body> </html>
和其它的一些模板程序不大相同,symfony的模板都是纯PHP文件。在layout模板中
包含一些PHP变量和函数调用。如$sf_content
就是一个非常有趣的变量:它由框架定义,包
含由动作(action)生成的HTML代码。
如果你浏览job
模块(http://jobeet.localhost/frontend_dev.php/job
),
你可以看到所有被layout装饰的动作。
样式、图片、脚本
这不是一个关于网页设计的教程,我们已经为读者准备好了全部的资源文件:你可以到
这里
下载图片放到web/legacy/images/
目录下;从这里
下载css文件放到web/css/
目录下。
note
在layout中包含一个收藏夹图标,你可以从这里
下载,放到web/
目录下。
tip
默认的情况下,generate:project
命令会自动在/web目录下生成3个目录来分别存放不
同文件:/web/legacy/images/
目录用来保存图片,/web/~css|CSS~/
保存css样式表(Stylesheets),
/web/js/
保存javascript文件。这些目录是symfony的默认目录,如果没有特别的声明,
symfony会自动到这些目录寻找相应类型的文件。当然,我们也可以通过显示的说明,而
将这些类型文件放到web/
下的任意目录中。
也许你会奇怪,当我们在浏览器中查看job页面源文件时,会发现页面使用一个名叫
main.css
的样式文件,但是在layout页面中我们却没有发现任何调用这个文件的语句,
这是怎么回事呢?
其实main.css
已经被调用了,只不过是使用了一种我们现在还不太熟悉的方式——辅助函数(helper)。
辅助函数是symfony内置辅助生成HTML代码的函数,很多情况下它可以帮助你节约书写
HTML代码的时间。include_stylesheets()
就是一个辅助函数,用来生成一个调用css文件
的<link>
标签。
但是如何知道include_stylesheets()函数是如何知道调用哪个css样式文件呢?
include_stylesheets()
函数是通过读取view.yml
文件中stylesheets
项来决定调用哪些文件的。
view.yml
被用来配置视图层,下面是generate:app
自动生成的view.yml
文件:
# apps/frontend/config/view.yml default: http_metas: content-type: text/html metas: #title: symfony project #description: symfony project #keywords: symfony, project #language: en #robots: index, follow stylesheets: [main.css] javascripts: [] has_layout: on layout: layout
这个view.yml
文件为程序下的所有模板设置默认
配置,例如stylesheets
项定义程
序每个页面要调用的css文件的数组(这个数组通过include_stylesheets()
调用)。
我们可以通过stylesheets
项为程序中每一个页面单独定义要调用的css文件。
note
在默认的view.yml
文件中调用的样式文件是main.css
而不是/css/main.css
。
事实上这是一个问题, 使用/~css|CSS~/
路径当做前缀(Prefix)和不是用其实是等价的。
当定义多个样式时,程序会按照数组中文件名的顺序,依次生成标签,按顺序调用每个样式:
stylesheets: [main.css, jobs.css, job.css]
你可以改变它们的media
属性,也可以省略样式文件名的.css
后缀:
stylesheets: [main.css, jobs.css, job.css, print: { media: print }]
这条设置将转化成下面的html标签:
<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css" /> <link rel="stylesheet" type="text/css" media="screen" href="/css/jobs.css" /> <link rel="stylesheet" type="text/css" media="screen" href="/css/job.css" /> <link rel="stylesheet" type="text/css" media="print" href="/css/print.css" />
tip
view.yml
也定义程序的使用全局模板,默认调用layout 模板,这个模板对应layout.php
文件。如果将has_layout
项设置为false
,装饰模式将被禁用。
view.yml
不仅可以定义所有模板共用css文件,还可以单独为每个模块设置css样式。
将程序的view.yml
文件改回只包含main.css
:
# apps/frontend/config/view.yml stylesheets: [main.css]
为了给job
模块单独配置视图,我们必须在apps/frontend/modules/job/config/
目录下,
给job模块创建一个新的view.yml
文件,加入代码:
# apps/frontend/modules/job/config/view.yml indexSuccess: stylesheets: [jobs.css] showSuccess: stylesheets: [job.css]
indexSuccess
和 showSuccess
两部分(对应index和show动作的模板名字,关于模板
的命名规则后面会详细讲述)可以使用程序view.yml
中的所有的设置项来配置。也可以
使用all
为模块中所有动作进行配置。当程序执行时,模块和程序的view.yml
文件将
被合并,合并的原则是:模块(的 view.yml
)中所有的设置项及模块(的view.yml
)
中没有而程序(的view.yml
)中有的设置项将全部保留;程序(的view.yml
)中与模
块(的view.yml
)中相同的设置将被覆盖;
一般来说,配置文件中的每个设置都可以使用PHP代码的动态实现,例如,你可以直接
使用use_stylesheet()
辅助函数来调用view.yml
中设置的css文件:
<?php use_stylesheet('main.css') ?>
你也可以在layout中使用这个辅助函数来调用一个全局css样式。
选择哪种方法只是一个爱好问题。一方面,view.yml
提供了一种为模块里所有动作设置
配置的方法,这在一个模板里是不可能实现的(模板中只能配置使用这个模板的动作)。
另一方面,view.yml
文件中设置是完全静态,当你需要动态调用的时候,使用
use_stylesheet()
helper会非常方便,另外使用辅助函数会使你css文件和HTML代码
放在一起,修改起来会方便许多。
在jobeet中我们将使用use_stylesheet()辅助函数的方式,你不需要配置view.yml文件。
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <?php use_stylesheet('jobs.css') ?> <!-- apps/frontend/modules/job/templates/showSuccess.php --> <?php use_stylesheet('job.css') ?>
note
相应的,javascript文件的调用可以通过view.yml
中javascripts
项设置,也可以
通过在模板使用use_javascript()
辅助函数来调用。
Job首页
在第3天教程中,job首页已经通过job
模块中index
动作生成。index
动作是首页
的控制层部分,关联的模板indexSuccess.php
是视图层部分:
apps/ frontend/ modules/ job/ actions/ actions.class.php templates/ indexSuccess.php
动作(Action)
每个动作都是类的一个方法。对于工作主页来说,类为jobActions
(命名方式:
模块名+Action
后缀),方法为executeIndex()
(命名方式:execute
+以驼峰式书写
的动作名),这个动作从数据库中获取全部的job记录:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = Doctrine::getTable('JobeetJob') ->createQuery('a') ->execute(); } // ... }
让我们仔细看一下这段代码: executeIndex()
方法(控制层)调用 JobeetJob
创建查询获
得全部的job记录。这个方法返回一个包含JobeetJob
对象的Doctrine_Collection
,并将这
个数组赋值给jobeet_job_list
对象属性。
让我们仔细看一下这段代码: the executeIndex()
method (the
Controller) calls the Table JobeetJob
to create a query to retrieve all the
jobs. It returns a Doctrine_Collection
of JobeetJob
objects that are assigned to
the jobeet_job_list
object property.
所有这样对象属性(jobeet_job_list)都会自动传递给模板(the View)。要将数据从 控制层传送到视图(或者说从动作传给模板),只需要定义一个新属性:
public function executeFooBar(sfWebRequest $request) { $this->foo = 'bar'; $this->bar = array('bar', 'baz'); }
你可以直接在模板中用$foo
和$bar
变量访问这个两个属性。
模板(Templates)
默认情况下,模板的名称与动作的名称是相对应的(模板的命名规则:
动作名+Success
后缀)。
indexSuccess.php
模板为所有工作生成HTML表格,用来展示工作信息:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <?php use_stylesheet('jobs.css') ?> <h1>Job List</h1> <table> <thead> <tr> <th>Id</th> <th>Category</th> <th>Type</th> <!-- more columns here --> <th>Created at</th> <th>Updated at</th> </tr> </thead> <tbody> <?php foreach ($jobeet_job_list as $jobeet_job): ?> <tr> <td> <a href="<?php echo url_for('job/show?id='.$jobeet_job->getId()) ?>"> <?php echo $jobeet_job->getId() ?> </a> </td> <td><?php echo $jobeet_job->getCategoryId() ?></td> <td><?php echo $jobeet_job->getType() ?></td> <!-- more columns here --> <td><?php echo $jobeet_job->getCreatedAt() ?></td> <td><?php echo $jobeet_job->getUpdatedAt() ?></td> </tr> <?php endforeach; ?> </tbody> </table> <a href="<?php echo url_for('job/new') ?>">New</a>
在模板代码中, foreach
遍历所有job
对象($jobeet_job_list
),输出数据表中
每条job记录中每个字段(column)值。我们可以通过调用存取器、很容易的访问记录中
的每个字段,存取器有统一的命名规则:是以get+驼峰式书写的字段(column)名(getColumn),
例如 getCreatedAt()
方法,可以获取一条记录的created_at
值【字段名中下划线(_)
被自动省略】。
我们修改一下模板,只显示需要的内容:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <?php use_stylesheet('jobs.css') ?> <div id="jobs"> <table class="jobs"> <?php foreach ($jobeet_job_list as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"><?php echo $job->getLocation() ?></td> <td class="position"> <a href="<?php echo url_for('job/show?id='.$job->getId()) ?>"> <?php echo $job->getPosition() ?> </a> </td> <td class="company"><?php echo $job->getCompany() ?></td> </tr> <?php endforeach; ?> </table> </div>
模板中url_for()
也是一个辅助函数,生成标签,我们明天再说它。
Job页面模板
现在我们定制job页模板,打开showSuccess.php
,用下面代码替换现有的内容:
<!-- apps/frontend/modules/job/templates/showSuccess.php --> <?php use_stylesheet('job.css') ?> <?php use_helper('Text') ?> <div id="job"> <h1><?php echo $job->getCompany() ?></h1> <h2><?php echo $job->getLocation() ?></h2> <h3> <?php echo $job->getPosition() ?> <small> - <?php echo $job->getType() ?></small> </h3> <?php if ($job->getLogo()): ?> <div class="logo"> <a href="<?php echo $job->getUrl() ?>"> <img src="/uploads/jobs/<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> </a> </div> <?php endif; ?> <div class="description"> <?php echo simple_format_text($job->getDescription()) ?> </div> <h4>How to apply?</h4> <p class="how_to_apply"><?php echo $job->getHowToApply() ?></p> <div class="meta"> <small>posted on <?php echo date('m/d/Y', strtotime($job->getCreatedAt())) ?></small> </div> <div style="padding: 20px 0"> <a href="<?php echo url_for('job/edit?id='.$job->getId()) ?>"> Edit </a> </div> </div>
这个模板直接使用$job
变量显示job信息,我们前边提到过,这个变量是在动作里定义的
一个对象属性。这里我们将$jobeet_job
改成了$job
,所以要相应的修改show
动作里
的对象属性名(注意,有两处要修改):
// apps/frontend/modules/job/actions/actions.class.php public function executeShow(sfWebRequest $request) { $this->job = Doctrine::getTable('JobeetJob')-> find($request->getParameter('id')); $this->forward404Unless($this->job); }
note
工作说明使用simple_format_text()
辅助函数格式化为HTML,将carriage替换为<br />
,
这个辅助函数属于Text
辅助函数组,这个组的辅助函数不会被程序自动加载,
需要使用use_helper()
加载。
槽(Slot)
现在,我们已经在layout中定义所有页面的<title>
标签:
<title>Jobeet - Your best job board</title>
但对于工作页,我们希望显示更有意义的title,比如公司名和工作地点。
symfony中,当layout中一个区域依赖模板显示(依赖于模板中定义的内容,而显示的位 置却在模板之外)时,需要定义一个槽:
给layout添加一个槽,让标题动态显示:
// apps/frontend/templates/layout.php <title><?php include_slot('title') ?></title>
每个槽都通过一个名字(如title
)定义,并通过include_slot()
调用。现在,
在showSuccess.php
模板开头,定义一个槽:
// apps/frontend/modules/job/templates/showSuccess.php <?php slot( 'title', sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition())) ?>
如果要生成复杂的title,slot()
辅助函数可以写成下面代码块的形式:
// apps/frontend/modules/job/templates/showSuccess.php <?php slot('title') ?> <?php echo sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition()) ?> <?php end_slot(); ?>
一般像首页这样的页面,我们只需要普通的title,我们只需要在layout定义一个默认title:
// apps/frontend/templates/layout.php <title> <?php if (!include_slot('title')): ?> Jobeet - Your best job board <?php endif; ?> </title>
如果名为title
的槽已经定义,include_slot()
将返回true
,显示title
槽
定义内容,否则显示默认title。
tip
我们已经看了不少以include_
开头的辅助函数,这些辅助函数可以直接输出HTML代码,
而以get_
开头的helper,大多数时候都只返回内容(需要输出语句显示内容):
<?php include_slot('title') ?> <?php echo get_slot('title') ?> <?php include_stylesheets() ?> <?php echo get_stylesheets() ?>
Job页动作
工作页由show
动作生成,show
动作在job
模块的executeShow()
方法中定义:
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = Doctrine::getTable('JobeetJob')-> find($request->getParameter('id')); $this->forward404Unless($this->job); } // ... }
同index
动作一样,the JobeetJob
table class is used to retrieve a job,
this time by using the find()
method. The parameter of this method
is the unique identifier of a job, its primary key. The next
section will explain why the $request->getParameter('id')
statement returns
the job primary key.
如果请求的job记录不存在,forward404Unless()
显示404错误页面,
它的第一参数为布尔型变量,如果为 true
,forward404Unless()
会停止执行当前进程。
当forward方法通过抛出sfError404Exception
异常停止执行动作时,不需要使用返回语句。
生产环境(Environments)和开发环境的404页面有些区别:
note
在部署Jobeet网站到生产服务器上之前,你将会学会如何定制默认的404页面。
请求与响应
当你在浏览器中浏览/job
或 /job/show/id/1
页面时,你便与服务器进行了一次交互,
浏览器发送一个请求(request)到服务器而服务器返回一个响应
(response)。
我们已经看到symfony将请求封装到sfWebRequest
对象中(看一下executeShow()
方法)。
作为一个面向对象的框架,响应也被封装到sfWebResponse
对象中。你可以通过调用
$this->getResponse()
访问响应对象。
这些对象提供许多方便的方法,访问PHP函数和PHP全局变量中的信息。
note
symfony为什么要打包已经存在的PHP功能?第一,因为symfony方法比PHP相应的方法更
加强大。第二,当你测试一个程序,symfony方法更容易模拟一个请求或回应对象,这
比起反复折腾全局变量或使用PHP中类似header()
的函数强很多。
请求(Request)
sfWebRequest
类包含了$_SERVER
, $_COOKIE
, $_GET
, $_POST
, $_FILES
PHP全局数组:
Method name | PHP equivalent |
---|---|
getMethod() |
$_SERVER['REQUEST_METHOD'] |
getUri() |
$_SERVER['REQUEST_URI'] |
getReferer() |
$_SERVER['HTTP_REFERER'] |
getHost() |
$_SERVER['HTTP_HOST'] |
getLanguages() |
$_SERVER['HTTP_ACCEPT_LANGUAGE'] |
getCharsets() |
$_SERVER['HTTP_ACCEPT_CHARSET'] |
isXmlHttpRequest() |
$_SERVER['X_REQUESTED_WITH'] == 'XMLHttpRequest' |
getHttpHeader() |
$_SERVER |
getCookie() |
$_COOKIE |
isSecure() |
$_SERVER['HTTPS'] |
getFiles() |
$_FILES |
getGetParameter() |
$_GET |
getPostParameter() |
$_POST |
getUrlParameter() |
$_SERVER['PATH_INFO'] |
getRemoteAddress() |
$_SERVER['REMOTE_ADDR'] |
我们已经用过getParameter()
方法访问请求的参数,它返回$_GET
或$_POST
全局变量的值或~PATH_INFO~
变量。
如果你想确定请求参数究竟属于上面哪一类,你需要使用getGetParameter()
,
getPostParameter()
, 和 getUrlParameter()
。
note
如果你想限制一个动作只接受某种特定HTTP method传递的参数,
比如,当你需要表单是否是通过POST
方式提交的,你可以使用isMethod()
方法:
$this->forwardUnless($request->isMethod(’POST’));
响应(Response)
sfWebResponse
类,包含~header|HTTP Headers~()
和
setraw~cookie|Cookies~()
PHP方法:
Method name | PHP equivalent |
---|---|
setCookie() |
setrawcookie() |
setStatusCode() |
header() |
setHttpHeader() |
header() |
setContentType() |
header() |
addVaryHttpHeader() |
header() |
addCacheControlHttpHeader() |
header() |
当然,sfWebResponse
也可以设置响应内容(setContent()
),和发送响应(send()
)到
浏览器的方法。
今天开始时候,我们讲了如何在view.yml
文件和模板中管理样式表和javascript
脚本。
现在我们用响应对象的addStylesheet()
和addJavascript()
方法可以实现同样的效果。
tip
sfAction
,
sfRequest
和
sfResponse
类提供了
许多有用的方法。不要犹豫了,去
API documentation学习更多的
symfony 内置类。
明天见
今天,我们已经描述了一些symfony使用的设计模式。希望项目结构现在会更有意义。 我们已经通过操作layout和模板文件,改变了模板。并使用插槽和动作让模板显示的 更加动态。
明天,我们将学习更多关于url_for()
辅助函数的用法,并通过它给子框架设置路由。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.