我们已经准备好开始开发应用背后的框架了!
在本章中,我们将:
- 从头开始创建一个简单的 PHP 框架
- 了解如何使用 Git 进行源代码管理
- 向骨骼添加处理 URL 请求的功能
- 构建对视图和布局的支持,以便我们可以向应用添加前端
- 添加代码以允许我们处理所有 HTTP 方法
- 设置复杂的路由并将其构建到示例应用中
- 添加使用公共文件的功能,并将其与我们的框架一起使用
- 将我们的代码发布到 GitHub,这样我们就可以管理我们的源代码了
让我们直接跳进去!
我们将在本书中构建什么
在本书的其余部分,我们将创建一个类似于 Twitter 的简单社交网络。我们叫它 Verge。
Verge将允许用户注册、登录和创建帖子。通过构建这个应用,我们将跨越大多数开发人员在构建应用时遇到的相同障碍,并且我们将学会依靠 CouchDB 完成一些繁重的工作。
为了构建 Verge,我们将制作一个轻量级的 PHP 包装器,它将处理我们在上一章中提到的基本路由和 HTTP 请求。让我们把这个框架称为 Bones。
骨头
在本书中,我们将构建一个名为 Bones的轻量级框架来运行我们的应用。你可能会想我们为什么要构建另一个框架?这是一个有效的问题!现在有很多 PHP 框架,比如:Zend 框架、Cake、Symfony 等等。这些都是功能强大的框架,但它们也有一个陡峭的学习曲线,在本书中不可能一一提及。相反,我们将创建一个非常轻量级的 PHP 框架,这将有助于简化我们的开发,但不会有太多其他的麻烦。通过构建这个框架,您将更好地理解 HTTP 方法以及如何从头构建轻型应用。一旦您使用 Bones 开发了这个应用,您就可以轻松地将您的知识应用到另一个框架中,因为我们将使用一些非常标准的流程。
如果您在本章中遇到任何问题,或者渴望看到最终产品,那么您可以访问 GitHub 上的完整 Bones 框架:https://github.com/timjuravich/bones 。在本章的最后,我还将介绍一种简单的方法,让您能够获取所有这些代码。
让我们从设置项目开始。
项目设置
在本节中,我们将完成为代码创建文件夹的步骤,并确保初始化 Git,以便在向项目添加新功能时跟踪源代码。
行动时间-为 Verge 创建目录
让我们通过在 /Library/WebServer/Documents文件夹中创建一个目录,并将该目录命名为 verge来开始项目的设置。此目录将包含项目的所有代码。为了简洁起见,在本章中,我们将调用 /Library/WebServer/Documents/verge我们的工作目录。
在我们的工作目录中,让我们为源文件创建四个新文件夹:
- 创建一个名为
classes的文件夹。此文件夹将包含我们将在此项目中使用的 PHP 类对象 - 创建一个名为
lib的文件夹。这个文件夹将包含我们的应用所依赖的 PHP 库,在我们的例子中,它将是Bones框架和与 CouchDB 通信的类。 - 创建一个名为
public的文件夹。此文件夹将包含我们的所有公共文件,如级联样式表(CSS)、JavaScript 和应用所需的图像。 - Create a folder called
views. This folder will contain our layout and the different pages of our web application.
如果您要查看您的工作目录,本节的最终结果应该类似于以下屏幕截图:

刚才发生了什么事?
我们很快创建了一些占位符文件夹来组织代码,我们将在本书的其余部分中添加这些代码。
使用 Git 进行源代码控制
为了跟踪我们的应用、进度,并允许我们在出错时回滚,我们需要在存储库上设置并运行源代码控制。我们在第 2 章中安装了 Git,设置您的开发环境,所以让我们好好利用它。有一些桌面客户端可以使用,但是为了简单起见,我们将使用命令行,这样它对每个人都适用。
行动时间-初始化 Git 存储库
Git 需要在每个开发项目的根目录中初始化,以便它能够跟踪所有项目文件。让我们为新创建的 verge项目这样做!
- 打开终端。
- 键入以下命令将目录更改为我们的工作目录:
cd /Library/Webserver/Documents/verge/
- 键入以下命令来初始化我们的 Git 目录:
git init
- Git 将响应以下内容:
Initialized empty Git repository in /Library/WebServer/Documents/verge/.git/
- 保持终端窗口打开,以便在本章中与 Git 交互。
刚才发生了什么事?
我们使用终端在我们的工作目录中使用 git init命令来初始化我们的 Git 存储库。Git 回应说让我们知道一切都很顺利。现在我们已经设置了 Git 存储库,我们需要在创建新文件时将每个文件添加到源代码管理下。向 Git 添加文件的语法很简单, git add path_to_file。您还可以使用通配符语句通过键入"git" add .递归地添加目录中的所有文件。在本章的大部分内容中,我们将快速添加文件,因此我们将使用"git add .".
实现基本路由
在我们开始创建 Bones之前,让我们先看看为什么我们需要它的帮助。让我们从创建一个简单的文件开始,该文件将确保我们的应用已全部设置好并准备就绪。
是时候采取行动了——创建我们的第一个文件:index.php
我们将创建的第一个文件是名为 index.php的文件。该文件将处理对我们的应用的所有请求,并最终成为与 Bones对话的主应用控制器。
- 在工作目录中创建
index.php,并添加以下文本:
<?php echo 'Welcome to Verge'; ?>
- 打开浏览器,转到 url:
http://localhost/verge/。 index.php文件将显示以下文字:
Welcome to Verge
刚才发生了什么事?
我们创建了一个名为 index.php的简单 PHP 文件,该文件此时只向我们返回文本。只有直接转到 http://localhost/verge/或 http://localhost/verge/index.php才能访问此文件。然而,我们的目标是,我们工作目录中的几乎每个请求都会命中 index.php(除了我们的 public文件)。为了做到这一点,我们需要添加一个 .htaccess文件,允许我们使用 URL 重写。
.htaccess 文件
.htaccess文件称为分布式配置文件,它们允许在目录基础上覆盖 Apache 配置。如果您还记得的话,在第一章CouchDB 简介中,我们通过将一些代码行更改为 Override All来确保可以使用 .htaccess文件。大多数 PHP 框架使用 .htaccess文件的方式与我们使用的方式相同,因此熟悉这个过程对您来说很重要。
行动时间-创建.htaccess 文件
为了处理对目录的所有请求,我们将在工作目录中创建一个 .htaccess文件。
- 在工作目录中创建一个名为
.htaccess的文件。 - 将以下代码添加到文件:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?request=$1 [QSA,L]
</IfModule>
- 打开工作目录中的文件
index.php。 - 更改
index.php内的代码以匹配以下内容:
<?php echo $_GET['request']; ?>
- Open your browser, go to
http://localhost/verge/test/abc, and go tohttp://localhost/verge/test/123. Notice that the page will respond back to you with the same value that you entered at the end of the root URL.

刚才发生了什么事?
我们首先创建了一个 .htaccess文件,使我们能够进行 URL 重写。第一行`检查以确保我们启用了 mod_rewrite模块。这将是 true,因为我们在[第 2 章](https://raw.githubusercontent.com/apachecn/apachecn-php-zh/master/docs/couchdb-php-web-dev-begin-guide/02.html "Chapter 2. Setting up your Development Environment")中的 http.conf文件中启用了 mod_rewrite`。
文件的下一行写着 RewriteEngine On,它做的和你想象的完全一样;它打开 Apache 的 RewriteEngine并等待一些条件和规则。接下来,我们设置两个 RewriteCond(重写条件)。第一个 RewriteCond告诉 RewriteEngine如果传递的 URL 与现有文件的位置不匹配(这就是 f所代表的),则重写 URL。第二个 RewriteCond告诉 RewriteEngine重写尚未存在的 URL(这就是 -d所代表的)。最后,我们设置了我们的 RewriteRule,即当为第一个值输入 URL 时,将其转发给第二个值(目标)。这个 RewriteRule告诉 RewriteEngine传递到此目录的任何 URL 都应该强制通过索引文件,并以名为 request的查询字符串的形式将路由传递到 index.php文件。
最后,字符串是 [QSA, L]。让我解释一下这意味着什么。 QSA表示如果请求中添加了任何查询字符串,则将其追加到重写目标。 L意味着,停止寻找匹配项,不再应用任何规则。
然后打开 index.php文件并更改代码以输出 request变量。您现在知道,输入浏览器的路由将以查询字符串的形式传递到 index.php文件。
所有这些代码就绪后,我们测试了所有内容,通过访问 URL http://localhost/verge/test/abc,我们的 index.php文件返回了 test/abc。当我们将 URL 更改为 http://localhost/verge/test/123时,我们的 index.php文件返回了 test/123。
破解 URL
在这一点上,从技术上讲,我们可以用一堆 if语句将内容组合在一起,从而允许我们的网站提供不同的内容。例如,我们可以根据 URL 显示不同的内容,只需在 index.php中添加几段代码,如下所示:
if ($_GET['request'] == '') {
echo -Welcome To Verge-;
} elseif ($_GET['request'] == 'signup') {
echo "Sign Up!";
}
在这段代码中,如果用户转到 URL http://localhost/verge,他们的浏览器将显示:
Welcome to Verge
同样,如果用户转到 http://localhost/verge/signup,其浏览器将显示:
Sign Up!
我们可以通过编写各种 if语句,将代码串成一个长文件,然后直接编写应用,将这种思维方式扩展得越来越远。然而,这对于维护来说绝对是一场噩梦,很难调试,而且通常是不好的做法。
相反,让我们删除 index.php文件中的所有代码,集中精力以正确的方式构建项目。在本章的其余部分,我们将致力于创建一个名为 Bones的简单框架,它将为我们处理一些繁重的请求。
打造骨骼骨架
正如我之前提到的, Bones是一个非常轻量级的框架,由总共 100 多行代码组成,驻留在一个文件中。在本节中,我们将开始形成一个结构,在接下来的几节中,我们将在该结构上构建越来越多的功能。
行动时间到了-将我们的应用连接到 Bones
让我们先创建 Bones库,然后将 index.php文件连接到它。
- 在我们工作目录(
/Library/Webserver/Documents/verge/lib/bones.php的lib文件夹中创建一个名为bones.php的文件。 - 将以下代码添加到我们工作目录中的
index.php文件中,以便我们可以与新创建的bones.php文件进行对话:
<?php
include 'lib/bones.php';
刚才发生了什么事?
这段代码所做的一切就是包含我们的 lib/bones.php文件,现在对我们来说已经足够好了!请注意,正如您可能已经习惯看到的那样,我们并没有以 ?>结束文件。实际上, ?>标记是可选的,在我们的例子中,如果我们愿意,保留它将允许我们减少不需要的空白,并在代码后面的响应中添加标题。
使用骨骼处理请求
为了说明我们打算对 Bones类做什么,让我们通过一个简单的例子来说明我们希望在本节结束时完成什么。
- 如果浏览器点击 URL
http://localhost/verge/signup,我们希望Bones截获该调用并将其解释为http://localhost/verge/index.php?request=signup。 - 然后,
Bones将查看我们在index.php文件中定义的路由列表,看看是否存在匹配。 - 如果确实存在匹配,
Bones将执行对匹配函数的回调,并在该路由内执行操作。
如果有什么让人困惑的话,别担心。希望在我们慢慢构建此功能时,它将开始变得有意义。
行动时间-创建骨骼类结构
让我们开始构建 Bones类,将以下代码添加到工作目录中的 lib/bones.php文件中:
/Library/Webserver/Documents/verge/lib/bones.php
<?php
class Bones {
private static $instance;
public static $route_found = false;
public $route = '';
public static function get_instance() {
if (!isset(self::$instance)) {
self::$instance = new Bones();
}
return self::$instance;
}
刚才发生了什么事?
我们刚刚创建了 Bones类,添加了几个 private和 public变量,以及一个名为 get_instance()的奇怪函数。私有静态变量 $instance与函数 get_instance()混合,形成了称为的单态模式。
单例模式允许我们的 Bones类不仅是一个简单的类,而且是一个对象。这意味着每次调用 Bones类时,我们都在访问单个现有对象。但如果对象不存在,它将创建一个新对象供我们使用。这是一个有点复杂的想法;然而,我希望它开始有意义,因为我们在今后的道路上利用它。
进入路线
现在,我们已经了解了类的基本概念,让我们添加一些函数来获取和解释每次创建新请求时传递给 Bones)的路由(URL)。然后,我们将在下一节将结果与每个可能的路由进行比较。
操作-创建功能访问骨骼创建路径的时间
为了让我们了解请求中传递了什么路由,我们需要在 get_instance()函数的右括号下方的 lib/bones.php文件中添加以下两个函数:
/Library/Webserver/Documents/verge/lib/bones.php
public static function get_instance() {
if (!isset(self::$instance)) {
self::$instance = new Bones();
}
return self::$instance;
}
public function __construct() {
$this->route = $this->get_route();
}
protected function get_route() {
parse_str($_SERVER['QUERY_STRING'], $route);
if ($route) {
return '/' . $route['request'];
} else {
return '/';
}
}
刚才发生了什么事?
在这段代码中,我们添加了一个名为 __construct()的函数,这是一个在每次创建类时自动调用的函数。然后,我们的 __construct()函数调用另一个名为 get_route()的函数,该函数将从我们的请求查询字符串中获取路由(如果有),并将其返回给实例的 route变量。
匹配 URL
为了匹配应用的路由,我们需要通过一个名为 register的函数推送每个可能的路由。
行动时间-创建注册函数以匹配路由
register函数将是 Bones类中最重要的函数之一,但我们将在 lib/bones.php文件的末尾添加以下代码开始:
/Library/Webserver/Documents/verge/lib/bones.php
public static function register($route, $callback) {
$bones = static::get_instance();
if ($route == $bones->route && !static::$route_found) {
static::$route_found = true;
echo $callback($bones);
} else {
return false;
}
}
刚才发生了什么事?
我们首先创建一个名为 register的公共静态函数。此函数有两个参数: $route和 $callback. $route包含我们试图与实际路由匹配的路由, $callback是如果路由匹配将执行的函数。请注意,在 register函数开始时,我们使用 static:get_instance()函数调用 Bones实例。这是正在运行的单例模式,将 Bones对象的单个实例返回给我们。
然后, register函数检查我们通过浏览器访问的路由是否与传递到函数中的路由匹配。如果存在匹配,我们的 $route_found变量将设置为 true,这将允许我们跳过查看其余的路由。然后, register函数将执行一个回调函数,该函数将执行路由中定义的工作。我们的 Bones实例也将通过回调函数传递,这样我们就可以充分利用它。如果路线不匹配,我们将返回 false以便知道路线不匹配。
我们现在在 Bones中要做的事情已经结束了。因此,请确保以一个结束括号结束您的课程,如下所示:
}
从我们的应用调用 register 函数
我们现在已经基本了解了 Bones应该做什么,但是我们缺少了一个将 index.php和 lib/bones.php文件连接在一起的函数。我们最终将创建四个函数来实现这一点,每个 HTTP 方法一个。但是,现在,让我们创建我们的 get函数。
行动时间到了-在 Bones 类中创建 get 函数
让我们在 lib/bones.php文件的顶部创建一个 get函数,就在<?php标记之后,在定义 Bones类之前:
/Library/Webserver/Documents/verge/lib/bones.php
<?php
ini_set('display_errors','On');
error_reporting(E_ERROR | E_PARSE);
function get($route, $callback) {
Bones::register($route, $callback);
}
class Bones {
...
}
刚才发生了什么事?
此函数位于 lib/bones.php文件中,并为您在 index.php文件中定义的每个 get路由调用。此函数是一个简单的传递函数,将路由和回调交给 Bones的 register函数。
我们意见一致吗?
我们在这一部分做了很多。让我们再次检查您的代码是否与我的代码匹配:
/Library/Webserver/Documents/verge/lib/bones.php
<?php
function get($route, $callback) {
Bones::register($route, $callback);
}
class Bones {
private static $instance;
public static $route_found = false;
public $route = '';
public function __construct() {
$this->route = $this->get_route();
}
public static function get_instance() {
if (!isset(self::$instance)) {
self::$instance = new Bones();
}
return self::$instance;
}
public static function register($route, $callback) {
$bones = static::get_instance();
if ($route == $bones->route && !static::$route_found) {
static::$route_found = true;
echo $callback($bones);
} else {
return false;
}
}
protected function get_route() {
parse_str($_SERVER['QUERY_STRING'], $route);
if ($route) {
return '/' . $route['request'];
} else {
return '/';
}
}
}
向我们的应用添加路由
现在我们的 lib/bones.php文件已经完成了。我们需要做的就是在 index.php文件中添加一些路由,这些路由调用 lib/bones.php文件夹中的 get函数。
行动的时间到了——为我们创建测试骨骼的路线
打开 index.php文件,添加以下两条路径,以便我们可以测试我们的新代码:
<?php
include 'lib/bones.php';
get('/', function($app) {
echo "Home";
});
get('/signup', function($app) {
echo "Signup!";
});
刚才发生了什么事?
我们刚刚为我们的 Bones类创建了两条路由来处理 /(根 URL)和 /signup。
我们刚刚添加的代码中有几点需要注意:
- 我们的两个
get路由现在是干净的小函数,包括我们的路由和一个用作回调函数的函数 - 函数执行后,我们使用
echo显示简单文本 - 当路由匹配并且从
Bones执行回调时,Bones的实例作为变量$app返回,可以在回调函数中的任何位置使用
测试一下!
我们已经准备好测试 Bones!的新功能,打开浏览器,进入 http://localhost/verge/。你会看到 Home这个词。然后将浏览器指向 http://localhost/verge/signup,您将看到文本 Signup!
虽然我们的应用仍然非常基础,但我希望您看到以这种简单方式添加路由的优势。在进入下一节之前,您可以随意玩一玩,再添加一些路线。
增加对 Git 的更改
在本节中,我们启动了 lib/bones.php库并添加了一些简单的路由。让我们将所有更改添加到 Git,以便跟踪进度。
- 打开终端。
- 键入以下命令将目录更改为我们的工作目录:
cd /Library/Webserver/Documents/verge/
- 通过键入以下命令添加我们在此目录中创建的所有文件:
git add .
- 向 Git 描述自上次提交以来我们所做的工作:
git commit am 'Created bones.php and added simple support for routing'
处理布局和视图
我们将从 routes 休息一会儿,添加一些有趣的前端功能。每个应用都由许多页面组成,我们称之为视图。每个视图都有一个标准布局,这些视图将填充该布局。布局是视图的包装器,可能包含指向 CSS 引用、导航或任何您认为对每个视图来说是公共的东西的链接。
使用骨骼支持视图和布局
为了支持视图和布局,我们需要在 Bones类中添加一些附加功能。
操作时间-使用常量获取工作目录的位置
我们需要做的第一件事是创建一个名为 ROOT的命名常量,它将为我们提供工作目录的完整位置。到目前为止,我们还没有做任何额外的包括文件在内的工作,但是对于我们的布局和视图,如果我们不添加一些功能来获取工作目录,这将开始变得有点困难。为了支持这一点,让我们在 lib/bones.php文件的顶部添加一行简单的代码。
<?php
ini_set('display_errors','On');
error_reporting(E_ERROR | E_PARSE);
define('ROOT', __DIR__ . '/..');
function get($route, $callback) {
...
}
刚才发生了什么事?
这行代码创建了一个名为 ROOT的常量,我们可以在整个代码中使用它来引用工作目录。 __DIR__给出当前文件的根目录(/Library/Webserver/Documents/verge/lib。所以,我们想通过添加 /.来查看后面的另一个目录。到小路上去。
行动时间-允许骨骼存储变量和内容路径
我们需要能够设置并接收来自 index.php的视图变量。那么,让我们将这种支持添加到 Bones中。
- 让我们定义一个名为
$vars的public数组,该数组允许我们将路由中的变量存储在index.php中,并定义一个名为$content的字符串,该字符串将包含加载到布局中的视图的路径。我们首先在lib/bones.php类中添加两个变量:
class Bones {
public $route = '';
public $content = '';
public $vars = array();
public function __construct() {
...
}
- 为了让我们从
index.php文件中设置变量,我们将创建一个名为set的简单函数,该函数允许我们传递变量的索引和值,并将其保存到当前Bones实例。让我们在lib/bones.php中创建一个名为set的函数,就在get_route()函数之后。
protected function get_route() {
...
}
public function set($index, $value) {
$this->vars[$index] = $value;
}
刚才发生了什么事?
我们在 Bones类中添加了两个新变量 $vars和 $content。下一节将使用这两种方法。然后我们创建了一个 set函数,允许我们从 index.php文件向 Bones类发送变量,以便在视图中显示它们。
接下来,我们需要添加从 index.php调用视图并显示它们的功能。容纳此功能的函数将被称为 render。
是时候采取行动了——允许我们的应用通过在 index.php 中调用来显示视图
我们将首先创建一个名为 render的 public函数,它接受两个参数。第一个是 $view,它是您要显示的视图的名称(或路径),第二个是 $layout,它将定义我们用于显示视图的布局。布局也将有一个默认值,这样我们可以保持简单,以便处理视图的显示。在 set功能之后的 lib/bones.php文件中添加以下代码:
public function set($index, $value) {
$this->vars[$index] = $value;
}
public function render($view, $layout = "layout") {
$this->content = ROOT. '/views/' . $view . '.php';
foreach ($this->vars as $key => $value) {
$$key = $value;
}
if (!$layout) {
include($this->content);
} else {
include(ROOT. '/views/' . $layout . '.php');
}
}
刚才发生了什么事?
我们创建了 render函数,该函数将设置要在布局中显示的视图的路径。所有视图都将保存在本章前面创建的 views目录中。然后,代码循环遍历实例的 vars数组中设置的每个变量。对于每个变量,我们使用一种奇怪的语法 $$,它允许我们使用数组中定义的键设置一个变量。这将允许我们在视图中直接引用变量。最后,我们添加了一个简单的 if语句,用于检查 layout文件是否已定义。如果没有定义 $layout,我们只返回视图的内容。如果 $layout已定义,我们将包括布局,它将返回已定义布局中包装的视图。我们这样做是为了避免在将来使用布局,如果我们愿意的话。例如,在 AJAX 调用中,我们可能只想返回没有布局的视图。
行动时间-创建简单布局文件
在本节中,我们将创建一个名为 layout.php的简单布局文件。请记住,在我们的 render函数中, $layout有一个默认值,它被设置为 layout。这意味着,默认情况下, Bones将查找 views/layout.php。现在,让我们创建该文件。
- 首先在我们的
views目录中创建一个名为layout.php的新文件。 - 在新创建的
views/layout.php:
<html>
<body>
<h1>Verge</h1>
<?php include($this->content); ?>
</body>
</html>
中添加以下代码
刚才发生了什么事?
我们创建了一个非常简单的 HTML 布局,将在应用的所有视图中使用。如果您还记得,我们在 Bones中的 render函数将路径集用作我们在上一个函数中设置的 $content变量,并将其包含在内,以便我们可以显示视图。
向我们的应用添加视图
现在我们已经准备好了视图的所有部分,我们只需要在 index.php文件中添加几行代码,这样我们就可以渲染视图了。
行动时间-在我们的路线内呈现视图
让我们用以下实际使用我们新框架的代码替换路由中的现有代码部分,这些代码部分只是回显文本:
get('/', function($app) {
$app->set('message', 'Welcome Back!');
$app->render('home');
});
get('/signup', function($app) {
$app->render('signup');
});
刚才发生了什么事?
对于根路径,我们使用新函数 set传递一个键为'message'且其内容为'Welcome Back!'的变量,然后我们让 Bones呈现主视图。对于 signup路线,我们只是渲染 signup视图。
行动时间-创建视图
我们已经准备好测试这段新代码,但是我们需要创建实际的视图以便显示它们。
- 首先在工作目录中的
views文件夹中创建两个新文件,分别称为home.php和signup.php。 - 通过写入以下代码将以下代码添加到
views/home.php文件中:
Home Page <br /><br />
<?php echo $message; ?>
- 在
views/signup.php文件中添加以下代码:
Signup Now!
刚才发生了什么事?
我们创建了两个简单的视图,它们将由 index.php文件呈现。 views/home.php中的代码行表示<?php echo $message; ?>,它将显示变量以及从 index.php文件传递到 Bones库的名称消息。试试看!
打开浏览器,进入 http://localhost/verge/或 http://localhost/verge/signup,您将看到我们所有的努力都得到了回报。我们的布局现在正在渲染,我们的视图正在显示。我们还可以从 index.php传递一个名为 message的变量,并在主视图上输出值。我希望你能开始看到我们目前正在增加的 Bones的力量!
增加对 Git 的更改
到目前为止,我们添加了对布局和视图的支持,这将帮助我们构建应用的所有页面。让我们将所有更改添加到 Git 中,以便跟踪进度。
- 打开终端。
- 键入以下命令将目录更改为我们的工作目录:
cd /Library/Webserver/Documents/verge/
- 通过键入以下命令添加我们在此目录中创建的所有文件:
git add .
- 向 Git 描述自上次提交以来我们所做的工作:
git commit am 'Added support for views and layouts'
增加对其他 HTTP 方法的支持
到目前为止,我们一直在处理 GET调用,但在 web 应用中,我们必须能够支持我们在上一章中提到的所有 HTTP方法: GET, PUT, POST和 DELETE。
操作时间-检索请求中使用的 HTTP 方法
我们已经完成了支持、捕获和处理 HTTP 请求所需的大部分繁重工作。我们只需要插入几行额外的代码。
- 让我们在
$route变量之后向Bones类添加一个名为$method的变量。此变量将存储对每个请求执行的HTTP方法:
class Bones {
private static $instance;
public static $route_found = false;
public $route = '';
public $method = '';
public $content = '';
- 为了获得每个请求的方法,我们需要在
__construct()函数中添加一行名为get_route()的代码,并将结果的值保存在实例变量$method中。这意味着当在每个请求上创建Bones时,它也将检索该方法并将其保存到Bones实例中,以便我们可以继续使用它。为此,请添加以下代码:
public function __construct() {
$this->route = $this->get_route();
$this->method = $this->get_method();
}
- 让我们创建名为
get_method()的函数,以便我们的__construct()函数可以调用它。让我们在我们的get_route()方法
protected function get_route() {
parse_str($_SERVER['QUERY_STRING'], $route);
if ($route) {
return '/' . $route['request'];
} else {
return '/';
}
}
protected function get_method() {
return isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
}
之后添加它
刚才发生了什么事?
我们在您的 Bones类中添加了一个变量 $method。此变量由函数 get_route()设置,每次通过 __construct()方法向 Bones发出请求时,都会向实例 $method变量返回一个值。这听起来可能非常令人困惑,但请容忍我。
get_route()函数使用由 web 服务器创建的名为 $_SERVER的数组,允许我们检索请求和执行信息。这个简单的一行代码表示,如果在 $_SERVER中设置了 REQUEST_METHOD,则返回它,但如果出于任何原因未设置 REQUEST_METHOD,则返回 GET以确保方法安全。
行动时间-更改寄存器以支持不同的方法
现在我们正在检索每个请求的方法,我们需要更改寄存器函数,以便我们可以将 $method与每个路由一起传递,以便它们能够正确匹配。
- 将
$method添加到lib/bones.php中的register函数中,这样我们就可以将一个方法传递到函数中:
public static function register($route, $callback, $method) {
$bones = static::get_instance();
- 我们现在需要更新注册函数中的简单路由匹配,以检查传递的路由
$method是否与实例变量$bones->method匹配,这是服务器上实际发生的方法:
public static function register($route, $callback, $method) {
$bones = static::get_instance();
if ($route == $bones->route && !static:: $route_found && $bones->method == $method) {
static::$route_found = true;
echo $callback($bones);
} else {
return false;
}
}
刚才发生了什么事?
我们添加了一个 $method参数以传递到 register函数中。然后,我们在 register函数中使用了这个 $method变量,将其添加到必须是 true的参数列表中,以便将路由视为匹配。因此,如果路由匹配,但是如果它是一种与预期不同的 HTTP方法,那么它将被忽略。这将允许您使用相同的名称创建路由,但根据所传递的方法执行不同的操作。听起来就像 REST,我们在上一章中讨论过,不是吗?
为了执行 register函数,让我们回顾一下 lib/bones.php文件开头的 get函数:
<?php
ini_set('display_errors','On');
error_reporting(E_ERROR | E_PARSE);
define('ROOT', dirname(dirname(__FILE__)));
function get($route, $callback) {
Bones::register($route, $callback);
}
希望很容易看到我们下一步要做什么。让我们扩展当前的 get函数,再创建三个函数,其余 HTTP 方法各一个,确保在 caps 中传入每个方法的名称。
<?php
ini_set('display_errors','On');
error_reporting(E_ERROR | E_PARSE);
define('ROOT', dirname(dirname(__FILE__)));
function get($route, $callback) {
Bones::register($route, $callback, 'GET');
}
function post($route, $callback) {
Bones::register($route, $callback, 'POST');
}
function put($route, $callback) {
Bones::register($route, $callback, 'PUT');
}
function delete($route, $callback) {
Bones::register($route, $callback, 'DELETE');
}
我们在 Bones 库中添加了所有需要的功能,允许我们使用其他 HTTP 方法,非常简单,对吗?
是时候采取行动了——为骨骼添加简单但强大的助手
让我们在 lib/bones.php文件中添加两个小函数,以帮助我们使用表单。
- 添加一个名为
form的函数,如下所示:
public function form($key) {
return $_POST[$key];
}
- 添加一个名为
make_route的函数。此函数将允许我们的Bones实例创建干净的链接,以便我们可以链接到应用中的其他资源:
public function make_route($path = '') {
$url = explode("/", $_SERVER['PHP_SELF']);
if ($url[1] == "index.php") {
return $path;
} else {
return '/' . $url[1] . $path;
}
}
刚才发生了什么事?
我们添加了一个名为 form的简单函数,作为 $_POST数组的包装器,该数组是通过 HTTP POST方法传递的变量数组。这将允许我们在 POST之后收集值。我们创建的下一个函数名为 make_route。这个函数很快将被用于创建干净的链接,以便我们可以链接到应用中的其他资源。
使用表单测试我们对 HTTP 方法的支持
我们在这里添加了一些很酷的东西。让我们继续测试对新添加的 HTTP 方法的支持。
打开文件 verge/views/signup.php,添加一个简单表单,如下图:
Signup
<form action="<?php echo $this->make_route('/signup') ?>" method="post">
<label for="name">Name</label>
<input id="name" name="name" type="text"> <br />
<input type="Submit" value="Submit">
</form>
我们使用 $this->make_route. $this->make_route设置表单的 action属性,并使用 Bones实例创建一条将解析为 signup路由的路由。然后,我们将该方法定义为使用 post方法。表单的其余部分非常标准,有一个标签和文本框用于 name,还有一个 submit按钮用于处理表单。
如果您在浏览器中转到 http://localhost/verge/signup,您现在会看到表单,但是如果您单击 submit按钮,您将被发送到一个空白页面。这是因为我们还没有在 index.php文件中定义 post方法。
打开 index.php文件,添加以下代码:
get('/signup', function($app) {
$app->render('signup');
});
post('/signup', function($app) {
$app->set('message', 'Thanks for Signing Up ' . $app->form('name') . '!');
$app->render('home');
});
让我们浏览一下这段代码,确保清楚我们在这里做什么。我们告诉 Bones寻找发送 post方法的 /signup路线。解决此路由后,回调将使用一些文本设置变量 message的值。文本包括我们创建的名为 $app->form('name')的新函数。此函数从属性为 name的表单中获取过账值。然后我们将告诉 Bones渲染主视图,让我们看到消息。
测试一下!
让我们现在就尝试所有这些!
- 打开浏览器,然后转到:
http://localhost/verge/signup。 - Your browser should show the following:

- Enter your name (I entered
Tim), and click on Submit.

增加对 Git 的更改
在本节中,我们添加了对所有 HTTP 方法的支持,这将允许我们处理任何类型的请求。让我们将所有更改添加到 Git 中,以便跟踪进度。
- 打开终端。
- 键入以下命令将目录更改为我们的工作目录:
cd /Library/Webserver/Documents/verge/
- 通过键入以下命令添加我们在此目录中创建的所有文件:
git add .
- 向 Git 描述自上次提交以来我们所做的工作:
git commit -am 'Added support for all HTTP methods'
增加对复杂路由的支持
我们的框架在技术上已经准备就绪,可以开始在顶部构建。但是,我们没有足够的支持来匹配和处理复杂的路由。因为大多数应用都需要它,所以让我们快速添加它。
处理复杂路线
例如,在 index.php文件中,我们希望能够为用户配置文件定义路由。这条路线可能是 /user/:username。在这种情况下, :username将是一个我们可以访问的变量。因此,如果您转到 URL /user/tim,您可以使用 Bones访问用户名 tim,以获取 URL 的该部分,并返回其值。
让我们首先在 __construct函数中向 lib/bones.php文件添加另一个变量和另一个调用:
public $content = '';
public $vars = array();
public $route_segments = array();
public $route_variables = array();
public function __construct() {
$this->route = $this->get_route();
$this->route_segments = explode('/', trim($this->route, '/'));
$this->method = $this->get_method();
}
我们刚刚在 Bones实例中添加了两个名为 $route_segments的变量,每次使用 __construct()创建 Bones对象时都会设置 $route_variables. $route_segments。 $route_segments数组通过在斜线(/)上拆分 $route将其拆分为可用段。这将允许我们检查浏览器发送到 Bones的 URL,然后确定路由是否匹配。 $route_variables将是通过路由传入的变量库,它将使我们能够使用 index.php文件。
现在,让我们开始修改 register函数,以便处理这些特殊路由。让我们删除其中的所有代码,然后慢慢添加一些代码。
public static function register($route, $callback, $method) {
if (!static::$route_found) {
$bones = static::get_instance();
$url_parts = explode('/', trim($route, '/'));
$matched = null;
我们添加了一个 if语句,检查路线是否已经匹配。如果有,我们就忽略 register函数中的所有其他内容。然后,我们添加了 $url_parts。这将分割我们传递到注册函数的路由,并将帮助我们将此路由与浏览器点击的实际路由进行比较。
注
完成本节后,我们将关闭 if语句和寄存器函数;别忘了做那件事!
让我们开始比较浏览器点击的路径 $bones->route_segments和我们试图匹配的路径 $url_parts。首先,让我们检查以确保 $route_segments和 $url_parts的长度相同。这将确保我们不深入挖掘函数来节省时间,因为我们已经知道它不匹配。
在 lib/bones.php:中的 register函数中添加以下代码
if (count($bones->route_segments) == count($url_parts)) {
} else {
// Routes are different lengths
$matched = false;
}
现在,让我们在 if语句中添加一个 for循环,它将循环 $url_parts中的每一个,并尝试将其与 route_segments匹配。
if (count($bones->route_segments) == count($url_parts)) {
foreach ($url_parts as $key=>$part) {
}
} else {
// Routes are different lengths
$matched = false;
}
为了识别变量,我们将检查冒号(:)是否存在。这表示此段包含一个变量值。
if (count($bones->route_segments) == count($url_parts)) {
foreach ($url_parts as $key=>$part) {
if (strpos($part, ":") !== false) {
// Contains a route variable
} else {
// Does not contain a route variable
}
}
} else {
// Routes are different lengths
$matched = false;
}
接下来,我们添加一行代码,获取段的值并将其保存到 $route_variables数组中,以便稍后使用。仅仅因为我们找到了一个匹配变量,并不意味着整个路由都是匹配的,所以我们现在还不打算设置 $matched = true。
if (strpos($part, ":") !== false) {
// Contains a route variable
$bones->route_variables[substr($part, 1)] = $bones->route_segments[$key];
} else {
// Does not contain a route variable
}
让我们将刚才添加的代码行分解。第二部分, $bones->route_segments[$key]获取传递到浏览器的段的值,该段的索引与我们当前循环的段相同。
然后, $bones->route_variables[substr($part, 1)]将值保存到 $route_variables数组中,索引设置为 $part值,然后使用 substr确保键中不包含冒号。
这段代码有点混乱。那么,让我们来看一个快速用例:
- 打开浏览器,输入 URL
/users/tim。 - 此注册路由开始检查路由
/users/:username。 $bones->route_segments[$key]将返回tim。$bones->route_variables[substr($part, 1)]将使用索引用户名保存该值,以便我们以后检索该值tim。
现在,让我们通过检查不包含路由变量的段(语句 if的 else部分)来结束这个 if 语句。在这个区域中,我们将检查以确保我们正在检查的段与从浏览器 URL 传递的段匹配。
} else {
// Does not contain a route variable
if ($part == $bones->route_segments[$key]) {
if (!$matched) {
// Routes match
$matched = true;
}
} else {
// Routes don't match
$matched = false;
}
}
我们刚刚添加的代码检查循环通过的 $part值是否与 $route_segments中的并行段匹配。然后检查是否已将此路线标记为不匹配。这表明我们已经在上一个段检查中将其标记为不匹配。如果路线不匹配,我们将设置 $matched = false。这将告诉我们 URL 不匹配,我们可以忽略路由的其余部分。
让我们将最后一块添加到路由匹配拼图中。这个语句看起来与我们以前的匹配语句有点相似,但实际上要干净一些。
if (!$matched || $bones->method != $method) {
return false;
} else {
static::$route_found = true;
echo $callback($bones);
}
这段代码通过查看 $matched变量来检查以确保我们的路由与上面的匹配语句匹配。然后,我们检查 HTTP 方法是否与我们检查的路由匹配。如果没有匹配项,则返回 false并退出此函数。如果有匹配,我们设置 $route_found = true,然后对路由进行回调,执行 index.php文件中定义的路由内部的代码。
最后,让我们通过添加结束括号来结束此函数,从而结束 if $route_found语句和 register函数。
}
}
在上一节中,我们添加了很多代码。因此,请检查以确保您的代码与我的代码匹配:
public static function register($route, $callback, $method) {
if (!static::$route_found) {
$bones = static::get_instance();
$url_parts = explode('/', trim($route, '/'));
$matched = null;
if (count($bones->route_segments) == count($url_parts)) {
foreach ($url_parts as $key=>$part) {
if (strpos($part, ":") !== false) {
// Contains a route variable
$bones->route_variables[substr($part, 1)] = $bones-> route_segments[$key];
} else {
// Does not contain a route variable
if ($part == $bones->route_segments[$key]) {
if (!$matched) {
// Routes match
$matched = true;
}
} else {
// Routes don't match
$matched = false;
}
}
}
} else {
// Routes are different lengths
$matched = false;
}
if (!$matched || $bones->method != $method) {
return false;
} else {
static::$route_found = true;
echo $callback($bones);
}
}
}
访问路由变量
现在我们正在将路由变量保存到数组中,我们需要在 lib/bones.php文件中添加一个名为 request的函数
public function request($key) {
return $this->route_variables[$key];
}
此函数接受一个名为 $key的变量,并通过使用相同的键返回值来返回 route_variables数组中对象的值。
向 index.php 添加更复杂的路由
我们做了很多艰苦的工作。让我们测试一下,确保一切顺利。
让我们在 index.php中添加一个快速路由来测试路由变量:
get('/say/:message', function($app) {
$app->set('message', $app->request('message'));
$app->render('home');
});
我们添加了一个带有路由变量 message的路由。当通过回调找到并执行路由时,我们将一个变量 message设置为路由变量消息的值。然后,我们呈现了主页,就像我们以前做过几次一样。
测试一下!
如果您打开浏览器并访问 URL http://localhost/verge/say/hello,浏览器将显示: hello。
如果您将该值更改为任何不同的值,它将立即向您显示相同的值。
增加对 Git 的更改
本节添加了更详细的路由匹配,并允许我们在 URL 中包含路由变量。让我们将所有更改添加到 Git,以便跟踪进度。
- 打开终端。
- 键入以下命令将目录更改为我们的工作目录:
cd /Library/Webserver/Documents/verge/
- 通过键入以下命令添加我们在此目录中创建的所有文件:
git add .
- 向 Git 描述自上次提交以来我们所做的工作:
git commit am 'Refactored route matching to handle more complex URLs and allow for route variables'
增加对公共文件的支持
开发 web 应用的一个重要部分是能够使用 CSS 和 JS 文件。目前,我们真的没有一个好的方法来使用和显示它们。让我们改变这一点!
是时候改变动作了。htaccess 支持公共文件
我们需要修改 .htaccess文件,以便 public文件的请求不会传递到 index.php文件,而是进入 public文件夹并找到请求的资源。
- 首先打开项目根目录中的.htaccess 文件。
- 添加以下突出显示的代码:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^css/([^/]+) public/css/$1 [L]
RewriteRule ^js/([^/]+) public/js/$1 [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?request=$1 [QSA,L]
</IfModule>
刚才发生了什么事?
我们刚刚添加了 RewriteRule来绕过我们的“catchall”规则,如果它是 public文件,则该规则将指导所有请求。然后,我们简化路由,允许 URL 解析为 /css和 /js,而不是 /public/css和 /public/js。
我们已经准备好使用公共文件了。我们只需要实现它们,这应该和设置它一样简单。
行动时间-为应用创建样式表
让我们首先添加一个样式表来更改应用的外观。
- 打开
views/layout.php。此文件当前驱动项目中所有页面的布局。我们只需要添加代码来包含样式表:
<html>
<head>
<link href="<?php echo $this->make_route('/css/master.css') ?>" rel="stylesheet" type="text/css" />
</head>
<body>
<?php include($this->view_content); ?>
</body>
</html>
- 创建一个名为
master.css的新文件,并将其放在我们工作目录的public/css文件夹中。 - 在
public/css/master.css中添加一段快速代码,该代码将显示不同的颜色背景,以便我们可以测试这一切是否正常。
body {background:#e4e4e4;}
刚才发生了什么事?
我们为我们的应用添加了一个名为 master.css的新样式表的引用。我们使用标准标记包含样式表,并使用 Bones, make_route的函数之一正确创建文件路径。
让我们测试以确保样式表现在正确显示。
- 打开浏览器,进入
http://localhost/verge/。 - Your browser should show the following:

- 请注意,我们页面的背景颜色已变为灰色,表示样式表已经开始使用!
增加对 Git 的更改
在本节中,我们添加了对公共文件(如样式表、JavaScript 和图像)的支持。然后我们通过创建一个 master.css文件来测试它。让我们将所有更改添加到 Git 中,以便跟踪进度。
- 打开终端。
- 键入以下命令将目录更改为我们的工作目录:
cd /Library/Webserver/Documents/verge/
- 通过键入以下命令添加我们在此目录中创建的所有文件:
git add .
- 向 Git 描述自上次提交以来我们所做的工作:
git commit am 'Added clean routes for public files, created a master.css file, linked to master.css in layout.php'
将您的代码发布到 GitHub
现在我们已经创建了框架和所有底层代码,我们可以将代码推送到任何支持 Git 的服务提供商。在本书中,我们将使用GitHub。
您可以通过转到:在 GitHub 上创建帐户 https://github.com/plans 。GitHub 有多种计划供您选择,但我建议您选择免费帐户,这样您就不必在此时支付任何费用。如果您已经拥有帐户,则可以登录并跳过创建新帐户。

点击创建免费账户。
注
需要注意的是,通过选择一个免费帐户,您的所有存储库都将是 public。这意味着任何人都可以看到您的代码。目前这还可以,但随着开发的深入,您可能希望注册一个付费帐户,这样它就不会公开。

您将看到一个快速注册表格。填写完整,完成后点击创建账户。
创建帐户后,将显示帐户仪表板。在该屏幕上,您将看到您的帐户或存储库中正在监视的任何活动。由于我们还没有任何存储库,我们应该首先点击新建存储库。

创建新存储库页面将允许您为代码创建一个新的存储库。

通过填写每个字段来完成此表格的其余部分。
- 项目名称:
verge - 说明:
A social network called verge built using Bones - 主页 URL:您可以暂时将此项留空
- 点击创建存储库
您的存储库现在已创建,可以将代码推送到。您所需要做的就是在终端中运行一些语句。
- 打开终端。
- 键入以下命令将目录更改为我们的工作目录:
cd /Library/WebServer/Documents/verge/
- 通过输入以下命令并用您的 GitHub 用户名替换用户名,将 GitHub 添加为您的远程存储库:
git remote add origin [email protected]:username/verge.git
- 将本地存储库推送到 GitHub。
git push -u origin master
- Git 将返回一堆文本,并在完成时停止。
如果在上刷新 Git 存储库的 URLhttps://github.com (我的 URL 是https://github.com/timjuravich/verge ),您将看到您的所有文件,如果您点击历史记录,您将看到您在每个章节中添加的所有更改,正如我们在本章中所述。

当您开始添加更多代码时,每次都必须通过执行命令 git push origin master手动将代码推送到 GitHub。在阅读本书时,我们将继续添加到此存储库。
从 GitHub 获取完整的代码
如果您在途中的某个地方迷路了,或者无法让一切正常工作,那么您可以从 GitHub 轻松地从 Git 存储库克隆骨骼,您将获得一份包含我们在本章中所做所有更改的新副本。
- 打开终端。
- 使用以下命令将目录更改为我们的工作目录:
cd /Library/WebServer/Documents
- 通过键入以下命令将存储库克隆到本地计算机:
git clone [email protected]:timjuravich/bones.git
- Git 将从 GitHub 获取所有文件,并将它们移动到本地计算机。
总结
我们在这一章中做了大量的工作!我们有:
- 从头开始创建一个 PHP 框架来处理 web 请求
- 增加了清洁 URL、路由变量、HTTP 方法支持、简单视图和布局引擎的功能,以及显示
public文件(如样式表、JavaScript 和图像)的系统 - 使用浏览器测试框架的每个部分,以确保我们可以访问所做的更改
- 将我们的代码发布到 GitHub,这样我们就可以查看更改并管理代码
准备好!在下一章中,我们将首先讨论如何将新创建的应用连接到 CouchDB。