CouchDB 和 PHP Web 开发初学者指南(二)
原文:
zh.annas-archive.org/md5/175c6f9b2383dfb7631db24032548544译者:飞龙
第五章:将您的应用程序连接到 CouchDB
现在我们已经建立了应用程序的框架,让我们谈谈我们的应用程序需要与 CouchDB 通信的情况。
在本章中,我们将讨论以下几点:
-
调查与 CouchDB 交互的快速简便方法,并讨论其缺点
-
查看现有库以便于 PHP 和 CouchDB 开发
-
安装 Sag 并将其集成到 Bones 中
-
让我们的注册表单创建 CouchDB 文档,并在 Futon 中进行验证
在我们开始之前
在我们做任何事情之前,让我们创建一个数据库,从此时起我们将在 Verge 中使用。与以前一样,让我们使用curl创建一个数据库。
行动时间-使用 curl 为 Verge 创建数据库
我们在第三章中使用curl创建了一个数据库,与 CouchDB 和 Futon 入门。让我们快速回顾如何使用PUT请求在 CouchDB 中创建一个新数据库。
- 通过在终端中运行以下命令来创建一个新的数据库。确保用第三章中创建的数据库管理员用户替换
username和password。
**curl -X PUT username:password@localhost:5984/verge**
- 终端将以以下输出做出响应:
**{"ok":true}**
刚刚发生了什么?
我们使用终端通过curl触发了一个PUT请求,使用 CouchDB 的RESTful JSON API创建了一个数据库。我们在 CouchDB 的根 URL 末尾传递verge作为数据库的名称。成功创建数据库后,我们收到了一条消息,说明一切都很顺利。
头顶冲入
在本节中,我们将创建一些快速脏代码来与 CouchDB 通信,然后讨论这种方法的一些问题。
向我们的注册脚本添加逻辑
在上一章中,我们在views/signup.php中创建了一个表单,具有以下功能:
-
我们要求用户在文本框中输入名称的值
-
我们获取了表单中输入的值并将其发布到注册路由
-
我们使用 Bones 来获取表单传递的值,并将其设置为名为
message的变量,以便我们可以在主页上显示它 -
我们呈现了主页并显示了
message变量
这是我们的一项重大工作,但我们无法保存任何东西以供以后阅读或写入。
让我们进一步采取一些步骤,并要求用户输入姓名和电子邮件地址,然后将这些字段保存为 CouchDB 中的文档。
行动时间-向注册表单添加电子邮件字段
让我们添加一个输入字段,以便用户可以在views/signup.php页面中输入电子邮件地址。
-
在文本编辑器中打开
signup.php(/Library/Webserver/Documents/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 />
**<label for="email">Email</label>
<input id="email" name="email" type="text"> <br />**
<input type="Submit" value="Submit">
</form>
刚刚发生了什么?
我们向注册表单添加了一个额外的字段,用于接受电子邮件地址的输入。通过向此表单添加email字段,我们将能够在表单提交时访问它,并最终将其保存为 CouchDB 文档。
使用 curl 调用将数据发布到 CouchDB
在以前的章节中,我们已经使用了终端通过curl与 CouchDB 进行交互。您会高兴地知道,您还可以通过 PHP 使用curl。为了在 CouchDB 中表示数据,我们首先需要将我们的数据转换为 JSON 格式。
行动时间-创建一个标准对象以编码为 JSON
让我们以 JSON 的形式表示一个简单的对象,以便 CouchDB 可以解释它。
在文本编辑器中打开index.php,并将以下代码添加到/signup POST路由中:
post('/signup', function($app) {
**$user = new stdClass;
$user->type = 'user';
$user->name = $app->form('name');
$user->email = $app->form('email');
echo json_encode($user);**
$app->set('message', 'Thanks for Signing Up ' . $app->form('name') . '!');
$app->render('home');
});
刚刚发生了什么?
我们添加了创建存储用户具体信息的对象的代码。我们使用了stdClass的一个实例,并将其命名为$user。stdClass是 PHP 的通用空类,对于匿名对象、动态属性和快速上手非常有用。因为文档要求应该设置一个类型来分类文档,我们将这个文档的类型设置为user。然后我们取自表单提交的值,并将它们保存为$user类的属性。最后,我们使用了一个名为json_encode的 PHP 函数,将对象转换为 JSON 表示形式。
让我们来测试一下。
-
在浏览器中打开
http://localhost/verge/signup。 -
在名称文本框中输入
John Doe,在电子邮件文本框中输入<john@example.com>。 -
点击提交。
-
您的浏览器将显示以下内容:
太好了!我们的表单已经正确提交了,并且我们能够在我们网站的顶部用 JSON 表示stdClass $user。
提交到 Git
让我们将我们的代码提交到 Git,这样我们以后可以回顾这段代码。
-
打开终端。
-
输入以下命令以更改目录到我们的工作目录:
**cd /Library/Webserver/Documents/verge/**
- 给 Git 一个描述,说明我们自上次提交以来做了什么:
**git commit am 'Added functionality to collect name and email through stdClass and display it onscreen.'**
现在我们已经用 JSON 表示了我们的数据,让我们使用一个curl语句来使用 PHP 创建一个 CouchDB 文档。
接下来的步骤——使用 PHP 和 curl 创建 CouchDB 文档
自本书开始以来,我们一直在使用命令行通过curl,但这次,我们将使用 PHP 触发一个curl语句。
- 让我们从初始化一个
curl会话开始,执行它,然后关闭它。在文本编辑器中打开index.php,并将以下代码添加到/signup POST路由中:
post('/signup', function($app) {
$user = new stdClass;
$user->type = 'user';
$user->name = $app->form('name');
$user->email = $app->form('email');
echo json_encode($user);
**$curl = curl_init();
// curl options
curl_exec($curl);
curl_close($curl);**
$app->set('message', 'Thanks for Signing Up ' . $app- >form('name') . '!');
$app->render('home');
});
- 现在,让我们告诉
curl实际要执行什么。我们使用一个options数组来做到这一点。在curl_init()和curl_exec语句之间添加以下代码:
post('/signup', function($app) {
$user = new stdClass;
$user->name = $app->form('name');
$user->email = $app->form('email');
echo json_encode($user);
$curl = curl_init();
// curl options
**$options = array(
CURLOPT_URL => 'localhost:5984/verge',
CURLOPT_POSTFIELDS => json_encode($user),
CURLOPT_HTTPHEADER => array ("Content-Type: application/json"),
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "utf-8",
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_AUTOREFERER => true
);
curl_setopt_array($curl, $options);**
curl_exec($curl);
curl_close($curl);
$app->set('message', 'Thanks for Signing Up ' . $app-> form('name') . '!');
$app->render('home');
});
刚刚发生了什么?
我们首先使用 PHP 初始化了一个curl会话,通过使用curl_init()资源设置了一个名为$curl的变量。然后我们创建了一个包含各种键和值的数组。我们选择所有这些选项的原因对我们现在来说并不太重要,但我想强调前三个对象:
-
我们将
CURLOPT_URL选项设置为我们要将文档保存到的数据库的 URL。请记住,此语句将使用 CouchDB 的 RESTful JSON API 在verge数据库中创建一个文档。 -
然后我们将
CURLOPT_POSTFIELDS设置为我们的$user的 JSON 编码值。这将把我们的 JSON 字符串作为数据与 URL 一起包含进去。 -
最后,我们将
CURLOPT_HTTPHEADER设置为array ("Content-Type: application/json"),以确保curl知道我们正在传递一个 JSON 请求。
设置了我们的选项数组之后,我们需要告诉我们的curl实例使用它:
curl_setopt_array($curl, $options);
然后我们用以下两行代码执行并关闭curl:
curl_exec($curl);
curl_close($curl);
有了这段代码,我们应该能够提交表单并将其发布到 CouchDB。让我们来测试一下。
-
在浏览器中打开
http://localhost/verge/signup。 -
在名称文本框中输入
John Doe,在电子邮件文本框中输入<john@example.com>。 -
点击提交。
-
您的浏览器将显示以下内容:
这次也没有出现任何错误,就像以前一样。但是这次应该已经创建了一个 CouchDB 文档。让我们通过 Futon 检查文档是否已经正确创建。
-
在浏览器中打开
http://localhost:5984/_utils/database.html?verge。这个直接链接将显示 verge 数据库。您会看到这里有一个新的文档!请记住,您的ID和rev将与我的不同: -
点击文档,以便您可以查看详细信息。
-
您文档中的数据应该与我们在
curl会话中传递的信息相匹配。请注意,type, email和name都已正确设置,CouchDB 为我们设置了_id和_rev。
将其提交到 Git
让我们将我们的代码提交到 Git,以便将来可以参考这段代码。
-
打开终端。
-
键入以下命令以更改目录到我们的工作目录:
cd /Library/Webserver/Documents/verge/
- 向 Git 描述我们自上次提交以来所做的工作:
git commit am 'CouchDB Documents can now be created through the signup form using curl.'
我们刚刚看了使用 PHP 创建 CouchDB 文档的最简单的方法之一。然而,我们需要评估我们刚刚编写的代码是否可持续,并且是否是我们开发应用程序的明智方式。
这种技术足够好吗?
棘手的问题。从技术上讲,我们可以以这种方式构建我们的应用程序,但我们需要添加更多的代码,并花费本书的其余时间重构我们对curl的调用,直到它完美运行。然后,我们需要花大量时间将我们的调用重构为一个简单的库,以便更容易修复问题。简而言之,这种技术不起作用,因为我们想专注于构建我们的应用程序,而不是解决 PHP 和 CouchDB 之间的所有通信问题。幸运的是,有各种各样的 CouchDB 库可以简化我们的开发过程。
可用的 CouchDB 库
有各种库可以在使用 PHP 和 CouchDB 开发时使我们的生活更轻松。所有这些库都是开源项目,这很棒!但是,其中一些库已经不再积极开发以支持较新版本的 CouchDB。因此,我们需要选择要使用的库。
一些 PHP 和 CouchDB 库的列表可以在这里看到:wiki.apache.org/couchdb/Getting_started_with_PHP,还有一些其他的库托管在 GitHub 上,需要更深入挖掘。
每个库都有其优势,但由于简单是 Bones 的关键概念,因此在我们的 PHP 库中也应该追求简单。说到这一点,我们最好的解决方案就是Sag。
Sag
Sag 是由 Sam Bisbee 创建的用于 CouchDB 的出色的 PHP 库。Sag 的指导原则是简单,创建一个功能强大的接口,几乎没有额外开销,可以轻松集成到任何应用程序结构中。它不强制您的应用程序使用框架、文档的特殊类或 ORM,但如果您愿意,仍然可以使用。Sag 接受基本的 PHP 数据结构(对象、字符串等),并返回原始 JSON 或响应和对象中的 HTTP 信息。
我将为您介绍 Sag 的安装和基本功能,但您也可以访问 Sag 的网站:www.saggingcouch.com/,了解示例和文档。
下载并设置 Sag
Sag 相当不显眼,将完全适应我们当前的应用程序结构。我们只需要使用 Git 从其 GitHub 存储库中获取 Sag,并将其放在我们的lib目录中。
采取行动——使用 Git 安装 Sag
Git 使设置第三方库变得非常容易,并允许我们在可用时更新到新版本。
-
打开终端。
-
键入以下命令以确保您在工作目录中:
**cd /Library/Webserver/Documents/verge/**
- 使用 Git 将 Sag 添加到我们的存储库:
**git submodule add git://github.com/sbisbee/sag.git lib/sag
git submodule init**
刚刚发生了什么?
我们使用 Git 使用git submodule add将 Sag 添加到我们的项目中,然后通过键入git submodule init来初始化子模块。Git 的子模块允许我们在我们的存储库中拥有一个完整的 Git 存储库。每当 Sag 发布新版本时,您可以运行git submodule update,您将收到最新和最棒的代码。
将 Sag 添加到 Bones
为了使用 Sag,我们将在Bones中添加几行代码,以确保我们的库可以看到并利用它。
行动时间-将 Sag 添加到 Bones
启用并设置 Sag 与Bones一起工作非常容易。让我们一起来看看!
- 打开我们的工作目录中的
lib/bones.php,并在我们的类顶部添加以下行:
<?php
define('ROOT', __DIR__ . '/..');
**require_once ROOT . '/lib/sag/src/Sag.php';**
- 我们需要确保 Sag 已准备好并在每个请求中可用。让我们通过在
Bones中添加一个名为$couch的新变量,并在我们的__construct函数中设置它来实现这一点:
public $route_segments = array();
public $route_variables = array();
**public $couch;**
public function __construct() {
$this->route = $this->get_route();
$this->route_segments = explode('/', trim($this->route, '/'));
$this->method = $this->get_method();
**$this->couch = new Sag('127.0.0.1', '5984');
$this->couch->setDatabase('verge');**
}
刚刚发生了什么?
我们确保Bones可以访问和使用 Sag,通过使用require_once加载 Sag 资源。然后,我们确保每次构造Bones时,我们都会定义数据库服务器和端口,并设置我们要使用的数据库。
注意
请注意,我们与Verge数据库交互时不需要任何凭据,因为我们尚未对此数据库设置任何权限。
使用 Sag 简化我们的代码
在我们的应用程序中包含 Sag 后,我们可以简化我们的数据库调用,将处理和异常处理交给 Sag,并专注于构建我们的产品。
行动时间-使用 Sag 创建文档
现在我们已经在应用程序中随处可用并准备好使用 Sag,让我们重构放置在/signup post路由中的用户类的保存。
打开index.php,删除我们在之前部分添加的所有额外代码,这样我们的/signup post路由看起来类似于以下代码片段:
post('/signup', function($app) {
$user = new stdClass;
$user->name = $app->form('name');
$user->email = $app->form('email');
**$app->couch->post($user);**
$app->set('message', 'Thanks for Signing Up ' . $app->form('name') . '!');
$app->render('home');
});
刚刚发生了什么?
我们使用 Sag 创建了一个到我们的 CouchDB 数据库的帖子,使用的代码大大减少了!Sag 的 post 方法允许您传递数据,因此触发起来非常容易。
让我们快速通过注册流程:
-
打开浏览器,输入
http://localhost/verge/signup。 -
在名称文本框中输入一个新名称,然后在电子邮件文本框中输入一个新电子邮件。
-
点击提交。
在 CouchDB 中创建了一个新文档,让我们检查一下 Futon,确保它在那里:
-
打开浏览器,输入
http://localhost:5984/_utils/database.html?verge,查看 verge 数据库。 -
点击列表中的第二个文档。
-
查看这个新文档的详细信息,您会发现它与我们制作的第一个文档具有相同的结构。
完美!结果与我们快速而肮脏的 curl 脚本完全一样,但我们的代码更简化,Sag 在幕后处理了很多事情。
注意
目前我们没有捕获或处理任何错误。我们将在以后的章节中更多地讨论如何处理这些错误。幸运的是,CouchDB 以友好的方式处理错误,并且 Sag 已经确保了很容易追踪问题。
添加更多结构
我们可以如此轻松地创建文档,这很棒,但对于我们的类来说,有一个强大的结构也很重要,这样我们可以保持有条理。
行动时间-包括类目录
为了我们能够使用我们的类,我们需要在Bones中添加一些代码,以便我们可以在使用时自动加载类名。这将实现这一点,这样我们就不必在添加新类时不断包含更多文件。
将以下代码添加到lib/bones.php:
<?php
define('ROOT', __DIR__ . '/..');
**require_once ROOT . '/lib/sag/src/Sag.php';
function __autoload($classname) {
include_once(ROOT . "/classes/" . strtolower($classname) . ".php");
}**
刚刚发生了什么?
我们在我们的Bones库中添加了一个__autoload函数,如果找不到类,它将给 PHP 最后一次尝试加载类名。__autoload函数传递了$classname,我们使用$classname来找到命名类的文件。我们使用strtolower函数使请求的$classname变成小写,这样我们就可以找到命名文件。然后我们添加了工作目录的根目录和classes文件夹。
使用类
现在我们有了加载类的能力,让我们创建一些!我们将从创建一个基类开始,所有其他类都将继承它的属性。
行动时间-创建一个基本对象
在这一部分,我们将创建一个名为base.php的基类,所有我们的类都将继承它。
-
让我们从创建一个名为
base.php的新文件开始,并将其放在工作目录内的/Library/Webserver/Documents/verge/classes/base.php文件夹中。 -
在
base.php中创建一个带有__construct函数的抽象类。在对象的__construct中,让我们将$type作为一个选项,并将其设置为一个受保护的变量,也称为$type。
<?php
abstract class Base
{
protected $type;
public function __construct($type)
{
$this->type = $type;
}
}
- 为了方便以后在我们的类中获取和设置变量,让我们在
__construct函数之后添加__get()和__set()函数。
<?php
abstract class Base
{
protected $type;
public function __construct($type)
{
$this->type = $type;
}
**public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}**
}
- 每次我们将对象保存到 Couch DB 时,我们希望能够将其表示为 JSON 字符串。因此,让我们创建一个名为
to_json()的辅助函数,它将把我们的对象转换成 JSON 格式。
<?php
abstract class Base
{
protected $type;
public function __construct($type)
{
$this->type = $type;
}
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
**public function to_json() {
return json_encode(get_object_vars($this));
}**
}
刚刚发生了什么?
我们创建了一个名为base.php的基类,它将作为我们构建的所有其他类的基础。在类内部,我们定义了一个受保护的变量$type,它将存储文档的分类,如user或post。接下来,我们添加了一个__construct函数,它将在每次创建对象时被调用。这个函数接受选项$type,我们将在每个继承Base的类中设置它。然后,我们创建了__get和__set函数。__get和__set被称为魔术方法,它们将允许我们使用get和set受保护的变量,而无需任何额外的代码。最后,我们添加了一个名为to_json的函数,它使用get_object_vars和json_encode来表示我们的对象为 JSON 字符串。在我们的基类中做这样的小事情将使我们未来的生活变得更加轻松。
时间来行动了——创建一个 User 对象
现在我们已经创建了我们的Base类,让我们创建一个User类,它将包含与用户相关的所有属性和函数。
-
创建一个名为
user.php的新文件,并将其放在base.php所在的classes文件夹中。 -
让我们创建一个继承我们
Base类的类。
<?php
class User extends Base
{
}
- 让我们添加我们已经知道需要的两个属性
name和email到我们的User类中。
<?php
class User extends Base
{
**protected $name;
protected $email;**
}
- 让我们添加一个
__construct函数,告诉我们的Base类,在创建时我们的文档类型是user。
<?php
class User extends Base
{
protected $name;
protected $email;
**public function __construct()
{
parent::__construct('user');
}**
}
刚刚发生了什么?
我们创建了一个简单的类user.php,它继承了Base。继承意味着它将继承可用的属性和函数,以便我们可以利用它们。然后,我们包括了两个受保护的属性$name和$email。最后,我们创建了一个__construct函数。在这种情况下,构造函数告诉父类(即我们的Base类),文档的类型是user。
时间来行动了——插入 User 对象
有了我们的新的User对象,我们可以轻松地将其插入到我们的应用程序代码中,然后就可以运行了。
- 打开
index.php文件,将stdClass改为User()。与此同时,我们还可以移除$user->type = 'user',因为现在这个问题已经在我们的类中处理了:
post('/signup', function($app) {
**$user = new User();**
$user->name = $app->form('name');
$user->email = $app->form('email');
$app->couch->post($user);
}
- 调整 Sag 的
post语句,以便我们可以以 JSON 格式传递我们的类。
post('/signup', function($app) {
$user = new User();
$user->name = $app->form('name');
$user->email = $app->form('email');
**$app->couch->post($user->to_json);**
}
刚刚发生了什么?
我们用User()替换了stdClass的实例。这将使我们完全控制获取和设置变量。然后,我们移除了$user->type = 'user',因为我们的User和Base对象中的__construct函数已经处理了这个问题。最后,我们添加了之前创建的to_json()函数,这样我们就可以将我们的对象作为 JSON 编码的字符串发送出去。
注意
Sag 在技术上可以自己处理一个对象的 JSON,但重要的是我们能够从我们的对象中检索到一个 JSON 字符串,这样你就可以以任何你想要的方式与 CouchDB 交互。将来可能需要回来使用curl或另一个库重写所有内容,所以重要的是你知道如何表示你的数据为 JSON。
测试一下
让我们快速再次通过我们的注册流程,确保一切仍然正常运行:
-
打开浏览器到
http://localhost/verge/signup。 -
在名称文本框中输入一个新名称,在电子邮件文本框中输入一个新电子邮件。
-
点击提交。
在 CouchDB 中应该已经创建了一个新文档。让我们检查 Futon,确保它在那里:
-
打开浏览器到
http://localhost:5984/_utils/database.html?verge查看 verge 数据库。 -
点击列表中的第三个文档
-
查看这个新文档的细节,你会发现它和我们制作的前两个文档结构相同。
完美!一切都和以前一样,但现在我们使用了一个更加优雅的解决方案,我们将能够在未来的章节中构建在其基础上。
提交到 Git
让我们把代码提交到 Git,这样我们就可以追踪我们到目前为止的进展:
-
打开终端。
-
输入以下命令以更改目录到我们的工作目录:
**cd /Library/Webserver/Documents/verge/**
- 我们在
classes文件夹中添加了一些新文件。所以,让我们确保将这些文件添加到 Git 中。
**git add classes/***
- 给 Git 一个描述,说明我们自上次提交以来做了什么:
**git commit am 'Added class structure for Users and tested its functionality'**
通过使用classes/*语法,我们告诉 Git 添加 classes 文件夹中的每个文件。当你添加了多个文件并且不想逐个添加每个文件时,这很方便。
总结
我们已经完成了这一章的代码。定期将代码推送到 GitHub 是一个很好的做法。事实上,当你与多个开发人员一起工作时,这是至关重要的。我不会在这本书中再提醒你这样做。所以,请确保经常这样做:
**git push origin master**
这行代码读起来像一个句子,如果你在其中加入一些词。这句话告诉 Git 要push到origin(我们已经定义为 GitHub),并且我们要发送master分支。
总结
希望你喜欢这一章。当所有这些技术一起工作,让我们能够轻松地保存东西到 CouchDB 时,这是很有趣的。
让我们回顾一下这一章我们谈到的内容:
-
我们看了几种不同的方法,我们可以用 PHP 与 CouchDB 交流
-
我们将 Sag 与 Bones 联系起来
-
我们建立了一个面向对象的类结构,这将为我们节省很多麻烦
-
我们测试了一下,确保当我们提交我们的注册表单时,CouchDB 文档被创建了
在下一章中,我们将积极地研究 CouchDB 已经为我们的用户提供的一些很棒的功能,以及我们如何使用 CouchDB 来构建大多数应用程序都具有的标准注册和登录流程。伸展你的打字手指,准备一大杯咖啡,因为我们即将开始真正的乐趣。
第六章:建模用户
信不信由你,我们已经做了很多工作,使我们与 CouchDB 的交互变得简单。在本章中,我们将直接进入 CouchDB 的核心,并开始对用户文档进行建模。
更具体地说,我们将:
-
安装 Bootstrap,这是 Twitter 的一个工具包,将处理 CSS、表单、按钮等繁重的工作
-
仔细观察 CouchDB 默认存储用户文档的方式以及我们如何向其中添加字段
-
为用户添加基本功能,以便他们可以在我们的应用程序中注册、登录和注销
-
学习如何处理异常和错误
这将是我们迄今为止最有价值的一章;您将喜欢将一些标准的身份验证和安全性外包给 CouchDB。系好安全带。这将是一次有趣的旅程!
在我们开始之前
我们已经玩弄了很多文件来测试 Bones 和 Sag,但您会注意到我们的应用程序看起来仍然相当空旷。所以,让我们稍微改善一下设计。由于设计和实现 UI 并不是本书的目的,我们将使用一个名为Bootstrap的工具包来为我们做大部分工作。Bootstrap (twitter.github.com/bootstrap/)是由 Twitter 创建的,旨在启动 Web 应用程序和网站的开发。它将使我们能够轻松进行前端开发而不需要太多工作。让我们先让 Bootstrap 运行起来,然后对我们的布局进行一些整理。
通过安装 Bootstrap 来清理我们的界面
设置 Bootstrap 非常容易。我们可以引用它们的远程服务器上的 CSS,但我们将下载并在本地调用 CSS,因为最佳实践是减少外部调用的数量。
执行时间-本地安装 Bootstrap
安装 Bootstrap 非常简单;我们将在本节中介绍安装它的基础知识。
-
打开您的浏览器,转到
twitter.github.com/bootstrap/。 -
点击下载 Bootstrap。
-
一个
.zip文件将被下载到您的downloads文件夹中;双击它或使用您喜欢的解压工具解压它。 -
您将在
bootstrap文件夹中找到三个目录,分别是css, img和js,每个目录中都包含若干文件。 -
将这些文件夹中的所有文件复制到您的
verge项目的相应文件夹中:/public/css, public/img和public/js。完成后,您的verge目录应该类似于以下屏幕截图:
刚刚发生了什么?
我们刚刚通过下载一个包含所有资产的.zip文件并将它们放在本地机器的正确文件夹中,将 Twitter 的 Bootstrap 安装到我们的项目中。
仅仅通过查看我们项目中的新文件,您可能会注意到每个文件似乎出现了两次,一个带有文件名中的min,一个没有。这两个文件是相同的,除了包含min在文件名中的文件已经被压缩。压缩意味着从代码中删除所有非必要的字符以减小文件大小。删除的字符包括空格、换行符、注释等。因为这些文件是从网站上按需加载的,所以它们尽可能小以加快应用程序的速度是很重要的。如果您尝试打开一个压缩文件,通常很难看出发生了什么,这没关系,因为我们一开始就不想对这些文件进行任何更改。
所有这些文件的作用可能很明显——css文件定义了 Bootstrap 的一些全局样式。img文件用于帮助我们在网站周围使用图标,如果我们愿意的话,js文件用于帮助我们为网站添加交互、过渡和效果。但是,在css文件夹中,有bootstrap和bootstrap-responsive两个 css 文件,这可能会让人感到困惑。响应式设计是近年来真正爆发的东西,本身已经有很多书籍写到了这个主题。简而言之,bootstrap包括了bootstrap-responsive文件中的样式,以帮助我们的网站在不同的分辨率和设备上工作。因此,我们的网站应该可以在 Web 和现代移动设备上正常工作(大部分情况下)。
现在,你可能能够理解为什么我选择使用 Bootstrap 了;我们只需复制文件到我们的项目中,就获得了很多功能。但是,还没有完全连接好;我们需要告诉我们的layout.php文件去哪里查找,以便它可以使用这些新文件。
采取行动——包括 Bootstrap 并调整我们的布局以适应它
因为 Bootstrap 框架只是一系列文件,所以我们可以像在第四章中处理master.css文件一样轻松地将其包含在我们的项目中,
- 在
layout.php文件中,在master.css之前添加一个链接到bootstrap.min.css和bootstrap-responsive.min.css:
<head>
**<link href="<?php echo $this->make_route('/css/bootstrap.min.css') ?>" rel="stylesheet" type="text/css" />
<link href="<?php echo $this->make_route('/css/master.css') ?>" rel="stylesheet" type="text/css" />
<link href="<?php echo $this->make_route('/css/bootstrap-responsive.min.css') ?>" rel="stylesheet" type="text/css" />**
</head>
- 接下来,让我们确保 Bootstrap 在较旧版本的 Internet Explorer 和移动浏览器中能够良好运行,通过添加以下一小段代码:
<link href="<?php echo $this->make_route('/css/bootstrap- responsive.min.css') ?>" rel="stylesheet" type="text/css" />
**<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js">
</script>
<![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">**
</head>
- 通过以下内容替换
views/layout.php文件的内容,为我们的应用程序创建一个干净简单的包装:
<body>
**<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data- target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="<?php echo $this->make_route('/') ?>">Verge</a>
<div class="nav-collapse">
<ul class="nav">
<li><a href="<?php echo $this->make_route('/') ?>">
Home
</a></li>
<li>
<a href="<?php echo $this->make_route('/signup') ?>">Signup</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="container">
<?php include($this->content); ?>
</div>**
</body>
- 删除
master.css文件的内容,并用以下内容替换,对我们的布局进行一些小的调整:
.page-header {margin-top: 50px;}
input {height: 20px;}
刚刚发生了什么?
我们在layout.php文件中包含了 Bootstrap,并确保了 Internet Explorer 的版本可以正常工作,通过添加了许多开发人员使用的 HTML5 shim。如果你想了解更多关于这是如何工作的,可以随时访问html5shim.googlecode.com/。
接下来,我们添加了一些 HTML 来符合 Bootstrap 中定义的 CSS。你不需要太在意 HTML 为什么设置成这样,但如果你好奇的话,你可以参考 Bootstrap 的主页了解更多(twitter.github.com/bootstrap/)。然后,我们在main.css文件中添加了一些规则,以在 Bootstrap 的基础上添加额外的样式。我这样做是为了在我们的应用程序中创造一些空间,使事情不会杂乱。
如果你现在去首页localhost/verge/,标题看起来确实很酷,但首页需要一些爱。让我们快速清理一下首页。
采取行动——装饰首页
Bootstrap 将再次为我们节省一些真正的时间;我们只需要一点 HTML 标记,我们的应用程序就会看起来相当不错!用以下内容替换views/home.php的内容:
<div class="hero-unit">
<h1>Welcome to Verge!</h1>
<p>Verge is a simple social network that will make you popular.</p>
<p>
<a href="<?php echo $this->make_route('/signup') ?>" class="btn btn-primary btn-large">
Signup Now
</a>
</p>
</div>
刚刚发生了什么?
我们刚刚为我们的首页添加了一个漂亮简洁的布局,还有一个按钮,提示人们在来到我们的网站时注册。请注意,我们从文件中删除了<? php echo $message; ?>,当我们最初添加它来向用户显示简单的消息时,但我们将在本章后面探索更清晰的方法。
准备看到一些魔法吗?打开你的浏览器,转到localhost/verge/。
我们几乎没有花费任何时间在设计上,但我们已经有了一个更友好的应用程序。当我们深入处理用户时,这种新设计将会派上用场。
准备看到一些很酷的东西吗?试着把浏览器窗口缩小,看看会发生什么。
注意内容如何根据屏幕大小调整;这意味着在移动设备上,您的应用程序将调整以便轻松查看。Bootstrap 的响应式样板代码只是一个开始。您可以选择根据浏览器的大小来显示和隐藏内容。
浏览器窗口变小后,您会注意到导航栏也被压缩了,而不是看到您的链接,您会看到一个有三条杠的按钮。尝试点击它...什么也没有发生!
这个组件需要 Bootstrap 的 JavaScript 文件,以及一个名为jQuery的 JavaScript 库。我们现在还没有必要让这一切都工作,所以让我们在下一章回来吧!
将所有用户文件移动到用户文件夹中
我们的应用程序在这一部分将开始大幅增长。如果我们继续像现在这样随意地把文件扔来扔去,我们的视图将变得非常混乱。让我们进行一些整理工作,并为我们的views目录添加一些结构。
行动时间 - 组织我们的用户视图
随着我们继续为我们的应用程序创建视图,对我们来说很聪明的是要有一些组织,以确保我们保持事情简单明了。
-
在
views目录中创建一个名为user的文件夹。 -
将现有的
signup.php视图移动到这个文件夹中。结果的目录结构将类似于以下截图: -
我们需要更新
index.php并让它知道在哪里找到我们刚刚移动的注册视图:
get('/signup', function($app) {
**$app->render('user/signup');**
});
刚刚发生了什么?
我们通过创建一个user文件夹来清理我们的views文件夹结构,将所有与用户相关的视图放入其中。然后我们将现有的signup.php文件移动到user文件夹,并告诉我们的index.php文件在哪里找到user/signup.php文件。请注意,注册页面的路由/signup并没有改变。
设计我们的用户文档
我们在第三章中已经看到了 CouchDB 如何查看用户文档。在本章中,我们将学习如何利用现有的 CouchDB 功能,并在其上添加一些额外的字段。
CouchDB 如何查看基本用户文档
CouchDB 已经有一个存储用户文档的机制,我们已经看到并使用过。我们将使用相同的结构来处理我们应用程序的用户:
{
"_id": "org.couchdb.user:your_username",
"_rev": "1-b9af54a7cdc392c2c298591f0dcd81f3",
"name": "your_username",
"password_sha": "3bc7d6d86da6lfed6d4d82e1e4d1c3ca587aecc8",
"roles": [],
"salt": "9812acc4866acdec35c903f0cc072c1d",
"type": "user"
}
这七个字段是 CouchDB 要求用户在 CouchDB 中正确操作所必需的:
-
_id是用户的唯一标识符。它需要以org.couchdb.user:开头,并以name属性的相同值结尾。这些角色由_auth设计文档强制执行。我们还没有太多讨论设计文档。但是,此时,您需要知道设计文档是直接在数据库中运行的代码。它们可以用于强制执行验证和业务角色。 -
_rev是文档的修订标识符。我们在第三章中快速涉及了修订。 -
name是用户的用户名。这个字段是_auth设计文档所必需的,并且它还需要与冒号后文档的_id的值匹配。 -
password_sha是密码与salt组合后进行 SHA-1 加密的值。我们稍后会介绍 SHA-1 加密。 -
password_sha是密码与salt组合后进行 SHA-1 加密的值。我们稍后会介绍 SHA-1 加密。 -
roles是用户可能拥有的特权数组。通过具有[]的值,我们知道这个用户没有特权。 -
salt是用户的唯一salt。salt与密码的明文值组合,并通过 SHA-1 加密得到password_sha的值。 -
type是 CouchDB 用来标识文档类型的标识符。请记住,CouchDB 是一个扁平的文档存储。这个type字段标识了文档的分类。
这些用户文档是独特的,因为它们需要一定结构,但我们总是可以向其添加额外字段。让我们接着做吧!
向用户文档添加更多字段
让我们谈谈一些额外的字段,我们知道我们将想要从 Verge 的用户那里收集信息。请记住,如果您的应用程序需要,您总是可以添加更多字段。
-
用户名: 我们知道我们将想要存储一个唯一的用户名,这样我们的用户将拥有一个唯一的 URL,例如
/user/johndoe。幸运的是,这个功能已经由 CouchDB 的name字段处理了。考虑到这一点,这里没有什么要做的。我们只需使用现有的name即可! -
全名: 用户的全名,这样我们就可以显示用户的名称为
John Doe。这将是一个用户友好的名称,我们可以用来展示给访问用户,我们需要向文档中添加一个字段来支持这一点。 -
电子邮件: 电子邮件地址,以便我们可以与用户进行通信,例如通知电子邮件:
<john@example.com>。实际上,我们已经在当前类中保存了电子邮件,所以我们也可以忽略这一点。
听起来很容易;我们只需要添加一个字段!每当您向文档添加新字段时,您都应该考虑如何格式化它。让我们讨论一下我们可以采用的 CouchDB 的不同方法。
讨论添加这些字段的选项
我们可能会使用各种方法来在 CouchDB 的基本用户文档上添加字段:
-
我们可以创建一个新类型的文档,称之为
verge_user。这个文档将包含我们在应用程序中需要的任何额外用户属性,然后将引用回用户文档。 -
我们可以在用户文档内创建一个数组,其中包含特定于应用程序的属性,并将所有用户属性添加到其中。
-
或者我们可以只是在用户文档内添加这两个新字段。
我认为,目前我们可以一致同意通过添加一个字段来选择最后提到的选项。
考虑到这一点,我们的最终文档将类似于以下内容:
{
"_id": "org.couchdb.user:johndoe",
"_rev": "1-b9af54a7cdc392c2c298591f0dcd81f3",
"name": "johndoe",
"full_name": "John Doe",
"email": "john@example.com",
"password_sha": "3bc7d6d86da6lfed6d4d82e1e4d1c3ca587aecc8",
"roles": [],
"salt": "9812acc4866acdec35c903f0cc072c1d",
"type": "user"
}
您可能会觉得在许多地方看到用户名称的变化很奇怪:_id、name和full_name。但请记住,CouchDB 有充分的理由这样做。通过将用户名存储在_id中,CouchDB 将自动检查每个用户名是否唯一。
注意
请记住,如果我们想要开始存储诸如网站、传记或位置等字段,我们可能会想要更有创意。我们将在本书后面更详细地讨论这个问题。
添加对额外字段的支持
为了向用户文档中添加这些字段,我们不需要在代码中做太多更改;我们只需要在user.php类中添加一些变量即可。
采取行动-添加字段以支持用户文档
我们已经在classes/user.php文件中设置了用户文档的基本结构,但让我们继续添加一些字段。
- 我们目前没有在任何项目中设置
_id,但我们需要为我们的用户文档这样做。让我们打开classes/base.php,并添加_id,这样我们就有了在任何文档上设置_id的选项。
<?php
abstract class Base {
**protected $_id;**
protected $type;
- 我们需要将我们刚刚讨论的所有用户字段添加到
classes/user.php文件中,以及一些其他字段。将以下代码添加到classes/user.php中,使其看起来如下:
<?php
class User extends Base {
protected $name;
protected $email;
**protected $full_name;
protected $salt;
protected $password_sha;
protected $roles;**
刚刚发生了什么?
我们添加了所有需要保存用户文档到系统中的字段。我们在base.php类中添加了_id,因为我们知道每个 CouchDB 文档都需要这个字段。到目前为止,我们已经能够在没有_id的情况下生活,因为 CouchDB 自动为我们设置了一个。然而,在本章中,我们需要能够设置和检索我们的用户文档的_id。然后,我们添加了full_name和其他一些可能让您感到困惑的字段。$salt和$password_sha用于安全存储密码。这个过程通过一个例子更容易解释,所以我们将在我们的注册过程中详细介绍这个过程。最后,我们添加了角色,在本书中将为空,但对于您开发基于角色的系统可能会有用,允许某些用户能够看到应用程序的某些部分等。
现在我们已经定义了用户结构,我们需要走一遍注册过程,这比我们迄今为止所做的 CouchDB 文档创建要复杂一些。
注册过程
现在我们已经支持用户类中的所有字段,让我们为用户注册 Verge 添加支持。注册是一个有点复杂的过程,但我们将尝试逐步分解它。在本节中,我们将:
-
定义我们的数据库管理员用户名和密码,以便我们可以创建新的用户文档
-
创建一个新的注册界面来支持我们添加的所有字段
-
添加一个 Bootstrap 助手,使创建表单输入更容易
-
开发一个快速而简单的注册过程的实现
-
深入了解我们密码的 SHA-1 加密
-
重构我们的注册过程,使其更加结构化
一点管理员设置
在第三章中,我们锁定了our _users数据库,这样我们就可以保护我们的用户数据,这意味着每当我们处理_users数据库时,我们需要提供管理员登录。为此,我们将在index.php文件的顶部添加用户和密码的 PHP 常量,以便我们在需要执行管理员功能时随时引用它。如果这看起来混乱,不要担心;我们将在本书的后面整理这一点。
<?php
include 'lib/bones.php';
**define('ADMIN_USER', 'tim');
define('ADMIN_PASSWORD', 'test');**
更新界面
如果您现在打开浏览器并转到http://localhost/verge/signup,您会注意到它与我们的新 Bootstrap 更改不符。实际上,您可能甚至看不到所有的输入框!让我们使用 Bootstrap 来帮助清理我们的注册界面,使其看起来正确。
- 用以下 HTML 代码替换
views/user/signup.php页面的所有内容:
<div class="page-header">
<h1>Signup</h1>
</div>
<div class="row">
<div class="span12">
<form class="form-vertical" action="<?php echo $this- >make_route('/signup') ?>" method="post">
<fieldset>
<label for="full_name">Full Name</label>
<input class="input-large" id="full_name" name="full_name" type="text" value="">
<label for="email">Email</label>
<input class="input-large" id="email" name="email" type="text" value="">
<div class="form-actions">
<button class="btn btn-primary">Sign Up!</button>
</div>
</fieldset>
</form>
</div>
</div>
- 刷新注册页面,您将看到我们的表单现在很棒!
- 我们的表单看起来很干净。但是,让我们诚实点,随着我们添加更多字段,为输入字段添加代码将开始变得痛苦。让我们创建一个小的辅助类,帮助我们创建一个可以与 Bootstrap 很好地配合的 HTML 标记:
-
在
lib目录中创建一个名为bootstrap.php的新文件。 -
在
bones.php中引用lib/bootstrap.php。
define('ROOT', __DIR__ . '/..');
**require_once ROOT . '/lib/bootstrap.php';**
require_once ROOT . '/lib/sag/src/Sag.php';
- 打开
lib/bootstrap.php,并创建一个基本类。
<?php
class Bootstrap {
}
- 我们将创建一个名为
make_input的函数,它将接受四个参数:$id, $label, $type和$value。
<?php
class Bootstrap {
**public static function make_input($id, $label, $type, $value = '') {
echo '<label for="' . $id . '">' . $label . '</label> <input class="input-large" id="' . $id . '" name="' . $id . '" type="' . $type . '" value="' . $value . '">';
}**
}
- 返回到
views/user/signup.php,并简化代码以使用新的make_input函数。
<div class="page-header">
<h1>Signup</h1>
</div>
<div class="row">
<div class="span12">
<form action="<?php echo $this->make_route('/signup') ?>" method="post">
<fieldset>
**<?php Bootstrap::make_input('full_name', 'Full Name', 'text'); ?>
<?php Bootstrap::make_input('email', 'Email', 'text'); ?>**
<div class="form-actions">
<button class="btn btn-primary">Sign Up!</button>
</div>
</fieldset>
</form>
</div>
</div>
- 现在我们有了
lib/bootstrap.php来让我们的生活更轻松,让我们向用户询问另外两个字段:username和password。
<fieldset>
<?php Bootstrap::make_input('full_name', 'Full Name', 'text'); ?>
<?php Bootstrap::make_input('email', 'Email', 'text'); ?>
**<?php Bootstrap::make_input('username', 'Username', 'text'); ?>
<?php Bootstrap::make_input('password', 'Password', 'password'); ?>**
<div class="form-actions">
<button class="btn btn-primary">Sign Up!</button>
</div>
</fieldset>
- 刷新您的浏览器,您会看到一个大大改进的注册表单。如果它看起来不像下面的截图,请检查您的代码是否与我的匹配。
我们的表单看起来很棒!不幸的是,当您点击**注册!**时,它实际上还没有注册用户。让我们在下一节中改变这一点。
快速而简单的注册
现在,我们将直接将用户注册代码写入index.php。我们将多次重构此代码,并在本章结束时,将大部分注册功能移至classes/user.php文件。
行动时间-处理简单用户注册
让我们逐步进行注册过程,在此过程中,我们将从头开始重建注册POST路由中的代码。我会逐步解释每段代码,然后在本节结束时进行全面回顾。
- 打开
index.php,并开始收集简单字段:full_name, email和roles。full_name和email字段将直接来自表单提交,roles我们将设置为空数组,因为此用户没有特殊权限。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name');
$user->email = $app->form('email');
$user->roles = array();
- 接下来,我们将捕获用户提交的用户名,但我们希望防止奇怪的字符或空格,因此我们将使用正则表达式将提交的用户名转换为不带任何特殊字符的小写字符串。最终结果将作为我们的
name字段,也将作为 ID 的一部分。请记住,用户文档要求_id必须以org.couchdb.user开头,并以用户的name结尾。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
**$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;**
- 为了加密用户输入的明文密码值,我们将临时设置一个字符串作为
salt的值。然后,我们将明文密码传递给 SHA-1 函数,并将其保存在password_sha中。我们将在接下来的几分钟内深入了解 SHA-1 的工作原理。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;
**$user->salt = 'secret_salt';
$user->password_sha = sha1($app->form('password') . $user- >salt);**
- 为了保存用户文档,我们需要将数据库设置为
_users,并以我们在 PHP 常量中设置的管理员用户身份登录。然后,我们将使用 Sag 将用户放入 CouchDB。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;
$user->salt = 'secret_salt';
$user->password_sha = sha1($app->form('password') . $user- >salt);
**$app->couch->setDatabase('_users');
$app->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$app->couch->put($user->_id, $user->to_json());**
- 最后,让我们关闭用户注册功能并呈现主页。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;
$user->salt = 'secret_salt';
$user->password_sha = sha1($app->form('password') . $user- >salt);
$app->couch->setDatabase('_users');
$app->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$app->couch->put($user->_id, $user->to_json());
**$app->render('home');**
});
刚刚发生了什么?
我们刚刚添加了代码来设置 CouchDB 用户文档的所有值。收集full_name, email和roles的值非常简单;我们只需从提交的表单中获取这些值。设置name变得更加复杂,我们将用户名的提交值转换为小写字符串,然后使用**正则表达式(Regex)**函数将任何特殊字符更改为空字符。有了干净的名称,我们将其附加到org.couchdb.user并保存到文档的_id中。哇!这真是一大堆。
迅速进入加密世界,我们设置了一个静态(非常不安全的)salt。将salt与明文密码结合在 SHA-1 函数中,得到了一个加密密码,保存在我们对象的password_sha字段中。接下来,我们使用setDatabase设置了 Sag 的数据库,以便我们可以与 CouchDB 的_users数据库进行通信。为了与用户进行通信,我们需要管理员凭据。因此,我们使用ADMIN_USER和ADMIN_PASSWORD常量登录到 CouchDB。最后,我们使用 HTTP 动词PUT在 CouchDB 中创建文档,并为用户呈现主页。
让我们测试一下,看看当我们提交注册表单时会发生什么。
-
在浏览器中打开注册页面,访问
http://localhost/verge/signup。 -
填写表格,将全名设置为
John Doe,电子邮件设置为<john@example.com>,用户名设置为johndoe,密码设置为temp123。完成后,点击注册! -
您的用户已创建!让我们通过访问
http://localhost:5984/_utils,并查看_users数据库的新文档。 -
完美,一切应该已经保存正确!查看完毕后,点击删除文档删除用户。如果您当前未以管理员用户身份登录,您需要先登录,然后 CouchDB 才允许您删除文档。
我让您删除用户,因为如果每个用户的“盐”等于secret_salt,我们的密码实际上就是明文。为了让您理解为什么会这样,让我们退一步看看 SHA-1 的作用。
SHA-1
在安全方面,存储明文密码是最大的禁忌之一。因此,我们使用 SHA-1 (en.wikipedia.org/wiki/SHA-1)来创建加密哈希。SHA-1 是由国家安全局(NSA)创建的加密哈希函数。SHA-1 的基本原理是我们将密码与盐结合在一起,使我们的密码无法辨认。盐是一串随机位,我们将其与密码结合在一起,使我们的密码以独特的方式加密。
在我们刚刚编写的注册代码中,我们忽略了一些非常重要的事情。我们的“盐”每次都被设置为secret_salt。我们真正需要做的是为每个密码创建一个随机的“盐”。
为了创建随机盐,我们可以使用 CouchDB 的 RESTful JSON API。Couch 在http://localhost:5984/_uuids提供了一个资源,当调用时,将为我们返回一个唯一的UUID供我们使用。每个UUID都是一个长而随机的字符串,这正是盐所需要的!Sag 通过一个名为generateIDs的函数非常容易地获取 UUID。
让我们更新我们的注册代码,以反映我们刚刚讨论的内容。打开index.php,并更改盐值的设置以匹配以下内容:
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;
**$user->salt = $app->couch->generateIDs(1)->body->uuids[0];**
$user->password_sha = sha1($app->form('password') . $user->salt);
$app->couch->setDatabase('_users');
$app->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$app->couch->put($user->_id, $user->to_json());
$app->render('home');
});
再次测试注册流程
现在我们已经解决了盐的不安全性,让我们回去再试一次注册流程。
-
通过在浏览器中转到
http://localhost/verge/signup来打开注册页面。 -
填写表格,全名为
John Doe,电子邮件为<john@example.com>,用户名为johndoe,密码为temp123。完成后,点击注册。 -
您的用户已创建!让我们通过转到
http://localhost:5984/_utils,并在_users数据库中查找我们的新文档来到 Futon。这次我们的“盐”是随机且唯一的!再次测试注册流程
重构注册流程
正如我之前提到的,我们将把这段代码重构为干净的函数,放在我们的用户类内部,而不是直接放在index.php中。我们希望保留index.php用于处理路由、传递值和渲染视图。
行动时间-清理注册流程
通过在User类内创建一个名为signup的公共函数来清理我们的注册代码。
- 打开
classes/user.php,并创建一个用于注册的public函数。
public function signup($username,$password) {
}
- 输入以下代码以匹配下面的代码。它几乎与我们在上一节输入的代码相同,只是不再引用
$user,而是引用$this。您还会注意到full_name和email不在这个函数中;您马上就会看到它们。
public function signup($username, $password) {
**$bones = new Bones();
$bones->couch->setDatabase('_users');
$bones->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$this->roles = array();
$this->name = preg_replace('/[^a-z0-9-]/', '', strtolower($username));
$this->_id = 'org.couchdb.user:' . $this->name;
$this->salt = $bones->couch->generateIDs(1)->body->uuids[0];
$this->password_sha = sha1($password . $this->salt);
$bones->couch->put($this->_id, $this->to_json());
}**
- 打开
index.php,清理注册路由,使其与以下代码匹配:
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name');
$user->email = $app->form('email');
**$user->signup($app->form('username'), $app->form('password'));**
$app->set('message', 'Thanks for Signing Up ' . $user->full_name . '!');
$app->render('home');
});
刚刚发生了什么?
我们创建了一个名为signup的公共函数,它将包含我们的用户注册所需的所有注册代码。然后我们从index.php注册路由中复制了大部分代码。你会注意到里面有一些以前没有看到的新东西。例如,所有对$user的引用都已更改为$this,因为我们使用的所有变量都附加到当前用户对象上。你还会注意到,在开始时,我们创建了一个新的Bones对象,以便我们可以使用它。我们还创建了 Sag,我们已经连接到 Bones,我们能够初始化而不会造成任何开销,因为我们使用了单例模式。请记住,单例模式允许我们在此请求中调用我们在其他地方使用的相同对象,而不创建新对象。最后,我们回到index.php文件,并简化了我们的注册代码路由,以便我们只处理直接来自表单的值。然后我们通过注册函数传递了未经修改的用户名和密码,以便我们可以处理它们并执行注册代码。
我们的注册代码现在清晰并且在类级别上运行,并且不再影响我们的应用程序。但是,如果你尝试测试我们的表单,你会意识到它还不够完善。
异常处理和解决错误
如果你试图返回到你的注册表单并保存另一个名为John Doe的文档,你会看到一个相似于以下截图的相当不友好的错误页面:
如果你使用的是 Chrome 以外的浏览器,你可能收到了不同的消息,但结果仍然是一样的。发生了我们没有预料到的不好的事情,更糟糕的是,我们没有捕获这些异常。
当出现问题时会发生什么?我们如何找出出了什么问题?答案是:我们查看日志。
解读错误日志
当 PHP 和 Apache 一起工作时,它们会为我们产生大量的日志。有些是访问级别的日志,而另一些是错误级别的。所以让我们看看是否可以通过查看 Apache 错误日志来调查这里发生了什么。
行动时间——检查 Apache 的日志
让我们开始找 Apache 的错误日志。
-
打开终端。
-
运行以下命令询问 Apache 的
config文件保存日志的位置:
**grep ErrorLog /etc/apache2/httpd.conf**
- 终端会返回类似以下的内容:
**# ErrorLog: The location of the error log file.
# If you do not specify an ErrorLog directive within a <VirtualHost>
ErrorLog "/private/var/log/apache2/error_log"**
- 通过运行以下命令检索日志的最后几行:
**tail /private/var/log/apache2/error_log**
- 日志会显示很多东西,但最重要的消息是这个,它说 PHP
致命错误。你的消息可能略有不同,但总体消息是一样的。
**[Sun Sep 11 22:10:31 2011] [error] [client 127.0.0.1] PHP Fatal error: Uncaught exception 'SagCouchException' with message 'CouchDB Error: conflict (Document update conflict.)' in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php:1126\nStack trace:\n#0 /Library/WebServer/Documents/verge/lib/sag/src/Sag.php(286): Sag->procPacket('PUT', '/_users/org.cou...', '{"name":"johndoe')\n#1 /Library/WebServer/Documents/verge/classes/user.php(30): Sag->put('org.couchdb.use...', '{"name":"johndoe')\n#2 /Library/WebServer/Documents/verge/index.php(20): User->signup('w')\n#3 /Library/WebServer/Documents/verge/lib/bones.php(91): {closure}(Object(Bones))\n#4 /Library/WebServer/Documents/verge/lib/bones.php(17): Bones::register('/signup', Object(Closure), 'POST')\n#5 /Library/WebServer/Documents/verge/index.php(24): post('/signup', Object(Closure))\n#6 {main}\n thrown in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php on line 1126, referer: http://localhost/verge/signup
[Sun Sep 11 22:10:31 2011] [error] [client 127.0.0.1] PHP Fatal error: Uncaught exception 'SagCouchException' with message 'CouchDB Error: conflict (Document update conflict.)' in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php:1126\nStack trace:\n#0 /Library/WebServer/Documents/verge/lib/sag/src/Sag.php(286): Sag->procPacket('PUT', '/_users/org.cou...', '{"name":"johndoe')\n#1 /Library/WebServer/Documents/verge/classes/user.php(30): Sag->put('org.couchdb.use...', '{"name":"johndoe')\n#2 /Library/WebServer/Documents/verge/index.php(20): User->signup('w')\n#3 /Library/WebServer/Documents/verge/lib/bones.php(91): {closure}(Object(Bones))\n#4 /Library/WebServer/Documents/verge/lib/bones.php(17): Bones::register('/signup', Object(Closure), 'POST')\n#5 /Library/WebServer/Documents/verge/index.php(24): post('/signup', Object(Closure))\n#6 {main}\n thrown in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php on line 1126, referer: http://localhost/verge/signup**
刚刚发生了什么?
我们询问 Apache 它存储日志的位置,一旦我们找到日志文件的保存位置。我们使用tail命令返回 Apache 日志的最后几行。
注意
有各种各样的方法来阅读日志,我们不会深入讨论,但你可以选择让自己感到舒适的方式。你可以通过搜索互联网来研究tail,或者你可以在预装在你的 Mac OSX 机器上的控制台应用程序中打开日志。
查看我们收到的 PHP 致命错误相当令人困惑。如果你开始深入研究,你会发现这是一个 CouchDB 错误。更具体地说,这个错误的主要行是:
**Uncaught exception 'SagCouchException' with message 'CouchDB Error: conflict (Document update conflict.)**
这个消息意味着 CouchDB 对我们传递给它的内容不满意,而且我们没有处理 Sag 以SagCouchException形式抛出的异常。SagCouchException是一个类,将帮助我们解读 CouchDB 抛出的异常,但为了做到这一点,我们需要知道 CouchDB 返回的状态码是什么。
为了获取状态码,我们需要查看我们的 CouchDB 日志。
行动时间:检查 CouchDB 的日志
由于我们都是用 Homebrew 相同的方式安装了 CouchDB,我们可以确保我们的 CouchDB 日志都在同一个位置。考虑到这一点,让我们看看我们的 CouchDB 日志。
-
打开终端。
-
通过运行以下命令检索日志的最后几行:
**tail /usr/local/var/log/couchdb/couch.log**
- 终端将返回类似以下内容:
**[Mon, 12 Sep 2011 16:04:56 GMT] [info] [<0.879.0>] 127.0.0.1 - - 'GET' /_uuids?count=1 200
[Mon, 12 Sep 2011 16:04:56 GMT] [info] [<0.879.0>] 127.0.0.1 - - 'PUT' /_users/org.couchdb.user:johndoe 409**
刚刚发生了什么?
我们使用tail命令返回 CouchDB 日志的最后几行。
您将注意到的第一条记录是/uuids?count=1,这是我们在signup函数中抓取salt的 UUID。请注意,它返回了200状态,这意味着它执行成功。
下一行说'PUT' /_users/org.couchdb.user:johndoe,并返回了409响应。409响应意味着存在更新冲突,这是因为我们传递给用户的名称与已存在的名称相同。这应该很容易解决,但首先我们需要讨论如何捕获错误。
捕获错误
幸运的是,借助我们友好的try...catch语句,捕获错误相对容易。try...catch语句允许您测试一段代码块是否存在错误。try块包含您要尝试运行的代码,如果出现问题,将执行catch块。
try...catch语句的语法看起来类似于以下内容:
try {
// Code to execute
} catch {
// A problem occurred, do this
}
正如我之前提到的,Sag 包括一个名为SagCouchException的异常类。这个类让我们能够看到 CouchDB 的响应,然后我们可以相应地采取行动。
行动时间 - 使用 SagCouchException 处理文档更新冲突
我们在上一节中确定,我们的代码由于409响应而中断。因此,让我们调整classes/user.php文件中的注册功能,以使用SagCouchException处理异常。
public function signup($username, $password) {
...
**try {
$bones->couch->put($this->_id, $this->to_json());
} catch(SagCouchException $e) {
if($e->getCode() == "409") {
$bones->set('error', 'A user with this name already exists.');
$bones->render('user/signup');
exit;
}
}**
}
刚刚发生了什么?
我们使用了try...catch语句来解决触发的重复文档更新冲突。通过将其转换为(SagCouchException $e),我们告诉它现在只捕获通过的SagCouchExceptions。一旦捕获到这个异常,我们就会检查返回的代码是什么。如果是409的代码,我们将设置一个带有错误消息的error变量。然后我们需要重新显示用户/注册表单,以便用户有机会再次尝试注册流程。为了确保在此错误之后不再执行任何代码,我们使用exit命令,以便应用程序停在那里。
我们刚刚设置了一个error变量。让我们讨论如何显示这个变量。
显示警报
在我们的应用程序中,我们将根据用户交互显示标准通知,我们将其称为警报。我们刚刚设置了一个错误变量,用于错误警报,但我们也希望能够显示成功消息。
行动时间 - 显示警报
在这一部分,我们将使用我们现有的变量在 bones 中允许我们向用户显示警报消息。
- 打开
lib/bones.php并创建一个名为display_alert()的新函数。将调用此函数以查看alert变量是否设置。如果设置了alert变量,我们将回显一些 HTML 以在布局上显示警报框。
public function display_alert($variable = 'error') {
if (isset($this->vars[$variable])) {
return "<div class='alert alert-" . $variable . "'><a class='close' data-dismiss='alert'>x</a>" . $this- >vars[$variable] . "</div>";
}
}
- 在
layout.php中添加代码,就在容器div内部显示 Flash 调用display_flash函数。
<div class="container">
**<?php echo $this->display_alert('error'); ?>
<?php echo $this->display_alert('success'); ?>**
<?php include($this->content); ?>
</div>
- 现在我们已经添加了这些 Flash 消息,让我们回到
index.php中的注册POST路由,并添加一个 Flash 消息,感谢用户注册。
$user->signup($app->form('username'), $app->form('password'));
**$app->set('success', 'Thanks for Signing Up ' . $user->full_name . '!');**
$app->render('home');
});
刚刚发生了什么?
我们创建了一个名为display_alert的函数,用于检查传递变量的变量是否设置。如果设置了,我们将借助 Bootstrap 在警报框中显示变量的内容。然后我们在layout.php中添加了两行代码,以便我们可以显示错误和成功的 Flash 消息。最后,我们为我们的注册流程添加了一个成功的 Flash 消息。
让我们测试一下。
- 返回并尝试再次注册用户名为
johndoe的用户。
你会看到这个友好的错误消息,告诉你有问题:
- 现在,让我们测试一下成功的警报消息。将用户名更改为
johndoe2。点击注册!,你将收到一个漂亮的绿色警报。
- 即使有了这些简单的警报,我们的注册表单还不完美。随机的异常和错误可能会发生,我们无法处理。更令人担忧的是,我们并没有要求表单中的字段填写。这些项目需要在我们的视线范围内,但我们无法在本书中涵盖所有这些。
让我们继续讨论用户认证。
用户认证
现在我们已经创建了用户,我们肯定需要找到一种让他们登录到我们系统的方法。幸运的是,CouchDB 和 Sag 在这个领域真的会为我们做很多繁重的工作。在这一部分,我们将:
-
设置登录表单
-
了解会话、cookie,以及 CouchDB 和 Sag 如何处理我们的认证
-
添加支持用户登出
-
为已登录和未登录的用户不同处理 UI
设置登录表单
让我们创建一些登录表单,这样我们的用户就可以登录到我们的网站并使用他们新创建的账户。
试试吧——设置登录的路由和表单
我们已经多次经历了创建页面、设置路由和创建表单的过程。所以,让我们看看这次你能否自己尝试一下。我不会完全不帮助你。我会先告诉你需要尝试做什么,然后当你完成时,我们会进行回顾,确保我们的代码匹配起来。
你需要做的是:
-
创建一个名为
user/login.php的新页面。 -
在
index.php文件中为登录页面创建新的GET和POST路由。 -
告诉登录页面的
GET路由渲染user/login视图。 -
使用
user/signup.php作为指南创建一个包含username和password字段的表单。 -
使用 Bootstrap 助手和
submit按钮添加名为username和password的字段。
在你这样做的时候,我会去看电视。当你准备好了,翻到下一页,我们看看进展如何!
干得好!我希望你能够在不需要太多帮助的情况下完成。如果你需要回头看旧代码寻求帮助,不要担心,因为当开发者陷入困境时,很多人最终都会这样做。让我们看看你的代码与我的代码匹配程度如何。
此外,你的index.php文件应该类似于以下内容:
get('/login', function($app) {
$app->render('user/login');
});
post('/login', function($app) {
});
你的views/user/login.php页面应该类似于以下内容:
<div class="page-header">
<h1>Login</h1>
</div>
<div class="row">
<div class="span12">
<form action="<?php echo $this->make_route('/login') ?>" method="post">
<fieldset>
<?php Bootstrap::make_input('username', 'Username', 'text'); ?>
<?php Bootstrap::make_input('password', 'Password', 'password'); ?>
<div class="form-actions">
<button class="btn btn-primary">Login</button>
</div>
</fieldset>
</form>
</div>
</div>
确保将你的代码更新到与我的相匹配,这样我们的代码在未来能够匹配起来。
登录和登出
现在我们已经准备好表单了,让我们谈谈我们需要做什么才能让表单真正起作用。让我们快速谈谈我们在登录过程中要实现的目标。
-
Sag 将连接到 CouchDB 的
_users数据库。 -
Sag 将从我们的 PHP 直接将登录信息传递给 CouchDB。
-
如果登录成功,CouchDB 将返回一个认证的 cookie。
-
然后,我们将查询 CouchDB 以获取当前登录的用户名,并将其保存到会话变量中以供以后使用。
如果你已经使用其他数据库开发了一段时间,你会立刻看到登录过程有多酷。CouchDB 正在处理我们通常需要自己处理的大部分认证问题!
让我们来看看登录功能。幸运的是,它比注册过程要简单得多。
行动时间——为用户添加登录功能
我们将慢慢地进行这个过程,但我认为你会喜欢我们能够如此快速地添加这个功能,因为我们迄今为止编写的所有代码。
-
打开
classes/user.php。 -
创建一个名为
login的public函数,我们可以将我们的明文$password作为参数传递。
public function login($password) {
}
Create a new bones object and set the database to _users.
public function login($password) {
**$bones = new Bones();
$bones->couch->setDatabase('_users');**
}
- 为我们的登录代码创建一个
try...catch语句。在catch块中,我们将捕获错误代码401。如果触发了错误代码,我们希望告诉用户他们的登录是不正确的。
public function login($password) {
$bones = new Bones();
$bones->couch->setDatabase('_users');
**try {
}
catch(SagCouchException $e) {
if($e->getCode() == "401") {
$bones->set('error', ' Incorrect login credentials.');
$bones->render('user/login');
exit;
}
}**
}
- 添加代码来启动会话,然后通过 Sag 将用户名和密码传递到 CouchDB。当用户成功登录时,从 CouchDB 获取当前用户的用户名。
public function login($password) {
$bones = new Bones();
$bones->couch->setDatabase('_users');
**try {
$bones->couch->login($this->name, $password, Sag::$AUTH_COOKIE);
session_start();
$_SESSION['username'] = $bones->couch->getSession()->body- >userCtx->name;
session_write_close();**
}
刚刚发生了什么?
我们在user类中创建了一个名为login的public函数,允许用户登录。然后我们创建了一个新的 Bones 引用,以便我们可以访问 Sag。为了处理无效的登录凭据,我们创建了一个try...catch块,并先处理catch块。这次,我们检查错误代码是否为401。如果错误代码匹配,我们设置error变量来显示错误消息,渲染登录页面,最后退出当前代码。
接下来,我们通过将用户名和明文密码传递给 Sag 的登录方法来处理登录代码,同时设置Sag::$AUTH_COOKIE。这个参数告诉我们使用 CouchDB 的 cookie 身份验证。通过使用 cookie 身份验证,我们可以处理身份验证,而无需每次传递用户名和密码。
在幕后,正在发生的是我们的用户名和密码被发布到/_session URL。如果登录成功,它将返回一个 cookie,我们可以在此之后的每个请求中使用它,而不是用户名和密码。幸运的是,Sag 为我们处理了所有这些!
接下来,我们使用session_start函数初始化了一个会话,这允许我们设置会话变量,只要我们的会话存在,它就会持续存在。然后,我们为用户名设置了一个会话变量,等于当前登录用户的用户名。我们通过使用 Sag 来获取会话信息,使用$bones->couch->getSession()。然后使用->body()获取响应的主体,最后使用userCtx获取当前用户,并进一步获取username属性。这一切都导致了一行代码,如下所示:
**$_SESSION['username'] = $bones->couch->getSession()->body->userCtx->name;**
最后,我们使用session_write_close来写入会话变量并关闭会话。这将提高速度并减少锁定的机会。别担心;通过再次调用session_start(),我们可以再次检索我们的session变量。
最后,我们需要将登录函数添加到index.php中的post路由。让我们一起快速完成。
post('/login', function($app) {
**$user = new User();
$user->name = $app->form('username');
$user->login($app->form('password'));
$app->set('success', 'You are now logged in!');
$app->render('home');**
});
我们现在可以去测试这个,但让我们完成更多的事情,以便完全测试这里发生了什么。
行动时间-为用户添加注销功能
我敢打赌你认为登录脚本非常简单。等到你看到我们如何让用户注销时,你会觉得更容易。
- 打开
classes/user.php,创建一个名为logout的public static函数。
public static function logout() {
$bones = new Bones();
$bones->couch->login(null, null);
session_start();
session_destroy();
}
- 在
index.php文件中添加一个路由,并调用logout函数。
get('/logout', function($app) {
User::logout();
$app->redirect('/');
});
- 注意,我们在 Bones 内部调用了一个新功能
redirect函数。为了使其工作,让我们在底部添加一个快速的新功能
public function redirect($path = '/') {
header('Location: ' . $this->make_route($path));
}
刚刚发生了什么?
我们添加了一个名为logout的public static函数。我们将其设置为public static的原因是,我们目前登录的用户对我们来说并不重要。我们只需要执行一些简单的会话级操作。首先,我们像往常一样创建了一个$bones实例,但接下来的部分非常有趣,所以我们设置了$bones->couch->login(null, null)。通过这样做,我们将当前用户设置为匿名用户,有效地注销了他们。然后,我们调用了session_start和session_destroy。请记住,通过session_start,我们使我们的会话可访问,然后我们销毁它,这将删除与当前会话相关的所有数据。
在完成login函数后,我们打开了index.php,并调用了我们的public static函数,使用User::logout()。
最后,我们使用了一个重定向函数,将其添加到了index.php文件中。因此,我们迅速在 Bones 中添加了一个函数,这样就可以使用make_route将用户重定向到一个路由。
处理当前用户
我们真的希望能够确定用户是否已登录,并相应地更改导航。幸运的是,我们可以在几行代码中实现这一点。
行动时间 - 处理当前用户
大部分拼图已经就位,让我们来看看根据用户是否已登录来更改用户布局的过程。
- 让我们在
classes/user.php中添加一个名为current_user的函数,这样我们就可以从会话中检索当前用户的用户名。
public static function current_user() {
session_start();
return $_SESSION['username'];
session_write_close();
}
- 在
classes/user.php中添加一个名为is_authenticated的public static函数,以便我们可以查看用户是否已经认证。
public static function is_authenticated() {
if (self::current_user()) {
return true;
} else {
return false;
}
}
- 既然我们的身份验证已经就绪,让我们来收紧
layout.php中的导航,以便根据用户是否已登录来显示不同的导航项。
<ul class="nav">
**<li><a href="<?php echo $this->make_route('/') ?>">Home</a></li>
<?php if (User::is_authenticated()) { ?>
<li>
<a href="<?php echo $this->make_route('/logout') ?>">
Logout
</a>
</li>
<?php } else { ?>
<li>
<a href="<?php echo $this->make_route('/signup') ?>">
Signup
</a>
</li>
<li>
<a href="<?php echo $this->make_route('/login') ?>">
Login
</a>
</li>
<?php } ?>**
</ul>
刚刚发生了什么?
我们首先创建了一个名为current_user的public static函数,用于检索存储在会话中的用户名。然后我们创建了另一个名为is_authenticated的public static函数。该函数检查current_user是否有用户名,如果有,则用户已登录。如果没有,则用户当前未登录。
最后,我们迅速进入我们的布局,这样我们就可以在用户登录时显示首页和注销的链接,以及在用户当前未登录时显示首页、注册和登录的链接。
让我们来测试一下:
-
通过转到
http://localhost/verge/登录页面在浏览器中打开。请注意,标题显示首页、注册和登录,因为您当前未登录。 -
使用您的一个用户帐户的凭据登录。您将收到一个很好的警报消息,并且标题更改为显示首页和注销。
总结
我希望你对我们在本章中所取得的成就感到震惊。我们的应用程序真的开始成形了。
具体来说,我们涵盖了:
-
如何通过使用 Twitter 的 Bootstrap 大大改善界面
-
如何在现有 CouchDB 用户文档的基础上创建额外的字段
-
如何处理错误并通过日志调试问题
-
如何完全构建出用户可以使用 Sag 和 CouchDB 注册、登录和注销应用程序的能力
这只是我们应用程序的开始。我们还有很多工作要做。在下一章中,我们将开始着手用户个人资料,并开始创建 CouchDB 中的新文档。这些文档将是我们用户的帖子。
第七章:用户个人资料和帖子建模
随着我们的应用程序的基础创建,我们允许用户注册并登录到我们的应用程序。这是任何应用程序的重要部分,但我们仍然缺少可以连接到用户帐户的内容的创建。我们将在本章中详细介绍所有内容!
在本章中,我们将:
-
创建一个用户个人资料,以公开显示用户的信息
-
使用 Bootstrap 清理个人资料
-
处理各种异常
-
讨论在 CouchDB 中对帖子和关系的建模
-
创建一个表单,从已登录用户的个人资料中创建帖子
有了我们的路线图,让我们继续讨论用户个人资料!
用户个人资料
任何社交网络的主要吸引力是用户的个人资料;用户个人资料通常显示用户的基本信息,并显示他们创建的任何内容。
到本节结束时,我们的用户个人资料将按以下方式工作:
-
如果访问者转到
http://localhost/verge/user/johndoe,我们的路由系统将将其与路由/user/:username匹配 -
index.php文件将johndoe作为username的值,并将其传递给User类,尝试查找具有匹配 ID 的用户文档 -
如果找到
johndoe,index.php将显示一个带有johndoe信息的个人资料 -
如果找不到
johndoe,访问者将看到一个404错误,这意味着该用户名的用户不存在
使用路由查找用户
为了找到用户,我们首先需要创建一个函数,该函数将以用户名作为参数,并在有效时返回一个用户对象。
行动时间-获取单个用户文档
您可能还记得,在第三章中,使用 CouchDB 和 Futon 入门,我们能够通过传递所需文档的 ID 来从 CouchDB 中检索文档。这一次,我们将使用 Sag 来找到用户的信息。需要注意的一点是,当我们使用 ID 查找用户时,我们需要确保在查找用户时,需要使用org.couchdb.user:命名空间进行前置。
让我们从打开classes/user.php并滚动到底部开始。
- 添加一个名为
get_by_username()的public static函数。
public static function get_by_username() {
}
- 为了通过 ID 查找用户,我们需要允许我们的函数接受参数
$username。
public static function get_by_username($username = null) {
}
- 现在,让我们设置数据库来实例化 Bones 和代理 Sag。记住,我们正在处理
_users数据库,所以我们需要以admin权限登录。
public static function get_by_username($username = null) {
**$bones = new Bones();
$bones->couch->setDatabase('_users');
$bones->couch->login(ADMIN_USER, ADMIN_PASSWORD);
}**
- 现在我们可以连接到
_users数据库,让我们通过 Sag 发出一个get调用,通过添加org.couchdb.user:来返回一个用户的传递用户名。
public static function get_by_username($username = null) {
$bones = new Bones()
$bones->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$bones->couch->setDatabase('_users');
**$user = new User();
$document = $bones->couch->get('org.couchdb.user:' . $username)- >body;
$user->_id = $document->_id;
$user->name = $document->name;
$user->email = $document->email;
$user->full_name = $document->full_name;
return $user;
}**
刚刚发生了什么?
我们创建了一个名为get_by_username的public static函数,允许我们传入$username。要实际获取文档,我们需要使用我们的ADMIN_USER和ADMIN_PASSWORD常量来访问_users数据库。为了返回一个用户对象,我们需要创建一个名为$user的新用户对象。然后我们使用 Sag 的get调用通过 ID 标识文档并将其作为名为$document的stdClass对象返回。然后我们从document变量中获取值,并将它们传递到$user对象上的相应值。最后,我们将用户文档返回到调用函数的地方。
现在我们有一个处理按用户名查找用户的函数,让我们在index.php中创建一个路由,将用户名传递给这个函数。
行动时间-为用户个档案创建路由
我们将创建一个路由,以便人们可以通过转到唯一的 URL 来查看个人资料。这将是我们真正利用我们的路由系统处理路由变量的能力的第一次。
- 打开
index.php,并创建一个用户个人资料的get路由,输入以下代码:
get('/user/:username', function($app) {
});
- 让我们使用路由变量
:username告诉我们要查找的用户名;我们将把这个变量传递给我们在User类中创建的get_by_username函数。最后,我们将返回的user对象传递给视图中的user变量:
get('/user/:username', function($app) {
**$app->set('user', User::get_by_username($app- >request('username')));**
});
- 最后,我们将呈现
user/profile.php视图,我们很快就会创建。
get('/user/:username', function($app) {
$app->set('user', User::get_by_username($app- >request('username')));
**$app->render('user/profile');**
});
刚刚发生了什么?
我们在短短的四行代码中做了很多事情!首先,我们通过使用route /user/:username定义了用户配置文件路由。接下来,我们创建了一段代码,将route变量中的:username传递给我们user类中的get_by_username函数。get_by_username函数将返回一个包含用户信息的对象,并且我们使用$app->set('user')将其发送到我们的视图中。最后,我们呈现了用户配置文件。
让我们继续创建用户配置文件,这样我们就可以看到我们的辛勤工作在发挥作用!
行动时间——创建用户配置文件
在本章中,我们将多次清理user视图。但是,让我们首先将所有用户文档内容都转储到我们的视图中。
-
在我们的
working文件夹中的views目录中创建一个名为user/profile.php的视图。 -
为配置文件创建一个简单的标题,使用以下 HTML:
<div class="page-header">
<h1>User Profile</h1>
</div>
- 由于我们还没有设计,让我们只使用
var_dump来显示User文档的所有内容:
<div class="page-header">
<h1>User Profile</h1>
</div>
<div class="container">
**<div class="row">
<?php var_dump($user); ?>
</div>
</div>**
刚刚发生了什么?
我们刚刚创建了一个非常基本的用户配置文件,其中包含一个标题,告诉我们这个页面是用户配置文件。然后,我们使用var_dump来显示user对象的所有内容。var_dump是一个通用的 PHP 函数,用于输出关于变量或对象的结构化信息,在你只想确保事情正常运行时非常有用。
测试一下
现在我们有了一个简单的用户配置文件设置,让我们看看它的效果如何。
-
打开你的浏览器,然后转到
http://localhost/verge/user/johndoe。 -
你的浏览器将显示以下内容:
- 还不错,但当然我们需要很快清理一下这些数据的格式。但是,现在让我们确保将我们的更改提交到 Git。
将你的更改添加到 Git。
在本节中,我们开始创建用户配置文件,并直接从 CouchDB 输出用户信息。让我们将所有更改添加到 Git,以便跟踪我们的进度。
-
打开终端。
-
输入以下命令以更改目录到我们的工作目录。
**cd /Library/Webserver/Documents/verge/**
- 我们只添加了一个文件
views/user/profile.php,所以让我们告诉 Git 将这个文件添加到源代码控制中。
**git add views/user/profile.php**
- 给
Git一个描述,说明自上次提交以来我们做了什么。
**git commit am 'Created the get_by_username function, a basic user profile, and a route to display it'**
修复一些问题
你可能已经注意到,我们忽略了一个潜在的问题,即当找不到用户配置文件时我们没有优雅地处理发生了什么。
例如:
如果你访问http://localhost/verge/user/someone,你的浏览器会显示这个非常不友好的错误消息:
查找错误
在第六章中,我们通过终端使用tail命令查看 Apache 的错误日志。我们将再次做同样的事情。让我们看看 Apache 的日志,看看我们能否弄清楚出了什么问题。
行动时间——检查 Apache 的日志
在第六章中,我们首先尝试定位我们的 Apache 日志。默认情况下,它保存在/private/var/log/apache2/error_log。如果在上一章中发现它位于其他位置,你可以通过在终端中输入grep ErrorLog /etc/apache2/httpd.conf来再次找到它的位置。
让我们找出问题出在哪里。
-
打开终端。
-
通过运行以下命令检索日志的最后几行:
**tail /private/var/log/apache2/error_log**
- 日志会显示很多东西,但最重要的消息是这个,说
PHP 致命错误。你的消息可能略有不同,但总体消息是一样的。
**[Wed Sep 28 09:29:49 2011] [error] [client 127.0.0.1] PHP Fatal error: Uncaught exception 'SagCouchException' with message 'CouchDB Error: not_found (missing)' in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php:1221\nStack trace:\n#0 /Library/WebServer/Documents/verge/lib/sag/src/Sag.php(206): Sag->procPacket('GET', '/_users/org.cou...')\n#1 /Library/WebServer/Documents/verge/classes/user.php(81): Sag->get('org.couchdb.use...')\n#2 /Library/WebServer/Documents/verge/index.php(44): User::get_by_username('someone')\n#3 /Library/WebServer/Documents/verge/lib/bones.php(91): {closure}(Object(Bones))\n#4 /Library/WebServer/Documents/verge/lib/bones.php(13): Bones::register('/user/:username', Object(Closure), 'GET')\n#5 /Library/WebServer/Documents/verge/index.php(46): get('/user/:username', Object(Closure))\n#6 {main}\n thrown in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php on line 1221**
刚刚发生了什么?
我们使用了tail命令来返回 Apache 日志的最后几行。如果你仔细看日志,你会看到CouchDB error。更具体地说,错误如下:
**error: Uncaught exception 'SagCouchException' with message 'CouchDB Error: not_found (missing)'**
这个消息意味着 CouchDB 对我们的操作不满意,Sag 以SagCouchException的形式抛出了一个错误。为了适当地处理SagCouchException,我们需要在对 Sag 的调用中添加一些代码。
在上一章中,我们通过检查状态代码并将其与分辨率进行匹配来修复了一个错误。我们可以继续这样做,但最终会发生我们不知道的错误。从现在开始,当发生未处理的异常时,我们希望显示友好的错误消息,以便我们可以调试它。
在接下来的部分,我们将使用 Bones 来帮助我们显示一个异常页面。
处理 500 错误
我们真正想解决的是如何处理应用程序中的 500 错误。500 错误指的是 HTTP 状态代码500,即*"内部服务器错误"。通常,这意味着发生了某些事情,我们没有正确处理。
行动时间 - 使用 Bones 处理 500 错误
让我们首先创建一个简单的视图,用于向我们显示错误。
-
让我们首先在我们的
views目录内创建一个名为error的新文件夹。 -
创建一个名为
500.php的新视图,并将其放入errors文件夹中(views/error/500.php)。 -
在
500.php中添加以下代码以输出异常信息:
<div class="hero-unit">
<h1>An Error Has Occurred</h1>
<p>
<strong>Code:</strong><?php echo $exception->getCode(); ?>
</p>
<p>
<strong>Message:</strong>
<?php echo $exception->getMessage(); ?>
</p>
<p><strong>Exception:</strong> <?php echo $exception; ?></p>
</div>
- 在
lib/bones.php中添加一个名为error500的函数,以便我们可以在我们的应用程序中轻松地显示 500 错误。
public function error500($exception) {
$this->set('exception', $exception);
$this->render('error/500');
exit;
}
刚才发生了什么?
我们在views目录中创建了一个名为error的新文件夹,其中包含了我们在应用程序中使用的所有错误视图。然后我们创建了一个名为500.php的新视图,以友好的方式显示我们的异常。异常是 Sag 扩展的内置类,使用SagCouchException类。有了这个,我们可以很容易地直接与我们的视图中的这个异常类交谈。这个Exception类有很多属性。但是,在这个应用程序中,我们只会显示代码、消息和以字符串格式表示的异常。最后,我们创建了一个函数在 Bones 中,允许我们传递异常进去,以便我们可以在视图中显示它。在这个函数中,我们将异常传递给error/500视图,然后使用exit,告诉 PHP 停止在我们的应用程序中做任何其他事情。这样做是因为发生了问题,我们的应用程序停止做任何其他事情。
行动时间 - 处理异常
现在我们可以处理异常了,让我们在get_by_username函数中添加一些代码,以便我们可以更深入地查看我们的问题。
- 让我们打开
classes/user.php,并在我们的 Sag 调用周围添加一个try...catch语句,以确保我们可以处理任何发生的错误。
public static function get_by_username($username = null) {
$bones = new Bones();
$bones->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$bones->couch->setDatabase('_users');
$user = new User();
**try {**
$document = $bones->couch->get('org.couchdb.user:' . $username)->body;
$user->_id = $document->_id;
$user->name = $document->name;
$user->email = $document->email;
$user->full_name = $document->full_name;
return $user;
**} catch (SagCouchException $e) {
}**
}
- 既然我们正在捕获错误,让我们在
error500函数中添加。
public static function get_by_username($username = null) {
$bones = new Bones();
$bones->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$bones->couch->setDatabase('_users');
$user = new User();
try {
$document = $bones->couch->get('org.couchdb.user:' . $username)->body;
$user->_id = $document->_id;
$user->name = $document->name;
$user->email = $document->email;
$user->full_name = $document->full_name;
return $user;
} catch (SagCouchException $e) {
**$bones->error500($e);**
}
}
- 当我们在
classes/user.php中时,让我们捕获一些可能的异常。让我们从public函数注册开始。
public function signup($username, $password) {
$bones = new Bones();
$bones->couch->setDatabase('_users');
$bones->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$this->roles = array();
$this->name = preg_replace('/[^a-z0-9-]/', '', strtolower($username));
$this->_id = 'org.couchdb.user:' . $this->name;
$this->salt = $bones->couch->generateIDs(1)->body->uuids[0];
$this->password_sha = sha1($password . $this->salt);
try {
$bones->couch->put($this->_id, $this->to_json());
}
catch(SagCouchException $e) {
if($e->getCode() == "409") {
$bones->set('error', 'A user with this name already exists.');
$bones->render('user/signup');
**} else {
$bones->error500($e);
}**
}
}
- 接下来,让我们在我们的公共函数登录中添加到
catch语句。
public function login($password) {
$bones = new Bones();
$bones->couch->setDatabase('_users');
try {
$bones->couch->logiBn($this->name, $password, Sag::$AUTH_COOKIE);
session_start();
$_SESSION['username'] = $bones->couch->getSession()->body- >userCtx->name;
session_write_close();
}
catch(SagCouchException $e) {
if($e->getCode() == "401") {
$bones->set('error', 'Incorrect login credentials.');
$bones->render('user/login');
exit;
**} else {
$bones->error500($e);
}**
}
}
刚才发生了什么?
现在我们可以优雅地处理异常了,我们通过我们的User类,并在发生意外情况时添加了抛出500错误的能力。在我们已经预期到某些问题的调用中,如果发生了意外情况,我们可以使用if...else语句触发500错误。
测试我们的异常处理程序
让我们再试一次,看看我们是否能找到异常的根源。
-
转到
http://localhost/verge/user/someone。 -
现在你会看到一个更友好的错误页面,告诉我们代码、消息和完整的错误,你会在错误日志中看到。
对我们来说,从中弄清楚发生了什么是更容易的。在我们调试应用程序的过程中,这个页面对我们来说将非常有用,以跟踪发生了什么错误。
通过查看这段代码,我们可以知道 CouchDB 正在抛出一个404错误。我们可能期望这个错误会发生,因为我们正在寻找一个不存在的用户文档。让我们进一步了解一下404错误是什么,以及我们如何处理它。
显示 404 错误
404错误指的是 HTTP 状态码404,意思是“未找到”。404错误通常发生在您尝试访问不存在的内容时,比如访问错误的 URL。在我们的情况下,我们收到404错误是因为我们试图查找一个不存在的 CouchDB 文档。
如果找不到用户,则显示 404
404错误是一种特殊的错误,我们会在应用程序的不同位置看到。让我们创建另一个错误页面,以便在发生404错误时使用。
采取行动:使用 Bones 处理 404 错误
让我们为应用程序中的404错误创建一个视图。
-
首先,在我们的
views/error/目录中创建一个名为404.php的新视图。 -
让我们在
404.php中添加一些非常基本的代码,以通知访问者我们的应用程序找不到请求的页面。
<div class="hero-unit">
<h1>Page Not Found</h1>
</div>
- 为了呈现这个视图,让我们在
lib/bones.php文件中添加另一个名为error404的函数。这个函数将为我们很好地显示404错误。
public function error404() {
$this->render('error/404');
exit;
}
刚才发生了什么?
我们创建了一个简单的视图,名为404.php,我们可以在应用程序中任何时候显示404错误。然后我们在lib/bones.php中创建了一个名为error404的简单函数,它呈现error/404.php并终止当前脚本,以便不会发生进一步的操作。
为未知用户显示 404 错误
现在我们有了404错误处理程序,让我们在classes/user.php的get_by_username函数中发生404错误时显示它。
打开classes/user.php,并修改get_by_username函数以匹配以下内容:
public static function get_by_username($username = null) {
$bones = new Bones();
$bones->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$bones->couch->setDatabase('_users');
$user = new User();
**try {**
$document = $bones->couch->get('org.couchdb.user:' . $username)- >body;
$user->_id = $document->_id;
$user->name = $document->name;
$user->email = $document->email;
$user->full_name = $document->full_name;
return $user;
**} catch (SagCouchException $e) {
if($e->getCode() == "404") {
$bones->error404();
} else {
$bones->error500($e);
}**
}
}
在整个站点上挂接 404
404错误的有趣之处在于,它们可以在访问者通过 Bones 不理解的路由时发生。因此,让我们在 Bones 中添加代码来处理这个问题。
采取行动-使用 Bones 处理 404 错误
让我们在lib/bones.php和index.php周围添加一些简单的代码,以便处理404错误。
- 打开
lib/bones.php,在Bones类内部创建一个名为resolve的函数,我们可以在路由的末尾调用它,并确定是否找到了路由。
public static function resolve() {
if (!static::$route_found) {
$bones = static::get_instance();
$bones->error404();
}
}
- 转到
lib/bones.php的顶部,并创建一个名为resolve的函数,放在Bones类之外(例如get, post, put或delete),我们可以在任何地方调用它。
function resolve() {
Bones::resolve();
}
- 我们要做的最后一件事就是在
index.php的最底部添加一行代码,如果没有找到路由,就可以调用它。随着添加更多的路由,确保resolve()始终位于文件的末尾。
get('/user/:username', function($app) {
$app->set('user', User::get_by_username($app- >request('username')));
$app->render('user/profile');
});
**resolve();**
刚才发生了什么?
我们创建了一个名为resolve的函数,在我们的index.php文件的底部执行,它会在所有路由之后执行。这个函数作为一个“清理”函数,如果没有匹配的路由,它将向访问者显示一个404错误并终止当前脚本。
测试一下
既然我们优雅地处理了404错误,让我们测试一下,看看会发生什么。
-
打开您的浏览器,转到
http://localhost/verge/user/anybody。 -
您的浏览器将显示以下内容:
- 太好了!我们的
User类正在转发给我们一个404错误,因为我们在get_by_username函数中添加了代码。
-
接下来,让我们检查一下我们的
index.php,看看如果找不到请求的路由,它是否会转发给我们一个404错误。 -
打开您的浏览器,转到
http://localhost/verge/somecrazyurl。 -
您的浏览器将显示以下内容:
完美!我们的404错误处理程序正是我们需要的。如果我们将来需要再次使用它,我们只需要在我们的Bones类中调用error404,然后一切都设置好了!
给用户一个链接到他们的个人资料
在大多数社交网络中,一旦您登录,就会显示一个链接,以查看当前登录用户的个人资料。让我们打开view/layout.php,并在导航中添加一个My Profile链接。
<ul class="nav">
<li><a href="<?php echo $this->make_route('/') ?>">Home</a></li>
<?php if (User::is_authenticated()) { ?> <li>
<a href="<?php echo $this->make_route('/user/' . User::current_user()) ?>">
My Profile
</a>
</li>
<li>
<a href="<?php echo $this->make_route('/logout') ?>">
Logout
</a>
</li>
<?php } else { ?>
<li>
<a href="<?php echo $this->make_route('/signup') ?>">
Signup
</a>
</li>
<li>
<a href="<?php echo $this->make_route('/login') ?>">
Login
</a>
</li>
<?php } ?>
</ul>
使用 Bootstrap 创建更好的个人资料
我们的个人资料并不是很好地组合在一起,这开始让我感到困扰,我们需要在本章后面再添加更多内容。让我们准备好我们的用户个人资料,以便我们可以很好地显示用户的信息和帖子。
行动时间-检查用户当前是否已登录
我们需要能够弄清楚用户正在查看的个人资料是否是他们自己的。所以,让我们在我们的视图中添加一个变量,告诉我们是否是这种情况。
- 打开
index.php,并添加一个名为is_current_user的变量,用于确定您正在查看的个人资料是否等于当前登录用户。
get('/user/:username', function($app) {
$app->set('user', User::get_by_username($app- >request('username')));
**$app->set('is_current_user', ($app->request('username') == User::current_user() ? true : false));**
$app->render('user/profile');
});
- 让我们更改
views/user/profile.php头部的代码,这样我们就可以输出用户的全名以及This is you!,如果这是当前用户的个人资料。
<div class=-page-header->
**<h1><?php echo $user->full_name; ?>
<?php if ($is_current_user) { ?>
<code>This is you!</code>
<?php } ?>
</h1>**
</div>
刚刚发生了什么?
我们使用了一个称为ternary的简写操作。ternary操作是if-else语句的简写形式。在这种情况下,我们说如果从路由传递的用户名等于当前登录用户的用户名,则返回true,否则返回false。然后,我们进入我们的个人资料,并且如果is_current_user变量设置为true,则显示This is you!。
清理个人资料的设计
再次,Bootstrap 将通过允许我们用有限的代码清理我们的个人资料来拯救我们。
- 让我们通过以下代码将我们的行
div分成两列:
<div class="page-header">
<h1><?php echo $user->full_name; ?>
<?php if ($is_current_user) { ?>
<code>This is you!</code>
<?php } ?>
</h1>
</div>
**<div class="container">
<div class="row">
<div class="span4">
<div class="well sidebar-nav">
<ul class="nav nav-list">
<li><h3>User Information</h3>
</ul>
</div>
</div>
<div class="span8">
<h2>Posts</h2>
</div>
</div>
</div>**
- 通过将更多的列表项添加到无序列表中,将用户的信息输出到左列。
<div class="container">
<div class="row">
<div class="span4">
<div class="well sidebar-nav">
<ul class="nav nav-list">
<li><h3>User Information</h3></li>
**<li><b>Username:</b> <?php echo $user->name; ?></li>
<li><b>Email:</b> <?php echo $user->email; ?></li>**
</ul>
</div>
</div>
<div class="span8">
<h2>Posts</h2>
</div>
</div>
</div>
让我们来看看我们的新个人资料
有了这个,我们的新的改进的个人资料已经出现了!让我们来看看。
-
通过转到
http://localhost/verge/user/johndoe,在浏览器中打开johndoe用户的 URL。 -
您的浏览器将显示一个精心改造的用户个人资料。
-
现在,让我们检查一下我们的
$is_current_user变量是否正常工作。为了做到这一点,请使用johndoe作为用户名登录,并转到http://localhost/verge/user/johndoe。 -
您的浏览器将显示用户个人资料,以及一个友好的消息告诉您这是您的个人资料。
太棒了!我们的个人资料真的开始变得完整起来了。这是我们应用程序的一个重要里程碑。所以,让我们确保将我们的更改提交到 Git。
将您的更改添加到 Git
在这一部分,我们添加了支持清晰处理异常的功能,并且还改进了用户个人资料。让我们把所有的更改都添加到 Git 中,这样我们就可以跟踪我们的进展。
-
打开终端。
-
输入以下命令以更改目录到我们的
working目录:
**cd /Library/Webserver/Documents/verge/**
- 我们在这一部分添加了一些文件。所以,让我们把它们都加入到源代码控制中。
**git add .**
- 给 Git 一个描述,说明自上次提交以来我们做了什么。
**git commit -am 'Added 404 and 500 error exception handling and spruced up the layout of the user profile'**
帖子
我们在个人资料中有一个帖子的占位符。但是,让我们开始填充一些真实内容。我们将允许用户发布小段内容,并将它们与用户帐户关联起来。
建模帖子
让我们讨论一下我们需要做什么才能将帖子保存到 CouchDB 并与用户关联起来。在我们使用 CouchDB 进行此操作之前,让我们尝试通过查看如何在 MySQL 中进行操作来加深理解。
如何在 MySQL 中建模帖子
如果我们要为 MySQL(或其他 RDBMS)建模这种关系,它可能看起来类似于以下截图:
简而言之,这个图表显示了一个posts表,它有一个外键user_id,引用了用户表的id。这种一对多的关系在大多数应用程序中都很常见,在这种情况下,意味着一个用户可以有多个帖子。
既然我们已经看过一个熟悉的图表,让我们再看看与 CouchDB 相关的相同关系。
如何在 CouchDB 中建模帖子
令人惊讶的是,CouchDB 以非常相似的方式处理关系。你可能会想,等一下,我以为你说它不是关系数据库。请记住,无论你使用什么数据库,它们处理关系的方式都可能有共同之处。让我们看看 CouchDB 如何说明相同的数据和模型。
这很相似,对吧?最大的区别始终是,在关系数据库中,数据存储在固定的行和列中,而在 CouchDB 中,它们存储在自包含的文档中,具有无模式的键和值集。无论你如何查看数据,关系都是相同的,即,通过对用户 ID 的引用,帖子与用户相连接。
为了确保我们在同一页面上,让我们逐个浏览post文档中的每个字段,并确保我们理解它们是什么。
-
_id是文档的唯一标识符。 -
_rev是文档的修订标识符。我们在第三章中提到过修订,如果你想重新了解这个概念。 -
type告诉我们我们正在查看什么类型的文档。在这种情况下,每个post文档都将等于post。 -
date_created是文档创建时的时间戳。 -
content包含我们想要放在帖子中的任何文本。 -
user包含创建帖子的用户的用户名,并引用回_users文档。有趣的是,我们不需要在这个字段中放入org.couchdb.user,因为 CouchDB 实际上会查看用户名。
现在我们已经定义了需要保存到 CouchDB 的值,我们准备在一个新类Post中对其进行建模。
试试看-设置帖子类
创建Post类将与我们的User类非常相似。如果你感到足够自信,请尝试自己创建基本类。
你需要做的是:
-
创建一个名为
post.php的新类,它扩展了Base类。 -
为之前定义的每个必需字段创建变量。
-
添加一个
construct函数来定义文档的类型。
完成后,继续阅读下一页,并确保你的工作与我的匹配。
让我们检查一下一切的结果。
你应该已经创建了一个名为post.php的新文件,并将其放在我们working文件夹中的classes目录中。post.php 的内容应该类似于以下内容:
<?php
class Post extends Base
{
protected $date_created;
protected $content;
protected $user;
public function __construct() {
parent::__construct('post');
}
}
这就是我们在 PHP 中处理帖子文档所需要的一切。现在我们已经建立了这个类,让我们继续创建帖子。
创建帖子
现在对我们来说,创建帖子将是小菜一碟。我们只需要添加几行代码,它就会出现在数据库中。
行动时间-制作处理帖子创建的函数
让我们创建一个名为create的公共函数,它将处理我们应用程序的帖子创建。
- 打开
classes/post.php,并滚动到底部。在这里,我们将创建一个名为create的新公共函数。
public function create() {
}
- 让我们首先获得一个新的 Bones 实例,然后设置当前
post对象的变量。
public function create() {
**$bones = new Bones();
$this->_id = $bones->couch->generateIDs(1)->body->uuids[0];
$this->date_created = date('r');
$this->user = User::current_user();
}**
- 最后,让我们使用 Sag 将文档放入 CouchDB。
public function create() {
$bones = new Bones();
$this->_id = $bones->couch->generateIDs(1)->body->uuids[0];
$this->date_created = date('r');
$this->user = User::current_user();
**$bones->couch->put($this->_id, $this->to_json());**
}
- 让我们用一个
try...catch语句包装对 CouchDB 的调用,在catch语句中,让我们像以前一样将其弹到500错误。
public function create() {
$bones = new Bones();
$this->_id = $bones->couch->generateIDs(1)->body->uuids[0];
$this->date_created = date('r');
$this->user = User::current_user();
**try {
$bones->couch->put($this->_id, $this->to_json());
}
catch(SagCouchException $e) {
$bones->error500($e);
}**
}
刚刚发生了什么?
我们刚刚创建了一个名为create的函数,使我们能够创建一个新的Post文档。我们首先实例化了一个 Bones 对象,以便我们可以使用 Sag。接下来,我们使用 Sag 为我们获取了一个UUID作为我们的post的 ID。然后,我们使用date('r')将日期输出为RFC 2822格式(这是 CouchDB 和 JavaScript 所喜欢的格式),并将其保存到帖子的date_created变量中。然后,我们将帖子的用户设置为当前用户的用户名。
在设置了所有字段后,我们使用 Sag 的put命令将帖子文档保存到 CouchDB。最后,为了确保我们没有遇到任何错误,我们将put命令包装在一个try...catch语句中。在catch部分中,如果出现问题,我们将用户传递给 Bones 的error500函数。就是这样!我们现在可以在我们的应用程序中创建帖子。我们唯一剩下的就是在用户个人资料中创建一个表单。
开始行动-创建一个表单来启用帖子创建
让我们直接在用户的个人资料页面中编写用于创建帖子的表单。只有当已登录用户查看自己的个人资料时,该表单才会显示出来。
-
打开
user/profile.php。 -
让我们首先检查用户正在查看的个人资料是否是他们自己的。
<div class="span8">
**<?php if ($is_current_user) { ?>
<h2>Create a new post</h2>
<?php } ?>**
<h2>Posts</h2>
</div>
- 接下来,让我们添加一个表单,允许当前登录的用户发布帖子。
<div class="span8">
<?php if ($is_current_user) { ?>
<h2>Create a new post</h2>
**<form action="<?php echo $this->make_route('/post')?>" method="post">
<textarea id="content" name="content" class="span8" rows="3">
</textarea>
<button id="create_post" class="btn btn-primary">Submit
</button>
</form>
<?php } ?>**
<h2>Posts</h2>
</div>
刚刚发生了什么?
我们使用$is_current_user变量来确定查看个人资料的用户是否等于当前登录的用户。接下来,我们创建了一个表单,该表单提交到post路由(接下来我们将创建)。在表单中,我们放置了一个id为content的textarea和一个submit按钮来实际提交表单。
现在我们已经准备好一切,让我们通过在index.php文件中创建一个名为post的路由来完成post的创建。
开始行动-创建一个路由并处理帖子的创建
为了实际创建一个帖子,我们需要创建一个路由并处理表单输入。
-
打开
index.php。 -
创建一个基本的
post路由,并将其命名为post。
post('/post', function($app) {
});
- 在我们的
post路由中,让我们接受传递的值content并在我们的Post类上使用create函数来实际创建帖子。帖子创建完成后,我们将用户重定向回他们的个人资料页面。
post('/post', function($app) {
**$post = new Post();
$post->content = $app->form('content');
$post->create();
$app->redirect('/user/' . User::current_user());**
});
- 我们已经做了很多工作,以确保用户在创建帖子时经过身份验证,但让我们再三检查用户是否在这里经过了身份验证。如果他们没有经过身份验证,我们的应用程序将将他们转发到用户登录页面,并显示错误消息。
post('/post', function($app) {
**if (User::is_authenticated()) {**
$post = new Post();
$post->content = $app->form('content');
$post->create();
$app->redirect('/user/' . User::current_user());
**} else {
$app->set('error', 'You must be logged in to do that.');
$app->render('user/login');
}**
});
刚刚发生了什么?
在这一部分,我们为post路由创建了一个post路由(抱歉,这是一个令人困惑的句子)。在post路由内部,我们实例化了一个Post对象,并将其实例变量content设置为来自提交表单的textarea的内容。接下来,我们通过调用公共的create函数创建了post。帖子保存后,我们将用户重定向回他/她自己的个人资料。最后,我们在整个route周围添加了功能,以确保用户已登录。如果他们没有登录,我们将把他们弹到登录页面,并要求他们登录以执行此操作。
测试一下
现在我们已经编写了创建帖子所需的一切,让我们一步一步地测试一下。
-
首先以
johndoe的身份登录,并通过在浏览器中打开http://localhost/verge/user/johndoe来转到他的个人资料。 -
您的浏览器将显示一个用户个人资料,就像我们以前看到的那样,但这次您将看到
post表单。 -
在文本区域中输入一些内容。我输入了
我不喜欢花生酱,但您可以随意更改。 -
完成后,点击提交按钮。
-
您已被转发回
johndoe的用户个人资料,但您还看不到任何帖子。因此,让我们登录 Futon,确保帖子已创建。 -
通过转到
http://localhost:5984/_utils/database.html?verge,在 Futon 中转到verge数据库。 -
太棒了!这里有一个文档;让我们打开它并查看内容。
这个完美解决了!当用户登录时,他们可以通过转到他们的个人资料并提交创建新帖子表单来创建帖子。
将您的更改添加到 Git
在这一部分,我们添加了一个基于我们的Post模型来创建帖子的函数。然后我们在用户个人资料中添加了一个表单,这样用户就可以真正地创建帖子。让我们把所有的更改都添加到 Git 中,这样我们就可以跟踪我们的进展。
-
打开终端。
-
输入以下命令以更改目录到我们的
working目录:
**cd /Library/Webserver/Documents/verge/**
- 我们添加了
classes/post.php文件,所以让我们把那个文件加入到源代码控制中:
**git add classes/post.php**
- 给
Git一个描述,说明自上次提交以来我们做了什么:
**git commit –am 'Added a Post class, built out basic post creation into the user profile. Done with chapter 7.'**
- 我知道我说过我不会再提醒你了,但我也只是个人。让我们把这些更改推送到 GitHub 上去。
**git push origin master**
总结
信不信由你,这就是我们在本章中要写的所有代码。收起你的抗议标语,上面写着“我们甚至还没有查询用户的帖子!”我们停在这里的原因是 CouchDB 有一种非常有趣的方式来列出和处理文档。为了讨论这个问题,我们需要定义如何使用设计文档来进行视图和验证。幸运的是,这正是我们将在下一章中涵盖的内容!
与此同时,让我们快速回顾一下我们在本章中取得的成就。
摘要
在本章中,我们涵盖了创建用户个人资料来显示用户信息,如何优雅地处理异常并向用户显示500和404错误页面,如何在 CouchDB 中对帖子进行建模,以及最后,创建一个为已登录用户创建帖子的表单。
正如我所说,在下一章中,我们将涉及一些 CouchDB 带来的非常酷的概念。这可能是本书中最复杂的一章,但会非常有趣。