为什么使用Symfony2比打开并编写纯PHP文件要好

如果你没有用过PHP框架、不熟悉MVC策略或者担心是Symfony2的炒作的话,那么本章适合你。不是告诉你使用Symfony2可以比使用纯PHP让你开发得更快更好,而是让你自己亲自去体会。

本章将让你用纯PHP写一个简单的应用程序,然后将其重构,使之更有条理。你将会穿越时间,了解为什么网站开发在过去几年中会演变成现在这样。

最后你将看到Symfony2是如何将你从烦杂的工作中解救出来,并让你收回对代码的控制权。

用纯PHP编写的简单博客

在本章,你将使用纯PHP构建一个象征性的博客应用程序。首先创建一个页面,用来显示已保存在数据库中的博文,使用纯PHP建构虽然快速,但却是“脏”的。

  1. <?php 
  2. // index.php 
  3.  
  4. $link = mysql_connect('localhost', 'myuser', 'mypassword'); 
  5. mysql_select_db('blog_db', $link); 
  6.  
  7. $result = mysql_query('SELECT id, title FROM post', $link); 
  8. ?> 
  9.  
  10. <html> 
  11.     <head> 
  12.         <title>List of Posts</title> 
  13.     </head> 
  14.     <body> 
  15.         <h1>List of Posts</h1> 
  16.         <ul> 
  17.             <?php while ($row = mysql_fetch_assoc($result)): ?> 
  18.             <li> 
  19.                 <a href="/show.php?id=<?php echo $row['id'] ?>"> 
  20.                     <?php echo $row['title'] ?> 
  21.                 </a> 
  22.             </li> 
  23.             <?php endwhile; ?> 
  24.         </ul> 
  25.     </body> 
  26. </html> 
  27.  
  28. <?php mysql_close($link); ?> 

这个文件很快就写好了,也可以快速执行,但如你所见,当应用程序增长时将变得不可维护,它需要解决以下几个问题:

  1. 没有错误检查:如果数据库连接失败如何处理?
  2. 条理性不足:如果应用程序增长将变得越来越难以维护。处理表单提交的代码放在哪里?如何验证数据?发送电子邮件的代码放在哪里?
  3. 代码难以重用:既然所有的代码都在一个文件中,那么将没有办法重用为其他“页面”所编写的应用程序其他部分代码。

在这里没有提及的另一个问题是数据库是与MySQL相连的。虽然在这里没有提及,Symfony2是完全与Doctrine(一个专门用于数据库抽象和映射的库)集成。

让我们着手来解决这些问题。

从展示中隔离

可以立即从准备进行HTML展示的代码中分离出代表应用程序的“逻辑”:

  1. <?php 
  2. // index.php 
  3.  
  4. $link = mysql_connect('localhost''myuser''mypassword'); 
  5. mysql_select_db('blog_db'$link); 
  6.  
  7. $result = mysql_query('SELECT id, title FROM post'$link); 
  8.  
  9. $posts = array(); 
  10. while ($row = mysql_fetch_assoc($result)) { 
  11.     $posts[] = $row
  12.  
  13. mysql_close($link); 
  14.  
  15. // 包含HTML展示的代码
  16. require 'templates/list.php';

现在HTML代码都保存在一个独立的文件(templates/list.php)中,该文件主要是由在其中使用了类模板PHP语法的HTML构成。

  1. <html> 
  2.     <head> 
  3.         <title>List of Posts</title> 
  4.     </head> 
  5.     <body> 
  6.         <h1>List of Posts</h1> 
  7.         <ul> 
  8.             <?php foreach ($posts as $post): ?> 
  9.             <li> 
  10.                 <a href="/read?id=<?php echo $post['id'] ?>"> 
  11.                     <?php echo $post['title'] ?> 
  12.                 </a> 
  13.             </li> 
  14.             <?php endforeach; ?> 
  15.         </ul> 
  16.     </body> 
  17. </html>

根据惯例,例子中的index.php文件包含了应用程序中所有的“逻辑”,被称为“控制器”。控制器这个术语,无论你使用的是框架还是语言,你都将会经常听到,简单来说它就是指你处理用户输入和准备响应的代码。

在本例中,控制器从数据库中准备数据,然后包含了一个准备显示该数据的模板。隔离出控制器之后,如果你需要用其它格式(如使用JSON格式的list.json.php)来渲染博文的话,你可以很方便地调整模板文件。

隔离应用程序(域)逻辑

到目前为止,应用程序只有一页,如果要是第二页也需要使用相同的数据库连接甚至是博文数组posts呢?重构整个程序,从应用程序中将核心行为和数据访问功能隔离出来,放入新的model.php文件中。

  1. <?php 
  2. // model.php 
  3.  
  4. function open_database_connection() 
  5.     $link = mysql_connect('localhost''myuser''mypassword'); 
  6.     mysql_select_db('blog_db'$link); 
  7.  
  8.     return $link
  9.  
  10. function close_database_connection($link
  11.     mysql_close($link); 
  12.  
  13. function get_all_posts() 
  14.     $link = open_database_connection(); 
  15.  
  16.     $result = mysql_query('SELECT id, title FROM post'$link); 
  17.     $posts = array(); 
  18.     while ($row = mysql_fetch_assoc($result)) { 
  19.         $posts[] = $row
  20.     } 
  21.     close_database_connection($link); 
  22.  
  23.     return $posts
  24. }

使用model.php来命名是因为应用程序逻辑和数据访问传统上被称为“Model”层。在一个代码组织良好的应用程序中,大多数代表“业务逻辑”的代码都在“Model”层中(而非控制器中)。而不象本例中模型(Model)只关注数据库访问。

现在的控制器变得十分简单:

  1. <?php 
  2. require_once 'model.php'
  3.  
  4. $posts = get_all_posts(); 
  5.  
  6. require 'templates/list.php'
  7. ?>

现在控制器的唯一任务就是从应用程序的“Model”层中得到数据,然后调用一个模板来呈现这些数据。这是一个最简单的MVC模式。

隔离布局

现在应用程序已经明显被重构成三个有着不同优势的部分,并且在不同的页面中有机会重用几乎所有的东西。

在代码中唯一不能被重用的就只有布局了,因此创建一个新的layout.php文件来修复这个问题。

  1. <!-- templates/layout.php --> 
  2. <html> 
  3.     <head> 
  4.         <title><?php echo $title ?></title> 
  5.     </head> 
  6.     <body> 
  7.         <?php echo $content ?> 
  8.     </body> 
  9. </html> 

现在模板文件(templates/list.php)可以简单地从layout.php文件中“扩展”出来。

  1. <?php $title = 'List of Posts' ?> 
  2.  
  3. <?php ob_start() ?> 
  4.     <h1>List of Posts</h1> 
  5.     <ul> 
  6.         <?php foreach ($posts as $post): ?> 
  7.         <li> 
  8.             <a href="/read?id=<?php echo $post['id'] ?>"> 
  9.                 <?php echo $post['title'] ?> 
  10.             </a> 
  11.         </li> 
  12.         <?php endforeach; ?> 
  13.     </ul> 
  14. <?php $content = ob_get_clean() ?> 
  15.  
  16. <?php include 'layout.php' ?>

现在你已经知道了重用布局(layout)的方法。但不幸地是,要实现这个方法,你不得不在模板中使用一些诸如ob_start()、ob_get_clean()这样丑陋的PHP函数。在Symfony2,可以使用Templating组件来让这一切变得干净和方便。

添加博文显示页

博客的“列表”页被重构得已经具有着更好的代码组织和可重用性。为了证明这一点,添加一个博文“显示”页,用以显示单个博文,该页通过ID查询参数标识。

首先在model.php文件中新增一个函数,以便基于指定ID检索单个博文。

  1. // model.php 
  2. function get_post_by_id($id
  3.     $link = open_database_connection(); 
  4.  
  5.     $id = mysql_real_escape_string($id); 
  6.     $query = 'SELECT date, title, body FROM post WHERE id = '.$id
  7.     $result = mysql_query($query); 
  8.     $row = mysql_fetch_assoc($result); 
  9.  
  10.     close_database_connection($link); 
  11.  
  12.     return $row

接下来创建一个新的show.php文件,作为新页面的控制器。

  1. <?php 
  2. require_once 'model.php'
  3.  
  4. $post = get_post_by_id($_GET['id']); 
  5.  
  6. require 'templates/show.php'
  7. ?> 

最后创建新的模板文件(templates/show.php),用以呈现单个博文。

  1. <?php $title = $post['title'] ?> 
  2.  
  3. <?php ob_start() ?> 
  4.     <h1><?php echo $post['title'] ?></h1> 
  5.  
  6.     <div class="date"><?php echo $post['date'] ?></div> 
  7.     <div class="body"> 
  8.         <?php echo $post['body'] ?> 
  9.     </div> 
  10. <?php $content = ob_get_clean() ?> 
  11.  
  12. <?php include 'layout.php' ?>

创建第二页非常容易,也没有复制代码。然而这一页存在着让人更加挥之不去的问题(框架可以很好地为你解决),例如缺省或无效的ID参数会引起页面的崩溃。如果能够引起404页面被渲染将会更好,但这一点并不容易做到。更糟地是,如果你忘记了用mysql_real_escape_string()函数对ID参数进行清理的话,你将会把整个数据库陷入到被SQL注入攻击的危险境地之中。

另一个问题就是每一个控制器都必须包含model.php文件。如果每个控制器必须突然需要包含一个附加文件或者执行其它全局任务(如强制安全)呢?目前的情况是这些代码必须添加到每个控制器中文件中。如果你忘了包含某个文件,希望它不涉及安全...

前端控制器的救援

解决方案就是使用“前端控制器”:单个PHP文件,通过它来处理所有的请求。有了前端控制器,应用程序的URI略有变化,但开始变得灵活多样了。

  1. 没有前端控制器 
  2. /index.php          => Blog 列表页 (index.php被执行) 
  3. /show.php           => Blog 显示页 (show.php被执行) 
  4.  
  5. 使用index.php作前端控制器 
  6. /index.php          => Blog 列表页 (index.php被执行) 
  7. /index.php/show     => Blog 显示页 (index.php被执行) 

如果使用了Apache的rewrite规则(或相同功能)的话,URI中的index.php部分可以省略。这样的话,Blog显示页的URI结果就会简单地用/show来表示。

当使用前端控制器时,单个PHP文件(在这里是index.php)将显示所有的请求,对于博文显示页来说,/index.php/show实际执行的是index.php,它现在负责基于全URI来进行内部路由请求。正如你看到的那样,前端控制器是个非常强大的工具。

创建前端控制器

你要在应用程序中采取重大举措了。一旦单个文件处理所有的请求,你可以集中进行诸如安全处理、配置加载和路由等事务的处理,在这个例子里,index.php要足够智能,以便根据请求的URL区别并渲染博客列表页和博文显示页。

  1. <?php 
  2. // index.php 
  3.  
  4. // 引导和初始化所有全局库
  5. require_once 'model.php'
  6. require_once 'controllers.php'
  7.  
  8. // 路由内部请求
  9. $uri = $_SERVER['REQUEST_URI']; 
  10. if ($uri == '/index.php') { 
  11.     list_action(); 
  12. elseif ($uri == '/index.php/show' && isset($_GET['id'])) { 
  13.     show_action($_GET['id']); 
  14. else { 
  15.     header('Status: 404 Not Found'); 
  16.     echo '<html><body><h1>Page Not Found</h1></body></html>'

为了更好地组织代码,将两个控制器(前身是index.php和show.php)写成两个PHP函数,并将其放入新的controllers.php文件中:

  1. function list_action() 
  2.     $posts = get_all_posts(); 
  3.     require 'templates/list.php'
  4.  
  5. function show_action($id
  6.     $post = get_post_by_id($id); 
  7.     require 'templates/show.php'

作为前端控制器,index.php完全进入了一个新的角色:加载核心库并且路由整个应用程序,以便使两个控制器之一(list_action()或show_action())被调用。实际上,前端控制器看来去也变得很象Symfony2处理请求和路由请求的机制了。

前端控制器另一个优点就是灵活的URL。注意,博客显示页的URL只需在一个位置修改一下,就可以从/show变成/read,而在此之前需要将整个文件重命名。在Symfony2中,URL更加灵活。

现在,应用程序已经从单个文件发展为拥有良好组织结构并允许代码重用的程序了。你应该更为高兴,但远未满足。例如,“路由”系统是多变的,不但应该可以通过/index.php来访问,也应该可以通过/来访问(如果添加了Apache重写规则的话)。此外,大量的时间花费在代码的“结构”(如路由、控制器调用和模板等)上,而非花在博客的开发上。你还需要在处理表单提交、输入验证、日志记录和安全上花费更多的时间。为什么你要重复设计这些日常问题的解决方案呢?

接触Symfony2

来自Symfony2的救援。在使用Symfony2之前,你需要让PHP找到Symfony2的类,它可以通过Symfony2提供的自动加载器来完成。自动加载器是一个工具,它可以在没有明确包含所用类文件时开始使用该类。

首先,下载Symfony2,并将它解压在vendor/symfony目录中,接下来创建一个app/bootstrap.php文件,用它去require应用程序中的两个文件,然后配置自动加载器。

  1. <?php 
  2. // bootstrap.php 
  3. require_once 'model.php'
  4. require_once 'controllers.php'
  5. require_once 'vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php'
  6.  
  7. $loader = new Symfony\Component\ClassLoader\UniversalClassLoader(); 
  8. $loader->registerNamespaces(array
  9.     'Symfony' => __DIR__.'/vendor/symfony/src'
  10. )); 
  11.  
  12. $loader->register(); 
  13. ?> 

它告诉自动加载器Symfony2的类在什么地方,这样你就可以使用Symfony2的类,而无须使用required包含Symfony2类的文件了。

Symfony2哲学的核心思想是:应用程序的主要任务就是解释请求并返回响应。因此,Symfony2提供了Request类和Response类,这两个类是原始HTTP中处理请求和返回响应的面向对象的表述。使用它们来提升博客:

  1. <?php 
  2. // index.php 
  3. require_once 'app/bootstrap.php'
  4.  
  5. use Symfony\Component\HttpFoundation\Request; 
  6. use Symfony\Component\HttpFoundation\Response; 
  7.  
  8. $request = Request::createFromGlobals(); 
  9.  
  10. $uri = $request->getPathInfo(); 
  11. if ($uri == '/') { 
  12.     $response = list_action(); 
  13. elseif ($uri == '/show' && $request->query->has('id')) { 
  14.     $response = show_action($request->query->get('id')); 
  15. else { 
  16.     $html = '<html><body><h1>Page Not Found</h1></body></html>'
  17.     $response = new Response($html, 404); 
  18.  
  19. // 显示头并发送响应
  20. $response->send(); 
  21. ?> 

现在应用程序通过Response对象来返回响应。为了更加方便,你可以使用render_template()函数,该函数的行为很有点象Symfony2的模板引擎。

  1. // controllers.php 
  2. use Symfony\Component\HttpFoundation\Response; 
  3.  
  4. function list_action() 
  5.     $posts = get_all_posts(); 
  6.     $html = render_template('templates/list.php'); 
  7.  
  8.     return new Response($html); 
  9.  
  10. function show_action($id
  11.     $post = get_post_by_id($id); 
  12.     $html = render_template('templates/show.php'array
  13.         'post' => $post
  14.     )); 
  15.  
  16.     return new Response($html); 
  17.  
  18. // 用于渲染模板的辅助函数
  19. function render_template($patharray $args
  20.     extract($args); 
  21.     ob_start(); 
  22.     require $path
  23.     $html = ob_get_clean(); 
  24.  
  25.     return $html

通过使用Symfony2很小的一部分,应用程序变得更加灵活可靠。Request类提供了一个可靠的方法去访问HTTP的请求信息。具体来说,getPathInfo()方法返回一个干净的URI(它总是返回/show,而永远不会返回/index.php/show),因此即使用户通过/index.php/show来进入,应用程序也会足够智能地将请求路由到show_action()。

在构造HTTP响应时,Response对象提供了足够的灵活性,它允许响应头和内容通过一个面向对象的接口添加到Response对象中,虽然应用程序中的响应十分简单,但当你应用程序增长时这种灵活性将带来好处。

使用Symfony2的示例程序

博客程序一路编来,对于如此简单的应用程序,它也包含了大量的代码。我们构造了一个路由系统,并且还使用ob_start()和ob_get_clean()方法来呈现模板。如果出于某种原因,你还需要继续“从零开始”搭建“框架”,那么你至少可以使用Symfony2中的独立Routine组件和Templating组件,因为它们已经解决了这些问题。

为了不用重新发明轮子,你可以让Symfony2来帮你实现,下面是相同的示例程序,只不过它们在Symfony2上实现。

  1. <?php 
  2. // src/Acme/BlogBundle/Controller/BlogController.php 
  3.  
  4. namespace Acme\BlogBundle\Controller; 
  5. use Symfony\Bundle\FrameworkBundle\Controller\Controller; 
  6.  
  7. class BlogController extends Controller 
  8.     public function listAction() 
  9.     { 
  10.         $blogs = $this->get('doctrine')->getEntityManager() 
  11.             ->createQuery('SELECT b FROM AcmeBlogBundle:Blog b'
  12.             ->execute(); 
  13.  
  14.         return $this->render('AcmeBlogBundle:Blog:list.html.php'array('blogs' => $blogs)); 
  15.     } 
  16.  
  17.     public function showAction($id
  18.     { 
  19.         $blog = $this->get('doctrine'
  20.             ->getEntityManager() 
  21.             ->getRepository('AcmeBlogBundle:Blog'
  22.             ->find($id); 
  23.  
  24.         if (!$blog) { 
  25.             // 导致显示404未发现页
  26.             throw $this->createNotFoundException(); 
  27.         } 
  28.  
  29.         return $this->render('AcmeBlogBundle:Blog:show.html.php'array('blog' => $blog)); 
  30.     } 

这两个控制器依然是轻量级的,它们都使用Doctrine的ORM库到数据库中检索对象,并且使用Templating组件去渲染模板并返回响应。模板文件现在相当简单:

  1. <!-- src/Acme/BlogBundle/Resources/views/Blog/list.html.php --> 
  2. <?php $view->extend('::layout.html.php') ?> 
  3.  
  4. <?php $view['slots']->set('title', 'List of Posts') ?> 
  5.  
  6. <h1>List of Posts</h1> 
  7. <ul> 
  8.     <?php foreach ($posts as $post): ?> 
  9.     <li> 
  10.         <a href="<?php echo $view['router']->generate('blog_show', array('id' => $post->getId())) ?>"> 
  11.             <?php echo $post->getTitle() ?> 
  12.         </a> 
  13.     </li> 
  14.     <?php endforeach; ?> 
  15. </ul> 

布局文件几乎一样:

  1. <!-- app/Resources/views/layout.html.php --> 
  2. <html> 
  3.     <head> 
  4.         <title><?php echo $view['slots']->output('title', 'Default title') ?></title> 
  5.     </head> 
  6.     <body> 
  7.         <?php echo $view['slots']->output('_content') ?> 
  8.     </body> 
  9. </html> 

在这里我们将show模板留做练习,实现它相对于实现list模板来说几乎微不足道。

在Symfony2引擎(我们称其为内核)启动时,它需要一张图来知道根据请求信息需要执行哪个控制器。路由配置文件以可读的方式提供了这样一张图。

  1. # app/config/routing.yml 
  2. blog_list: 
  3.     pattern:  /blog 
  4.     defaults: { _controller: AcmeBlogBundle:Blog:list } 
  5.  
  6. blog_show: 
  7.     pattern:  /blog/show/{id} 
  8.     defaults: { _controller: AcmeBlogBundle:Blog:show }

现在Symfony2处理所有的日常任务,前端控制器却非常简单,而且内容又如此之少,它一旦被创建之后就无须再去接触它(如果你使用Symfony2的发行版,你都无须去创建它)。

  1. <?php 
  2. // web/app.php 
  3. require_once __DIR__.'/../app/bootstrap.php'
  4. require_once __DIR__.'/../app/AppKernel.php'
  5.  
  6. use Symfony\Component\HttpFoundation\Request; 
  7.  
  8. $kernel = new AppKernel('prod', false); 
  9. $kernel->handle(Request::createFromGlobals())->send(); 
  10. ?> 

前端控制器唯一的工作就是初始化引擎(或称内核),并将其放入Request对象去处理。Symfony2内核然后使用路由图以确认调用哪个控制器。象以前一样,控制器负责返回最终的响应对象。对它来说就真的没有别的了。

至于Symfony2处理请求过程的可视化展示,参见请求流程图

是时候推出Symfony2了

在接下来的章节中,我们将学到更多关于Symfony2的各部分是如何工作的,以及推荐的项目组织形式。现在,让我们看看从纯PHP迁移到Symfony2上的博客程序优势:

1、你的应用程序现在是干净的,并且代码组织良好(虽然Symfony2并未强制你做到这一点),这提高了程序的可重用性,并且新项目的开发者能够很快进入角色;
2、你所写的代码100%是为了程序,而非开发或维护诸如自动加载、路由或渲染控制器这样的低级工具;
3、Symfony2让你可以使用开源工具,象Doctrine、Templating、Security、Form、Validation和Translation组件等(仅举几个例子);
4、感谢路由组件让应用程序拥有十分灵活的URL
5、Symfony2以HTTP为中心的架构可以让你使用强大的工具,例如使用Symfony2的HTTP内部缓存或更为强大的Varnish工具来实现HTTP缓存。这将在稍后的缓冲一章中进行说明

最值得高兴的是,通过使用Symfony2,你现在可以获得一整套Symfony2社区开发的高品质开源工具集,更多详情请查阅Symfony2Bundles.org

更好的模板

Symfony2标配的模板引擎叫Twig,如果你选择使用它,它将使你的模板写得更快,也更易理解。这意味着示例程序可以使用更少的代码。例如,列表模板使用Twig书写如下:

  1. {# src/Acme/BlogBundle/Resources/views/Blog/list.html.twig #} 
  2.  
  3. {% extends "::layout.html.twig" %} 
  4. {% block title %}List of Posts{% endblock %} 
  5.  
  6. {% block body %} 
  7.     <h1>List of Posts</h1> 
  8.     <ul> 
  9.         {% for post in posts %} 
  10.         <li> 
  11.             <a href="{{ path('blog_show', { 'id': post.id }) }}"> 
  12.                 {{ post.title }} 
  13.             </a> 
  14.         </li> 
  15.         {% endfor %} 
  16.     </ul> 
  17. {% endblock %} 

同样的,layout.html.twig更容易写:

  1. {# app/Resources/views/layout.html.twig #} 
  2.  
  3. <html> 
  4.     <head> 
  5.         <title>{% block title %}Default title{% endblock %}</title> 
  6.     </head> 
  7.     <body> 
  8.         {% block body %}{% endblock %} 
  9.     </body> 
  10. </html> 

Twig在Symfony2中被很好地支持。虽然PHP永远被Symfony2支持,但我们将继续讨论Twig的更多优势,详情请参见模板章节。