Symfony 对比原生 PHP
为什么 Symfony 比仅仅打开文件编写原生 PHP 更好?
如果您从未使用过 PHP 框架,不熟悉 模型-视图-控制器 (MVC) 理念,或者只是想知道关于 Symfony 的所有炒作是什么,那么本文正适合您。您将亲自看到,Symfony 让您能够比使用原生 PHP 更快、更好地开发软件,而不是告诉您。
在本文中,您将使用原生 PHP 编写一个基本应用程序,然后重构它以使其更有条理。您将穿越时空,了解过去几年 Web 开发演变为今天的样子的决策过程。
最后,您将看到 Symfony 如何将您从日常任务中解救出来,让您重新掌控您的代码。
原生 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
<?php
// index.php
$connection = new PDO("mysql:host=localhost;dbname=blog_db", 'myuser', 'mypassword');
$result = $connection->query('SELECT id, title FROM post');
?>
<!DOCTYPE html>
<html>
<head>
<title>List of Posts</title>
</head>
<body>
<h1>List of Posts</h1>
<ul>
<?php while ($row = $result->fetch(PDO::FETCH_ASSOC)): ?>
<li>
<a href="/show.php?id=<?= $row['id'] ?>">
<?= $row['title'] ?>
</a>
</li>
<?php endwhile ?>
</ul>
</body>
</html>
<?php
$connection = null;
?>
这种方式编写快速,部署和运行也很快,但是随着应用程序的增长,维护起来将变得不可能。有几个问题需要解决
- 没有错误检查:如果数据库连接失败怎么办?
- 组织性差:如果应用程序增长,这个单一文件将变得越来越难以维护。应该在哪里放置处理表单提交的代码?如何验证数据?发送电子邮件的代码应该放在哪里?
- 难以重用代码:由于一切都在一个文件中,因此无法为博客的其他“页面”重用应用程序的任何部分。
注意
此处未提及的另一个问题是数据库与 MySQL 绑定。虽然本文未涵盖,但 Symfony 完全集成了 Doctrine,这是一个致力于数据库抽象和映射的库。
隔离表示层
代码可以立即从将应用程序“逻辑”与准备 HTML “表示”的代码分离中获益
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// index.php
$connection = new PDO("mysql:host=localhost;dbname=blog_db", 'myuser', 'mypassword');
$result = $connection->query('SELECT id, title FROM post');
$posts = [];
while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
$posts[] = $row;
}
$connection = null;
// 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 19
<!-- 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="/show.php?id=<?= $post['id'] ?>">
<?= $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
// model.php
function open_database_connection()
{
$connection = new PDO("mysql:host=localhost;dbname=blog_db", 'myuser', 'mypassword');
return $connection;
}
function close_database_connection(&$connection)
{
$connection = null;
}
function get_all_posts()
{
$connection = open_database_connection();
$result = $connection->query('SELECT id, title FROM post');
$posts = [];
while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
$posts[] = $row;
}
close_database_connection($connection);
return $posts;
}
提示
使用文件名 model.php
是因为应用程序的逻辑和数据访问传统上被称为“模型”层。在一个组织良好的应用程序中,代表您的“业务逻辑”的大部分代码应该位于模型中(而不是位于控制器中)。与本示例不同,只有一部分(或全部)模型实际上与访问数据库有关。
控制器 (index.php
) 现在只有几行代码
1 2 3 4 5 6
// index.php
require_once 'model.php';
$posts = get_all_posts();
require 'templates/list.php';
现在,控制器的唯一任务是从应用程序的模型层(模型)获取数据,并调用模板来呈现该数据。这是模型-视图-控制器模式的一个非常简洁的示例。
隔离布局
此时,应用程序已被重构为三个不同的部分,提供了各种优势,并有机会在不同页面上重用几乎所有内容。
唯一不能重用的代码部分是页面布局。通过创建一个新的 templates/layout.php
文件来修复它
1 2 3 4 5 6 7 8 9 10
<!-- templates/layout.php -->
<!DOCTYPE html>
<html>
<head>
<title><?= $title ?></title>
</head>
<body>
<?= $content ?>
</body>
</html>
模板 templates/list.php
现在可以简化为“扩展” templates/layout.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<!-- templates/list.php -->
<?php $title = 'List of Posts' ?>
<?php ob_start() ?>
<h1>List of Posts</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<a href="/show.php?id=<?= $post['id'] ?>">
<?= $post['title'] ?>
</a>
</li>
<?php endforeach ?>
</ul>
<?php $content = ob_get_clean() ?>
<?php include 'layout.php' ?>
您现在拥有一个允许您重用布局的设置。不幸的是,要实现这一点,您不得不使用一些丑陋的 PHP 函数(模板中的 ob_start()
, ob_get_clean()
)。Symfony 使用 Twig 解决了这个问题。您很快就会看到它的实际效果。
添加博客“显示”页面
博客“列表”页面现在已被重构,代码组织得更好且可重用。为了证明这一点,添加一个博客“显示”页面,该页面显示由 id
查询参数标识的单个博客帖子。
首先,在 model.php
文件中创建一个新函数,该函数根据给定的 id 检索单个博客结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// model.php
function get_post_by_id($id)
{
$connection = open_database_connection();
$query = 'SELECT created_at, title, body FROM post WHERE id=:id';
$statement = $connection->prepare($query);
$statement->bindValue(':id', $id, PDO::PARAM_INT);
$statement->execute();
$row = $statement->fetch(PDO::FETCH_ASSOC);
close_database_connection($connection);
return $row;
}
接下来,创建一个名为 show.php
的新文件 - 此新页面的控制器
1 2 3 4 5 6
// show.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 13
<!-- templates/show.php -->
<?php $title = $post['title'] ?>
<?php ob_start() ?>
<h1><?= $post['title'] ?></h1>
<div class="date"><?= $post['created_at'] ?></div>
<div class="body">
<?= $post['body'] ?>
</div>
<?php $content = ob_get_clean() ?>
<?php include 'layout.php' ?>
现在创建第二个页面只需要很少的工作,并且没有代码重复。尽管如此,此页面引入了框架可以为您解决的更多挥之不去的问题。例如,缺少或无效的 id
查询参数将导致页面崩溃。如果这种情况导致呈现 404 页面会更好,但这实际上还无法完成。
另一个主要问题是每个单独的控制器文件都必须包含 model.php
文件。如果每个控制器文件突然需要包含一个附加文件或执行一些其他全局任务(例如,强制安全性)怎么办?按照目前的情况,该代码需要添加到每个控制器文件中。如果您忘记在一个文件中包含某些内容,希望它与安全性无关...
“前端控制器”来救援
解决方案是使用前端控制器:一个处理所有请求的单个 PHP 文件。使用前端控制器,应用程序的 URI 略有变化,但开始变得更加灵活
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)
提示
通过在您的 Web 服务器配置中使用重写规则,将不需要 index.php
,并且您将拥有美观、简洁的 URL(例如 /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
// 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('HTTP/1.1 404 Not Found');
echo '<html><body><h1>Page Not Found</h1></body></html>';
}
为了组织,两个控制器(以前是 /index.php
和 /index.php/show
)现在都是 PHP 函数,并且每个函数都已移动到名为 controllers.php
的单独文件中
1 2 3 4 5 6 7 8 9 10 11 12
// 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 处理和路由请求的方式。
但请注意不要混淆术语前端控制器和控制器。您的应用通常只有一个前端控制器,它启动您的代码。您将有许多控制器函数:每个页面一个。
提示
前端控制器的另一个优点是灵活的 URL。请注意,通过仅在一个位置更改代码,可以将博客文章显示页面的 URL 从 /show
更改为 /read
。之前,需要重命名整个文件。在 Symfony 中,URL 更加灵活。
到目前为止,应用程序已从单个 PHP 文件演变为组织良好并允许代码重用的结构。您应该更快乐,但远未感到满意。例如,路由系统不稳定,并且无法识别列表页面 - /index.php
- 也应该可以通过 /
访问(如果添加了 Apache 重写规则)。此外,与其开发博客,不如将大量时间用于代码的“架构”(例如,路由、调用控制器、模板等)。将需要花费更多时间来处理表单提交、输入验证、日志记录和安全性。为什么您必须重新发明解决所有这些例行问题的方案?
添加一点 Symfony
Symfony 来救援了。在实际使用 Symfony 之前,您需要下载它。这可以通过使用 Composer 完成,Composer 负责下载正确的版本及其所有依赖项,并提供自动加载器。自动加载器是一种工具,可以开始使用 PHP 类,而无需显式包含包含该类的文件。
在您的根目录中,创建一个具有以下内容的 composer.json
文件
1 2 3 4 5 6 7 8
{
"require": {
"symfony/http-foundation": "^4.0"
},
"autoload": {
"files": ["model.php","controllers.php"]
}
}
接下来,下载 Composer,然后运行以下命令,该命令将 Symfony 下载到 vendor/
目录中
1
$ composer install
除了下载您的依赖项之外,Composer 还会生成一个 vendor/autoload.php
文件,该文件负责自动加载 Symfony 框架中的所有文件以及 composer.json
的自动加载部分中提到的文件。
Symfony 哲学的核心思想是,应用程序的主要工作是解释每个请求并返回响应。为此,Symfony 同时提供了 Request 和 Response 类。这些类是正在处理的原始 HTTP 请求和正在返回的 HTTP 响应的面向对象表示。使用它们来改进博客
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 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();
控制器现在负责返回 Response
对象。为了简化操作,您可以添加一个新的 render_template()
函数,顺便说一句,它的行为很像 Symfony 模板引擎
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', ['posts' => $posts]);
return new Response($html);
}
function show_action($id)
{
$post = get_post_by_id($id);
$html = render_template('templates/show.php', ['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() 方法返回一个清理后的 URI(始终返回 /show
,而不是 /index.php/show
)。因此,即使用户访问 /index.php/show
,应用程序也足够智能,可以通过 show_action()
路由请求。
Response
对象在构建 HTTP 响应时提供了灵活性,允许通过面向对象的接口添加 HTTP 标头和内容。虽然此应用程序中的响应很简单,但随着应用程序的增长,这种灵活性将带来回报。
Symfony 中的示例应用
博客已经走了很长的路,但对于这样一个基本的应用程序来说,它仍然包含很多代码。在此过程中,您制作了一个基本的路由系统和一个使用 ob_start()
和 ob_get_clean()
渲染模板的函数。如果出于某种原因,您需要继续从头开始构建此“框架”,那么您至少可以使用 Symfony 的独立 路由 组件和 Twig,它们已经解决了这些问题。
您可以让 Symfony 为您处理常见问题,而不是重新解决这些问题。这是相同的示例应用程序,现在在 Symfony 中构建
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
// src/Controller/BlogController.php
namespace App\Controller;
use App\Entity\Post;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class BlogController extends AbstractController
{
public function list(ManagerRegistry $doctrine)
{
$posts = $doctrine->getRepository(Post::class)->findAll();
return $this->render('blog/list.html.twig', ['posts' => $posts]);
}
public function show(ManagerRegistry $doctrine, $id)
{
$post = $doctrine->getRepository(Post::class)->find($id);
if (!$post) {
// cause the 404 page not found to be displayed
throw $this->createNotFoundException();
}
return $this->render('blog/show.html.twig', ['post' => $post]);
}
}
请注意,两个控制器函数现在都位于“控制器类”中。这是对相关页面进行分组的好方法。控制器函数有时也称为操作。
两个控制器(或操作)仍然是轻量级的。每个控制器都使用 Doctrine ORM 库从数据库检索对象,并使用 Twig 渲染模板并返回 Response
对象。list.html.twig
模板现在简单得多,并且使用了 Twig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
{# templates/blog/list.html.twig #}
{% extends 'base.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.php
文件几乎相同
1 2 3 4 5 6 7 8 9 10 11 12 13
<!-- templates/base.html.twig -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
{% block javascripts %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
注意
show.html.twig
模板作为练习留给读者:更新它应该与更新 list.html.twig
模板非常相似。
当 Symfony 引擎(称为 Kernel)启动时,它需要一个映射,以便知道根据请求信息调用哪些控制器。路由配置映射 - config/routes.yaml
- 以可读格式提供此信息
1 2 3 4 5 6 7 8
# config/routes.yaml
blog_list:
path: /blog
controller: App\Controller\BlogController::list
blog_show:
path: /blog/show/{id}
controller: App\Controller\BlogController::show
既然 Symfony 正在处理所有繁琐的任务,前端控制器 public/index.php
就简化为引导启动。而且由于它做得很少,你永远不必去碰它
1 2 3 4 5 6 7 8
// public/index.php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../src/Kernel.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new Kernel('prod', false);
$kernel->handle(Request::createFromGlobals())->send();
前端控制器的唯一工作是初始化 Symfony 的引擎(称为 Kernel)并将 Request
对象传递给它进行处理。Symfony 核心要求路由器检查请求。路由器将传入的 URL 匹配到特定路由,并返回有关路由的信息,包括应该调用的控制器。从匹配的路由中调用正确的控制器,并且控制器内部的代码创建并返回适当的 Response
对象。Response
对象的 HTTP 标头和内容被发送回客户端。
这真是太棒了。
Symfony 的优势
在文档的其余文章中,您将了解更多关于 Symfony 的每个部分如何工作以及如何组织项目的信息。现在,庆祝将博客从扁平 PHP 迁移到 Symfony 如何改善了您的生活
- 您的应用程序现在具有 清晰且组织一致的代码(尽管 Symfony 不会强迫您这样做)。这提高了 可重用性,并使新开发人员能够更快地在您的项目中进行生产;
- 您编写的 100% 代码都是为了 您的 应用程序。您 无需开发或维护底层实用程序,例如自动加载、路由或渲染 控制器;
- Symfony 为您提供 对开源工具的访问权限,例如 Doctrine 和 Twig、Security、Form、Validator 和 Translation 组件(仅举几例);
- 借助 Routing 组件,应用程序现在可以享受 完全灵活的 URL;
- Symfony 以 HTTP 为中心的架构使您可以访问强大的工具,例如由 Symfony 内部 HTTP 缓存或更强大的工具(如 Varnish)驱动的 HTTP 缓存。这在另一篇关于 缓存 的文章中进行了介绍。
也许最重要的是,通过使用 Symfony,您现在可以访问由 Symfony 社区开发的整套高质量开源工具!在 GitHub 上可以找到精选的 Symfony 社区工具。