为什么说Symfony2超过一些框架和自己写原生php?
如果你从来没有使用过一个php框架,也不了解MVC,或者对关于Symfony2好处的传言感到好奇,那本章正是为你准备的。我们并不会灌输为什么Symfony2可以帮助你更快更好地开发代码,而是让你自己做这个判断。
本章将让你用纯PHP写一个简单的应用程序,然后将其重构,使之更有条理。你将会穿越时间,了解为什么网站开发在过去几年中会演变成现在这样。
然后你将体会到为什么Symfony2可以让开发工作不再繁琐,让你真正掌控你的代码。
用原生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); // 去包含HTML的代码 require 'templates/list.php'; |
现在HTML代码都保存在一个独立的文件(templates/list.php)中,文件里嵌套的是模板风格的PHP代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<html> <head> <title>文章列表</title> </head> <body> <h1>文章列表</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文件包含了应用程序中所有的“逻辑”,被称为“控制器”。控制器这个术语,无论你使用的是框架还是语言,你都将会经常听到,简单来说它就是指你处理用户输入和准备响应的代码。
在上面的例子里,我们的控制器从数据库里读取数据,然后调用一个模板文件来呈现这些数据。通过分离控制器的代码,你将可以轻松地修改模板文件,比如以另外的格式(如创建一个对应JSON格式的list.json.php 模板)来输出博客文章。
应用程序(域)逻辑分离
到目前为止,应用程序只有一页。但是,如果第二个页面需要使用相同的数据库连接或者需要相同的博客文章数组呢?重构整个程序,从应用程序中将核心行为和数据访问功能分离出来放入新的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来命名是因为应用程序逻辑和数据访问传统上被称为“Model”层。在一个代码组织良好的应用程序中,大多数代表“业务逻辑”的代码都在“Model”层中(而非控制器中)。而不象本例中模型(Model)只关注数据库访问。
现在的控制器(index.php)变得十分简单:
1 2 3 4 5 6 |
<?php require_once 'model.php'; $posts = get_all_posts(); require 'templates/list.php'; |
现在控制器的唯一任务就是从应用程序的“Model”层中得到数据,然后调用一个模板来呈现这些数据。这是一个最简单的MVC模式。
布局分离
现在应用程序已经明显被重构成三个有着不同优势的部分,并且在不同的页面中有机会重用几乎所有的东西。
在代码中唯一不能被重用的就只有布局了,因此创建一个新的layout.php文件来修复这个问题。
1 2 3 4 5 6 7 8 9 |
<!-- templates/layout.php --> <html> <head> <title><?php echo $title ?></title> </head> <body> <?php echo $content ?> </body> </html> |
现在模板文件(templates/list.php)可以简单地从layout.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' ?> |
现在你已经知道了重用布局(layout)的方法。但不幸地是,要实现这个方法,你不得不在模板中使用一些诸如ob_start()、ob_get_clean()这样丑陋的PHP函数。在Symfony2,可以使用Templating组件来让这一切变得干净和方便。
添加一个博文显示页面
博客的“列表”页已经重构并具有着更好的代码组织性和可重用性。为了证明这一点,添加一个博文“显示”页,用以显示单个博文,该博文通过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页面被渲染将会更好,但这一点并不容易做到。更糟地是,如果你忘记了用 mysql_real_escape_string()函数对ID参数进行清理的话,你将会把整个数据库陷入到被SQL注入攻击的危险境地之中。
另一个问题就是每一个控制器都必须包含model.php文件。如果每个控制器突然需要包含一个附加文件或者执行其它全局任务(如强制安全)呢?目前的情况是这些代码必须添加到每个控制器中文件中。如果你忘了包含某个文件,希望这不会给我们带来不安全的因素…
解救一个前端控制器
解决方案是使用一个前端控制器:单个PHP文件,通过它来处理所有的请求。有了前端控制器,应用程序的URI略有变化,但开始变得灵活多样了。
1 2 3 4 5 6 7 |
没有前端控制器 /index.php => Blog post list page (index.php executed) /show.php => Blog post show page (show.php executed) 使用index.php作前端控制器 /index.php => Blog post list page (index.php executed) /index.php/show => Blog post show page (index.php executed) |
如果使用了Apache的rewrite规则(或相同功能)的话,URI中的index.php部分可以省略。这样的话,Blog显示页的URI结果就会简单地用/show来表示。
当使用前端控制器时,单个PHP文件(在这里是index.php)将显示所有的请求,对于博文显示页来说,/index.php/show实际执行的是index.php,它现在负责基于全URI来进行内部路由请求。正如你看到的那样,前端控制器是个非常强大的工具。
创建前端控制器
你要在应用程序中采取重大举措了。一旦单个文件处理所有的请求,你可以集中进行诸如安全处理、配置加载和路由等事务的处理,在这个例子里,index.php要足够智能,以便根据请求的URL区别并渲染博客列表页和博文显示页。
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处理请求和路由请求的机制了。
前端控制器另一个优点就是灵活的URL。注意,博客显示页的URL只需在一个位置修改一下,就可以从/show变成/read,而在此之前需要将整个文件重命名。在Symfony2中,URL更加灵活。
现在,应用程序已经从单个文件发展为拥有良好组织结构并允许代码重用的程序了。你应该更为高兴,但远未满足。例如,“路由”系统是多变的,不但应该可以通过/index.php来访问,也应该可以通过/来访问(如果添加了Apache重写规则的话)。此外,大量的时间花费在代码的“结构”(如路由、控制器调用和模板等)上,而非花在博客的开发上。你还需要在处理表单提交、输入验证、日志记录和安全上花费更多的时间。为什么你要重复设计这些日常问题的解决方案呢?
接触一下symfony2
symfony2前来救援了。在使用Symfony2之前,你需要先下载它。你可以使用composer,它可以下载正确的版本并自动下载相关依赖进行自动装载。自动加载器是一个工具,它可以在没有明确包含所用类文件时开始使用该类。
在你的根目录,创建一个 composer.json 文件并在文件中加入以下内容:
1 2 3 4 5 6 7 8 |
{ "require": { "symfony/symfony": "2.4.*" }, "autoload": { "files": ["model.php","controllers.php"] } } |
下一步,下载 composer 并运行以下命令,把symfony下载到一个 vendor/ 目录下:
1 |
$ php composer.phar install |
你在一旁下载依赖的时候,composer会生成一个vendor/autoload.php 文件,那么这个文件会自动装载所有的文件到symfony框架和composer.json 自动装载的文件一样。
Symfony2哲学的核心思想是:应用程序的主要任务就是解释请求并返回响应。因此,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, Response::HTTP_NOT_FOUND); } // echo the headers and send the response $response->send(); |
symfony新的2.4版:已经支持了引入HTTP状态代码
现在应用程序通过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()方法返回一个干净的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 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库到数据库中检索对象,并且使用Templating组件去渲染模板并返回响应。模板文件现在相当简单:
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> |
在这里我们将show模板留做练习,实现它相对于实现list模板来说几乎微不足道。
在Symfony2引擎(我们称其为Kernel)启动时,它需要根据一个图判断请求信息需要执行哪个控制器。路由配置文件则提供了这样一张图。
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的引擎(Kernel)并将其传递给一个Request
对象。symfony2的核心再根据路由分析调用哪个controller 。像从前一样,控制器方法负责返回最终的Response
对象。对它来说就真的没有别的了。
至于Symfony2处理请求过程的可视化展示,参见请求流程图 。
symfony从这里正式开始了
在接下来的章节中,我们将学到更多关于Symfony2的各部分是如何工作的,以及推荐的项目组织形式。现在,让我们看看从纯PHP迁移到Symfony2上的博客程序优势:
1、你的应用程序现在是干净的,并且代码组织良好(虽然Symfony2并未强制你做到这一点),这提高了程序的可重用性,并且新项目的开发者能够很快进入角色;
2、你所写的代码100%是为了程序,而非开发或维护诸如autoloading、routing或渲染controllers这样的低级工具;
3、Symfony2让你可以使用开源工具,象Doctrine、Templating、Security、Form、Validation和Translation组件等(仅举几个例子);
4、感谢路由组件让应用程序拥有十分灵活的URL
5、Symfony2以HTTP为中心的架构可以让你使用强大的工具,例如使用Symfony2的HTTP内部缓存或更为强大的Varnish工具来实现HTTP缓存。这将在稍后的缓冲一章中进行说明
最值得高兴的是,通过使用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的更多优势,详情请参见模板章节。
学习更多的内容请来到cookbook
讲的很透彻 希望再接再厉