更新于2020-02-11:重构答案以包含一些最佳实践并更接近 PHP 7.4。
最简单的 PHP MVC 方法
数千个单词无法与一个干净的示例竞争,因此这里是一个简单的用例:
想象一下,您想要显示一个描述来自虚构汽车供应商的“汽车”(给定“汽车 ID”)的页面:http://example.com/car.php?id=42(将http://example.com/car/42 later).
基本上,您可以使用如下层次结构来构建代码:
配置目录(这不是 MVC 架构模式的一部分):
+ config/
- database.php
<?php
return new PDO(getenv("DB_DSN"), getenv("DB_USER"), getenv("DB_PASSWORD"));
文档根目录的文件夹(脚本的作用类似于控制器):
+ htdocs/
- car.php
<?php
$carService = new CarService(require "config/database.php");
$car = $carService->getById($_GET["id"]);
require "car.php";
一个文件夹,封装了你的Model/业务逻辑(提示:“瘦控制器,胖模型”):
+ src/
- CarService.php
<?php
class CarService {
private PDO $database;
public function __construct(PDO $database) {
$this->database = $database;
}
public function getById(int $id): CarEntity {
return $this->database->query(
"SELECT model, year, price " .
"FROM car " .
"WHERE id = $id"
)->fetch(PDO::FETCH_CLASS, CarEntity::class);
}
}
最后一个文件夹包含您的所有内容Views(/模板):
+ views/
- car.php
<!DOCTYPE html>
<html>
<head>
<title>Car - <?= htmlspecialchars($car->model) ?></title>
</head>
<body>
<h1><?= htmlspecialchars($car->model) ?></h1>
Year: <?= htmlspecialchars($car->year) ?>
Price: <?= htmlspecialchars($car->price) ?>
</body>
</html>
为了使上面的代码工作,您需要配置 PHP:
include_path="/the/path/to/src:/the/path/to/views"
为了走得更远
不错的网址
您可能需要漂亮的 URL,如果使用 Apache,您可以通过以下方式实现此目的:
RewriteEngine On
RewriteRule ^/car/(\d+)$ /car.php?id=$1 [L]
这使得可以编写如下 URL:http://example.com/car/42这将在内部转换为http://example.com/car.php?id=42
视图作为类
在上述解决方案中,car.php
包含在全球范围, 因此$car
是可以直接使用的,但是$carService
too!
限制模板可以访问的内容的一种方法是将其转换为类:
views/CarView.php
:
<?php
class CarView {
private CarEntity $car;
public function __construct(CarEntity $car) {
$this->car = $car;
}
public function __invoke(): void {
?>
<!DOCTYPE html>
<html>
<head>
<title>Car - <?= htmlspecialchars($this->car->model) ?></title>
</head>
<body>
<h1><?= htmlspecialchars($this->car->model) ?></h1>
Year: <?= htmlspecialchars($this->car->year) ?>
Price: <?= htmlspecialchars($this->car->price) ?>
</body>
</html>
<?php
}
}
然后调整控制器:
htdocs/car.php
:
<?php
$carService = new CarService(require "config/database.php");
$view = new CarView($carService->getById($_GET["id"]));
$view();
重用视图
使用纯 PHP 文件作为模板,没有什么可以阻止您创建 headers.php、footers.php、menu.php...,您可以重复使用它们include()/require()以避免重复的 HTML。
使用类,可以通过组合它们来获得可重用性,例如,LayoutView
可以负责全局布局,并依次调用另一个View
成分:
<?php
class LayoutView {
protected string $lang;
public function __construct(string $lang) {
$this->lang = $lang;
}
// __invoke(): for embracing the "Single Responsibility" principle
public function __invoke(View $view): void {
?>
<!DOCTYPE html>
<html lang="<?= $this->lang ?>">
<head>
<meta charset="utf-8" />
<title><?= htmlentities($view->getTitle()) ?></title>
</head>
<body>
<?php ($view)(); ?>
</body>
</html>
<?php
}
}
CarView 可以这样实现:
views/CarView.php
:
<?php
class CarView implements View {
private CarEntity $car;
public function __construct(CarEntity $car) {
$this->car = $car;
}
public function getTitle(): string {
return $this->car->model;
}
// __invoke(): for embracing the "Single Responsibility" principle
public function __invoke(): void {
?>
<h1><?= htmlspecialchars($this->car->model) ?></h1>
Year: <?= htmlspecialchars($this->car->year) ?>
Price: <?= htmlspecialchars($this->car->price) ?>
<?php
}
}
反过来,控制器会像这样使用它:
htdocs/car.php
:
<?php
$carService = new CarService(require "config/database.php");
(new LayoutView("en"))(
new CarView($carService->getById($_GET["id"]))
);
结论
这远不是生产就绪的代码,因为这些示例没有解决其他方面:依赖注入/控制反转 (IoC)、输入过滤、类自动加载、命名空间……这个答案的目标是尽可能关注 MVC 的主要方面。
这与 Rasmus Lerdorf 提到的精神非常一致:https://toys.lerdorf.com/the-no-framework-php-mvc-framework.
人们不应该忘记 MVC 仍然是一种pattern。软件模式是解决常见问题的可重用原则(如果它们是可重用的)code,它们将被命名为“图书馆”。
Zend Framework、Symfony、Laravel、CakePHP 等框架提出了一种采用 MVC 方法的结构,但无法强制执行它,MVC 作为一种特殊情况关注点分离需要是learned and 明白了要实现的。