如果你从来没有用过PHP框架, 不熟悉MVC架构, 或者仅仅是好奇围绕在symfony2周围的宣传, 那么这一章正适合你. 我们不会对你说“Symfony2比传统的PHP代码更能可以让你更快的开发出好的程序”,看完本章你便会有一个自主的选择.
在本章, 你将要用传统的PHP代码方式写一个简单的应用程序, 然后对其重构使其更具组织性. 通过这一过程, 看一看是什么在背后决定了Web开发进化到它现在的样子.
最后, 你将体验到Symfony2怎样把你从众多繁杂的任务中解救出来重新让你掌控你的代码.
用传统PHP代码编写一个简单的博客程序¶
在本章, 你将要通过使用传统的PHP代码编写方式构建一个博客程序. 首先, 创建一个简单页用来显示存储在数据库中的博客目录.以传统方式编写的PHP代码虽然很快但是很混乱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);
$result = mysql_query('SELECT id, title FROM post', $link);
?>
<!DOCTYPE html>
<html>
<head>
<title>List of Posts</title>
</head>
<body>
<h1>List of Posts</h1>
<ul>
<?php while ($row = mysql_fetch_assoc($result)): ?>
<li>
<a href="/show.php?id=<?php echo $row['id'] ?>">
<?php echo $row['title'] ?>
</a>
</li>
<?php endwhile; ?>
</ul>
</body>
</html>
<?php
mysql_close($link);
?>
|
这样虽然写的很快执行也不慢,但是随着应用程序规模的增长, 维护变得困难. 有几个问题值得注意:
- 无错误检查: 万一连接数据库失败怎么办?
- 组织性差: 如果程序的规模变得很大, 这样单一的文件会变的越来越难以维护. 在哪里我可以放置处理表单提交的代码? 怎样验证数据? 发送邮件的代码该如何实现?
- 代码难以重用: 因为所有的东西都在一个文件, 所以在博客的其它页重用程序的代码变得相当困难.
另一个没有提及的问题是上例数据库是和MySQL绑定的. Symfony2充分集成Doctrine, 从而实现数据库抽象及表字段的映射.
让我们解决上述提及的问题.
分离表现层¶
以下代码是我们将应用程序的逻辑代码和HTML代码分离后得出的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);
$result = mysql_query('SELECT id, title FROM post', $link);
$posts = array();
while ($row = mysql_fetch_assoc($result)) {
$posts[] = $row;
}
mysql_close($link);
// include the HTML presentation code
require 'templates/list.php';
|
现在HTML代码被存储在一个单独的文件里 (templates/list.php), 这是一个使用模板的HTML文件语法像PHP语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!DOCTYPE html>
<html>
<head>
<title>List of Posts</title>
</head>
<body>
<h1>List of Posts</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<a href="/read?id=<?php echo $post['id'] ?>">
<?php echo $post['title'] ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</body>
</html>
|
按照惯例, 这样的文件应该包含所有的应用程序逻辑- index.php - 被称为"控制器". 无论你使用的是何种编程语言或框架,"控制器"这个术语你应该常常听到人们提及. 它指的是你程序中处理用户请求和准备响应的代码部分.
在本例, 控制器从数据库中取得数据然后交由模板文件将数据呈现给用户. 随着控制层的分离,如果你想以其它数据格式 (例. list.json.php 为 JSON 格式)呈现博客目录你需要改变的仅仅是模板文件.
分离应用程序的逻辑
到目前为止我们的应用程序仅仅有一个页面. 但是如果有第二个页面需要相同的数据库连接, 或者甚至需要相同的博客数据, 那么我们该怎么办呢? 一种方式是重构代码将数据获取函数从程序的核心表现中分离形成一个叫做 model.php文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?php
// model.php
function open_database_connection()
{
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);
return $link;
}
function close_database_connection($link)
{
mysql_close($link);
}
function get_all_posts()
{
$link = open_database_connection();
$result = mysql_query('SELECT id, title FROM post', $link);
$posts = array();
while ($row = mysql_fetch_assoc($result)) {
$posts[] = $row;
}
close_database_connection($link);
return $posts;
}
|
之所以像 model.php 这样的文件名被使用是因为程序的逻辑和数据获取通常被放置在"模型"层. 为了更好地组织程序, 代码中主要的业务逻辑应该放置在模型层 (而不是控制层). 而不像本例, 模型层实际上仅仅只是一部分(或没有)与数据的获取相关.
控制器 (index.php)现在变得非常简单:
1 2 3 4 5 6 |
<?php
require_once 'model.php';
$posts = get_all_posts();
require 'templates/list.php';
|
现在控制器的唯一任务是从程序的模型层获取数据并通过调用模板呈现数据. 这是MVC模式的一个简单的例子.
分离布局¶
此时此刻, 应用程序被重构为3个不同的部分(index.php、model.php、list.php)用来在不同页面上提供多数代码的重用.
到目前为止代码中不能够被重用的部分是页面的布局(index.php). 这可以通过创建layout.php文件来实现修复:
1 2 3 4 5 6 7 8 9 10 |
<!-- templates/layout.php -->
<!DOCTYPE html>
<html>
<head>
<title><?php echo $title ?></title>
</head>
<body>
<?php echo $content ?>
</body>
</html>
|
模板 (templates/list.php) 文件现在可以被简化用来 "扩展" 布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php $title = 'List of Posts' ?>
<?php ob_start() ?>
<h1>List of Posts</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<a href="/read?id=<?php echo $post['id'] ?>">
<?php echo $post['title'] ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php $content = ob_get_clean() ?>
<?php include 'layout.php' ?>
|
你现在已经知道如何重用你的布局. 不完美的是为了完成布局的重用你必须使用一些不太美观的PHP函数 (ob_start(), ob_get_clean()) 在你的布局文件中. Symfony2 将使用模板组件来出色地完成布局的重用.你不久将看到.
添加一个博客的"显示"页¶
博客的列表页已经被重构以便于代码能够被更好地组织和重用. 为了证明这一点, 添加一个博客的展示页, 通过id这个参数来查询单独的博文.首先, 我们要在model.php文件中创建一个新函数用来根据id取得博文:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// model.php
function get_post_by_id($id)
{
$link = open_database_connection();
$id = intval($id);
$query = 'SELECT date, title, body FROM post WHERE id = '.$id;
$result = mysql_query($query);
$row = mysql_fetch_assoc($result);
close_database_connection($link);
return $row;
}
|
接下来, 创建一个 show.php的文件 - 这个新页面的控制器如下:
1 2 3 4 5 6 |
<?php
require_once 'model.php';
$post = get_post_by_id($_GET['id']);
require 'templates/show.php';
|
最后, 我们创建一个模板文件 - templates/show.php - 来呈现我们查询到的博文:
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php $title = $post['title'] ?>
<?php ob_start() ?>
<h1><?php echo $post['title'] ?></h1>
<div class="date"><?php echo $post['date'] ?></div>
<div class="body">
<?php echo $post['body'] ?>
</div>
<?php $content = ob_get_clean() ?>
<?php include 'layout.php' ?>
|
创建的第二个页面现在非常的容易并且没有冗余的代码. 不过, 这个页面有一些挥之不去的问题而框架恰可以解决. 例如, 一个丢失的或无效的id查询参数可能会引起的页面崩溃. 如果出现404错误页那还不错, 但是这不是那么容易实现的. 糟糕的是如果你的id没有通过intval()函数处理, 那么你的整个数据库将会面临SQL注入的危险.
另一个问题是每一个单独的控制文件必须包含model.php. 万一每一个控制文件突然需要包含额外的文件或者执行一些其他的全局性任务 (例. 强制安全)? 就目前来说, 每一个控制器文件都将需要添加这样的代码. 如果你忘记了在其中一个文件中包含一些文件, 但愿这与网站安全性无关吧...
前端控制器¶
解决上述讨论的问题的办法是使用前端控制器: 所有的用户请求将通过一个单独的PHP文件统一处理. 使用前端控制器程序的 URIs会有所改变, 但这可以使其变得更加灵活:
1 2 3 4 5 6 7 |
Without a front controller
/index.php => Blog post list page (index.php executed)
/show.php => Blog post show page (show.php executed)
With index.php as the front controller
/index.php => Blog post list page (index.php executed)
/index.php/show => Blog post show page (index.php executed)
|
如果使用Apache的重写规则URI的index.php部分将会被移除. 在这种情况下, 博客的展示页的URI将会是/show.
当使用前端控制器, 一个单独的PHP文件 (index.php 在本例) 接受每一个请求. 对于一个博文显示页 /index.php/show 实际上执行的是 index.php 文件, 它现在负责路由基于完整URI内部请求. 如你所见前端控制器是一个非常有用的工具.
创建前端控制器¶
一个文件处理所有的请求, 这让我们能集中做一些事像安全处理, 配置载入, 和路由. 在这个程序中index.php 必须足够的灵巧来呈现博文列表页或者展示基于URI请求的博文页:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php
// index.php
// load and initialize any global libraries
require_once 'model.php';
require_once 'controllers.php';
// route the request internally
$uri = $_SERVER['REQUEST_URI'];
if ('/index.php' == $uri) {
list_action();
} elseif ('/index.php/show' == $uri && isset($_GET['id'])) {
show_action($_GET['id']);
} else {
header('Status: 404 Not Found');
echo '<html><body><h1>Page Not Found</h1></body></html>';
}
|
为了便于组织, 以前的 index.php 和 show.php这两个控制器现在是PHP的两个函数并且都被移到一个单独的文件controllers.php中 :
1 2 3 4 5 6 7 8 9 10 11 |
function list_action()
{
$posts = get_all_posts();
require 'templates/list.php';
}
function show_action($id)
{
$post = get_post_by_id($id);
require 'templates/show.php';
}
|
作为前端控制器 index.php 已有了一个全新的角色, 其中之一就是加载核心库路由程序以便于这两个控制器其中之一( list_action() 和show_action() 函数) 被调用. 事实上前端控制器已经和Symfony2处理和路由请求的原理有些许相似了.
使用前端控制器的另一个优势是更灵活的URLs. 注意博文展示页的URL可以从/show 变为 /read而只需在一处改编代码. 在此之前, 整个文件需要被改名. 在 Symfony2中, URLs 相当的灵活.
到目前我们的程序已经从一个单一的PHP文件进化到一个可以被组织、代码可被重用的结构. 你应该更高兴些但不要自满. 例如, 路由系统是变化很大的, 不会认识到列表页 (/index.php) 应该可以通过 /(如果Apache重写规则被添加)来获得. 与其说是开发一个博客系统倒不如说是为代码的体系结构工作因为我们在这上花费了大量的时间. 在表单提交、输入数据验证、登陆和安全上将需要大量的时间. 你为什么还要为这些路由问题去重新写解决方案呢?
初识Symfony2¶
在使用Symfony2之前, 你需要下载它. 这个可以通过使用 Composer来完成, 它关心的是下载正确的版本和解决依赖并且提供一个自动加载器. 自动加载器是一个不需要包含PHP类所在文件就能够直接使用PHP类的一个工具.
在你的根目录, 创建一个名为 composer.json 的文件包含如下内容:
1 2 3 4 5 6 7 8 |
{
"require": {
"symfony/symfony": "2.2.*"
},
"autoload": {
"files": ["model.php","controllers.php"]
}
}
|
接下来, 下载Composer 然后运行如下命令, 它将下载Symfony到 vendor/ 目录:
1 |
$ php composer.phar install
|
另外下载你的依赖, Composer 生成一个 vendor/autoload.php 文件, 它自动加载Symfony框架内的所有文件以及在你的composer.json文件自动加载区域提及的文件 .
Symfony的核心是一个应用程序的主要工作是处理每一个用户请求和返回给用户响应. 为了这一点, Symfony2 提供了 Request
和 Response
这两个类. 这些类以面向对象的方式对原始的HTTP请求和响应进行处理. 使用这些类改进我们的博客:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php
// index.php
require_once 'vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$uri = $request->getPathInfo();
if ('/' == $uri) {
$response = list_action();
} elseif ('/show' == $uri && $request->query->has('id')) {
$response = show_action($request->query->get('id'));
} else {
$html = '<html><body><h1>Page Not Found</h1></body></html>';
$response = new Response($html, 404);
}
// echo the headers and send the response
$response->send();
|
现在控制器负责返回一个 Response 对象. 为了使其变得容易, 你可以添加一个新函数 render_template(), 它的作用类似Symfony2的模板引擎:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// controllers.php
use Symfony\Component\HttpFoundation\Response;
function list_action()
{
$posts = get_all_posts();
$html = render_template('templates/list.php', array('posts' => $posts));
return new Response($html);
}
function show_action($id)
{
$post = get_post_by_id($id);
$html = render_template('templates/show.php', array('post' => $post));
return new Response($html);
}
// helper function to render templates
function render_template($path, array $args)
{
extract($args);
ob_start();
require $path;
$html = ob_get_clean();
return $html;
}
|
通过引入Symfony2的一小部分程序变得更加灵活、可靠. Request提供了一个可靠的方式获取关于HTTP的请求信息. 特别地, getPathInfo() 函数返回一个简洁的URL(总是返回/show 而不是 /index.php/show). 所以即便用户使用 /index.php/show访问程序也可以路由到 show_action().
Response 对象在构建HTTP响应时可以通过面向对象的接口添加HTTP头和内容. HTTP响应在这个程序中是简单的, 当程序的规模增大时这种构建HTTP响应的方式的灵活性就变得愈加明显.
Symfony2程序样本¶
博客程序在上我们已经描述的很多了但对于这样简单的程序来说仍然包含许多代码. 在以上的过程中我们已经写出了一个简单的路由系统并使用 ob_start() 和 ob_get_clean() 函数来呈现模板.如果出于一些原因你需要从头构建这个框架,你至少需要Symfony单独的路由和模板系统, 它早已解决了上述出现的问题.
你可以使用 Symfony2.下面是相同的博客程序现由Symfony2构建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
public function listAction()
{
$posts = $this->get('doctrine')->getManager()
->createQuery('SELECT p FROM AcmeBlogBundle:Post p')
->execute();
return $this->render(
'AcmeBlogBundle:Blog:list.html.php',
array('posts' => $posts)
);
}
public function showAction($id)
{
$post = $this->get('doctrine')
->getManager()
->getRepository('AcmeBlogBundle:Post')
->find($id)
;
if (!$post) {
// cause the 404 page not found to be displayed
throw $this->createNotFoundException();
}
return $this->render(
'AcmeBlogBundle:Blog:show.html.php',
array('post' => $post)
);
}
}
|
两个控制器均是轻量级的. 每一个都用到了 Doctrine ORM 库来获取来自数据库的对象, 模板组建呈现一个模板 返回一个响应对象. 列表模板现在将会更简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!-- src/Acme/BlogBundle/Resources/views/Blog/list.html.php -->
<?php $view->extend('::layout.html.php') ?>
<?php $view['slots']->set('title', 'List of Posts') ?>
<h1>List of Posts</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<a href="<?php echo $view['router']->generate(
'blog_show',
array('id' => $post->getId())
) ?>">
<?php echo $post->getTitle() ?>
</a>
</li>
<?php endforeach; ?>
</ul>
|
布局文件变化不大:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!-- app/Resources/views/layout.html.php -->
<!DOCTYPE html>
<html>
<head>
<title><?php echo $view['slots']->output(
'title',
'Default title'
) ?></title>
</head>
<body>
<?php echo $view['slots']->output('_content') ?>
</body>
</html>
|
展示模板将用于练习.
当Symfony2引擎 (被称作内核) 启动, 它需要一个映射以便于让其知道哪个控制器去执行基于请求的信息. 路由映射配置提供了这样的可读格式的信息:
1 2 3 4 5 6 7 8 |
# app/config/routing.yml
blog_list:
path: /blog
defaults: { _controller: AcmeBlogBundle:Blog:list }
blog_show:
path: /blog/show/{id}
defaults: { _controller: AcmeBlogBundle:Blog:show }
|
现在通过Symfony2 来处理众多的任务, 前端控制器一旦创建你不必去动它(如果你使用 Symfony2 发行版,你甚至不需要去创建它!):
1 2 3 4 5 6 7 8 |
// web/app.php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../app/AppKernel.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('prod', false);
$kernel->handle(Request::createFromGlobals())->send();
|
前端控制器的工作是初始化 Symfony2引擎并传递 Request 对象来处理. Symfony2的核心使用路由映射来决定哪一个控制器将要被调用 . 就像以前控制器的方法负责返回最终的响应对象.
Symfony如何处理每个请求请参照上一章请求流程图解.
Symfony2的分发在哪里¶
在接下来的章节里,你将了解到更多关于Symfony每一部分是如何工作的及项目的组织结构. 现在让我们看一下如何将传统方式编写的博客迁移到Symfony2来改进生活:
- 你的程序现在有干净并且一致组织的代码(尽管 Symfony 没要求你这样). 这提高了代码的可重用性对于一个新的开发者将会使其更快的在你的项目中发挥作用;
- 专注掌控你的代码.你不必去开发或者维护一个低级单元像自动加载、路由或者控制器;
- Symfony2 给你获取开源工具如 Doctrine 、模板引擎、安全、表单、验证、翻译这样的组件 (举几个来说);
- 应用程序使用 Routing 组件URLs变得相当灵活;
- Symfony2 HTTP为中心的体系结构让你可以获取到非常好用的工具像HTTP caching 采用Symfony2内部的 HTTP 缓存 或者更好用的工具像 Varnish. 这个将在后面关于缓存的章节提及.
大概使用Symfony2最大的好处是你现在可以获取整个由Symfony2社区开发的高质量开源工具集合! 在 KnpBundles.com你可以找到这样的工具.
更好的模板¶
如果你打算使用Symfony2 那么它的标准模板引擎是 Twig , 它能够使模板更快的书写且易读这意味着在样例程序中将包含更少的代码! 例如列表模板用 Twig书写如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{# src/Acme/BlogBundle/Resources/views/Blog/list.html.twig #}
{% extends "::layout.html.twig" %}
{% block title %}List of Posts{% endblock %}
{% block body %}
<h1>List of Posts</h1>
<ul>
{% for post in posts %}
<li>
<a href="{{ path('blog_show', {'id': post.id}) }}">
{{ post.title }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %}
|
相应的 layout.html.twig 模板也是很容易去写的:
1 2 3 4 5 6 7 8 9 10 |
{# app/Resources/views/layout.html.twig #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default title{% endblock %}</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
|
Twig 在 Symfony2中得到了很好的支持. PHP模板在Symfony2中总会得到支持的, Twig 的优势将会继续讨论. 获取更多信息 见模板章节
译者言:由于以前用过Symfony1.4最近需要写一网站,CMS像WordPress、Drupal等用起来感觉不是很好,所以决定用2来写。在读2的文档的过程中发现好多还没有中文翻译所以在我读的过程中就索性翻译一下,里面会有很多翻译上的错误欢迎大家指正,转载或者什么的注明一下出处哈 本章英文原文出处链接是http://symfony.com/doc/current/book/from_flat_php_to_symfony2.html
so lovely you:)