Symfony vs 纯PHP
为啥symfony比普通的php文件访问要好?
这一章我们写一个简单的php文件项目,然后组织它,你会发现为什么web应用会发展到现在这个样子。最后我们将学习symfony如何重用代码。
使用纯PHP创建一个简单博客程序
这里我们先使用纯php(flat php我擦 ,怎么翻译呢,就是php文件,但是谁不是php文件呢?)创建一个博客程序,先写一个文章列表,这段代码很直接,但是很脏。
<?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);
?>
这个很简单,也很好写,但是随着应用逻辑增多很难维护。这里有一些问题:
1.没有错误检查,如果数据库连接失败怎么办
2.没有组织,如果应用变大,逻辑增多,这个文件将会变得很大,不可维护,从那里验证输入,从那里处理请求,最终写成一团乱码
3.代码不可重用,所有代码都放在一个文件中,没法重用
还有一个问题没有提到,如何从数据库中取数据,symfony使用Doctrine(一种ORM)来获取数据很方便。
展现分离
下面做一些该进将逻辑和html展现分离,代码如下:
<?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';
现在把hmtl内容放在另外一个文件中templates/list.php类似模板
<!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就很容易了
业务逻辑分离
目前这个应用只包含一个页面,但是如果增加第二个页面也使用相同的数据连接,相同的传递数据,所以我们将主要的获取数据的逻辑分离出来放在一个model.php中,如下:
<?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里面。
现在这个控制器index.php可易写成下面这样:
<?php
require_once 'model.php';
$posts = get_all_posts();
require 'templates/list.php';
现在这个控制器主要的功能就是从model中获取数据然后调用模板渲染数据,这是个很简单的模型-视图-控制器的例子
布局分离
到目前为止我们已经涉及到三个不同的文件,几乎复用了所有的代码,只有一个地方我们没有用到就是布局文件,下面创建一个布局文件layout.php
<!-- templates/layout.php -->
<!DOCTYPE html>
<html>
<head>
<title><?php echo $title ?></title>
</head>
<body>
<?php echo $content ?>
</body>
</html>
模板templete/list.php,现在可以继承布局了。
<?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了,但是还需要一些丑陋的php函数例如ob_start(),ob_get_clean()方法,下面再将symfony的处理方式。
添加博客”show”页面
博客列表页面已经重新设计,代码可以复用了。现在添加一个展示博客的页面,向这个页面传递参数ID。首先在model.php中新建一个方法,如下:
// 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,控制器
<?php
require_once 'model.php';
$post = get_post_by_id($_GET['id']);
require 'templates/show.php';
最后新建一个模板templates/show.php,渲染文件
<?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错误到还好,最坏的情况是sql注入。
另外一个问题是每个控制器文件都必须应用model.php,当我们要访问另外一个表,就要在这个控制器中添加另外一个model,这个是比较麻烦的。
前端控制器解决方案
解决这个问题的方法是前端控制器,通过这个控制器文件所有的请求都可以被处理,唯一要做的是修改url,这样更加灵活。
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中的重写功能可以省略“index.php”这样的话访问就更加简单了,例如/show
使用前端控制器只有单独的一个index.php文件可以处理所有的请求,例如访问show页面,/index.php/show,将会最终执行index.php,这是一个很强大的功能。我怎么没看出来。
创建前端控制器
现在我们再往前走一大步,用一个文件处理所有的请求,修改index.php文件如下:
<?php
// index.php
// load and initialize any global libraries
require_once 'model.php';
require_once 'controllers.php';
// route the request internally
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
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)现在变成了方法并且放在一个文件中,controllers.php如下:
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()。事实上这里的前端路由已经和symfony的机制很接近了。
现在,这个这个应用已经从一个简单的php文件重构成一个有组织的结构,最大程度的复用代码,但是还是看到一些代码不协调。为了完成这个blog我们可能需要写很多类似的代码,还要处理用户输入,验证,日志,安全等等。
初始symfony
牛逼哄哄的symfony出场了。(Symfony to the rescue)我想对手册作者说,你能不能不装逼,把要讲的东西讲清楚就好了!在使用之前我们要下载symfony,可以使用composer,这个工具可以下载正确版本的symfony和它所依赖的所有文件,提供一个自动下载器,我擦你妹 ,autoloader是个可以一个工具,用来使用类但是不显示的包含类文件,我擦啊 你他妈还玩花啊,不引用就用,构高级的。
有没有考虑windows用户的感受你!
在根目录下创建一个文件composer.json,如下:
{
"require": {
"symfony/symfony": "2.6.*"
},
"autoload": {
"files": ["model.php","controllers.php"]
}
}
然后下载Composer并安装,命令如下:
$ composer install
下载依赖文件的时候Composer创建了一个文件vendor/autoload.php,这个文件中有所有symfony frameword中所有需要的文件,就是composer.jeson中的。当初说好的symfony处理请求响应呢, 这里干吗呢?
symfony提供Request和Response两个类,他们处理请求和响应,这个不知道重复多少遍了!下面用symfony来写这个blog
<?php
// index.php
require_once 'vendor/autoload.php';
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
$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();
控制器现在返回一个response对象,可以添加一个render_template()方法,和symfony中的模板很像,如下:
// controllers.php
use SymfonyComponentHttpFoundationResponse;
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;
}
好吧, 通过使用symfony我们的程序更加灵活,又来,request提供访问http请求的可靠方法,getPathInfo()方法返回一个干净的url,例如:/show,/index.php/show,这样即使访问index.php/show,应用程序可以聪明的调用show_action()方法。
返回响应的时候response对象可以灵活的返回结果。
symfony简单示例
现在这个blog已经做好,但是里面还有很多代码,但是有没有办法使用更好的代码来实现这个blog,有没有办法来代替ob_start(),和ob_get_clean()呢?可以使用symfony来简化这些,如下:
// src/AppBundle/Controller/BlogController.php
namespace AppBundleController;
use SymfonyBundleFrameworkBundleControllerController;
class BlogController extends Controller
{
public function listAction()
{
$posts = $this->get('doctrine')
->getManager()
->createQuery('SELECT p FROM AcmeBlogBundle:Post p')
->execute();
return $this->render('Blog/list.html.php', array('posts' => $posts));
}
public function showAction($id)
{
$post = $this->get('doctrine')
->getManager()
->getRepository('AppBundle:Post')
->find($id);
if (!$post) {
// cause the 404 page not found to be displayed
throw $this->createNotFoundException();
}
return $this->render('Blog/show.html.php', array('post' => $post));
}
}
这两个控制器依然很重要哦,用Doctrin ORM来从数据库中来获取数据,模板组件渲染模板并返回一个Response对象,模板现在看上去比较简单,
<!-- app/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>
layout布局是差不多的
<!-- 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>
妈蛋 $view从那里来的,没交代清除。
当symfony引擎启动的时候,妈蛋啊 , 说的这么高级 ,不就是访问网站的时候么。它需要通过请求信息和一个映射表知道执行那一个控制器。路由配置提供这个信息,如下:
# app/config/routing.yml
blog_list:
path: /blog
defaults: { _controller: AppBundle:Blog:list }
blog_show:
path: /blog/show/{id}
defaults: { _controller: AppBundle:Blog:show }
然后symfony处理一些简单的任务,前端控制器是很简单的,创建之后你就不需要再管它,说的轻巧,吃根灯草。还有如果使用symfony distribution根本不需要创建它,好吧,被你的装逼精神深深的折服了!
// web/app.php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../app/AppKernel.php';
use SymfonyComponentHttpFoundationRequest;
$kernel = new AppKernel('prod', false);
$kernel->handle(Request::createFromGlobals())->send();
前端控制器的唯一任务就是初始化symfony引擎Kernel,然后发送一个请求对象,symfony核心然后用这个路由映射找到执行那个控制器,最后控制器方法返回最终的响应对象。(编者一再强调很简单)
symfony的优点
开启装逼模式
什么是symfony framework,symfony framework是一个php类库,包含两个主要的任务
1.提供可选的第三方的类库组件(symfony components)
2.提供直观的配置和一个可以把很多php代码片段组合起来的胶水类库
symfony的终极目标是整合很多互不影响的组件来为开发者提供一致的使用体验。symfony本身也是一个束可以被配置和替换。symfony提供一套快速开发的工具而不需要在项目中添加额外的组件。普通用户可以快速开发,高手可以任意驰骋。