CouchDB-和-PHP-Web-开发初学者指南(全)

CouchDB 和 PHP Web 开发初学者指南(全)

原文:zh.annas-archive.org/md5/175c6f9b2383dfb7631db24032548544

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

PHP 和 CouchDB Web 开发将教您结合 CouchDB 和 PHP 的基础知识,从构思到部署创建一个完整的应用程序。本书将指导您开发一个基本的社交网络,并引导您避免与 NoSQL 数据库经常相关的一些常见问题。

这本书涵盖了什么

第一章,“CouchDB 简介”,快速定义了 NoSQL 和 CouchDB 的概述。

第二章,“设置您的开发环境”,为使用 PHP 和 CouchDB 开发应用程序的计算机进行设置。

第三章,“开始使用 CouchDB 和 Futon”,定义了 CouchDB 文档,并展示了如何从命令行和 Futon(CouchDB 内置的管理实用程序)中管理它们。

第四章,“启动您的应用程序”,创建一个简单的 PHP 框架来容纳您的应用程序,并将此代码发布到 GitHub。

第五章,“将您的应用程序连接到 CouchDB”,使用各种方法将您的应用程序连接到 CouchDB,并最终为您的应用程序选择合适的解决方案。

第六章,“建模用户”,在您的应用程序中创建用户,并处理使用 CouchDB 进行文档创建和身份验证。

第七章,“用户配置文件和建模帖子”,使用 Bootstrap 完善您的用户配置文件,并将内容发布到 CouchDB。

第八章,“使用设计文档进行视图和验证”,探索了 CouchDB 专门使用设计文档来提高应用程序质量。

第九章,“为您的应用程序添加花里胡哨的东西”,利用现有工具简化和改进您的应用程序。

第十章,“部署您的应用程序”,向世界展示您的应用程序,并教您如何使用各种云服务启动应用程序和数据库。

额外章节,“复制您的数据”,了解如何使用 CouchDB 的复制系统来扩展您的应用程序。

您可以从www.packtpub.com/sites/default/files/downloads/Replicating_your_Data.pdf下载额外章节

您需要为本书做些什么

您需要一台安装了 Mac OSX 的现代计算机。第一章,“CouchDB 简介”,将为 Linux 和 Windows 机器提供设置说明,并且本书中编写的代码将在任何机器上运行。但是,本书中使用的大多数命令行语句和应用程序都是特定于 Mac OSX 的。

这本书是为谁准备的

这本书适用于初学者和中级 PHP 开发人员,他们有兴趣在项目中使用 CouchDB 开发。高级 PHP 开发人员将欣赏 PHP 架构的熟悉性,并可以轻松学习如何将 CouchDB 纳入其现有的开发经验中。

约定

在本书中,您会经常看到几个标题。

为了清晰地说明如何完成某个过程或任务,我们使用:

行动时间 - 标题

  1. 行动 1

  2. 行动 2

  3. 行动 3

指示通常需要一些额外的解释,以便它们有意义,因此它们后面跟着:

刚刚发生了什么?

这个标题解释了您刚刚完成的任务或指令的工作原理。

您还会在本书中找到其他一些学习辅助工具,包括:

小测验 - 标题

这些是简短的多项选择题,旨在帮助您测试自己的理解。

尝试一下英雄 — 标题

这些设置了实际的挑战,并为您提供了尝试所学内容的想法。

你还会发现一些文本样式,用于区分不同类型的信息。以下是一些样式的示例,以及它们的含义解释。

文本中的代码单词显示如下:“很难为 Linux 标准化install方法,因为有许多不同的风味和配置。”

代码块设置如下:

<Directory />
Options FollowSymLinks
AllowOverride None
Order deny,allow
Allow from all
</Directory>

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

<Directory />
Options FollowSymLinks
**AllowOverride All** 
Order deny,allow
Allow from all
</Directory>

任何命令行输入或输出都是这样写的:

**sudo apt-get install php5 php5-dev libapache2-mod-php5 php5-curl php5-mcrypt** 

新术语重要单词以粗体显示。例如,屏幕上看到的单词,如菜单或对话框中的单词,会以这种方式出现在文本中:“通过打开终端开始”

注意

警告或重要提示会以这样的框出现。

提示

提示和技巧会以这种方式出现。

第一章:CouchDB 简介

欢迎来到 CouchDB 和 PHP Web 开发初学者指南。在这本书中,我们将学习使用 CouchDB 和 PHP 构建一个简单但功能强大的网站的方方面面。为了让你理解为什么我们在 CouchDB 中做某些事情,你首先需要了解 NoSQL 数据库的历史,并了解 CouchDB 在数据库历史中的地位是非常重要的。

在本章中,我们将:

  • 简要介绍数据库的历史及其在技术中的地位

  • 谈论数据库是如何演变成 NoSQL 概念的

  • 通过理解 NoSQL 数据库的不同分类、CAP 定理及其避免 ACID 模型来定义 NoSQL 数据库

  • 查看 CouchDB 的历史及其主要贡献者

  • 谈论 CouchDB 的特殊之处

让我们首先看看数据库的演变以及 NoSQL 是如何出现的。

NoSQL 数据库的演变

在 20 世纪 60 年代初,术语“数据库”被引入世界,作为信息系统背后的一个简单层。将应用程序与数据分离的简单概念是新颖而令人兴奋的,它为应用程序提供了更加强大的可能性。在这一点上,数据库首先存在于基于磁带的设备上,但很快就变得更加可用,作为系统直接访问磁盘上的存储。

在 1970 年,埃德加·科德提出了一种更有效的存储数据的方式——关系模型。这个模型也将使用 SQL 来允许应用程序找到其表中存储的数据。这个关系模型几乎与我们今天所知的传统关系数据库相同。虽然这个模型被广泛接受,但直到 1980 年代中期才有硬件能够有效地使用它。到了 1990 年,硬件终于赶上了,关系模型成为了存储数据的主要方法。

就像在任何技术领域一样,与关系数据库管理系统(RDBMS)竞争出现了。一些流行的 RDMBS 系统的例子包括 Oracle、Microsoft SQL Server、MySQL 和 PostgreSQL。

随着我们走过 2000 年,应用程序开始通过更复杂的应用程序产生大量的数据。社交网络进入了舞台。公司希望理解可用的大量数据。这种转变引发了关于关系模型似乎无法处理的数据结构、可扩展性和数据可用性的严重关切。面对如何管理这么大量不断变化的数据的不确定性,NoSQL 这个术语出现了。

术语“NoSQL”并不是“不使用 SQL”的缩写;它实际上代表“不仅仅是 SQL”。NoSQL 数据库是一组持久性解决方案,不遵循关系模型,也不使用 SQL 进行查询。除此之外,NoSQL 并不是为了取代关系数据库而引入的,而是为了补充关系数据库的不足之处。

什么使 NoSQL 不同

除了 NoSQL 数据库不使用 SQL 来查询数据之外,还有一些关键特征。为了理解这些特征,我们需要涵盖大量的术语和定义。重要的不是你要记住或记住这里的一切,而是你要知道究竟是什么构成了 NoSQL 数据库。

使 NoSQL 数据库与众不同的第一件事是它们的数据结构。有多种不同的方式可以对 NoSQL 数据库进行分类。

NoSQL 数据库的分类

NoSQL 数据库(在大多数情况下)可以分为四种主要的数据结构:

  • 键值存储:它们使用唯一的键和值保存数据。它们的简单性使它们能够非常快速地扩展到巨大的规模。

  • 列存储:它们类似于关系数据库,但是不是存储记录,而是将一列中的所有值一起存储在流中。

  • 文档存储: 它们保存数据而无需在模式中进行结构化,其中包含自包含对象内的键值对桶。这种数据结构让人想起 PHP 中的关联数组。这就是 CouchDB 所在的领域。我们将在第三章中深入探讨这个主题,开始使用 CouchDB 和 Futon

  • 图数据库: 它们以灵活的图模型存储数据,其中包含每个对象的节点。节点具有属性和与其他节点的关系。

我们不会深入讨论每种数据库的示例,但重要的是要看看现有的不同选项。通过在这个层面上查看数据库,我们可以相对容易地看到(一般来说)数据将如何按规模和复杂性进行扩展,通过查看以下屏幕截图:

NoSQL 数据库的分类

如果你看一下这个图表,你会看到我放置��一个典型的关系数据库和一个粗糙的性能线。这条性能线给出了一个简单的想法,即数据库可能如何按规模和复杂性进行扩展。NoSQL 数据库在数据规模和复杂性方面表现得如此出色是如何可能的呢?

在大多数情况下,NoSQL 数据库是可扩展的,因为它们依赖于分布式系统并忽略了 ACID 模型。让我们通过分布式系统讨论我们获得了什么,以及我们放弃了什么,然后定义 ACID 模型。

谈到任何分布式系统(不仅仅是存储或数据库),都有一个概念定义了你可以做什么的限制。这就是所谓的 CAP 定理。

CAP 定理

Eric Brewer在 2000 年引入了 CAP 定理。它指出在任何分布式环境中,不可能提供三个保证。

  • 一致性: 系统中的所有服务器将具有相同的数据。因此,无论用户与分布式系统中的哪个节点交谈,他们都将获得最新的数据。

  • 可用性: 所有服务器将始终返回数据。

  • 分区容错性: 即使单个服务器失败或无法访问,系统仍将作为一个整体运行。

通过观察这些选择,你可以知道肯定希望这三个东西都能得到保证,但从理论上讲是不可能的。在现实世界中,每个 NoSQL 数据库选择了三个选项中的两个,并通常开发了某种过程来减轻第三个未处理属性的影响。

我们很快会谈论 CouchDB 采取的方法,但还有一些关于 NoSQL 数据库避免的另一个概念要学习:ACID。

ACID

ACID是适用于数据库事务的一组属性,这些事务是传统关系数据库的核心。虽然事务非常强大,但它们也是使关系数据库的读写变得相当慢的因素之一。

ACID 由四个主要属性组成:

  • 原子性: 这是一种处理数据的全盘或无。事务中的所有内容必须成功发生,否则所有更改都不会被提交。这是在系统中处理货币或货币时的关键属性,并需要一套检查和平衡系统。

  • 一致性: 数据只有在通过数据库上的所有验证(如触发器、数据类型和约束)后才会被接受。

  • 隔离性: 事务不会影响正在发生的其他事务,其他用户也不会看到正在进行的事务的部分结果。

  • 持久性: 一旦数据保存,它就会在错误、崩溃和其他软件故障中得到保护。

当你阅读 ACID 的定义时,你可能会想:“这些都是必须的!”也许是这样,但请记住,大多数 NoSQL 数据库并没有完全采用 ACID,因为要同时具备所有这些限制并且仍然能够对数据进行快速写入几乎是不可能的。

那么所有这些意味着什么呢?

我现在给了你很多定义,但让我们试着把它们整合成几个简单的列表。让我们讨论一下 NoSQL 数据库的优缺点,何时使用,何时避免使用 NoSQL 数据库。

NoSQL 数据库的优势

随着 NoSQL 数据库的引入,有很多优势:

  • 你可以做一些传统关系数据库的处理和查询能力无法做到的事情。

  • 你的数据是可扩展和灵活的,可以更快地适应规模和复杂性,直接开箱即用。

  • 有新的数据模型需要考虑。如果没有意义,你不必强迫你的数据适应关系模型。

  • 写入数据非常快。

正如你所看到的,NoSQL 数据库有一些明显的优势,但正如我之前提到的,仍然有一些需要考虑的负面影响。

NoSQL 数据库的负面影响

然而,除了好处,也存在一些坏处:

  • 没有通用标准;每个数据库都有一点点不同的做法

  • 查询数据不涉及熟悉的 SQL 模型来查找记录

  • NoSQL 数据库仍然相对不成熟且不断发展

  • 有新的数据模型需要考虑;有时让你的数据适应可能会令人困惑

  • 因为 NoSQL 数据库避免了 ACID 模型,所以不能保证所有数据都能成功写入。

其中一些负面影响可能对你来说很容易接受,除了 NoSQL 避免 ACID 模型这一点。

你应该使用 NoSQL 数据库的情况

现在我们对优缺点有了很好的了解,让我们谈谈使用 NoSQL 数据库的一些绝佳用例:

  • 有大量写入的应用程序

  • 数据的模式和结构可能会发生变化的应用

  • 大量的非结构化或半结构化数据

  • 传统的关系数据库感觉限制很多,你想尝试一些新的东西。

这个列表并不是排他性的,但在何时可以使用 NoSQL 数据库上并没有明确的定义。实际上,你可以在几乎每个项目中使用它们。

你应该避免使用 NoSQL 数据库的情况

然而,有一些明显的领域,你在其中存储数据时应该避免使用 NoSQL。

  • 任何涉及金钱或交易的事情。如果由于 NoSQL 避免 ACID 模型或分布式系统的数据不是 100%可用,会发生什么?

  • 业务关键数据或业务应用程序,如果丢失一行数据可能会导致巨大问题。

  • 需要在关系数据库中实现功能的高度结构化数据。

对于所有这些用例,你应该真正专注于使用关系数据库,以确保你的数据安全可靠。当然,在有意义的情况下,你也可以包含 NoSQL 数据库。

在选择数据库时,重要的是要记住“没有银弹”。这个短语在谈论技术时经常被使用,它意味着没有一种技术可以解决所有问题而没有任何副作用或负面后果。所以要明智选择!

CouchDB 简介

对于这本书和我的一些项目和创业公司,我选择了 CouchDB。让我们来历史性地看一下 CouchDB,然后快速触及它对 CAP 定理的处理方式,以及它的优势和劣势。

CouchDB 的历史

2005 年 4 月,Damien Katz发布了一篇关于他正在开发的新数据库引擎的博客文章,后来被称为 CouchDB,这是Cluster Of Unreliable Commodity Hardware的缩写。Katz 是 IBM 的前 Lotus Notes 开发人员,试图在 C++中创建一个容错的文档数据库,但很快转向Erlang OTP平台。随着时间的推移,CouchDB 在 Damien Katz 的自我资助下开始发展,并于 2008 年 2 月被引入 Apache 孵化器项目。最后,2008 年 11 月,它毕业为一个顶级项目。

Damien 的团队CouchOne在 2011 年与 Membase 团队合并,组成了一个名为Couchbase的新公司。这家公司成立的目的是将CouchDBMembase合并成一个新产品,并增加产品的文档和可见性。

2012 年初,Couchbase 宣布将把重点从促进 CouchDB 转移到创建 Couchbase Server 2.0。这个新的数据库采用了与数据库不同的方法,这意味着它将不再为 CouchDB 社区做出贡献。这一消息在 CouchDB 社区引起了一些不安,直到 Cloudant 介入。

Cloudant,CouchDB 的主要托管公司和 BigCouch 的创建者,BigCouch 是为 CouchDB 构建的容错和水平可扩展的集群框架,宣布他们将把他们的更改合并回 CouchDB,并承担继续开发 CouchDB 的角色。

2012 年初,在撰写本文时,CouchDB 的最重要的版本是 2011 年 3 月 31 日的 1.1.1 版。但 CouchDB 1.2 即将发布!

定义 CouchDB

根据couchdb.apache.org/,CouchDB 可以被定义为:

  • 一个通过 RESTful JSON API 访问的文档数据库服务器

  • 自由式和无模式,具有平面地址空间

  • 分布式,具有强大的增量复制和双向冲突检测和管理

  • 可查询和可索引,具有使用 JavaScript 作为查询语言的面向表的报告引擎。

你可能能够在行间读出,但 CouchDB 从 CAP 定理中选择了可用性和部分容忍,并专注于使用复制实现最终一致性。

我们可以深入探讨每个这些要点的含义,因为在我们深入讨论它们之前,这将占据本书的剩余部分。在每一章中,我们将开始建立在我们对 CouchDB 的知识之上,直到我们拥有一个完全运行的应用程序。

总结

希望你喜欢这一章,并准备深入学习 CouchDB 的方方面面。让我们回顾一下这一章学到的一切。

  • 我们谈到了数据库的历史和 NoSQL 数据库的出现

  • 我们定义了使用 NoSQL 的优缺点

  • 我们看了 CouchDB 的定义和历史

历史课就讲到这里。打开你的电脑。在下一章中,我们将设置好一切,用 CouchDB 和 PHP 开发 Web 应用程序,并确保一切设置正确。

第二章:设置你的开发环境

在本章中,我们将设置你的计算机,以便你可以使用 PHP 和 CouchDB 开发 Web 应用程序。在开发 Web 应用程序时涉及到很多技术,所以在开始编写代码之前,我们需要确保我们的系统配置正确。

在本章中,我们将:

  • 讨论你的操作系统以及如何安装必要的组件

  • 了解开发 PHP 和 CouchDB 应用程序所需的工具

  • 配置我们的 Web 开发环境

  • 了解 Homebrew 并安装 CouchDB

  • 使用 Homebrew 来安装 Git 进行版本控制

  • 确认你可以向 CouchDB 发出请求

准备好了吗?很好!让我们开始谈论操作系统以及它们在设置你的开发环境中所起的作用。

操作系统

本书将主要关注 Mac OS X 操作系统(10.5 及更高版本)。虽然在任何操作系统上都可以开发 PHP 和 CouchDB 应用程序,但为了简单和简洁起见,我将大部分讨论限制在 Mac OS X 上。如果你使用的是 Mac,你可以直接跳到下一节,标题为在 Mac OS X 上设置你的 Web 开发环境

如果你使用的是 Windows 或 Linux,不用担心!我会给你一些设置提示,然后你可以自己操作。值得注意的是,我在本书中使用的命令行语句是为 Mac OS 设计的。因此,一些东西,比如导航到你的工作目录,文件的位置等等可能不会按照描述的方式工作。

Windows

如果你使用 Windows,有一些简单的步骤需要遵循才能让你的机器正常运行。

安装 Apache 和 PHP

你可以使用 WAMP (www.wampserver.com/en/)或 XAMPP (www.apachefriends.org/en/xampp.html)来简化 Apache 和 PHP 环境的设置。这两个选项都可以让你通过几次鼠标点击轻松设置 Apache 和 PHP。

安装 Git

Git适用于每个操作系统。要在 Windows 上安装 Git,请转到 Git 的主页(git-scm.com/),然后点击 Windows 图标。

安装 CouchDB

你可以在这里找到更多关于在 Windows 上使用 Apache 的安装页面安装 CouchDB 的信息:wiki.apache.org/couchdb/Installing_on_Windows

Linux

由于 Linux 的不同版本和配置很多,很难标准化 Linux 的install方法。但是如果你使用的是通用发行版,比如 Ubuntu,所有必需的工具都可以通过几个简单的命令行语句来安装。

安装 Apache 和 PHP

apt-get是一个强大的工具,我们将使用它来在你的系统中安装应用程序和实用工具。让我们首先确保apt-get是最新的,通过运行以下命令:

**sudo apt-get update** 

通过安装 Apache 来确保我们可以托管我们的 PHP 页面:

**sudo apt-get install apache2** 

既然我们有了 Apache,让我们安装 PHP 和其他一些运行本书中代码所需的组件:

**sudo apt-get install php5 php5-dev libapache2-mod-php5 php5-curl php5-mcrypt** 

我们已经准备好托管网站所需的一切。因此,让我们重新启动 Apache 以使我们的更改生效:

**sudo /etc/init.d/apache2 restart** 

安装 Git

我们将使用 Git 进行源代码控制;幸运的是,通过我们的朋友apt-git的帮助,安装它非常容易。通过运行以下命令来安装 Git:

**sudo apt-get install git-core** 

安装 CouchDB

CouchDB 是本书中我们将使用的数据库。在本节中,我们将使用命令行安装和启动它。

  1. 使用apt-get安装 CouchDB:
**sudo apt-get install couchDB** 

  1. 通过运行以下命令将 CouchDB 作为服务启动:
**sudo /etc/init.d/couchdb start** 

是不是很容易?如果你使用的是其他 Linux 发行版,那么你可能需要研究如何安装所有必需的应用程序和工具。

既然我们已经解决了这个问题,让我们讨论一下在 Mac OS X 上设置 Web 开发环境。

在 Mac OS X 上设置你的 Web 开发环境

在这一部分,我们将逐步确保我们的开发环境设置正确。从现在开始,我假设你正在使用运行 Mac OS X 的机器,没有对 Apache 或 PHP 进行任何特殊修改。如果你对开发环境进行了很多定制,那么你可能已经知道如何配置你的机器,使一切正常工作。

现在我已经用免责声明把你弄得厌烦了,让我们开始吧!我们旅程的第一部分是认识一个我们将花费大量时间的应用程序:Terminal

Terminal

Terminal是 Mac OS X 内置的命令行实用程序。当你刚开始使用命令行时,可能会有点奇怪的体验,但一旦掌握了,它就非常强大。如果基本命令,如cd, lsmkdir对你来说像胡言乱语,那么你可能需要快速了解一下 UNIX 命令行。

这是如何打开Terminal的:

  1. 打开Finder

  2. 点击应用程序

  3. 找到名为实用工具的文件夹,并打开它。

  4. Terminal图标拖到你的 dock 中;你会经常使用它!

  5. 点击你 dock 中的Terminal图标。Terminal

行动时间——使用 Terminal 显示隐藏文件

现在我们已经启动了Terminal,让我们通过运行一个快速命令来熟悉它,这个命令会显示你电脑上所有的隐藏文件。不管你知不知道,有各种各样的文件是隐藏的,它们需要可见才能完成我们的开发环境设置。

  1. 首先打开Terminal

  2. 输入以下命令以允许 Finder 显示隐藏文件,准备好后按Enter

**defaults write com.apple.finder AppleShowAllFiles TRUE** 

  1. 为了看到文件,你需要重新启动Finder,输入以下命令,然后按Enter
**killall Finder** 

刚刚发生了什么?

我们刚刚使用Terminal运行了一个特殊命令,配置了Finder显示隐藏文件,然后运行了另一个命令重新启动了Finder。你不需要记住这些命令或完全理解它们的含义;你可能永远不需要再次输入这些命令。如果你四处看看你的电脑,你应该会看到很多以前没见过的文件。这是我电脑上的一个快速示例:

刚刚发生了什么?

提示

如果看到这么多文件让你烦恼,你可以在设置完成后再次隐藏隐藏文件。你只需在Terminal中运行以下命令:defaults write com.apple.finder AppleShowAllFiles FALSE。然后通过运行以下命令重新启动Finderkillall Finder

现在我们的机器上所有的文件都显示出来了,让我们谈谈文本编辑器,这将是你查看和编辑开发项目的主要方式。

文本编辑器

为了编写代码,你需要一个可靠的文本编辑器。有很多文本编辑器可供选择,你可以使用任何你喜欢的。本书中的所有代码都适用于任何文本编辑器。我个人更喜欢TextMate,因为它简单易用。

你可以在这里下载并安装TextMatemacromates.com/

Apache

Apache 是一个开源的 Web 服务器,也是将在本书中编写的 PHP 代码运行的引擎。幸运的是,Apache 预装在所有的 Mac OS X 安装中,所以我们只需要使用Terminal来启动它。

  1. 打开Terminal

  2. 运行以下命令启动 Apache:

**sudo apachectl start** 

这就是在你的电脑上启动 Apache 所需的全部。如果 Apache 已经在运行,它不会让你启动它。尝试再次输入相同的语句;你的机器会提醒你它已经在运行了:

Apache

注意

这很不可能,但万一您的机器没有安装 Apache,您可以按照 Apache 网站上的说明进行安装:httpd.apache.org/docs/2.0/install.html

Web 浏览器

您可能每天都在上网时使用 Web 浏览器,但它也可以作为我们强大的调试工具。我将使用 Chrome 作为我的 Web 浏览器,但 Safari、Firefox 或 Internet Explorer 的最新版本也可以正常工作。让我们使用我们的 Web 浏览器来检查 Apache 是否可访问。

行动时间-打开您的 Web 浏览器

我们将通过打开 Web 浏览器并导航到 Apache 的 URL 来访问我们机器上的 Apache 服务。

  1. 打开您的 Web 浏览器。

  2. 在地址栏中输入http://localhost,然后按Enter键。

  3. 您的浏览器将向您显示以下消息:行动时间-打开您的 Web 浏览器

刚刚发生了什么?

我们使用 Web 浏览器访问了 Apache,作为回报,它向我们显示了一个快速的验证,证明一切都正确连接。我们的计算机知道我们正在尝试访问本地 Apache 服务,因为 URLhttp://localhost。URLhttp://localhost实际上映射到地址http://127.0.0.1:80,这是 Apache 服务的地址和端口。当我们讨论 CouchDB 时,您将再次看到127.0.0.1

PHP

PHP是本书的标题,因此您知道它将在开发过程中发挥重要作用。PHP 已经安装在您的机器上,因此您无需安装任何内容。让我们通过 Terminal 再次检查您是否可以访问 PHP。

行动时间-检查您的 PHP 版本

我们将通过 Terminal 访问您的计算机上的 PHP 来检查 PHP 是否正常工作。

  1. 打开Terminal

  2. 运行以下命令以返回 PHP 的版本:

**php -v** 

  1. Terminal将以类似以下内容的方式做出响应:行动时间-检查您的 PHP 版本

刚刚发生了什么?

我们使用Terminal确保我们的机器上的 PHP 正确运行。我们不仅检查了 PHP 是否可访问,还要求其版本。您的版本可能与我的略有不同,但只要您的版本是 PHP 5.3 或更高版本即可。

注意

如果您的版本低于 PHP 5.3 或无法使 PHP 响应,您可以通过查看 PHP 手册进行安装或升级:php.net/manual/en/install.macosx.php

行动时间-确保 Apache 可以连接到 PHP

为了创建一个 Web 应用程序,Apache 需要能够运行 PHP 代码。因此,我们将检查 Apache 是否可以访问 PHP。

  1. 使用Finder导航到以下文件夹:/etc/apache2

  2. 在您的文本编辑器中打开名为httpd.conf的文件。

  3. 查看文件,并找到以下行(它应该在第 116 行左右):

**#LoadModule php5_module libexec/apache2/libphp5.so** 

  1. 删除config文件中位于此字符串前面的哈希(#)符号以取消此行的注释。您的配置文件可能已经取消了此注释。如果是这样,那么您无需更改任何内容。无论如何,最终结果应如下所示:
**LoadModule php5_module libexec/apache2/libphp5.so** 

  1. 打开Terminal

  2. 通过运行以下命令重新启动 Apache:

**sudo apachectl restart** 

刚刚发生了什么?

我们打开了 Apache 的主配置文件httpd.conf,并取消了一行的注释,以便 Apache 可以加载 PHP。然后我们重新启动了 Apache 服务器,以便更新的配置生效。

行动时间-创建一个快速信息页面

我们将通过快速创建一个phpinfo页面来双重检查 Apache 是否能够渲染 PHP 脚本,该页面将显示有关您配置的大量数据。

  1. 打开您的文本编辑器。

  2. 创建一个包含以下代码的新文件:

<?php phpinfo(); ?>

  1. 将文件保存为info.php,并将该文件保存在以下位置:/Library/WebServer/Documents/info.php

  2. 打开您的浏览器。

  3. 将浏览器导航到http://localhost/info.php

  4. 您的浏览器将显示以下页面:Time for action — creating a quick info page

刚刚发生了什么?

我们使用文本编辑器创建了一个名为info.php的文件,其中包含一个名为phpinfo的特殊 PHP 函数。我们将info.php文件保存到文件夹:/Library/Webserver/Documents。这个文件夹是 Apache 服务将显示的所有文件的默认位置(仅适用于 Mac OS X)。当您的浏览器访问info.php页面时,phpinfo会查看您的 PHP 安装并返回一个包含有关您配置详细信息的 HTML 文件。您可以看到这里有很多事情要做。在我们继续之前,随意浏览并查看一些信息。

微调 Apache

我们最终建立了基本的 Web 开发环境。但是,为了构建我们的应用程序,我们需要调整 Apache 中的一些内容;首先是启用一个名为mod_rewrite的内置模块。

行动时间 — 进一步配置 Apache

mod_rewrite将允许我们动态重写请求的 URL,这将帮助我们构建具有清晰 URL 的应用程序。重写本身在另一个名为.htaccess的 Apache 配置文件中处理,我们将在第四章 启动您的应用程序中介绍。在下一节中,我们将配置 Apache,以便启用mod_rewrite

  1. 使用Finder导航到以下文件夹:/etc/apache2

  2. 在文本编辑器中找到并打开名为httpd.conf的文件。

  3. 浏览文件,并找到此行(应该是第 114 行):

**#LoadModule rewrite_module libexec/apache2/mod_rewrite.so** 

  1. 取消注释行,删除井号(#)符号。您的系统可能已经配置为启用mod_rewrite。无论如何,请确保它与以下代码匹配:
**LoadModule rewrite_module libexec/apache2/mod_rewrite.so** 

  1. 浏览文件,并找到此代码块(应该从第 178-183 行开始):
<Directory />
Options FollowSymLinks
AllowOverride None
Order deny,allow
Allow from all
</Directory>

  1. 更改代码行,将AllowOverride None更改为AllowOverride All。结果部分应如下所示:
<Directory />
Options FollowSymLinks
**AllowOverride All** 
Order deny,allow
Allow from all
</Directory>

  1. 继续滚动文件,直到找到这段代码(应该从第 210-215 行开始):
#
# AllowOverride controls what directives may be placed in #.htaccess files.
# It can be —All—, —None—, or any combination of the keywords:
# Options FileInfo AuthConfig Limit
#
AllowOverride None

  1. 更改此代码行,将AllowOverride None更改为AllowOverride All。结果部分应如下所示:
#
# AllowOverride controls what directives may be placed in #.htaccess files.
# It can be "All", "None", or any combination of the keywords:
# Options FileInfo AuthConfig Limit
#
AllowOverride All

  1. 打开Terminal

  2. 通过运行以下命令重新启动 Apache:

**sudo apachectl restart** 

刚刚发生了什么?

我们刚刚配置了 Apache,使其可以更自由地运行,并包括使用mod_rewrite模块重写 URL 的能力。然后,我们将AllowOverride的配置从None更改为AllAllowOverride告诉服务器在找到.htaccess文件时该怎么做。将此设置为None时,将忽略.htaccess文件。将设置更改为All允许在.htaccess文件中覆盖设置。这正是我们将在第四章中开始构建应用程序时要做的事情。

我们的 Web 开发设置已经完成!

我们现在已经设置好了创建标准 Web 应用程序的一切。我们有 Apache 处理请求。我们已经连接并响应来自 Apache 的 PHP 调用,并且我们的文本编辑器已准备好接受我们可以投入其中的任何代码。

我们还需要一些部分才能拥有完整的开发环境。在下一节中,我们将安装 CouchDB 作为我们的数据库。

安装 CouchDB

在本节中,我们将在您的计算机上安装 CouchDB 1.0 并为其进行开发准备。有多种方法可以在您的计算机上安装 CouchDB,因此如果您在以下安装过程中遇到问题,请参考 CouchDB 的网站:wiki.apache.org/couchdb/Installation

Homebrew

为了安装本书中将使用的其余组件,我们将使用一个名为Homebrew的实用程序。Homebrew 是安装苹果 OSX 中遗漏的 UNIX 工具的最简单方法。在我们可以使用 Homebrew 安装其他工具之前,我们首先需要安装 Homebrew。

安装 Homebrew 的时间到了

我们将使用终端下载 Homebrew 并将其安装到我们的计算机上。

  1. 打开终端

  2. 终端中输入以下命令,每行后按Enter

**sudo mkdir -p /usr/local
sudo chown -R $USER /usr/local
curl -Lf http://github.com/mxcl/homebrew/tarball/master | tar xz -- strip 1 -C/usr/local** 

  1. 终端将会显示一个进度条,告诉你安装过程进行得如何。安装完成后,你会收到一个成功的消息,然后你就可以再次控制终端了。

刚刚发生了什么?

我们添加了目录/usr/local,这是 Homebrew 将保存所有文件的位置。然后我们确保该文件夹归我们所有(当前用户)。然后我们使用cURL语句从 Github 获取存储库(我将在本章后面介绍cURL;我们将经常使用它)。获取存储库后,使用命令行函数tar解压缩,并将其放入/usr/local文件夹中。

安装 CouchDB 的时间到了

现在我们已经安装了 Homebrew,终于可以安装 CouchDB 了。

注意

请注意,在 Homebrew 安装 CouchDB 之前,它将安装所有的依赖项,包括:Erlang,Spidermonkey,ICU 等。这一部分可能需要 10-15 分钟才能完成,因为它们是在您的计算机上本地编译的。如果看起来花费的时间太长,不要担心;这是正常的。

我们将使用 Homebrew 安装 CouchDB。

  1. 打开终端

  2. 运行以下命令:

**brew install couchdb -v** 

  1. 接下来的几分钟,终端将会回复大量的文本。您将看到它获取每个依赖项,然后安装它。最后,您将收到一个成功的消息。

刚刚发生了什么?

我们刚刚从源代码安装了 CouchDB 并下载了所有的依赖项。安装完成后,Homebrew 将所有内容放在正确的文件夹中,并配置了我们使用 CouchDB 所需的一切。

检查我们的设置是否完成

在过去的几页中,我们已经完成了很多工作。我们已经设置好了我们的 Web 开发环境并安装了 CouchDB。现在,让我们再次确认我们能否运行和访问 CouchDB。

启动 CouchDB

CouchDB 很容易管理。它作为一个服务运行,我们可以使用命令行启动和停止它。让我们用命令行语句启动 CouchDB。

  1. 打开终端

  2. 运行以下命令:

**couchdb -b** 

  1. 终端将回复以下内容:
**Apache CouchDB has started, time to relax.** 

太好了!现在我们已经将 CouchDB 作为后台进程启动,它将在后台处理请求,直到我们关闭它。

检查 CouchDB 是否正在运行的时间到了

我们将尝试使用命令行实用程序cURL(为了简单起见,我们将其拼写为curl)来访问我们机器上的 CouchDB 服务,它允许您发出原始的 HTTP 请求。curl是我们与 CouchDB 进行通信的主要方法。让我们从一个curl语句开始与 CouchDB 交流。

  1. 打开终端

  2. 运行以下语句创建一个将访问 CouchDB 的请求:

**curl http://127.0.0.1:5984/** 

  1. 终端将回复以下内容:
**{"couchdb":"Welcome","version":"1.0.2"}** 

刚刚发生了什么?

我们刚刚使用curl通过发出GET请求与 CouchDB 服务进行通信。默认情况下,curl会假定我们正在尝试发出GET请求,除非我们告诉它不是。我们向http://127.0.0.1:5984发出了我们的curl语句。将这个资源分解成几部分,http://127.0.0.1是我们本地计算机的 IP,5984是 CouchDB 默认运行的端口。

作为后台进程运行 CouchDB

如果我们在这里停止配置,您将不得不在每次开始开发时运行couchdb b。这对我们来说很快就会成为一个痛点。因此,让我们将 CouchDB 作为一个系统守护程序运行,即使在重新启动计算机后也会一直在后台运行。为了做到这一点,我们将使用一个名为launchd的 Mac OS X 服务管理框架,该框架管理系统守护程序并允许启动和停止它们。

  1. 打开终端

  2. 通过运行以下命令杀死 CouchDB 的后台进程:

**couchdb -k** 

  1. 如果 CouchDB 正在运行,它将返回以下文本:
**Apache CouchDB has been killed.** 

  1. 让我们将 CouchDB 作为一个真正的后台进程运行,并确保每次启动计算机时都会运行它,通过运行以下语句:
**launchctl load -w /usr/local/Cellar/couchdb/1.0.2/Library/LaunchDaemons/org.apache.couchdb.plist** 

注意

如果您的 CouchDB 版本与我的不同,您将不得不更改此脚本中的版本,该脚本显示为"1.0.2",以匹配您的版本。

CouchDB 现在在后台运行,即使我们重新启动计算机,也不必担心在尝试使用它之前启动服务。

如果由于某种原因,您决定不希望 CouchDB 在后台运行,您可以通过运行以下命令卸载它:

**launchctl unload /usr/local/Cellar/couchdb/1.0.2/Library/LaunchDaemons/org.apache.couchdb.plist** 

您可以通过使用我们之前使用的curl语句来双重检查 CouchDB 是否正在运行:

  1. 打开终端

  2. 运行以下命令:

**curl http://127.0.0.1:5984/** 

  1. 终端将回复以下内容:
**{"couchdb":"Welcome","version":"1.0.2"}** 

安装版本控制

版本控制系统允许开发人员跟踪代码更改,合并其他开发人员的代码,并回滚任何无意的错误。对于有几个开发人员的项目,版本控制系统是必不可少的,但对于单个开发人员项目也可能是一个救命稻草。把它想象成一个安全网-如果您不小心做了一些您不想做的事情,那么源代码控制就在那里保护您。在版本控制方面有几种选择,但在本书中,我们将使用 Git。

Git

Git (git-scm.com/)已经成为更受欢迎和广泛采用的版本控制系统之一,因为它的分布式性和易用性。实际使用 Git 的唯一比安装更容易的事情就是安装它。我们将使用 Homebrew 来安装 Git,就像我们用 CouchDB 一样。

行动时间-安装和配置 Git

准备好了!我们将使用 Homebrew 在计算机上安装 Git。

  1. 打开终端

  2. 运行以下命令使用 Homebrew 安装 Git:

**brew install git** 

  1. 终端将在短短几分钟内为您下载并安装 Git。然后,它将以成功消息回复您,告诉您 Git 已安装。

  2. 安装 Git 后,您需要配置它,以便在提交数据更改时知道您是谁。运行以下命令来标识自己,并确保在我放置Your Nameyour_email@domain.com的地方填写您自己的信息:

**git config global user.name "Your Name"
git config global user.email your_email@domain.com** 

刚刚发生了什么?

我们刚刚使用 Homebrew 从源代码安装了 Git。然后,我们配置了 Git 以使用我们的姓名和电子邮件地址。这些设置将确保从这台机器提交到源代码控制的任何更改都能被识别。

你遇到了什么问题吗?

我们已经完成了配置我们的系统!在一个完美的世界中,一切都会顺利安装,但完全有可能有些东西安装不完美。如果似乎有些东西不正常,或者您认为自己可能在某个地方打错了字,我有一个脚本可以帮助您重新回到正轨。这个命令可以通过在终端中调用我在 github 上的一个文件来本地执行。您只需在终端中运行以下命令,它将运行本章所需的所有必要代码:

sh <(curl -s https://raw.github.com/timjuravich/environment-setup/master/ configure.sh)

这个脚本将完成本节中提到的所有工作,并且可以安全地运行多次。我本可以给你这个命令并在几页前结束这一章,但这一章对教会你如何使用我们将在接下来的章节中使用的工具和实用程序是至关重要的。

小测验

  1. 当我们使用默认的 Apache 安装进行 Web 开发时,默认的工作目录在哪里?

  2. 为了在本地开发环境中使用 CouchDB,我们需要确保两个服务正在运行。它们是什么,你如何在终端中让它们运行?

  3. 你使用什么命令行语句来向 CouchDB 发出Get请求?

总结

让我们快速回顾一下本章涵盖的所有内容:

  • 我们熟悉了终端并用它来显示隐藏文件

  • 我们安装了一个文本编辑器供我们在开发中使用

  • 我们学会了如何配置 Apache 以及如何通过命令行与其交互

  • 我们学会了如何创建简单的 PHP 文件,并将它们放在正确的位置,以便 Apache 可以显示它们

  • 我们学会了如何安装 Homebrew,然后用它来安装 CouchDB 和 Git

  • 我们检查了一下,确保 CouchDB 已经启动运行

在下一章中,我们将更加熟悉 CouchDB,并探索如何在创建我们的 Web 应用程序中使用它。

第三章:开始使用 CouchDB 和 Futon

在上一章中,我们设置了开发环境,我相信你迫不及待地想知道 CouchDB 对我们有什么作用。在这一点上,我们将花费整整一章的时间来深入了解 CouchDB。

具体来说,我们将:

  • 深入了解 CouchDB 的含义,学习它在数据库和文档中的样子

  • 学习我们将如何通过其 RESTful JSON API 与 CouchDB 交互

  • 使用 CouchDB 内置的管理控制台:Futon

  • 学习如何向 CouchDB 数据库添加安全性

什么是 CouchDB?

CouchDB 的定义(由couchdb.apache.org/)定义)的第一句是:

CouchDB 是一个文档数据库服务器,可通过 RESTful JSON API 访问。

让我们解剖这个句子,充分理解它的含义。让我们从术语数据库服务器开始。

数据库服务器

CouchDB 采用了面向文档的数据库管理系统,提供了一组没有模式、分组或层次结构的文档。这是NoSQL引入的概念,与关系数据库(如 MySQL)有很大的不同,您会期望在那里看到表、关系和外键。每个开发人员都经历过一个项目,他们不得不将关系数据库模式强加到一个真正不需要表和复杂关系的项目中。这就是 CouchDB 与众不同的地方;它将所有数据存储在一个自包含的对象中,没有固定的模式。下面的图表将有助于说明这一点:

数据库服务器

在前面的例子中,我们可能希望方便许多用户属于一对多的组。为了在关系数据库(如 MySQL)中处理这个功能,我们需要创建一个用户表、一个组表,以及一个名为users_groups的链接表,允许您将许多用户映射到许多组。这种做法对大多数 Web 应用程序都很常见。

现在看看 CouchDB 文档。这里没有表或链接表,只有文档。这些文档包含与单个对象相关的所有数据。

注意

这个图表非常简化。如果我们想在 CouchDB 中创建更多关于组的逻辑,我们将不得不创建文档,并在用户文档和组文档之间建立简单的关系。随着我们深入学习,我们将介绍如何处理这种类型的关系。

在本节中,我们经常看到术语文档。所以让我们进一步了解文档是什么,以及 CouchDB 如何使用它们。

文档

为了说明你可能如何使用文档,首先想象一下你在填写工作申请表的纸质表格。这个表格包含关于你、你的地址和过去地址的信息。它还包含关于你过去的工作、教育、证书等许多信息。一个文档会将所有这些数据保存下来,就像你在纸质表格上看到的那样 - 所有信息都在一个地方,没有任何不必要的复杂性。

在 CouchDB 中,文档被存储为包含键值对的 JSON 对象。每个文档都有保留字段用于元数据,如id, revisiondeleted。除了保留字段,文档是 100%无模式的,这意味着每个文档可以根据需要格式化和独立处理,有许多不同的变化。

CouchDB 文档的示例

让我们看一个 CouchDB 文档可能是什么样子的例子:

{
"_id": "431f956fa44b3629ba924eab05000553",
"_rev": "1-c46916a8efe63fb8fec6d097007bd1c6",
"title": "Why I like Chicken",
"author": "Tim Juravich",
"tags": [
"Chicken",
"Grilled",
"Tasty"
],
"body": "I like chicken, especially when it's grilled."
}

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中购买的所有 Packt 图书下载示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,文件将直接通过电子邮件发送给您。

JSON 格式

你可能注意到的第一件事是文档的奇怪标记,这是 JavaScript 对象表示法(JSON)。JSON 是一种基于 JavaScript 语法的轻量级数据交换格式,非常便携。CouchDB 使用 JSON 进行与其所有通信,因此在阅读本书的过程中,您将对其非常熟悉。

键值存储

接下来,您可能注意到这个文档中有很多信息。有一些简单易懂的键值对,比如 "title", "author""body",但您还会注意到 "tags" 是一个字符串数组。CouchDB 允许您直接将尽可能多的信息嵌入到文档中。这对于习惯于规范化和结构化数据库的关系数据库用户来说可能是一个新概念。

保留字段

我们之前提到了保留字段。让我们来看看您在前面的示例文档中看到的两个保留字段:_id_rev

_id 是文档的唯一标识符。这意味着 _id 是必需的,没有两个文档可以具有相同的值。如果在创建文档时没有定义 _id,CouchDB 将为您选择一个唯一的值。

_rev 是文档的修订版本,是帮助驱动 CouchDB 版本控制系统的字段。每次保存文档时,都需要修订号,以便 CouchDB 知道哪个版本的文档是最新的。这是必需的,因为 CouchDB 不使用锁定机制,这意味着如果两个人同时更新文档,那么先保存更改的人获胜。CouchDB 修订系统的一个独特之处在于,每次保存文档时,原始文档不会被覆盖,而是创建一个新的带有新数据的文档,同时 CouchDB 以其原始形式存储先前文档的备份。旧的修订版本保持可用,直到数据库被压缩或发生某些清理操作。

定义句子的最后一部分是 RESTful JSON API。接下来让我们来介绍它。

RESTful JSON API

为了理解 REST,让我们首先定义 超文本传输协议(HTTP)。HTTP 是互联网的基础协议,它定义了消息的格式和传输方式,以及在使用各种方法时服务应该如何响应。这些方法包括 GET, PUT, POSTDELETE 等四个主要动词。为了充分理解 HTTP 方法的功能,让我们首先定义 REST。

表述状态转移(REST) 是一种通过 HTTP 方法访问可寻址资源的无状态协议。无状态 意味着每个请求包含了完全理解和使用请求中的数据所需的所有信息,可寻址资源 意味着您可以通过 URL 访问对象。

这本身可能并不意味着太多,但是将所有这些想法结合在一起,它就成为了一个强大的概念。让我们通过两个例子来说明 REST 的强大之处:

资源 获取 放置 发布 删除
http://localhost/collection 读取 collection 中所有项目的列表 更新 另一个 collection 创建 一个新的 collection 删除 collection
http://localhost/collection/abc123 读取 collectionabc123 项目的详细信息 更新 collectionabc123 的详细信息 创建 collection 中的新对象 abc123 collection 中删除 abc123

通过查看表格,您可以看到每个资源都以 URL 的形式存在。第一个资源是 collection,第二个资源是 abc123,它位于 collection 中。当您对这些资源传递不同的方法时,每个资源会有不同的响应。这就是 REST 和 HTTP 共同工作的美妙之处。

请注意我在表中使用的粗体字:读取,更新,创建删除。这些词实际上是另一个概念,当然,它有自己的术语;CRUD。这个不太动听的术语 CRUD 代表创建、读取、更新和删除,是 REST 用来定义当 HTTP 方法与 URL 形式的资源结合时发生的情况的概念。因此,如果您要将所有这些都归纳起来,您将得到以下图表:

RESTful JSON APIRESTexamples

这个图表的含义是:

  • 为了创建一个资源,您可以使用POSTPUT方法

  • 为了读取一个资源,您需要使用GET方法

  • 为了更新一个资源,您需要使用PUT方法

  • 为了删除一个资源,您需要使用DELETE方法

正如您所看到的,CRUD 的这个概念使得清楚地找出您在执行特定操作时需要使用的方法。

现在我们已经了解了 REST 的含义,让我们继续学习术语API,它代表应用程序编程接口。虽然有很多不同的用例和 API 的概念,但 API 是我们将用来与 CouchDB 进行编程交互的工具。

现在我们已经定义了所有术语,RESTful JSON API 可以定义如下:我们可以通过向 CouchDB API 发出 HTTP 请求并定义资源、HTTP 方法和任何额外数据来与 CouchDB 进行交互。结合所有这些意味着我们正在使用 REST。在 CouchDB 处理我们的 REST 请求后,它将返回一个 JSON 格式的响应,其中包含请求的结果。

当我们逐个浏览每个 HTTP 方法时,所有这些背景知识将开始变得有意义,因为我们将使用 CouchDB 的 RESTful JSON API 进行操作。

我们将使用curl(我们在上一章中学习使用的)通过发出原始 HTTP 请求来探索每个 HTTP 方法。

执行操作——获取 CouchDB 中所有数据库的列表

在本书的前面,当我们使用curl语句:curl http://localhost:5984时,您已经看到了一个GET请求。

这一次,让我们发出一个GET请求来访问 CouchDB 并获取服务器上所有数据库的列表。

  1. 终端中运行以下命令:
**curl -X GET http://localhost:5984/_all_dbs** 

  1. 终端将会返回以下内容:
**["_users"]** 

刚刚发生了什么?

我们使用终端触发了一个GET请求到 CouchDB 的 RESTful JSON API。我们使用了curl的一个选项:-X,来定义 HTTP 方法。在这种情况下,我们使用了GETGET是默认方法,所以在技术上,如果您愿意,您可以省略-X。一旦 CouchDB 处理了请求,它会返回一个包含 CouchDB 服务器中所有数据库列表的响应。目前,只有_users数据库,这是 CouchDB 用于验证用户的默认数据库。

执行操作——在 CouchDB 中创建新的数据库

在这个练习中,我们将发出一个PUT请求,这将在 CouchDB 中创建一个新的数据库。

  1. 通过在终端中运行以下命令创建一个新的数据库:
**curl -X PUT http://localhost:5984/test-db** 

  1. 终端将会返回以下内容:
**{"ok":true}** 

  1. 尝试使用终端中的以下命令创建另一个同名数据库:
**curl -X PUT http://localhost:5984/test-db** 

  1. 终端将会返回以下内容:
**{"error":"file_exists","reason":"The database could not be created, the file already exists."}** 

  1. 好吧,那没用。所以让我们尝试通过在终端中运行以下命令创建一个不同名称的数据库:
**curl -X PUT http://localhost:5984/another-db** 

  1. 终端将会返回以下内容:
**{"ok":true}** 

  1. 让我们快速检查test-db数据库的详细信息,并查看更多关于它的详细信息。为此,请在终端中运行以下命令:
**curl -X GET http://localhost:5984/test-db** 

  1. 终端将会返回类似于以下内容(我重新格式化了我的内容以便阅读):
**{
"committed_update_seq": 1,
"compact_running": false,
"db_name": "test-db",
"disk_format_version": 5,
"disk_size": 4182,
"doc_count": 0,
"doc_del_count": 0,
"instance_start_time": "1308863484343052",
"purge_seq": 0,
"update_seq": 1
}** 

刚刚发生了什么?

我们刚刚使用终端来触发了一个PUT方法,通过 CouchDB 的 RESTful JSON API 来创建数据库,将test-db作为我们想要在 CouchDB 根 URL 的末尾创建的数据库的名称。当数据库成功创建时,我们收到了一条一切正常的消息。

接下来,我们发出了一个PUT请求来创建另一个同名test-db数据库。因为不能有多个同名数据库,我们收到了一个错误消息。

然后我们使用PUT请求再次创建一个新的数据库,名为another-db。当数据库成功创建时,我们收到了一条一切正常的消息。

最后,我们向我们的test-db数据库发出了一个GET请求,以了解更多关于数据库的信息。知道每个统计数据的确切含义并不重要,但这是一个了解数据库概况的有用方式。

值得注意的是,在最后的GET请求中调用的 URL 与我们创建数据库时调用的 URL 相同。唯一的区别是我们将 HTTP 方法从PUT改为了GET。这就是 REST 的工作原理!

行动时间——删除数据库在 CouchDB

在这个练习中,我们将调用DELETE请求来删除another-db数据库。

  1. 通过在终端中运行以下命令删除another-db
**curl -X DELETE http://localhost:5984/another-db** 

  1. 终端将会回复以下内容:
**{"ok":true}** 

刚刚发生了什么?

我们使用终端来触发 CouchDB 的 RESTful JSON API 的DELETE方法。我们在根 URL 的末尾传递了我们想要删除的数据库的名称another-db。当数据库成功删除时,我们收到了一条一切正常的消息。

行动时间——创建 CouchDB 文档

在这个练习中,我们将通过发起POST调用来创建一个文档。你会注意到我们的curl语句将开始变得有点复杂。

  1. 通过在终端中运行以下命令在test-db数据库中创建一个文档:
**curl -X POST -H "Content-Type:application/json" -d '{"type": "customer", "name":"Tim Juravich", "location":"Seattle, WA"}' http://localhost:5984/test-db** 

  1. 终端将会回复类似以下的内容:
**{"ok":true,"id":"39b1fe3cdcc7e7006694df91fb002082","rev":"1-8cf37e845c61cc239f0e98f8b7f56311"}** 

  1. 让我们从 CouchDB 中检索新创建的文档。首先复制你在终端最后一次响应中收到的 ID 到你的剪贴板;我的是39b1fe3cdcc7e7006694df91fb002082,但你的可能不同。然后在终端中运行这个命令,将你的 ID 粘贴到 URL 的末尾:
**curl -X GET http://localhost:5984/test-db/41198fc6e20d867525a8faeb7a000015 | python -mjson.tool** 

  1. 终端将会回复类似以下的内容:
**{
"_id": "41198fc6e20d867525a8faeb7a000015",
"_rev": "1-4cee6ca6966fcf1f8ea7980ba3b1805e",
"location": "Seattle, WA",
"name": "Tim Juravich",
"type:": "customer"
}** 

刚刚发生了什么?

我们使用终端来触发 CouchDB 的 RESTful JSON API 的POST调用。这一次,我们的curl语句增加了一些以前没有使用过的选项。-H选项使我们能够设置POST方法的 HTTP 请求头。我们需要将content-type设置为 JSON,以便 CouchDB 的 RESTful API 知道传入的格式是什么。我们还使用了一个新选项,-d选项,代表数据。数据选项允许我们在字符串形式与我们的curl语句一起传递数据。

创建完我们的文档后,我们通过向http://localhost:5984/test-db/41198fc6e20d867525a8faeb7a000015提交GET请求来在终端中检索它。作为响应,我们收到了一个包含所有文档数据的 JSON 对象。在这个请求的最后,我们做了一些不同的事情。我们添加了python mjson.tool,这是 Python 的内置组件,它使我们能够很好地格式化我们的 JSON 响应,以便我们能更好地理解它们。当我们开始查看更复杂的文档时,这将会很有用。

注意

我之前没有提到你需要在书中早些时候安装 Python,因为这是一个很好有的功能。如果你因为缺少 Python 而收到错误,你可以通过这里安装它:python.org/download/

我知道这有点烦人,但curl将是我们的 PHP 代码与 CouchDB 交流的主要方法,因此熟悉它的工作原理非常重要。幸运的是,有一种更容易的方法可以通过名为Futon的工具来访问和管理您的数据。

Futon

CouchDB 自带一个名为 Futon 的内置基于 Web 的管理控制台。Futon 允许您在一个简单的界面中管理数据库、用户和文档。Futon 最好的部分是它已经安装并准备就绪,因为它已经与 CouchDB 捆绑在一起。

让我们来看看:

  1. 打开您的浏览器。

  2. 转到http://localhost:5984/_utils/Futon

这是 Futon 的概述页面。在此页面上,您可以看到 CouchDB 安装中的所有数据库以及创建新数据库的功能。您应该看到我们在之前步骤中创建的数据库test-db,还可以看到 CouchDB 默认安装的_users数据库。

如果您看窗口右侧,您会看到工具。我们将在本书后面介绍复制器时使用它。

  1. 点击概述页面上数据库列表中test-db的链接,深入了解我们的数据库test-dbFuton

您看到的页面是数据库详细信息。在此页面上,您可以看到我们数据库中的所有文档列表,以及一些可以在所选数据库上执行的操作,例如新文档、安全性、压缩和清理...、删除数据库、搜索等。值得注意的是,Futon 只是一个辅助工具,所有这些功能也可以通过curl来实现。

  1. 让我们通过点击文档来深入了解 Futon,您将被转到文档详细信息页面。Futon

这些数据应该看起来很熟悉!我们所有的键都列在左边,值都列在右边。

行动时间——在 Futon 中更新文档

使用 Futon,您可以轻松更新此文档的值。让我们通过一个快速示例来看看。

  1. 确保您在浏览器中打开了文档。

  2. 注意文档中_rev的值。

  3. 双击位置字段的值:“西雅图,华盛顿州”,将其更改为“纽约州纽约市”。

  4. 点击页面顶部的保存文档

  5. 检查一下您文档中_rev的值是否已更改,以及“纽约州纽约市”现在是否是位置的值。

刚刚发生了什么?

您刚刚使用 Futon 更改了文档中字段的值,然后保存更改以更新文档。当文档刷新时,您应该已经注意到_rev字段已更改,并且您对字段的更改已更新。

您可能也注意到“上一个版本”现在看起来是可点击的。点击它,看看会发生什么。Futon 显示了文档的旧版本,其中位置是“华盛顿州西雅图”,而不是新值“纽约州纽约市”。

现在您将看到 CouchDB 的修订版本生效。如果您愿意,可以使用“上一个版本”和“下一个版本”链接循环浏览文档的所有版本。

注意

有两件关于 CouchDB 的修订系统需要注意的重要事项:

您无法更新文档的旧版本;如果您尝试保存文档的旧版本,CouchDB 将返回文档更新冲突错误。这是因为文档的唯一真实版本是最新版本。

您的修订历史仅是临时的。如果您的数据库记录了每一次更改,它将开始变得臃肿。因此,CouchDB 有一个名为压缩的功能,可以清除任何旧的修订版本。

行动时间——在 Futon 中创建文档

我们已经了解了更新现有文档。让我们在 Futon 中从头开始创建一个文档。

  1. 通过点击页眉中的数据库名称test-db转到数据库概述。

  2. 点击“新文档”。

  3. 一个空白文档已经创建好,准备让我们放入新的字段。请注意,_id已经为我们设置好了(但如果我们愿意,我们可以更改它)。

  4. 点击添加字段创建一个新字段,并称其为location

  5. 双击标签旁边的值,该标签显示为null,然后输入您当前的位置。

  6. 点击添加字段创建一个新字段,并称其为name

  7. 双击标签旁边的值,该标签显示为null,然后输入您的名字。

  8. 点击页面顶部的保存文档

  9. 文档已保存。请注意,现在它有一个_rev值。

刚刚发生了什么?

你刚刚使用 Futon 从头开始创建了一个文档。当文档第一次创建时,CouchDB 为您创建了一个唯一的 ID,以便您将其设置为_id字段的值。接下来,您添加了name字段,并输入了您的名字作为其值。最后,您保存它以创建一个新文档。我们已经讨论过文档可以有完全不同的字段,但这是我们实际做到的第一次!

安全

到目前为止,我们已经创建、读取、更新和删除了文档和数据库,而且所有这些都没有任何安全性。当您的 CouchDB 实例上没有任何管理员时,称为管理员派对,这意味着 CouchDB 将处理来自任何人的任何请求。

行动时间-将 CouchDB 从管理员派对中带出来

当您在本地编程时,CouchDB 不安全并不是什么坏事,但如果您在公开可访问的服务器上意外地有一个不安全的数据库,那可能是灾难性的。现在让我们简要地添加安全性,以确保您知道将来如何做。

  1. 打开 Futon 到概述,并查看右下角。您会看到文字说:
**Welcome to Admin Party!
Everyone is admin. Fix this.** 

  1. 点击修复此问题链接。

  2. 一个新窗口将弹出,提示您创建服务器管理员行动时间-将 CouchDB 从管理员派对中带出来

  3. 输入您想要用于管理员帐户的用户名和密码,然后点击创建

刚刚发生了什么?

您刚刚使用 Futon 向 CouchDB 安装添加了一个服务器管理员。创建服务器管理员弹出窗口说,一旦添加了服务器管理员,您将能够创建和销毁数据库,并执行其他管理功能。所有其他用户(包括匿名用户)仍然可以读取和写入数据库。考虑到这一点,我们也希望在数据库上添加一些安全性。

行动时间-匿名访问用户数据库

让我们快速练习一下调用一个curl语句到_users数据库,看看为什么安全我们的数据很重要。

  1. 打开终端

  2. 运行以下命令,将your_username替换为您刚刚创建的服务器管理员的用户名。

**curl localhost:5984/_users/org.couchdb.user:your_username | python -mjson.tool** 

  1. 终端将会回复类似于:
**{
"_id": "org.couchdb.user:your_username",
"_rev": "1-b9af54a7cdc392c2c298591f0dcd81f3",
"name": "your_username",
"password_sha": "3bc7d6d86da6lfed6d4d82e1e4d1c3ca587aecc8",
"roles": [],
"salt": "9812acc4866acdec35c903f0cc072c1d",
"type": "user"
}** 

刚刚发生了什么?

你使用终端创建了一个curl请求来读取包含服务器管理员数据的文档。数据库中的密码是加密的,但有可能有人仍然可以解密密码或使用用户的用户名对他们进行攻击。考虑到这一点,让我们保护数据库,只有管理员才能访问这个数据库。

行动时间-保护用户数据库

让我们保护_users数据库,以便只有服务器管理员可以读取、写入和编辑系统中的其他用户。

  1. 打开 Futon 到概述

  2. 点击_users数据库。

  3. 点击屏幕顶部的安全行动时间-保护用户数据库

  4. 更改管理员读者角色的值为["admins"],使其如下所示:行动时间-保护用户数据库

刚刚发生了什么?

您刚刚将_users数据库的管理员读者角色更改为["admins"],这样只有管理员才能读取或更改设计文档和读者列表。我们将角色的格式设置为["admins"],因为它接受数组形式的角色。

行动时间 - 检查数据库是否安全

您的_users数据库应该是安全的,这样只有管理员才能读取或更改数据库的结构。让我们快速测试一下:

  1. 打开终端

  2. 通过运行以下命令再次尝试读取用户文档。再次用服务管理员的用户名替换your_username

**curl localhost:5984/_users/org.couchdb.user:your_username** 

  1. 终端将会返回以下内容:
**{"error":"unauthorized","reason":"You are not authorized to access this db."}** 

刚刚发生了什么?

通过关闭 CouchDB 实例的管理员模式,认证模块开始起作用,以确保匿名用户无法读取数据库。

注意

我们将在以后进一步增强数据库的安全性,但这是向数据库添加安全性的最简单方法之一。

如果您再次尝试在命令行上玩耍,您将受到对_users数据库的限制,但您还会注意到test-db数据库的操作方式与之前一样,非常好!这正是我们想要的。您可能会问,既然启用了安全性,我如何通过命令行访问_users数据库?您必须通过将您的凭据传递给 RESTful JSON API 来证明您是管理员。

行动时间 - 访问启用了安全性的数据库

让我们尝试快速访问一个启用了安全性的数据库,通过在请求中传递用户名和密码。

  1. 打开终端

  2. 通过运行以下命令查看在_users数据库中保存的所有文档。用您的管理员用户名和密码替换usernamepassword

**curl username:password@localhost:5984/_users/_all_docs** 

  1. 终端将会返回您在添加认证之前看到的相同数据。
**{
"_id": "org.couchdb.user:your_username",
"_rev": "1-b9af54a7cdc392c2c298591f0dcd81f3",
"name": "your_username",
"password_sha": "3bc7d6d86da6lfed6d4d82e1e4d1c3ca587aecc8",
"roles": [],
"salt": "9812acc4866acdec35c903f0cc072c1d",
"type": "user"
}** 

刚刚发生了什么?

您刚刚向_users数据库发出了一个GET请求,并使用了我们之前创建的服务器管理员的用户名和密码进行了身份验证。一旦经过身份验证,我们就能够正常访问数据。如果您想在安全数据库上执行任何操作,只需在要处理的资源的 URL 之前添加username:password@即可。

突发测验

  1. 根据couchdb.apache.org/,CouchDB 的定义的第一句是什么?

  2. HTTP 使用的四个动词是什么,每个动词与 CRUD 的匹配是什么?

  3. 访问 Futon 的 URL 是什么?

  4. 对 CouchDB 来说,“管理员模式”是什么意思,以及如何将 CouchDB 退出这种模式?

  5. 您如何通过命令行对安全数据库进行用户认证?

摘要

在本章中,我们学到了很多关于 CouchDB。让我们快速回顾一下

  • 我们通过查看数据库、文档和 RESTful JSON API 来定义 CouchDB

  • 我们将 CouchDB 与传统的关系型数据库(如 MySQL)进行了比较

  • 我们使用curl语句与 CouchDB 的 RESTful JSON API 进行交互

  • 我们使用 Futon 创建和更改文档

  • 我们学会了如何向数据库添加安全性并测试其有效性

准备好了!在下一章中,我们将开始构建 PHP 框架,这将是我们在本书的其余部分中开发的平台。

第四章:开始你的应用程序

我们准备开始开发我们应用程序的框架!

在本章中,我们将:

  • 从头开始创建一个简单的 PHP 框架 - Bones

  • 学习如何使用 Git 进行源代码控制

  • 添加功能到 Bones 来处理 URL 请求

  • 构建视图和布局的支持,以便我们可以为我们的应用程序添加一个前端

  • 添加代码以允许我们处理所有的 HTTP 方法

  • 设置复杂的路由并将其构建到一个示例应用程序中

  • 添加使用公共文件并在我们的框架中使用它们的能力

  • 将我们的代码发布到 GitHub,以便我们可以管理我们的源代码

让我们开始吧!

在本书中我们将构建什么

在本书的其余部分,我们将创建一个类似 Twitter 的简单社交网络。让我们称之为Verge

Verge将允许用户注册、登录和创建帖子。通过构建这个应用程序,我们将跳过大多数开发人员在构建应用程序时遇到的障碍,并学会依赖 CouchDB 来完成一些繁重的工作。

为了构建 Verge,我们将制作一个轻量级的 PHP 包装器,用于处理基本路由和 HTTP 请求,这些在前一章中提到过。让我们称这个框架为Bones

骨架

在本书中,我们将构建一个非常轻量级的框架Bones来运行我们的应用程序。你可能会想为什么我们要构建另一个框架?这是一个合理的问题!有很多 PHP 框架,比如:Zend 框架,Cake,Symfony 等等。这些都是强大的框架,但它们也有一个陡峭的学习曲线,而且在本书中不可能涉及到它们的每一个。相反,我们将创建一个非常轻量级的 PHP 框架,它将帮助简化我们的开发,但不会有很多其他的花里胡哨。通过构建这个框架,你将更好地理解 HTTP 方法以及如何从头开始构建轻量级应用程序。一旦你使用 Bones 开发了这个应用程序,你应该很容易将你的知识应用到另一个框架上,因为我们将使用一些非常标准的流程。

如果你在本章遇到任何问题或渴望看到最终成品,那么你可以在 GitHub 上访问完整的 Bones 框架:github.com/timjuravich/bones。我还将在本章末尾介绍一个简单的方法,让你可以获取所有这些代码。

让我们开始设置我们的项目。

项目设置

在本节中,我们将逐步创建用于我们代码的文件夹,并确保我们初始化 Git,以便我们可以跟踪我们向项目添加新功能时的源代码。

行动时间 - 为 Verge 创建目录

让我们通过在/Library/WebServer/Documents文件夹中创建一个名为verge的目录来开始设置我们的项目,并将该目录包含所有项目的代码。为了简洁起见,在本章中,我们将称/Library/WebServer/Documents/verge为我们的工作目录。

在我们的工作目录中,让我们创建四个新的文件夹,用于存放我们的源文件:

  1. 创建一个名为classes的文件夹。这个文件夹将包含我们在这个项目中将要使用的 PHP 类对象

  2. 创建一个名为lib的文件夹。这个文件夹将包含我们的应用程序依赖的 PHP 库,也就是我们的Bones框架和将与 CouchDB 通信的类。

  3. 创建一个名为public的文件夹。这个文件夹将包含我们所有的公共文件,比如层叠样式表(CSS),JavaScript 和我们的应用程序需要的图片。

  4. 创建一个名为views的文件夹。这个文件夹将包含我们的布局和网页应用程序的不同页面。

如果你查看你的工作目录,本节的最终结果应该类似于以下截图:

开始行动-为 Verge 创建目录

刚刚发生了什么?

我们快速创建了一些占位符文件夹,用于组织本书其余部分中将添加的代码。

使用 Git 进行源代码控制

为了跟踪我们的应用程序、我们的进展,并允许我们在犯错时回滚,我们需要在我们的仓库上运行源代码控制。我们在第二章中安装了 Git,设置您的开发环境,所以让我们好好利用它。虽然有一些桌面客户端可以使用,但为了简单起见,我们将使用命令行,以便适用于所有人。

开始行动-初始化 Git 仓库

Git 需要在每个开发项目的根目录中初始化,以便跟踪所有项目文件。让我们为我们新创建的verge项目做这个!

  1. 打开终端

  2. 输入以下命令以更改目录到我们的工作目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 输入以下命令以初始化我们的 Git 目录:
**git init** 

  1. Git 将回应以下内容:
**Initialized empty Git repository in /Library/WebServer/Documents/verge/.git/** 

  1. 保持您的终端窗口打开,以便在本章中与 Git 进行交互。

刚刚发生了什么?

我们使用终端通过在工作目录中使用命令git init来初始化我们的 Git 仓库。Git 回应让我们知道一切都进行得很顺利。现在我们已经设置好了 Git 仓库,当创建新文件时,我们需要将每个文件添加到源代码控制中。将文件添加到 Git 的语法很简单,git add path_to_file。您还可以通过输入"git add ."的通配符语句递归地添加目录中的所有文件。在本章的大部分部分,我们将快速添加文件,因此我们将使用"git add ."

实现基本路由

在我们开始创建Bones之前,让我们先看看为什么我们需要它的帮助。让我们首先创建一个简单的文件,确保我们的应用程序已经设置好并准备就绪。

开始行动-创建我们的第一个文件:index.php

我们将创建的第一个文件是一个名为index.php的文件。这个文件将处理我们应用程序的所有请求,并最终将成为主要的应用程序控制器,将与Bones进行通信。

  1. 在工作目录中创建index.php,并添加以下文本:
<?php echo 'Welcome to Verge'; ?>

  1. 打开您的浏览器,转到网址:http://localhost/verge/

  2. index.php文件将显示以下文字:

**Welcome to Verge** 

刚刚发生了什么?

我们创建了一个简单的 PHP 文件,名为index.php,目前只是简单地返回文本给我们。我们只能在直接访问http://localhost/verge/http://localhost/verge/index.php时访问这个文件。然而,我们的目标是index.php将被我们工作目录中的几乎每个请求所访问(除了我们的public文件)。为了做到这一点,我们需要添加一个.htaccess文件,允许我们使用 URL 重写。

.htaccess 文件

.htaccess文件被称为分布式配置文件,它允许 Apache 配置在目录基础上被覆盖。如果您记得,在第一章中,CouchDB 简介,我们确保可以通过改变一些代码行来使用.htaccess文件,以Override All。大多数 PHP 框架都以我们将要使用的方式利用.htaccess文件,因此您需要熟悉这个过程。

开始行动-创建.htaccess 文件

为了处理对目录的所有请求,我们将在工作目录中创建一个.htaccess文件。

  1. 在工作目录中创建一个名为.htaccess的文件。

  2. 将以下代码添加到文件中:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?request=$1 [QSA,L]
</IfModule>

  1. 在工作目录中打开index.php文件。

  2. 更改index.php中的代码以匹配以下内容:

<?php echo $_GET['request']; ?>

  1. 打开浏览器,转到http://localhost/verge/test/abc,然后转到http://localhost/verge/test/123。注意页面会以你在根 URL 末尾输入的相同值回应你。执行操作-创建.htaccess 文件

刚刚发生了什么?

首先,我们创建了一个.htaccess文件,以便启用 URL 重写。第一行<IfModule mod_rewrite.c>检查我们是否启用了mod_rewrite模块。这将是true,因为我们在第二章的http.conf文件中启用了mod_rewrite

文件的下一行说RewriteEngine On,它确切地做了你认为它会做的事情;它打开了 Apache 的RewriteEngine并等待一些条件和规则。接下来,我们设置了两个RewriteCond(重写条件)。第一个RewriteCond告诉RewriteEngine,如果传递的 URL 与现有文件的位置不匹配(这就是f的含义),则重写 URL。第二个RewriteCond告诉RewriteEngine,重写 URL 如果它不是已经存在的目录(这就是-d的含义)。最后,我们设置了我们的RewriteRule,它表示当输入一个 URL 时,将其转发到第二个值(目标)。这个RewriteRule告诉RewriteEngine,传递到这个目录的任何 URL 都应该被强制通过索引文件,并将路由传递给index.php文件,形成一个名为request的查询字符串。

最后,字符串是[QSA, L]。让我解释一下这是什么意思。QSA表示,如果有任何查询字符串添加到请求中,请将其附加到重写目标。L表示,停止尝试查找匹配项,并且不应用任何其他规则。

然后,你打开了index.php文件,并更改了代码以输出request变量。现在你知道,输入浏览器的路由将以查询字符串的形式传递给index.php文件。

有了所有这些代码,我们测试了一切,通过转到 URLhttp://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!";
}

在这段代码中,如果用户转到 URLhttp://localhost/verge,他们的浏览器将显示:

**Welcome to Verge** 

同样,如果用户转到http://localhost/verge/signup,他们的浏览器将显示:

**Sign Up!** 

我们可以进一步扩展这种思维方式,通过编写各种if语句,将我们的代码串联成一个长文件,并立即开始编写我们的应用程序。然而,这将是一个维护的噩梦,难以调试,并且一般来说是不好的做法。

相反,让我们删除index.php文件中的所有代码,专注于以正确的方式构建我们的项目。在本章的其余部分,我们将致力于创建一个名为Bones的简单框架,它将为我们处理一些请求的繁重工作。

创建 Bones 的骨架

正如我之前提到的,Bones是一个非常轻量级的框架,总共只有 100 多行代码,全部都在一个文件中。在本节中,我们将开始形成一个结构,以便在接下来的章节中构建更多的功能。

执行操作-将我们的应用程序连接到 Bones

让我们首先创建Bones库,然后将我们的index.php文件连接到它。

  1. 在我们的工作目录的lib文件夹中创建一个名为bones.php的文件(/Library/Webserver/Documents/verge/lib/bones.php)。

  2. 将以下代码添加到我们工作目录中的index.php文件中,以便我们可以与新创建的bones.php文件进行通信:

<?php
include 'lib/bones.php';

刚刚发生了什么?

这段代码所做的就是包含我们的lib/bones.php文件,现在这已经足够了!请注意,我们没有用?>结束文件,这可能不是你习惯看到的。?>标签实际上是可选的,在我们的情况下,不使用它可以减少不需要的空白,并且在代码后面添加响应头,如果需要的话。

使用 Bones 处理请求

为了说明我们计划使用Bones类做什么,让我们通过一个快速示例来看看我们希望在本节结束时实现的目标。

  • 如果浏览器访问 URL http://localhost/verge/signup,我们希望Bones拦截调用并将其解释为http://localhost/verge/index.php?request=signup

  • 然后,Bones将查看我们在index.php文件中定义的路由列表,并查看是否有匹配。

  • 如果确实有匹配,Bones将执行匹配函数的回调并执行该路由内的操作。

如果以上内容有些令人困惑,不用担心。随着我们慢慢构建这个功能,希望它会开始变得有意义。

行动时间-创建 Bones 的类结构

让我们通过向我们的工作目录中的lib/bones.php文件添加以下代码来开始构建Bones类:

/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类,添加了一些privatepublic变量,以及一个名为get_instance()的奇怪函数。私有静态变量$instance与函数get_instance()结合在一起,形成了所谓的单例模式

单例模式允许我们的Bones类不仅仅是一个简单的类,还可以是一个对象。这意味着每次调用我们的Bones类时,我们都在访问一个现有的对象。但如果对象不存在,它将为我们创建一个新的对象来使用。这是一个有点复杂的想法;然而,我希望随着我们在后面使用它,它开始变得有意义。

访问路由

现在我们已经有了我们类的基本概念,让我们添加一些函数来获取和解释路由(传递给Bones的 URL)每次创建新请求时。然后我们将在下一节中将结果与每个可能的路由进行比较。

行动时间-创建函数以访问 Bones 创建时的路由

为了弄清楚请求中传递了什么路由,我们需要在lib/bones.php文件的get_instance()函数的结束括号下面添加以下两个函数:

/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函数的开头,我们调用了我们的Bones实例,使用static:get_instance()函数。这就是单例模式的作用,将Bones对象的单一实例返回给我们。

然后register函数检查我们通过浏览器访问的路由是否与传入函数的路由匹配。如果匹配,我们的$route_found变量将被设置为true,这将允许我们跳过查看其余的路由。register函数将执行一个回调函数,该函数将执行我们在路由中定义的工作。我们的Bones实例也将与回调函数一起传递,这样我们就可以利用它。如果路由不匹配,我们将返回false,以便我们知道路由不匹配。

现在我们已经完成了我们在Bones中的工作。所以,请确保用以下方式结束你的类:

}

从我们的应用程序调用register函数

我们现在对Bones应该做什么有了基本的了解,但我们缺少一个将我们的index.phplib/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路由。这个函数是一个简单的传递函数,将路由和回调传递给Bonesregister函数。

我们是否在同一页面上?

在这一部分我们做了很多事情。让我们仔细检查一下你的代码是否与我的代码匹配:

/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函数。

行动时间——为我们测试Bones的路由创建路由

打开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 中,这样我们就可以跟踪我们的进度了。

  1. 打开终端

  2. 输入以下命令以更改目录到我们的工作目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 通过输入以下命令将我们在此目录中创建的所有文件添加进来:
**git add .** 

  1. 给 Git 一个描述,说明自上次提交以来我们做了什么:
**git commit am 'Created bones.php and added simple support for routing'** 

处理布局和视图

我们将暂时停止路由的操作,添加一些有趣的前端功能。每个应用程序都由一些页面组成,我们将称之为视图。每个视图都有一个标准的布局,这些视图将填充。布局是视图的包装器,可能包含到 CSS 引用、导航或其他你认为对每个视图都是通用的内容。

使用 Bones 支持视图和布局

为了支持视图和布局,我们需要向我们的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)。因此,我们将希望通过在路径后添加/.来查看另一个目录。

行动时间-允许 Bones 存储变量和内容路径

我们需要能够从index.php中设置和接收变量到我们的视图中。因此,让我们将这个支持添加到Bones中。

  1. 让我们定义一个名为$varspublic数组,它将允许我们从index.php中的路由中存储变量,并且定义一个名为$content的字符串,它将存储视图的路径,这些视图将加载到我们的布局中。我们将首先在lib/bones.php类中添加两个变量:
class Bones {
public $route = '';
**public $content = '';
public $vars = array();** 
public function __construct() {
...
}

  1. 为了能够从index.php文件中设置变量,我们将创建一个简单的名为set的函数,它将允许我们传递一个索引和一个变量的值,并将其保存到当前的Bones实例中。让我们在lib/bones.php中的get_route()函数之后创建一个名为set的函数。
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 中调用它来允许我们的应用程序显示视图

我们将首先创建一个名为renderpublic函数,它接受两个参数。第一个是$view,它是你想要显示的视图的名称(或路径),第二个是$layout,它将定义我们用来显示视图的布局。布局也将有一个默认值,以便我们可以保持简单,以处理视图的显示。在lib/bones.php文件中的set函数之后添加以下代码:

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。所以,现在让我们创建这个文件。

  1. 首先,在我们的views目录中创建一个名为layout.php的新文件。

  2. 在新创建的views/layout.php中添加以下代码:

<html>
<body>
<h1>Verge</h1>
<?php include($this->content); ?>
</body>
</html>

刚才发生了什么?

我们创建了一个非常简单的 HTML 布局,它将在应用程序的所有视图中使用。如果你记得,我们在Bonesrender函数中使用了路径设置为$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视图。

行动时间——创建视图

我们几乎准备好测试这段新代码了,但我们需要创建实际的视图,这样我们才能显示它们。

  1. 首先,在我们的工作目录中的views文件夹中创建两个新文件,分别命名为home.phpsignup.php

  2. 通过编写以下代码将以下代码添加到views/home.php文件中:

Home Page <br /><br />
<?php echo $message; ?>

  1. 将以下代码添加到views/signup.php文件中:
Signup Now!

刚才发生了什么?

我们创建了两个简单的视图,它们将由index.php文件呈现。views/home.php文件中的一行代码<?php echo $message; ?>将显示传递给我们的Bones库的index.php文件中的名称为 message 的变量。试一下吧!

打开你的浏览器,转到http://localhost/verge/http://localhost/verge/signup,你会看到我们所有的辛勤工作都得到了回报。我们的布局现在正在呈现,我们的视图正在显示。我们还能够从index.php传递一个名为message的变量,并在我们的主页视图上输出该值。我希望你能开始看到我们迄今为止为Bones添加的功能的强大之处!

将更改添加到 Git

到目前为止,我们已经为布局和视图添加了支持,这将帮助我们构建应用程序的所有页面。让我们把所有的改变都添加到 Git 中,这样我们就可以跟踪我们的进展。

  1. 打开终端

  2. 输入以下命令以更改目录到我们的工作目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 通过输入以下命令,将我们在该目录中创建的所有文件都添加进去:
**git add .** 

  1. 给 Git 一个描述,说明我们自上次提交以来做了什么:
**git commit am 'Added support for views and layouts'** 

添加对其他 HTTP 方法的支持

到目前为止,我们一直在处理GET调用,但在 Web 应用程序中,我们将需要支持我们在上一章中讨论过的所有HTTP方法:GET, PUT, POSTDELETE

行动时间-检索请求中使用的 HTTP 方法

我们已经完成了支持、捕获和处理 HTTP 请求所需的大部分繁重工作。我们只需要插入几行额外的代码。

  1. 让我们在我们的Bones类中添加一个变量$method,在我们的$route变量之后。这个变量将存储每个请求上执行的HTTP方法:
class Bones {
private static $instance;
public static $route_found = false;
public $route = '';
**public $method = '';** 
public $content = '';

  1. 为了让我们在每个请求中获取方法,我们需要在我们的__construct()函数中添加一行代码,名为get_route(),并将结果的值保存在我们的实例变量$method中。这意味着当Bones在每个请求中被创建时,它也将检索方法并将其保存到我们的Bones实例中,以便我们以后可以使用它。通过添加以下代码来实现这一点:
public function __construct() {
$this->route = $this->get_route();
**$this->method = $this->get_method();** 
}

  1. 让我们创建一个名为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()函数使用一个名为$_SERVER的数组,这个数组是由 Web 服务器创建的,并允许我们检索有关请求和执行的信息。这个简单的一行代码是在说,如果$_SERVER中设置了REQUEST_METHOD,那么就返回它,但如果由于某种原因REQUEST_METHOD没有设置,就返回GET以确保方法的安全。

行动时间-修改注册以支持不同的方法

现在我们在每个请求中检索方法,我们需要修改我们的注册函数,以便我们可以在每个路由中传递$method,以便它们能够正确匹配。

  1. lib/bones.phpregister函数中添加$method,以便我们可以将一个方法传递到函数中:
**public static function register($route, $callback, $method) {** 
$bones = static::get_instance();

  1. 现在,我们需要更新我们在注册函数中的简单路由匹配,以检查传递的路由$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;
}
}

刚刚发生了什么?

我们在我们的register函数中添加了一个$method参数。然后我们在我们的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 方法,确保我们以大写形式传递每种方法的名称。

<?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 方法,非常简单对吧?

行动时间-向 Bones 添加简单但强大的辅助功能

让我们在我们的lib/bones.php文件中添加两个小函数,这将帮助我们使用表单。

  1. 添加一个名为form的函数,如下所示:
public function form($key) {
return $_POST[$key];
}

  1. 添加一个名为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设置了表单的action属性。$this->make_route使用我们的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查找/signup路由,并将post方法发送到它。一旦解析了这个路由,回调将使用一些文本设置变量message的值。文本包括我们创建的新函数$app->form('name')。这个函数正在从具有属性name的表单中获取发布的值。然后我们将告诉Bones渲染主视图,以便我们可以看到消息。

测试一下!

现在让我们试试这些!

  1. 打开您的浏览器,转到:http://localhost/verge/signup

  2. 您的浏览器应该显示以下内容:测试一下!

  3. 输入您的名字(我输入了Tim),然后单击提交测试一下!

将更改添加到 Git

在这一部分,我们为所有的 HTTP 方法添加了支持,这将允许我们处理任何类型的请求。让我们将所有的更改添加到 Git,以便我们可以跟踪我们的进展。

  1. 打开终端

  2. 输入以下命令以更改目录到我们的工作目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 通过输入以下命令来添加我们在此目录中创建的所有文件:
**git add .** 

  1. 给 Git 描述我们自上次提交以来所做的工作:
**git commit -am 'Added support for all HTTP methods'** 

添加对复杂路由的支持

我们的框架在技术上已经准备好让我们开始构建。但是,我们还没有足够的支持来匹配和处理复杂的路由。由于大多数应用程序都需要这个,让我们快速添加它。

处理复杂的路由

例如,在index.php文件中,我们希望能够为用户配置文件定义路由。这个路由可能是/user/:username。在这种情况下,:username将是一个我们可以访问的变量。因此,如果您访问 URL/user/tim,您可以使用Bones来获取 URL 的该部分,并返回其值。

让我们首先在我们的lib/bones.php文件的__construct函数中添加另一个变量和另一个调用:

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$route_variables。$route_segments每次使用__construct()创建Bones对象时都会设置。$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.phpregister 函数中添加以下代码:

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 确保我们不包括键中的冒号。

这段代码有点混乱。所以,让我们快速通过一个使用案例:

  1. 打开你的浏览器,输入 URL /users/tim

  2. 这个注册路由开始检查路由 /users/:username

  3. $bones->route_segments[$key] 将返回 tim

  4. $bones->route_variables[substr($part, 1)] 将保存值,并使我们能够稍后检索值 tim

现在,让我们完成这个 if 语句,检查不包含路由变量的段(if 语句的 else 部分)。在这个区域,我们将检查我们正在检查的段是否与从浏览器的 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设置为路由变量 message 的值。然后,我们渲染了主页,就像我们之前做了几次一样。

测试一下!

如果你打开浏览器并访问 URL http://localhost/verge/say/hello,浏览器将显示:hello

如果你将值更改为任何不同的值,它将把相同的值显示回给你。

将更改添加到 Git

这一部分添加了更详细的路由匹配,并允许我们在 URL 中使用路由变量。让我们把所有的改变都添加到 Git 中,这样我们就可以跟踪我们的进展。

  1. 打开终端

  2. 输入以下命令以更改目录到我们的工作目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 通过输入以下命令,将我们在这个目录中创建的所有文件都添加进去:
**git add .** 

  1. 给 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文件夹并找到请求的资源。

  1. 首先打开我们项目根目录中的.htaccess 文件。

  2. 添加以下突出显示的代码:

<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来绕过我们的“捕获所有”规则,如果是public文件的话,就将所有请求定向。然后我们简化路由,允许 URL 解析为/css/js,而不是/public/css/public/js

我们准备好使用公共文件了。我们只需要实现它们,这应该和设置一样容易。

行动时间——为应用程序创建一个样式表

让我们首先添加一个样式表来改变我们应用程序的外观。

  1. 打开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>

  1. 创建一个名为master.css的新文件,并将其放在我们工作目录的public/css文件夹中。

  2. public/css/master.css中添加一小段代码,以显示不同颜色的背景,这样我们就可以测试所有这些是否有效。

body {background:#e4e4e4;}

刚才发生了什么?

我们添加了一个新的应用程序样式表master.css的引用。我们使用标准标记来包含样式表,并使用Bones, make_route的一个函数来正确创建文件的路径。

让我们测试一下,确保我们的样式表现在被正确显示。

  1. 打开你的浏览器,然后转到http://localhost/verge/

  2. 你的浏览器应该显示以下内容:What just happened?

  3. 注意我们页面的背景颜色已经变成了灰色,显示出样式表已经生效了!

将更改添加到 Git

在这一部分,我们添加了对样式表、JavaScript 和图像等公共文件的支持。然后我们通过创建一个master.css文件来测试它。让我们把所有的改变都添加到 Git 中,这样我们就可以跟踪我们的进展。

  1. 打开终端

  2. 通过键入以下命令,将目录更改为我们的工作目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 通过键入以下命令,将我们在此目录中创建的所有文件添加进来:
**git add .** 

  1. 给 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 上创建一个帐户:github.com/plans。GitHub 有各种不同的计划供您选择,但我建议您选择免费帐户,这样您就不必在此时支付任何费用。如果您已经有帐户,可以登录并跳过创建新帐户的步骤。

将您的代码发布到 GitHub

点击创建免费帐户

注意

重要的是要注意,选择免费帐户后,您的所有存储库都将是“公共”的。这意味着任何人都可以看到您的代码。现在这样做没问题,但随着开发的进展,您可能希望注册一个付费帐户,以便它不是公开可用的。

将您的代码发布到 GitHub

您将看到一个快速注册表单。填写完整,并在完成后点击创建帐户

创建完帐户后,您将看到您的帐户仪表板。在此屏幕上,您将看到您的帐户或您正在关注的存储库的任何活动。由于我们还没有任何存储库,因此应该首先点击新存储库

将您的代码发布到 GitHub

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

将您的代码发布到 GitHub

通过填写每个字段来完成此表单的其余部分。

  • 项目名称:verge

  • 描述:使用 Bones 构建的名为 verge 的社交网络

  • 主页 URL:现在您可以将其留空

  • 点击创建存储库

您的存储库现在已创建并准备好推送您的代码。您只需要在终端中运行几条语句。

  1. 打开终端

  2. 键入以下命令以更改目录到我们的工作目录:

**cd /Library/WebServer/Documents/verge/** 

  1. 通过输入以下命令并将用户名替换为您的 GitHub 用户名,将 GitHub 添加为您的远程存储库:
**git remote add origin git@github.com:username/verge.git** 

  1. 将您的本地存储库推送到 GitHub。
**git push -u origin master** 

  1. Git 将返回一大堆文本,并在完成时停止。

如果刷新您在github.com上的 Git 存储库的 URL(我的 URL 是github.com/timjuravich/verge),您将看到所有文件,如果点击历史记录,您将看到我们在本章中进行的每个部分中添加的所有更改。

将您的代码发布到 GitHub

随着您不断添加更多的代码,您必须手动每次将代码推送到 GitHub,执行命令git push origin master。在我们继续阅读本书的过程中,我们将继续向此存储库添加内容。

从 GitHub 获取完整的代码

如果您在某个地方迷失了方向,或者无法使一切都像应该的那样工作,您可以轻松地从 GitHub 的 Git 存储库中克隆 Bones,并且您将获得一个包含我们在本章中所做的所有更改的新副本。

  1. 打开终端

  2. 使用以下命令将目录更改为我们的工作目录:

**cd /Library/WebServer/Documents** 

  1. 通过键入以下命令,将存储库克隆到您的本地计算机:
**git clone git@github.com:timjuravich/bones.git** 

  1. Git 将从 GitHub 获取所有文件,并将它们移动到您的本地计算机。

摘要

在本章中,我们已经做了大量的工作!我们已经:

  • 从头开始创建一个 PHP 框架来处理 Web 请求

  • 添加了清晰的 URL、路由变量、HTTP 方法支持、简单的视图和布局引擎,以及一个用于显示public文件(如样式表、JavaScript 和图像)的系统

  • 用我们的浏览器测试了框架的每个部分,以确保我们能够访问我们的更改

  • 将我们的代码发布到 GitHub,这样我们就可以看到我们的更改并管理我们的代码。

准备好了!在下一章中,我们将直奔主题,将我们新创建的应用程序连接到 CouchDB。

第五章:将您的应用程序连接到 CouchDB

现在我们已经建立了应用程序的框架,让我们谈谈我们的应用程序需要与 CouchDB 通信的情况。

在本章中,我们将讨论以下几点:

  • 调查与 CouchDB 交互的快速简便方法,并讨论其缺点

  • 查看现有库以便于 PHP 和 CouchDB 开发

  • 安装 Sag 并将其集成到 Bones 中

  • 让我们的注册表单创建 CouchDB 文档,并在 Futon 中进行验证

在我们开始之前

在我们做任何事情之前,让我们创建一个数据库,从此时起我们将在 Verge 中使用。与以前一样,让我们使用curl创建一个数据库。

行动时间-使用 curl 为 Verge 创建数据库

我们在第三章中使用curl创建了一个数据库,与 CouchDB 和 Futon 入门。让我们快速回顾如何使用PUT请求在 CouchDB 中创建一个新数据库。

  1. 通过在终端中运行以下命令来创建一个新的数据库。确保用第三章中创建的数据库管理员用户替换usernamepassword
**curl -X PUT username:password@localhost:5984/verge** 

  1. 终端将以以下输出做出响应:
**{"ok":true}** 

刚刚发生了什么?

我们使用终端通过curl触发了一个PUT请求,使用 CouchDB 的RESTful JSON API创建了一个数据库。我们在 CouchDB 的根 URL 末尾传递verge作为数据库的名称。成功创建数据库后,我们收到了一条消息,说明一切都很顺利。

头顶冲入

在本节中,我们将创建一些快速脏代码来与 CouchDB 通信,然后讨论这种方法的一些问题。

向我们的注册脚本添加逻辑

在上一章中,我们在views/signup.php中创建了一个表单,具有以下功能:

  • 我们要求用户在文本框中输入名称的值

  • 我们获取了表单中输入的值并将其发布到注册路由

  • 我们使用 Bones 来获取表单传递的值,并将其设置为名为message的变量,以便我们可以在主页上显示它

  • 我们呈现了主页并显示了message变量

这是我们的一项重大工作,但我们无法保存任何东西以供以后阅读或写入。

让我们进一步采取一些步骤,并要求用户输入姓名和电子邮件地址,然后将这些字段保存为 CouchDB 中的文档。

行动时间-向注册表单添加电子邮件字段

让我们添加一个输入字段,以便用户可以在views/signup.php页面中输入电子邮件地址。

  1. 在文本编辑器中打开signup.php/Library/Webserver/Documents/verge/views/signup.php

  2. 添加突出显示的代码以为电子邮件地址添加标签和输入字段:

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的一个实例,并将其命名为$userstdClass是 PHP 的通用空类,对于匿名对象、动态属性和快速上手非常有用。因为文档要求应该设置一个类型来分类文档,我们将这个文档的类型设置为user。然后我们取自表单提交的值,并将它们保存为$user类的属性。最后,我们使用了一个名为json_encode的 PHP 函数,将对象转换为 JSON 表示形式。

让我们来测试一下。

  1. 在浏览器中打开http://localhost/verge/signup

  2. 名称文本框中输入John Doe,在电子邮件文本框中输入<john@example.com>

  3. 点击提交

  4. 您的浏览器将显示以下内容:刚刚发生了什么?

太好了!我们的表单已经正确提交了,并且我们能够在我们网站的顶部用 JSON 表示stdClass $user

提交到 Git

让我们将我们的代码提交到 Git,这样我们以后可以回顾这段代码。

  1. 打开终端

  2. 输入以下命令以更改目录到我们的工作目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 给 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语句。

  1. 让我们从初始化一个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');
});

  1. 现在,让我们告诉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的变量。然后我们创建了一个包含各种键和值的数组。我们选择所有这些选项的原因对我们现在来说并不太重要,但我想强调前三个对象:

  1. 我们将CURLOPT_URL选项设置为我们要将文档保存到的数据库的 URL。请记住,此语句将使用 CouchDB 的 RESTful JSON API 在verge数据库中创建一个文档。

  2. 然后我们将CURLOPT_POSTFIELDS设置为我们的$user的 JSON 编码值。这将把我们的 JSON 字符串作为数据与 URL 一起包含进去。

  3. 最后,我们将CURLOPT_HTTPHEADER设置为array ("Content-Type: application/json"),以确保curl知道我们正在传递一个 JSON 请求。

设置了我们的选项数组之后,我们需要告诉我们的curl实例使用它:

curl_setopt_array($curl, $options);

然后我们用以下两行代码执行并关闭curl

curl_exec($curl);
curl_close($curl);

有了这段代码,我们应该能够提交表单并将其发布到 CouchDB。让我们来测试一下。

  1. 在浏览器中打开http://localhost/verge/signup

  2. 名称文本框中输入John Doe,在电子邮件文本框中输入<john@example.com>

  3. 点击提交

  4. 您的浏览器将显示以下内容:刚刚发生了什么?

这次也没有出现任何错误,就像以前一样。但是这次应该已经创建了一个 CouchDB 文档。让我们通过 Futon 检查文档是否已经正确创建。

  1. 在浏览器中打开http://localhost:5984/_utils/database.html?verge。这个直接链接将显示 verge 数据库。您会看到这里有一个新的文档!请记住,您的IDrev将与我的不同:刚刚发生了什么?

  2. 点击文档,以便您可以查看详细信息。

  3. 您文档中的数据应该与我们在curl会话中传递的信息相匹配。请注意,type, emailname都已正确设置,CouchDB 为我们设置了_id_rev刚刚发生了什么?

将其提交到 Git

让我们将我们的代码提交到 Git,以便将来可以参考这段代码。

  1. 打开终端

  2. 键入以下命令以更改目录到我们的工作目录:

cd /Library/Webserver/Documents/verge/

  1. 向 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 使设置第三方库变得非常容易,并允许我们在可用时更新到新版本。

  1. 打开终端

  2. 键入以下命令以确保您在工作目录中:

**cd /Library/Webserver/Documents/verge/** 

  1. 使用 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一起工作非常容易。让我们一起来看看!

  1. 打开我们的工作目录中的lib/bones.php,并在我们的类顶部添加以下行:
<?php
define('ROOT', __DIR__ . '/..');
**require_once ROOT . '/lib/sag/src/Sag.php';** 

  1. 我们需要确保 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 方法允许您传递数据,因此触发起来非常容易。

让我们快速通过注册流程:

  1. 打开浏览器,输入http://localhost/verge/signup

  2. 名称文本框中输入一个新名称,然后在电子邮件文本框中输入一个新电子邮件。

  3. 点击提交

在 CouchDB 中创建了一个新文档,让我们检查一下 Futon,确保它在那里:

  1. 打开浏览器,输入http://localhost:5984/_utils/database.html?verge,查看 verge 数据库。

  2. 点击列表中的第二个文档。

  3. 查看这个新文档的详细信息,您会发现它与我们制作的第一个文档具有相同的结构。

完美!结果与我们快速而肮脏的 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的基类,所有我们的类都将继承它。

  1. 让我们从创建一个名为base.php的新文件开始,并将其放在工作目录内的/Library/Webserver/Documents/verge/classes/base.php文件夹中。

  2. base.php中创建一个带有__construct函数的抽象类。在对象的__construct中,让我们将$type作为一个选项,并将其设置为一个受保护的变量,也称为$type

<?php
abstract class Base
{
protected $type;
public function __construct($type)
{
$this->type = $type;
}
}

  1. 为了方便以后在我们的类中获取和设置变量,让我们在__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;
}** 
}

  1. 每次我们将对象保存到 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,它将存储文档的分类,如userpost。接下来,我们添加了一个__construct函数,它将在每次创建对象时被调用。这个函数接受选项$type,我们将在每个继承Base的类中设置它。然后,我们创建了__get__set函数。__get__set被称为魔术方法,它们将允许我们使用getset受保护的变量,而无需任何额外的代码。最后,我们添加了一个名为to_json的函数,它使用get_object_varsjson_encode来表示我们的对象为 JSON 字符串。在我们的基类中做这样的小事情将使我们未来的生活变得更加轻松。

时间来行动了——创建一个 User 对象

现在我们已经创建了我们的Base类,让我们创建一个User类,它将包含与用户相关的所有属性和函数。

  1. 创建一个名为user.php的新文件,并将其放在base.php所在的classes文件夹中。

  2. 让我们创建一个继承我们Base类的类。

<?php
class User extends Base
{
}

  1. 让我们添加我们已经知道需要的两个属性nameemail到我们的User类中。
<?php
class User extends Base
{
**protected $name;
protected $email;** 
}

  1. 让我们添加一个__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对象,我们可以轻松地将其插入到我们的应用程序代码中,然后就可以运行了。

  1. 打开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);
}

  1. 调整 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',因为我们的UserBase对象中的__construct函数已经处理了这个问题。最后,我们添加了之前创建的to_json()函数,这样我们就可以将我们的对象作为 JSON 编码的字符串发送出去。

注意

Sag 在技术上可以自己处理一个对象的 JSON,但重要的是我们能够从我们的对象中检索到一个 JSON 字符串,这样你就可以以任何你想要的方式与 CouchDB 交互。将来可能需要回来使用curl或另一个库重写所有内容,所以重要的是你知道如何表示你的数据为 JSON。

测试一下

让我们快速再次通过我们的注册流程,确保一切仍然正常运行:

  1. 打开浏览器到http://localhost/verge/signup

  2. 名称文本框中输入一个新名称,在电子邮件文本框中输入一个新电子邮件。

  3. 点击提交

在 CouchDB 中应该已经创建了一个新文档。让我们检查 Futon,确保它在那里:

  1. 打开浏览器到http://localhost:5984/_utils/database.html?verge查看 verge 数据库。

  2. 点击列表中的第三个文档

  3. 查看这个新文档的细节,你会发现它和我们制作的前两个文档结构相同。

完美!一切都和以前一样,但现在我们使用了一个更加优雅的解决方案,我们将能够在未来的章节中构建在其基础上。

提交到 Git

让我们把代码提交到 Git,这样我们就可以追踪我们到目前为止的进展:

  1. 打开终端

  2. 输入以下命令以更改目录到我们的工作目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 我们在classes文件夹中添加了一些新文件。所以,让我们确保将这些文件添加到 Git 中。
**git add classes/*** 

  1. 给 Git 一个描述,说明我们自上次提交以来做了什么:
**git commit am 'Added class structure for Users and tested its functionality'** 

通过使用classes/*语法,我们告诉 Git 添加 classes 文件夹中的每个文件。当你添加了多个文件并且不想逐个添加每个文件时,这很方便。

总结

我们已经完成了这一章的代码。定期将代码推送到 GitHub 是一个很好的做法。事实上,当你与多个开发人员一起工作时,这是至关重要的。我不会在这本书中再提醒你这样做。所以,请确保经常这样做:

**git push origin master** 

这行代码读起来像一个句子,如果你在其中加入一些词。这句话告诉 Git 要pushorigin(我们已经定义为 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 非常简单;我们将在本节中介绍安装它的基础知识。

  1. 打开您的浏览器,转到twitter.github.com/bootstrap/

  2. 点击下载 Bootstrap

  3. 一个.zip文件将被下载到您的downloads文件夹中;双击它或使用您喜欢的解压工具解压它。

  4. 您将在bootstrap文件夹中找到三个目录,分别是css, imgjs,每个目录中都包含若干文件。执行时间-本地安装 Bootstrap

  5. 将这些文件夹中的所有文件复制到您的verge项目的相应文件夹中:/public/css, public/imgpublic/js。完成后,您的verge目录应该类似于以下屏幕截图:执行时间-本地安装 Bootstrap

刚刚发生了什么?

我们刚刚通过下载一个包含所有资产的.zip文件并将它们放在本地机器的正确文件夹中,将 Twitter 的 Bootstrap 安装到我们的项目中。

仅仅通过查看我们项目中的新文件,您可能会注意到每个文件似乎出现了两次,一个带有文件名中的min,一个没有。这两个文件是相同的,除了包含min在文件名中的文件已经被压缩。压缩意味着从代码中删除所有非必要的字符以减小文件大小。删除的字符包括空格、换行符、注释等。因为这些文件是从网站上按需加载的,所以它们尽可能小以加快应用程序的速度是很重要的。如果您尝试打开一个压缩文件,通常很难看出发生了什么,这没关系,因为我们一开始就不想对这些文件进行任何更改。

所有这些文件的作用可能很明显——css文件定义了 Bootstrap 的一些全局样式。img文件用于帮助我们在网站周围使用图标,如果我们愿意的话,js文件用于帮助我们为网站添加交互、过渡和效果。但是,在css文件夹中,有bootstrapbootstrap-responsive两个 css 文件,这可能会让人感到困惑。响应式设计是近年来真正爆发的东西,本身已经有很多书籍写到了这个主题。简而言之,bootstrap包括了bootstrap-responsive文件中的样式,以帮助我们的网站在不同的分辨率和设备上工作。因此,我们的网站应该可以在 Web 和现代移动设备上正常工作(大部分情况下)。

现在,你可能能够理解为什么我选择使用 Bootstrap 了;我们只需复制文件到我们的项目中,就获得了很多功能。但是,还没有完全连接好;我们需要告诉我们的layout.php文件去哪里查找,以便它可以使用这些新文件。

采取行动——包括 Bootstrap 并调整我们的布局以适应它

因为 Bootstrap 框架只是一系列文件,所以我们可以像在第四章中处理master.css文件一样轻松地将其包含在我们的项目中,

  1. layout.php文件中,在master.css之前添加一个链接到bootstrap.min.cssbootstrap-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>

  1. 接下来,让我们确保 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>

  1. 通过以下内容替换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>

  1. 删除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目录添加一些结构。

行动时间 - 组织我们的用户视图

随着我们继续为我们的应用程序创建视图,对我们来说很聪明的是要有一些组织,以确保我们保持事情简单明了。

  1. views目录中创建一个名为user的文件夹。

  2. 将现有的signup.php视图移动到这个文件夹中。结果的目录结构将类似于以下截图:行动时间 - 组织我们的用户视图

  3. 我们需要更新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是用户的唯一saltsalt与密码的明文值组合,并通过 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、namefull_name。但请记住,CouchDB 有充分的理由这样做。通过将用户名存储在_id中,CouchDB 将自动检查每个用户名是否唯一。

注意

请记住,如果我们想要开始存储诸如网站、传记位置等字段,我们可能会想要更有创意。我们将在本书后面更详细地讨论这个问题。

添加对额外字段的支持

为了向用户文档中添加这些字段,我们不需要在代码中做太多更改;我们只需要在user.php类中添加一些变量即可。

采取行动-添加字段以支持用户文档

我们已经在classes/user.php文件中设置了用户文档的基本结构,但让我们继续添加一些字段。

  1. 我们目前没有在任何项目中设置_id,但我们需要为我们的用户文档这样做。让我们打开classes/base.php,并添加_id,这样我们就有了在任何文档上设置_id的选项。
<?php
abstract class Base {
**protected $_id;** 
protected $type;

  1. 我们需要将我们刚刚讨论的所有用户字段添加到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 添加支持。注册是一个有点复杂的过程,但我们将尝试逐步分解它。在本节中,我们将:

  1. 定义我们的数据库管理员用户名和密码,以便我们可以创建新的用户文档

  2. 创建一个新的注册界面来支持我们添加的所有字段

  3. 添加一个 Bootstrap 助手,使创建表单输入更容易

  4. 开发一个快速而简单的注册过程的实现

  5. 深入了解我们密码的 SHA-1 加密

  6. 重构我们的注册过程,使其更加结构化

一点管理员设置

在第三章中,我们锁定了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 来帮助清理我们的注册界面,使其看起来正确。

  1. 用以下 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>

  1. 刷新注册页面,您将看到我们的表单现在很棒!更新界面
  • 我们的表单看起来很干净。但是,让我们诚实点,随着我们添加更多字段,为输入字段添加代码将开始变得痛苦。让我们创建一个小的辅助类,帮助我们创建一个可以与 Bootstrap 很好地配合的 HTML 标记:
  1. lib目录中创建一个名为bootstrap.php的新文件。

  2. bones.php中引用lib/bootstrap.php

define('ROOT', __DIR__ . '/..');
**require_once ROOT . '/lib/bootstrap.php';** 
require_once ROOT . '/lib/sag/src/Sag.php';

  1. 打开lib/bootstrap.php,并创建一个基本类。
<?php
class Bootstrap {
}

  1. 我们将创建一个名为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 . '">';
}** 
}

  1. 返回到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>

  1. 现在我们有了lib/bootstrap.php来让我们的生活更轻松,让我们向用户询问另外两个字段:usernamepassword
<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>

  1. 刷新您的浏览器,您会看到一个大大改进的注册表单。如果它看起来不像下面的截图,请检查您的代码是否与我的匹配。更新界面

我们的表单看起来很棒!不幸的是,当您点击注册!时,它实际上还没有注册用户。让我们在下一节中改变这一点。

快速而简单的注册

现在,我们将直接将用户注册代码写入index.php。我们将多次重构此代码,并在本章结束时,将大部分注册功能移至classes/user.php文件。

行动时间-处理简单用户注册

让我们逐步进行注册过程,在此过程中,我们将从头开始重建注册POST路由中的代码。我会逐步解释每段代码,然后在本节结束时进行全面回顾。

  1. 打开index.php,并开始收集简单字段:full_name, emailrolesfull_nameemail字段将直接来自表单提交,roles我们将设置为空数组,因为此用户没有特殊权限。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name');
$user->email = $app->form('email');
$user->roles = array();

  1. 接下来,我们将捕获用户提交的用户名,但我们希望防止奇怪的字符或空格,因此我们将使用正则表达式将提交的用户名转换为不带任何特殊字符的小写字符串。最终结果将作为我们的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;** 

  1. 为了加密用户输入的明文密码值,我们将临时设置一个字符串作为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);** 

  1. 为了保存用户文档,我们需要将数据库设置为_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());** 

  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);
$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, emailroles的值非常简单;我们只需从提交的表单中获取这些值。设置name变得更加复杂,我们将用户名的提交值转换为小写字符串,然后使用正则表达式(Regex)函数将任何特殊字符更改为空字符。有了干净的名称,我们将其附加到org.couchdb.user并保存到文档的_id中。哇!这真是一大堆。

迅速进入加密世界,我们设置了一个静态(非常不安全的)salt。将salt与明文密码结合在 SHA-1 函数中,得到了一个加密密码,保存在我们对象的password_sha字段中。接下来,我们使用setDatabase设置了 Sag 的数据库,以便我们可以与 CouchDB 的_users数据库进行通信。为了与用户进行通信,我们需要管理员凭据。因此,我们使用ADMIN_USERADMIN_PASSWORD常量登录到 CouchDB。最后,我们使用 HTTP 动词PUT在 CouchDB 中创建文档,并为用户呈现主页。

让我们测试一下,看看当我们提交注册表单时会发生什么。

  1. 在浏览器中打开注册页面,访问http://localhost/verge/signup

  2. 填写表格,将全名设置为John Doe电子邮件设置为<john@example.com>用户名设置为johndoe密码设置为temp123。完成后,点击注册刚刚发生了什么?

  3. 您的用户已创建!让我们通过访问http://localhost:5984/_utils,并查看_users数据库的新文档。刚刚发生了什么?

  4. 完美,一切应该已经保存正确!查看完毕后,点击删除文档删除用户。如果您当前未以管理员用户身份登录,您需要先登录,然后 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');
});

再次测试注册流程

现在我们已经解决了盐的不安全性,让我们回去再试一次注册流程。

  1. 通过在浏览器中转到http://localhost/verge/signup来打开注册页面。

  2. 填写表格,全名John Doe电子邮件<john@example.com>用户名johndoe密码temp123。完成后,点击注册

  3. 您的用户已创建!让我们通过转到http://localhost:5984/_utils,并在_users数据库中查找我们的新文档来到 Futon。这次我们的“盐”是随机且唯一的!再次测试注册流程

重构注册流程

正如我之前提到的,我们将把这段代码重构为干净的函数,放在我们的用户类内部,而不是直接放在index.php中。我们希望保留index.php用于处理路由、传递值和渲染视图。

行动时间-清理注册流程

通过在User类内创建一个名为signup的公共函数来清理我们的注册代码。

  1. 打开classes/user.php,并创建一个用于注册的public函数。
public function signup($username,$password) {
}

  1. 输入以下代码以匹配下面的代码。它几乎与我们在上一节输入的代码相同,只是不再引用$user,而是引用$this。您还会注意到full_nameemail不在这个函数中;您马上就会看到它们。
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());
}** 

  1. 打开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 的错误日志。

  1. 打开终端。

  2. 运行以下命令询问 Apache 的config文件保存日志的位置:

**grep ErrorLog /etc/apache2/httpd.conf** 

  1. 终端会返回类似以下的内容:
**# 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"** 

  1. 通过运行以下命令检索日志的最后几行:
**tail /private/var/log/apache2/error_log** 

  1. 日志会显示很多东西,但最重要的消息是这个,它说 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 日志。

  1. 打开终端。

  2. 通过运行以下命令检索日志的最后几行:

**tail /usr/local/var/log/couchdb/couch.log** 

  1. 终端将返回类似以下内容:
**[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 中允许我们向用户显示警报消息。

  1. 打开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>";
}
}

  1. 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>

  1. 现在我们已经添加了这些 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 消息。

让我们测试一下。

  1. 返回并尝试再次注册用户名为johndoe的用户。

你会看到这个友好的错误消息,告诉你有问题:

刚刚发生了什么?

  1. 现在,让我们测试一下成功的警报消息。将用户名更改为johndoe2。点击注册!,你将收到一个漂亮的绿色警报。刚刚发生了什么?
  • 即使有了这些简单的警报,我们的注册表单还不完美。随机的异常和错误可能会发生,我们无法处理。更令人担忧的是,我们并没有要求表单中的字段填写。这些项目需要在我们的视线范围内,但我们无法在本书中涵盖所有这些。

让我们继续讨论用户认证。

用户认证

现在我们已经创建了用户,我们肯定需要找到一种让他们登录到我们系统的方法。幸运的是,CouchDB 和 Sag 在这个领域真的会为我们做很多繁重的工作。在这一部分,我们将:

  • 设置登录表单

  • 了解会话、cookie,以及 CouchDB 和 Sag 如何处理我们的认证

  • 添加支持用户登出

  • 为已登录和未登录的用户不同处理 UI

设置登录表单

让我们创建一些登录表单,这样我们的用户就可以登录到我们的网站并使用他们新创建的账户。

试试吧——设置登录的路由和表单

我们已经多次经历了创建页面、设置路由和创建表单的过程。所以,让我们看看这次你能否自己尝试一下。我不会完全不帮助你。我会先告诉你需要尝试做什么,然后当你完成时,我们会进行回顾,确保我们的代码匹配起来。

你需要做的是:

  1. 创建一个名为user/login.php的新页面。

  2. index.php文件中为登录页面创建新的GETPOST路由。

  3. 告诉登录页面的GET路由渲染user/login视图。

  4. 使用user/signup.php作为指南创建一个包含usernamepassword字段的表单。

  5. 使用 Bootstrap 助手和submit按钮添加名为usernamepassword的字段。

在你这样做的时候,我会去看电视。当你准备好了,翻到下一页,我们看看进展如何!

干得好!我希望你能够在不需要太多帮助的情况下完成。如果你需要回头看旧代码寻求帮助,不要担心,因为当开发者陷入困境时,很多人最终都会这样做。让我们看看你的代码与我的代码匹配程度如何。

此外,你的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>

确保将你的代码更新到与我的相匹配,这样我们的代码在未来能够匹配起来。

登录和登出

现在我们已经准备好表单了,让我们谈谈我们需要做什么才能让表单真正起作用。让我们快速谈谈我们在登录过程中要实现的目标。

  1. Sag 将连接到 CouchDB 的_users数据库。

  2. Sag 将从我们的 PHP 直接将登录信息传递给 CouchDB。

  3. 如果登录成功,CouchDB 将返回一个认证的 cookie。

  4. 然后,我们将查询 CouchDB 以获取当前登录的用户名,并将其保存到会话变量中以供以后使用。

如果你已经使用其他数据库开发了一段时间,你会立刻看到登录过程有多酷。CouchDB 正在处理我们通常需要自己处理的大部分认证问题!

让我们来看看登录功能。幸运的是,它比注册过程要简单得多。

行动时间——为用户添加登录功能

我们将慢慢地进行这个过程,但我认为你会喜欢我们能够如此快速地添加这个功能,因为我们迄今为止编写的所有代码。

  1. 打开classes/user.php

  2. 创建一个名为loginpublic函数,我们可以将我们的明文$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');** 
}

  1. 为我们的登录代码创建一个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;
}
}** 
}

  1. 添加代码来启动会话,然后通过 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类中创建了一个名为loginpublic函数,允许用户登录。然后我们创建了一个新的 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');** 
});

我们现在可以去测试这个,但让我们完成更多的事情,以便完全测试这里发生了什么。

行动时间-为用户添加注销功能

我敢打赌你认为登录脚本非常简单。等到你看到我们如何让用户注销时,你会觉得更容易。

  1. 打开classes/user.php,创建一个名为logoutpublic static函数。
public static function logout() {
$bones = new Bones();
$bones->couch->login(null, null);
session_start();
session_destroy();
}

  1. index.php文件中添加一个路由,并调用logout函数。
get('/logout', function($app) {
User::logout();
$app->redirect('/');
});

  1. 注意,我们在 Bones 内部调用了一个新功能redirect函数。为了使其工作,让我们在底部添加一个快速的新功能
public function redirect($path = '/') {
header('Location: ' . $this->make_route($path));
}

刚刚发生了什么?

我们添加了一个名为logoutpublic static函数。我们将其设置为public static的原因是,我们目前登录的用户对我们来说并不重要。我们只需要执行一些简单的会话级操作。首先,我们像往常一样创建了一个$bones实例,但接下来的部分非常有趣,所以我们设置了$bones->couch->login(null, null)。通过这样做,我们将当前用户设置为匿名用户,有效地注销了他们。然后,我们调用了session_startsession_destroy。请记住,通过session_start,我们使我们的会话可访问,然后我们销毁它,这将删除与当前会话相关的所有数据。

在完成login函数后,我们打开了index.php,并调用了我们的public static函数,使用User::logout()

最后,我们使用了一个重定向函数,将其添加到了index.php文件中。因此,我们迅速在 Bones 中添加了一个函数,这样就可以使用make_route将用户重定向到一个路由。

处理当前用户

我们真的希望能够确定用户是否已登录,并相应地更改导航。幸运的是,我们可以在几行代码中实现这一点。

行动时间 - 处理当前用户

大部分拼图已经就位,让我们来看看根据用户是否已登录来更改用户布局的过程。

  1. 让我们在classes/user.php中添加一个名为current_user的函数,这样我们就可以从会话中检索当前用户的用户名。
public static function current_user() {
session_start();
return $_SESSION['username'];
session_write_close();
}

  1. classes/user.php中添加一个名为is_authenticatedpublic static函数,以便我们可以查看用户是否已经认证。
public static function is_authenticated() {
if (self::current_user()) {
return true;
} else {
return false;
}
}

  1. 既然我们的身份验证已经就绪,让我们来收紧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_userpublic static函数,用于检索存储在会话中的用户名。然后我们创建了另一个名为is_authenticatedpublic static函数。该函数检查current_user是否有用户名,如果有,则用户已登录。如果没有,则用户当前未登录。

最后,我们迅速进入我们的布局,这样我们就可以在用户登录时显示首页和注销的链接,以及在用户当前未登录时显示首页、注册和登录的链接。

让我们来测试一下:

  1. 通过转到http://localhost/verge/登录页面在浏览器中打开。请注意,标题显示首页、注册登录,因为您当前未登录。刚刚发生了什么?

  2. 使用您的一个用户帐户的凭据登录。您将收到一个很好的警报消息,并且标题更改为显示首页注销刚刚发生了什么?

总结

我希望你对我们在本章中所取得的成就感到震惊。我们的应用程序真的开始成形了。

具体来说,我们涵盖了:

  • 如何通过使用 Twitter 的 Bootstrap 大大改善界面

  • 如何在现有 CouchDB 用户文档的基础上创建额外的字段

  • 如何处理错误并通过日志调试问题

  • 如何完全构建出用户可以使用 Sag 和 CouchDB 注册、登录和注销应用程序的能力

这只是我们应用程序的开始。我们还有很多工作要做。在下一章中,我们将开始着手用户个人资料,并开始创建 CouchDB 中的新文档。这些文档将是我们用户的帖子。

第七章:用户个人资料和帖子建模

随着我们的应用程序的基础创建,我们允许用户注册并登录到我们的应用程序。这是任何应用程序的重要部分,但我们仍然缺少可以连接到用户帐户的内容的创建。我们将在本章中详细介绍所有内容!

在本章中,我们将:

  • 创建一个用户个人资料,以公开显示用户的信息

  • 使用 Bootstrap 清理个人资料

  • 处理各种异常

  • 讨论在 CouchDB 中对帖子和关系的建模

  • 创建一个表单,从已登录用户的个人资料中创建帖子

有了我们的路线图,让我们继续讨论用户个人资料!

用户个人资料

任何社交网络的主要吸引力是用户的个人资料;用户个人资料通常显示用户的基本信息,并显示他们创建的任何内容。

到本节结束时,我们的用户个人资料将按以下方式工作:

  • 如果访问者转到http://localhost/verge/user/johndoe,我们的路由系统将将其与路由/user/:username匹配

  • index.php文件将johndoe作为username的值,并将其传递给User类,尝试查找具有匹配 ID 的用户文档

  • 如果找到johndoeindex.php将显示一个带有johndoe信息的个人资料

  • 如果找不到johndoe,访问者将看到一个404错误,这意味着该用户名的用户不存在

使用路由查找用户

为了找到用户,我们首先需要创建一个函数,该函数将以用户名作为参数,并在有效时返回一个用户对象。

行动时间-获取单个用户文档

您可能还记得,在第三章中,使用 CouchDB 和 Futon 入门,我们能够通过传递所需文档的 ID 来从 CouchDB 中检索文档。这一次,我们将使用 Sag 来找到用户的信息。需要注意的一点是,当我们使用 ID 查找用户时,我们需要确保在查找用户时,需要使用org.couchdb.user:命名空间进行前置。

让我们从打开classes/user.php并滚动到底部开始。

  1. 添加一个名为get_by_username()public static函数。
public static function get_by_username() {
}

  1. 为了通过 ID 查找用户,我们需要允许我们的函数接受参数$username
public static function get_by_username($username = null) {
}

  1. 现在,让我们设置数据库来实例化 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);
}**

  1. 现在我们可以连接到_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_usernamepublic static函数,允许我们传入$username。要实际获取文档,我们需要使用我们的ADMIN_USERADMIN_PASSWORD常量来访问_users数据库。为了返回一个用户对象,我们需要创建一个名为$user的新用户对象。然后我们使用 Sag 的get调用通过 ID 标识文档并将其作为名为$documentstdClass对象返回。然后我们从document变量中获取值,并将它们传递到$user对象上的相应值。最后,我们将用户文档返回到调用函数的地方。

现在我们有一个处理按用户名查找用户的函数,让我们在index.php中创建一个路由,将用户名传递给这个函数。

行动时间-为用户个档案创建路由

我们将创建一个路由,以便人们可以通过转到唯一的 URL 来查看个人资料。这将是我们真正利用我们的路由系统处理路由变量的能力的第一次。

  1. 打开index.php,并创建一个用户个人资料的get路由,输入以下代码:
get('/user/:username', function($app) {
});

  1. 让我们使用路由变量:username告诉我们要查找的用户名;我们将把这个变量传递给我们在User类中创建的get_by_username函数。最后,我们将返回的user对象传递给视图中的user变量:
get('/user/:username', function($app) {
**$app->set('user', User::get_by_username($app- >request('username')));** 
});

  1. 最后,我们将呈现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视图。但是,让我们首先将所有用户文档内容都转储到我们的视图中。

  1. 在我们的working文件夹中的views目录中创建一个名为user/profile.php的视图。

  2. 为配置文件创建一个简单的标题,使用以下 HTML:

<div class="page-header">
<h1>User Profile</h1>
</div>

  1. 由于我们还没有设计,让我们只使用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 函数,用于输出关于变量或对象的结构化信息,在你只想确保事情正常运行时非常有用。

测试一下

现在我们有了一个简单的用户配置文件设置,让我们看看它的效果如何。

  1. 打开你的浏览器,然后转到http://localhost/verge/user/johndoe

  2. 你的浏览器将显示以下内容:测试

  • 还不错,但当然我们需要很快清理一下这些数据的格式。但是,现在让我们确保将我们的更改提交到 Git。

将你的更改添加到 Git。

在本节中,我们开始创建用户配置文件,并直接从 CouchDB 输出用户信息。让我们将所有更改添加到 Git,以便跟踪我们的进度。

  1. 打开终端。

  2. 输入以下命令以更改目录到我们的工作目录。

**cd /Library/Webserver/Documents/verge/** 

  1. 我们只添加了一个文件views/user/profile.php,所以让我们告诉 Git 将这个文件添加到源代码控制中。
**git add views/user/profile.php** 

  1. 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来再次找到它的位置。

让我们找出问题出在哪里。

  1. 打开终端。

  2. 通过运行以下命令检索日志的最后几行:

**tail /private/var/log/apache2/error_log** 

  1. 日志会显示很多东西,但最重要的消息是这个,说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 错误

让我们首先创建一个简单的视图,用于向我们显示错误。

  1. 让我们首先在我们的views目录内创建一个名为error的新文件夹。

  2. 创建一个名为500.php的新视图,并将其放入errors文件夹中(views/error/500.php)。

  3. 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>

  1. 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函数中添加一些代码,以便我们可以更深入地查看我们的问题。

  1. 让我们打开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) {
}** 
}

  1. 既然我们正在捕获错误,让我们在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);** 
}
}

  1. 当我们在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);
}** 
}
}

  1. 接下来,让我们在我们的公共函数登录中添加到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错误。

测试我们的异常处理程序

让我们再试一次,看看我们是否能找到异常的根源。

  1. 转到http://localhost/verge/user/someone

  2. 现在你会看到一个更友好的错误页面,告诉我们代码、消息和完整的错误,你会在错误日志中看到。测试我们的异常处理程序

对我们来说,从中弄清楚发生了什么是更容易的。在我们调试应用程序的过程中,这个页面对我们来说将非常有用,以跟踪发生了什么错误。

通过查看这段代码,我们可以知道 CouchDB 正在抛出一个404错误。我们可能期望这个错误会发生,因为我们正在寻找一个不存在的用户文档。让我们进一步了解一下404错误是什么,以及我们如何处理它。

显示 404 错误

404错误指的是 HTTP 状态码404,意思是“未找到”。404错误通常发生在您尝试访问不存在的内容时,比如访问错误的 URL。在我们的情况下,我们收到404错误是因为我们试图查找一个不存在的 CouchDB 文档。

如果找不到用户,则显示 404

404错误是一种特殊的错误,我们会在应用程序的不同位置看到。让我们创建另一个错误页面,以便在发生404错误时使用。

采取行动:使用 Bones 处理 404 错误

让我们为应用程序中的404错误创建一个视图。

  1. 首先,在我们的views/error/目录中创建一个名为404.php的新视图。

  2. 让我们在404.php中添加一些非常基本的代码,以通知访问者我们的应用程序找不到请求的页面。

<div class="hero-unit">
<h1>Page Not Found</h1>
</div>

  1. 为了呈现这个视图,让我们在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.phpget_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.phpindex.php周围添加一些简单的代码,以便处理404错误。

  1. 打开lib/bones.php,在Bones类内部创建一个名为resolve的函数,我们可以在路由的末尾调用它,并确定是否找到了路由。
public static function resolve() {
if (!static::$route_found) {
$bones = static::get_instance();
$bones->error404();
}
}

  1. 转到lib/bones.php的顶部,并创建一个名为resolve的函数,放在Bones类之外(例如get, post, putdelete),我们可以在任何地方调用它。
function resolve() {
Bones::resolve();
}

  1. 我们要做的最后一件事就是在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错误,让我们测试一下,看看会发生什么。

  1. 打开您的浏览器,转到http://localhost/verge/user/anybody

  2. 您的浏览器将显示以下内容:测试一下

  • 太好了!我们的User类正在转发给我们一个404错误,因为我们在get_by_username函数中添加了代码。
  1. 接下来,让我们检查一下我们的index.php,看看如果找不到请求的路由,它是否会转发给我们一个404错误。

  2. 打开您的浏览器,转到http://localhost/verge/somecrazyurl

  3. 您的浏览器将显示以下内容:测试一下

完美!我们的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 创建更好的个人资料

我们的个人资料并不是很好地组合在一起,这开始让我感到困扰,我们需要在本章后面再添加更多内容。让我们准备好我们的用户个人资料,以便我们可以很好地显示用户的信息和帖子。

行动时间-检查用户当前是否已登录

我们需要能够弄清楚用户正在查看的个人资料是否是他们自己的。所以,让我们在我们的视图中添加一个变量,告诉我们是否是这种情况。

  1. 打开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');
});

  1. 让我们更改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 将通过允许我们用有限的代码清理我们的个人资料来拯救我们。

  1. 让我们通过以下代码将我们的行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>** 

  1. 通过将更多的列表项添加到无序列表中,将用户的信息输出到左列。
<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>

让我们来看看我们的新个人资料

有了这个,我们的新的改进的个人资料已经出现了!让我们来看看。

  1. 通过转到http://localhost/verge/user/johndoe,在浏览器中打开johndoe用户的 URL。

  2. 您的浏览器将显示一个精心改造的用户个人资料。让我们来看看我们的新个人资料

  3. 现在,让我们检查一下我们的$is_current_user变量是否正常工作。为了做到这一点,请使用johndoe作为用户名登录,并转到http://localhost/verge/user/johndoe

  4. 您的浏览器将显示用户个人资料,以及一个友好的消息告诉您这是您的个人资料。让我们来看看我们的新个人资料

太棒了!我们的个人资料真的开始变得完整起来了。这是我们应用程序的一个重要里程碑。所以,让我们确保将我们的更改提交到 Git。

将您的更改添加到 Git

在这一部分,我们添加了支持清晰处理异常的功能,并且还改进了用户个人资料。让我们把所有的更改都添加到 Git 中,这样我们就可以跟踪我们的进展。

  1. 打开终端。

  2. 输入以下命令以更改目录到我们的working目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 我们在这一部分添加了一些文件。所以,让我们把它们都加入到源代码控制中。
**git add .** 

  1. 给 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)建模这种关系,它可能看起来类似于以下截图:

如何在 MySQL 中建模帖子

简而言之,这个图表显示了一个posts表,它有一个外键user_id,引用了用户表的id。这种一对多的关系在大多数应用程序中都很常见,在这种情况下,意味着一个用户可以有多个帖子。

既然我们已经看过一个熟悉的图表,让我们再看看与 CouchDB 相关的相同关系。

如何在 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类非常相似。如果你感到足够自信,请尝试自己创建基本类。

你需要做的是:

  1. 创建一个名为post.php的新类,它扩展了Base类。

  2. 为之前定义的每个必需字段创建变量。

  3. 添加一个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的公共函数,它将处理我们应用程序的帖子创建。

  1. 打开classes/post.php,并滚动到底部。在这里,我们将创建一个名为create的新公共函数。
public function create() {
}

  1. 让我们首先获得一个新的 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();
}** 

  1. 最后,让我们使用 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());** 
}

  1. 让我们用一个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函数。就是这样!我们现在可以在我们的应用程序中创建帖子。我们唯一剩下的就是在用户个人资料中创建一个表单。

开始行动-创建一个表单来启用帖子创建

让我们直接在用户的个人资料页面中编写用于创建帖子的表单。只有当已登录用户查看自己的个人资料时,该表单才会显示出来。

  1. 打开user/profile.php

  2. 让我们首先检查用户正在查看的个人资料是否是他们自己的。

<div class="span8">
**<?php if ($is_current_user) { ?>
<h2>Create a new post</h2>
<?php } ?>** 
<h2>Posts</h2>
</div>

  1. 接下来,让我们添加一个表单,允许当前登录的用户发布帖子。
<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路由(接下来我们将创建)。在表单中,我们放置了一个idcontenttextarea和一个submit按钮来实际提交表单。

现在我们已经准备好一切,让我们通过在index.php文件中创建一个名为post的路由来完成post的创建。

开始行动-创建一个路由并处理帖子的创建

为了实际创建一个帖子,我们需要创建一个路由并处理表单输入。

  1. 打开index.php

  2. 创建一个基本的post路由,并将其命名为post

post('/post', function($app) {
});

  1. 在我们的post路由中,让我们接受传递的值content并在我们的Post类上使用create函数来实际创建帖子。帖子创建完成后,我们将用户重定向回他们的个人资料页面。
post('/post', function($app) {
**$post = new Post();
$post->content = $app->form('content');
$post->create();
$app->redirect('/user/' . User::current_user());** 
});

  1. 我们已经做了很多工作,以确保用户在创建帖子时经过身份验证,但让我们再三检查用户是否在这里经过了身份验证。如果他们没有经过身份验证,我们的应用程序将将他们转发到用户登录页面,并显示错误消息。
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周围添加了功能,以确保用户已登录。如果他们没有登录,我们将把他们弹到登录页面,并要求他们登录以执行此操作。

测试一下

现在我们已经编写了创建帖子所需的一切,让我们一步一步地测试一下。

  1. 首先以johndoe的身份登录,并通过在浏览器中打开http://localhost/verge/user/johndoe来转到他的个人资料。

  2. 您的浏览器将显示一个用户个人资料,就像我们以前看到的那样,但这次您将看到post表单。测试一下

  3. 在文本区域中输入一些内容。我输入了我不喜欢花生酱,但您可以随意更改。

  4. 完成后,点击提交按钮。

  5. 您已被转发回johndoe的用户个人资料,但您还看不到任何帖子。因此,让我们登录 Futon,确保帖子已创建。

  6. 通过转到http://localhost:5984/_utils/database.html?verge,在 Futon 中转到verge数据库。

  7. 太棒了!这里有一个文档;让我们打开它并查看内容。测试一下

这个完美解决了!当用户登录时,他们可以通过转到他们的个人资料并提交创建新帖子表单来创建帖子。

将您的更改添加到 Git

在这一部分,我们添加了一个基于我们的Post模型来创建帖子的函数。然后我们在用户个人资料中添加了一个表单,这样用户就可以真正地创建帖子。让我们把所有的更改都添加到 Git 中,这样我们就可以跟踪我们的进展。

  1. 打开终端。

  2. 输入以下命令以更改目录到我们的working目录:

**cd /Library/Webserver/Documents/verge/** 

  1. 我们添加了classes/post.php文件,所以让我们把那个文件加入到源代码控制中:
**git add classes/post.php** 

  1. Git一个描述,说明自上次提交以来我们做了什么:
**git commit –am 'Added a Post class, built out basic post creation into the user profile. Done with chapter 7.'**

  1. 我知道我说过我不会再提醒你了,但我也只是个人。让我们把这些更改推送到 GitHub 上去。
**git push origin master**

总结

信不信由你,这就是我们在本章中要写的所有代码。收起你的抗议标语,上面写着“我们甚至还没有查询用户的帖子!”我们停在这里的原因是 CouchDB 有一种非常有趣的方式来列出和处理文档。为了讨论这个问题,我们需要定义如何使用设计文档来进行视图和验证。幸运的是,这正是我们将在下一章中涵盖的内容!

与此同时,让我们快速回顾一下我们在本章中取得的成就。

摘要

在本章中,我们涵盖了创建用户个人资料来显示用户信息,如何优雅地处理异常并向用户显示500404错误页面,如何在 CouchDB 中对帖子进行建模,以及最后,创建一个为已登录用户创建帖子的表单。

正如我所说,在下一章中,我们将涉及一些 CouchDB 带来的非常酷的概念。这可能是本书中最复杂的一章,但会非常有趣。

第八章:使用设计文档进行视图和验证

到目前为止,我们的应用程序与使用 MySQL 或其他关系数据库时并没有太大的不同。但是,在本章中,我们将真正发挥 CouchDB 的作用,通过它来处理以前在关系数据库中可能是痛点的许多事情。

在本章中,我们将:

  • 定义设计文档

  • 了解视图以及如何使用它们来查询数据

  • 发现 MapReduce 函数的威力

  • 使用 CouchDB 的 validation 函数

让我们不浪费时间,直接谈论设计文档。

设计文档

设计文档 是 CouchDB 的特殊功能之一,你可能没有从数据库中预期到。表面上,设计文档看起来和普通文档一样。它们有标准字段:_id_rev,可以创建、读取、更新和删除。但与普通文档不同的是,它们包含 JavaScript 形式的应用代码,并且具有特定的结构。这些 JavaScript 可以驱动验证,使用 mapreduce 函数显示视图,以及更多功能。我们将简要介绍每个功能以及如何使用它们。

一个基本的设计文档

一个基本的设计文档可能看起来类似于以下内容:

{
—_id— : —_design/application—,
"_rev" : "3-71c0b0bd73a9c9a45ea738f1e9612798",
"views" : {
"example" : {
"map" : "function(doc){ emit(doc._id, doc)}"
}
}
}

_id_rev 应该看起来很熟悉,但与迄今为止的其他文档不同,_id 有一个可读的名称:_design/example。设计文档通过名称中包含 _design 来标识。因此,您需要遵循这种格式。

_id_rev 过渡后,您会注意到键视图。视图是设计文档的重要组成部分,让我们更多地谈谈它们。

视图

视图 是 CouchDB 提供给我们的用于索引、查询和报告数据库文档的工具。如果您在 MySQL 经验之后阅读本书,那么视图将替代典型的 SQL SELECT 语句。

现在您对视图有了一些了解,您会注意到在前面的设计文档中,我们创建了一个名为 test 的视图。

映射函数

example 键内,我们放置了一个名为 map 的函数。Map 函数是 JavaScript 函数,用于消耗文档,然后将它们从原始结构转换为应用程序可以使用的新的键/值对。了解 Map 函数至关重要。因此,让我们看一下 map 函数的最简单实现,以确保我们都在同一个页面上。

-example- : {
-map- : -function(doc){ emit(doc._id, doc)}-
}

当调用示例 map 函数时,CouchDB 将尝试索引数据库中的每个文档,并使用 doc 参数以 JSON 格式将它们传递给这个函数。然后,我们调用一个名为 emit 的函数,它接受一个键和一个值,从中键和值将保存到一个数组中,并在索引完成后返回。

emit 函数的键和值可以是文档中的任何字段。在这个例子中,我们将 doc._id 作为键,doc 作为值传递给 emit 函数。doc._id 可能是被索引的文档的 _id 字段,doc 是以 JSON 格式表示的整个文档。

在下一节中,我们将使用视图来处理我们的数据。为了确保您完全理解视图对我们的数据做了什么,请确保您在 verge 数据库中至少创建了五到六篇帖子。

行动时间 — 创建临时视图

CouchDB 为我们提供了临时视图,供我们在开发或尝试测试视图结果时使用。让我们使用 Futon 创建一个临时视图,以便我们可以处理一些数据。

  1. 打开浏览器,转到 Futon (http://localhost:5984/_utils/)。

  2. 确保您已登录到 admin 帐户,通过检查右侧列的底部。

  3. 通过单击 verge 进入我们的 verge 数据库。

  4. 点击下拉框,选择临时视图...进行操作的时间-创建临时视图

  5. 这个表单将允许我们玩弄视图并实时测试它们与数据。进行操作的时间-创建临时视图

  6. 让我们编辑Map Function文本区域中的代码,使其与我们之前查看的示例代码匹配:

function(doc) {
emit(doc._id, doc)
}

  1. 点击运行以查看map函数的结果。进行操作的时间-创建临时视图

  2. 让我们确保我们只能通过检查doc.type是否等于post:来看到帖子。

function(doc) {
if (doc.type == 'post') {
emit(doc._id, doc);
}
}

  1. 再次点击运行,你会看到相同的结果。

刚刚发生了什么?

我们刚刚学习了如何在 CouchDB 中创建一个临时视图,以便我们可以测试之前查看的map函数。使用 Futon 给我们的临时视图界面,我们运行了我们的示例map函数,并显示了一系列键/值对。

最后,我们稍微加强了我们的map函数,以确保我们只查看type等于post的文档。现在,这个改变对我们的map函数没有任何影响,但是一旦我们添加了一个不同类型的文档,情况就会改变。如果你记得的话,这是因为 CouchDB 将文档存储在一个扁平的数据存储中;这意味着当我们添加新的文档类型时,我们希望具体指出我们要处理哪些文档。因此,通过在我们的代码中添加if语句,我们告诉 CouchDB 忽略那些type未设置为post的文档。

进行操作的时间-创建用于列出帖子的视图

你可能已经注意到了临时视图页面上的警告,内容如下:

**Warning: Please note that temporary views that we'll create are not suitable for use in production and will respond much slower as your data increases. It's recommended that you use temporary views in experimentation and development, but switch to a permanent view before using them in an application.** 

让我们听从这个警告,创建一个设计文档,这样我们就可以开始将所有这些构建到我们的应用程序中。

  1. 打开你的浏览器到 Futon。

  2. 导航到我们正在使用的临时视图页面:(http://localhost:5984/_utils/database.html?verge/_temp_view)。

  3. 让我们让我们的函数更有用一些,将我们的键改为doc.user

function(doc) {
if (doc.type == 'post') {
emit(doc.user, doc);
}
}

  1. 点击运行以查看结果。进行操作的时间-创建用于列出帖子的视图

  2. 现在我们的视图中有我们想要在应用程序中使用的代码,点击另存为...以保存此视图并为我们创建一个设计文档。

  3. 将显示一个窗口,要求我们给设计文档和视图命名。将_design/application输入为设计文档名称,posts_by_user输入为视图名称,然后点击保存进行操作的时间-创建用于列出帖子的视图

刚刚发生了什么?

我们从临时视图中创建了一个设计文档,以便我们的应用程序可以使用它。这一次,我们将键从doc._id更改为doc.user,以便我们可以选择具有特定用户名的文档,这将在几分钟内有所帮助。然后,我们将这个临时视图保存为一个名为posts_by_user的视图,并将其保存到一个名为_design/application的新设计文档中。

你可以使用 Futon 的界面轻松检查我们的设计文档是否成功创建。

  1. 打开你的浏览器,进入 Futon 中的verge数据库(http://localhost:5984/_utils/database.html?verge)。

  2. 点击视图下拉框,选择设计文档

  3. 你只会在这里看到一个文档,那就是我们新创建的设计文档,名为_design/application

  4. 点击文档,你会看到完整的设计文档。刚刚发生了什么?

趁热打铁,让我们快速看看如何使用 Futon 来测试设计文档及其视图:

  1. 打开你的浏览器到 Futon,并确保你正在查看verge数据库(http://localhost:5984/_utils/database.html?verge)。

  2. 点击视图下拉框,你会看到应用程序(我们设计文档的名称)。点击名为posts_by_user的视图。

  3. 您将看到视图的结果,以及当前与之关联的代码。刚刚发生了什么?

从这个页面,您可以点击结果并查看文档详细信息。您甚至可以通过简单地输入新代码并点击保存来更改视图的代码。

玩弄这些简单视图很有趣,但让我们深入一点,看看我们实际上如何使用这些视图来查询我们的文档。

查询 map 函数

我们可以在我们的map查询中使用各种选项。我将涉及最常见的一些选项,但您可以通过查看 CouchDB 的 wiki 找到更多:wiki.apache.org/couchdb/HTTP_view_API#Querying_Options

最常见的查询选项是:

  • reduce

  • startkey

  • endkey

  • key

  • limit

  • skip

  • descending

  • include_docs

让我们使用一些这些选项与我们的posts_by_user视图,看看我们可以得到什么样的结果。

行动时间-查询 posts_by_user 视图

请记住,设计文档仍然是一个文档,这意味着我们可以像查询常规文档一样查询它。唯一的区别是我们需要使用稍微不同的 URL 模式来命中正确的文件。

  1. 打开终端。

  2. 使用一个curl语句通过传递johndoe的关键字(或者您数据库中帖子数量较多的其他用户)来查询我们的设计文档,然后通过python mjson.tool使其变得更漂亮:

**curl http://127.0.0.1:5984/verge/_design/application/_view/posts_by_user?key=%22johndoe%22 | python -mjson.tool** 

  1. 终端将返回类似以下的内容:
{
"offset": 0,
"rows": [
{
"id": "352e5c2d51fb1293c44a2146d4003aa3",
"key": "johndoe",
"value": {
"_id": "352e5c2d51fb1293c44a2146d4003aa3",
"_rev": "3-ced38337602bd6c0587dc2d9792f6cff",
"content": "I don\\'t like peanut butter.",
"date_created": "Wed, 28 Sep 2011 13:44:09 -0700",
"type": "post",
"user": "johndoe"
}
},
{
"id": "d3dd453dbfefab8c8ea62a7efe000fad",
"key": "johndoe",
"value": {
"_id": "d3dd453dbfefab8c8ea62a7efe000fad",
"_rev": "2-07c7502eecb088aad5ee8bd4bc6371d1",
"content": "I do!\r\n",
"date_created": "Mon, 17 Oct 2011 21:36:18 -0700",
"type": "post",
"user": "johndoe"
}
}
],
"total_rows": 4
}

刚刚发生了什么?

我们刚刚使用了一个curl语句来查询我们应用程序设计文档中的posts_by_user视图。我们将johndoe作为我们的视图搜索的关键字传递,CouchDB 用它来返回只匹配该关键字的文档。然后我们使用python mjson.tool,这样我们就可以以友好的方式看到我们的输出。

让我们再玩一会儿,通过几个快速场景来讨论一下,确定我们如何使用 map 的query选项来解决它们。

  1. 如果您真的只想检索我们的map函数为johndoe返回的第一篇帖子,您可以通过在查询字符串的末尾添加limit=1来实现这一点:
**curl 'http://127.0.0.1:5984/verge/_design/application/_view/posts_by_user?key=%22johndoe%22&limit=1'| python -mjson.tool** 

  1. 您的终端将返回以下输出。请注意,这次您只会得到一篇帖子:
{
"offset": 0,
"rows": [
{
"id": "352e5c2d51fb1293c44a2146d4003aa3",
"key": "johndoe",
"value": {
"_id": "352e5c2d51fb1293c44a2146d4003aa3",
"_rev": "3-ced38337602bd6c0587dc2d9792f6cff",
"content": "I don\\'t like peanut butter.",
"content": "I don\\'t like peanut butter.",
"date_created": "Wed, 28 Sep 2011 13:44:09 -0700",
"type": "post",
"user": "johndoe"
}
}
],
"total_rows": 4
}

  1. 现在,如果我们想要看到我们的map函数为johndoe返回的最后一篇帖子,您可以通过在我们的语句末尾添加descending=true以及limit=1来实现这一点,以获取最新的帖子,如下所示:
**curl 'http://127.0.0.1:5984/verge/_design/application/_view/posts_by_user?key=%22johndoe%22&limit=1&descending=true'| python -mjson.tool** 

  1. 您的命令行将精确返回您要查找的内容:由johndoe创建的最后一篇帖子。
{
"offset": 2,
"rows": [
{
"id": "d3dd453dbfefab8c8ea62a7efe000fad",
"key": "johndoe",
"value": {
"_id": "d3dd453dbfefab8c8ea62a7efe000fad",
"_rev": "2-07c7502eecb088aad5ee8bd4bc6371d1",
"content": "I do!\r\n",
"date_created": "Mon, 17 Oct 2011 21:36:18 -0700",
"type": "post",
"user": "johndoe"
}
}
],
"total_rows": 4
}

通过这些示例,您应该清楚地知道我们可以链式和组合我们的query选项以各种方式检索数据。我们可以玩一会儿查询视图,但让我们继续尝试将posts_by_user视图构建到我们的应用程序中,以便我们可以在用户的个人资料上显示用户的帖子。

在我们的应用程序中使用视图

我们已经完成了查询数据库所需的大部分繁重工作;我们只需要向我们的应用程序添加几行代码。

行动时间-在帖子类中添加对 get_posts_by_user 的支持

  1. 在文本编辑器中打开classes/post.php

  2. 创建一个名为get_posts_by_user的新的public函数,它将接受$username作为参数。

public function get_posts_by_user($username) {
}

  1. 现在,让我们创建一个新的Bones实例,以便我们可以查询 CouchDB。让我们还实例化一个名为$posts的数组,在这个函数的最后返回它。
public function get_posts_by_user($username) {
**$bones = new Bones();
$posts = array();
return $posts;** 
}

  1. 接下来,让我们通过传递$username作为关键字来查询我们的视图,并使用foreach函数来遍历所有结果到一个名为$_post的变量中。
public function get_posts_by_user($username) {
$bones = new Bones();
$posts = array();
**foreach ($bones->couch- >get('_design/application/_view/posts_by_user?key="' . $username . '"&descending=true')->body->rows as $_post) {
}** 
return $posts;
}

  1. 最后,让我们使用$_post变量中的数据创建和填充一个新的Post实例。然后,让我们将$post添加到$posts数组中。
public function get_posts_by_user($username) {
$bones = new Bones();
$posts = array();
foreach ($bones->couch- >get('_design/application/_view/posts_by_user?key="' . $username . '"')->body->rows as $_post) {
**$post = new Post();
$post->_id = $_post->id;
$post->date_created = $_post->value->date_created;
$post->content = $_post->value->content;
$post->user = $_post->value->user;
array_push($posts, $post);
}** 
return $posts;
}

刚刚发生了什么?

我们创建了一个名为get_posts_by_user的函数,并将其放在我们的Post类中。这个函数接受一个名为$username的参数。get_posts_by_user函数使用get_posts_by_user视图将帖子列表返回到一个通用类中,我们遍历每个文档,创建单独的Post对象,并将它们推入数组中。您会注意到,我们必须使用$_post->value来获取帖子文档。请记住,这是因为我们的视图返回一个键和值的列表,每个文档一个,我们整个文档都存在于value字段中。

简而言之,这个函数使我们能够传入用户的用户名,并检索由传入用户创建的帖子数组。

行动时间——将帖子添加到用户资料

现在我们已经完成了所有繁重的工作,获取了用户的帖子,我们只需要再写几行代码,就可以让它们显示在用户资料中。让我们首先在index.php文件中添加一些代码,接受路由中的用户名,将其传递给get_posts_by_user函数,并将数据传递给资料视图:

  1. 打开index.php,找到/user/:username路由,并添加以下代码,将我们的get_posts_by_user函数返回的帖子传递给一个变量,以便我们的视图访问:
get('/user/:username', function($app) {
$app->set('user', User::find_by_username($app- >request('username')));
$app->set('is_current_user', ($app->request('username') == User::current_user() ? true : false));
**$app->set('posts', Post::get_posts_by_user($app- >request('username')));** 
$app->render('user/profile');
});

  1. 打开views/user/profile.php,并在创建新帖子文本区域的下面添加以下代码,以便我们可以在用户资料页面上显示帖子列表:
<h2>Posts</h2>
**<?php foreach ($posts as $post): ?>
<div class="post-item row">
<div class="span7">
<strong><?php echo $user->name; ?></strong>
<p>
<?php echo $post->content; ?>
</p>
<?php echo $post->date_created; ?>
</div>
<div class="span1">
<a href=#">(Delete)</a>
</div>
<div class="span8"></div>
</div>** 
<?php endforeach; ?>

  1. 最后,为了支持我们添加的一些新代码,让我们更新我们的public/css/master.css文件,使资料看起来漂亮整洁。
.post-item {padding: 10px 0 10px 0;}
.post-item .span8 {margin-top: 20px; border-bottom: 1px solid #ccc;}
.post-item .span1 a {color:red;}

发生了什么?

我们刚刚在index.php文件中添加了一些代码,这样当用户导航到用户的资料时,我们的应用程序将从路由中获取用户名,传递给get_posts_by_user函数,并将该函数的结果传递给一个名为posts的变量。然后,在views/user/profile.php页面中,我们循环遍历帖子,并使用 Bootstrap 的 CSS 规则使其看起来漂亮。最后,我们在我们的master.css文件中添加了几行代码,使一切看起来漂亮。

在本节中,我们还在每篇帖子旁边添加了一个(删除)链接,目前还没有任何功能。我们将在本章后面再连接它。

打开我们的浏览器,让我们检查一下,确保一切都正常工作。

  1. 打开您的浏览器,以一个用户的身份登录。

  2. 点击我的个人资料查看用户资料。

  3. 现在,您应该能够看到包含用户所有帖子的完整资料。发生了什么?

  4. 让我们测试一下,确保我们的列表正常工作,输入一些文本到文本区域中,然后点击提交

  5. 您的个人资料已刷新,您的新帖子应该显示在列表的顶部。发生了什么?

随意在这里暂停一下,以几个不同的用户身份登录,并创建大量的帖子!

完成后,让我们继续讨论map函数的伴侣:reduce

Reduce 函数

Reduce允许您处理map函数返回的键/值对,然后将它们分解为单个值或更小的值组。为了让我们的工作更容易,CouchDB 带有三个内置的reduce函数,分别是_count, _sum_stats

  • _count: 它返回映射值的数量

  • _sum: 它返回映射值的总和

  • _stats: 它返回映射值的数值统计,包括总和、计数、最小值和最大值

由于reduce函数对于新开发者来说可能不是 100%直观,让我们直截了当地在我们的应用程序中使用它。

在下一节中,我们将为我们的get_posts_by_user视图创建一个reduce函数,显示每个用户创建的帖子数量。看一下我们现有的设计文档,显示了reduce函数的样子:

{
"_id": "_design/application",
"_rev": "3-71c0b0bd73a9c9a45ea738f1e9612798",
"language": "javascript",
"views": {
"posts_by_user": {
"map": "function(doc) {emit(doc.user, doc)}",
**"reduce": "_count"** 
}
}
}

在这个例子中,reduce函数将map函数中的所有用户名分组,并返回每个用户名在列表中出现的次数。

执行操作-在 Futon 中创建 reduce 函数

使用 Futon 向视图添加reduce函数非常容易。

  1. 打开你的浏览器,进入 Futon 中的verge数据库(http://localhost:5984/_utils/database.html?verge)。

  2. 点击视图下拉框,你会看到应用程序(我们设计文档的名称)。你可以点击名为posts_by_user的视图。

  3. 点击查看代码,这样你就可以看到MapReduce的文本区域。

  4. Reduce文本区域输入_count,然后点击保存

  5. 你可以通过点击保存按钮下面的Reduce复选框来验证你的reduce函数是否正常工作。

  6. 你应该看到类似以下的屏幕截图:执行操作-在 Futon 中创建 reduce 函数

刚刚发生了什么?

我们刚刚使用 Futon 更新了我们的视图以使用_count reduce函数。然后,我们通过点击Reduce复选框在同一视图中测试了reduce函数。你会注意到我们的reduce函数也返回了一个键/值对,键等于用户名,值等于他们创建的帖子的数量。

执行操作-为我们的应用程序添加支持以使用 reduce 函数

现在我们已经创建了reduce函数,让我们向我们的应用程序添加一些代码来检索这个值。

  1. 打开classes/post.php

  2. 现在我们已经创建了一个reduce函数,我们需要确保get_posts_by_user函数在不使用reduce函数的情况下使用该视图。我们将通过在查询字符串中添加reduce=false来实现这一点。这告诉视图不要运行reduce函数。

public function get_posts_by_user($username) {
$bones = new Bones();
$posts = array();
**foreach ($bones->couch- >get('_design/application/_view/posts_by_user?key="' . $username . '"&descending=true&reduce=false')->body->rows as $_post) {** 

  1. 创建一个名为get_post_count_by_user的新的public函数,它将接受$username作为参数。
public function get_post_count_by_user($username) {
}

  1. 让我们添加一个调用我们的视图,模仿我们的get_posts_by_user函数。但是,这一次,我们将在查询字符串中添加reduce=true。一旦我们从视图中获得结果,就遍历数据以获取位于第一个返回行的值中的值。
public function get_post_count_by_user($username) {
**$bones = new Bones();
$rows = $bones->couch- >get('_design/application/_view/posts_by_user?key="' . " $username . '"&reduce=true')->body->rows;
if ($rows) {
return $rows[0]->value;
} else {
return 0;
}** 
}

  1. 打开index.php,找到/user/:username路由。

  2. 添加代码将get_post_count_by_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->set('posts', Post::get_posts_by_user($app- >request('username')));
**$app->set('post_count', Post::get_post_count_by_user($app- >request('username')));** 
$app->render('user/profile');
});

  1. 最后,打开用户资料(views/user/profile.php)并在我们的post列表顶部显示$post_count 变量。
<h2>Posts (<?php echo $post_count; ?>)</h2>

刚刚发生了什么?

我们通过更新现有的get_posts_by_user函数开始本节,并告诉它不要运行reduce函数,只运行map函数。然后,我们创建了一个名为get_post_count_by_user的函数,它访问了我们的posts_by_user视图。但是,这一次,我们告诉它通过在调用中传递reduce=true来运行reduce函数。当我们从reduce函数接收到值时,我们进入第一行的值并返回它。我们只看一个行,因为我们只传入一个用户名,这意味着只会返回一个值。

然后我们从用户资料路由调用get_post_count_by_user并将其传递给user/profile.php视图。在视图中,我们在帖子列表的顶部输出了$post_count

通过这么少的代码,我们为我们的资料添加了一个很酷的功能。让我们测试一下看看$post_count显示了什么。

  1. 打开你的浏览器,通过http://localhost/verge/user/johndoe进入 John Doe 的用户资料。

  2. 请注意,我们现在在post列表的顶部显示了帖子的数量。刚刚发生了什么?

更多关于 MapReduce

使用mapreduce函数一起通常被称为MapReduce,当它们一起使用时,它们可以成为数据分析的强大方法。不幸的是,我们无法在本书中介绍各种案例研究,但我会在本章末尾包含一些进一步学习的参考资料。

验证

在本节中,我们将揭示并讨论 CouchDB 的另一个非常独特的属性-其内置的文档函数支持。这个功能允许我们对我们的数据进行更严格的控制,并可以保护我们免受一些可能在 Web 应用程序中发生的严重问题。

请记住,我们的verge数据库可以被任何用户读取,这对我们来说还不是一个问题。但是,例如,如果有人找出了我们的数据库存储位置怎么办?他们可以很容易地在我们的数据库中创建和删除文档。

为了充分说明这个问题,让我们添加一个功能,允许我们的用户删除他们的帖子。这个简单的功能将说明一个潜在的安全漏洞,然后我们将用 CouchDB 的validation函数来修补它。

行动时间-为我们的类添加对$_rev 的支持

直到这一点,我们在 CouchDB 文档中看到了_rev键,但我们实际上并没有在我们的应用程序中使用它。为了能够对已经存在的文档采取任何操作,我们需要传递_rev以及_id,以确保我们正在处理最新的文档。

让我们通过向我们的base类添加一个$_rev变量来为此做好准备。

  1. 在您的工作目录中打开classes/base.php,并添加$_rev变量。
abstract class Base
{
protected $_id;
**protected $_rev;** 
protected $type;

  1. 不幸的是,现在每次调用to_json函数时,无论是否使用,_rev都将始终包含在内。如果我们向 CouchDB 发送一个null _rev,它将抛出错误。因此,让我们在classes/base.phpto_json函数中添加一些代码,如果没有设置值,就取消设置我们的_rev变量。
public function to_json() {
**if (isset($this->_rev) === false) {
unset($this->_rev);
}** 
return json_encode(get_object_vars($this));
}

刚刚发生了什么?

我们将$_rev添加到我们的base类中。直到这一点,我们实际上并没有需要使用这个值,但在处理现有文档时,这是一个要求。在将$_rev添加到base类之后,我们不得不修改我们的to_json函数,以便在没有设置值时取消设置$_rev

行动时间-在我们的应用程序中添加删除帖子的支持

现在我们在base类中有访问_rev变量的支持,让我们添加支持,以便我们的应用程序可以从用户个人资料中删除帖子。

  1. 让我们从打开classes/post.php并向get_posts_by_user函数添加一行代码开始,以便我们可以使用_rev
public function get_posts_by_user($username) {
$bones = new Bones();
$posts = array();
foreach $bones->couch- >get('_design/application/_view/posts_by_user?key="' . $username . '"&descending=true&reduce=false')->body->rows as $_post) {
$post = new Post();
$post->_id = $_post->value->_id;
**$post->_rev = $_post->value->_rev;** 
$post->date_created = $_post->value->date_created;

  1. 接下来,让我们在classes/post.php文件中创建一个简单的delete函数,以便我们可以删除帖子。
public function delete() {
$bones = new Bones();
try {
$bones->couch->delete($this->_id, $this->_rev);
}
catch(SagCouchException $e) {
$bones->error500($e);
}
}

  1. 现在我们有了删除帖子的后端支持,让我们在我们的index.php文件中添加一个接受_id_rev的路由。通过这个路由,我们可以触发从我们的个人资料页面删除帖子。
get('/post/delete/:id/:rev', function($app) {
$post = new Post();
$post->_id = $app->request('id');
$post->_rev = $app->request('rev'
$post->delete();
$app->set('success', 'Your post has been deleted');
$app->redirect('/user/' . User::current_user());
});

  1. 最后,让我们更新我们的views/user/profile.php页面,以便用户点击delete链接时,会命中我们的路由,并传递必要的变量。
<?php foreach ($posts as $post): ?>
<div class="post-item row">
<div class="span7">
<strong><?php echo $user->name; ?></strong>
<p>
<?php echo $post->content; ?>
</p>
<?php echo $post->date_created; ?>
</div>
<div class="span1">
**<a href="<?php echo $this->make_route('/post/delete/' . $post->_id . '/' . $post->_rev)?>" class="delete">
(Delete)
</a>** 
</div>
<div class="span8"></div>
</div>
<?php endforeach; ?>

刚刚发生了什么?

我们刚刚添加了支持用户从其个人资料中删除帖子。我们首先确保在get_posts_by_user函数中将_rev返回到我们的帖子对象中,以便在尝试删除帖子时可以传递它。接下来,我们在我们的post类中创建了一个接受$id$rev作为属性并调用 Sag 的delete方法的delete函数。然后,我们创建了一个名为/post/delete的新路由,允许我们向其传递_id_rev。在这个路由中,我们创建了一个新的Post对象,为其设置了_id_rev,然后调用了delete函数。然后我们设置了success变量并刷新了个人资料。

最后,我们通过将$post->_id$post->_rev传递给/post/delete路由,使用户个人资料中的delete链接可操作。

太棒了!现在我们可以点击网站上任何帖子旁边的删除,它将从数据库中删除。让我们试一试。

  1. 打开浏览器,转到http://localhost/verge

  2. 以任何用户身份登录,转到他们的用户资料。

  3. 点击(删除)按钮。

  4. 页面将重新加载,您的帖子将神奇地消失!

这段代码从技术上讲确实按照我们的计划工作,但是如果您玩了几分钟删除帖子,您可能会注意到我们这里有一个问题。现在,任何用户都可以从任何个人资料中删除帖子,这意味着我可以转到您的个人资料并删除您的所有帖子。当然,我们可以通过隐藏删除按钮来快速解决这个问题。但是,让我们退一步,快速思考一下。

如果有人找到(或猜到)用户帖子的_id_rev,并将其传递给/post/delete路由,会发生什么?帖子将被删除,因为我们没有任何用户级别的验证来确保试图删除文档的人实际上是文档的所有者。

让我们首先在数据库级别解决这个问题,然后我们将逆向工作,并在界面中正确隐藏删除按钮。

CouchDB 对验证的支持

CouchDB 通过设计文档中的validate_doc_update函数为文档提供验证。如果操作不符合我们的标准,此函数可以取消文档的创建/更新/删除。验证函数具有定义的结构,并且可以直接适用于设计文档,如下所示:

{
"_id": "_design/application",
"_rev": "3-71c0b0bd73a9c9a45ea738f1e9612798",
"language": "javascript",
**"validate_doc_update": "function(newDoc, oldDoc, userCtx) { //JavaScript Code }",** 
"views": {
"posts_by_user": {
"map": "function(doc) {emit(doc.user, doc)}",
"reduce": "_count"
}
}
}

让我们看看validate_doc_update函数,并确保我们清楚这里发生了什么。

function(newDoc, oldDoc, userCtx) { //JavaScript Code }

  • newDoc:它是您要保存的文档

  • oldDoc:它是现有的文档(如果有的话)

  • userCtx:它是用户对象和他们的角色

现在我们知道我们可以使用哪些参数,让我们创建一个简单的validate函数,确保只有文档的创建者才能更新或删除该文档。

行动时间-添加一个验证函数,以确保只有创建者可以更新或删除他们的文档

添加validate函数可能有点奇怪,因为与视图不同,在 Futon 中没有一个很好的界面供我们使用。添加validate_doc_update函数的最快方法是将其视为文档中的普通字段,并将代码直接输入值中。这有点奇怪,但这是调整设计文档的最快方法。在本章的末尾,如果您想更清晰地了解如何管理设计文档,我会给您一些资源。

  1. 打开浏览器,转到 Futon(http://localhost:5984/_utils/)。

  2. 确保您已登录到admin帐户,方法是检查右下角列是否显示欢迎

  3. 通过单击verge转到我们的verge数据库。

  4. 点击我们的_design/application设计文档。

  5. 点击添加字段,并将此字段命名为validate_doc_update

  6. 文本区域中,添加以下代码(格式和缩进无关紧要):

function(newDoc, oldDoc, userCtx) {
if (newDoc.user) {
if(newDoc.user != userCtx.name) {
throw({"forbidden": "You may only update this document with user " + userCtx.name});
}
}
}

  1. 点击保存,您的文档将被更新以包括验证函数。

刚刚发生了什么?

我们刚刚使用 Futon 更新了我们的_design/application设计文档。我们使用简单的界面创建了validate_doc_update函数,并将验证代码放在值中。代码可能看起来有点混乱;让我们快速浏览一下。

  1. 首先,我们检查要保存的文档是否使用此if语句附加了一个用户变量:
if (newDoc.user).

  1. 然后,我们检查文档上的用户名是否与当前登录用户的用户名匹配:
if(newDoc.user != userCtx.name).

  1. 如果事实证明文档确实与用户相关联,并且尝试保存的用户不是已登录用户,则我们使用以下代码行抛出禁止错误(带有状态码403的 HTTP 响应),并说明为什么无法保存文档:
throw({"forbidden": "You may only update this document with user " + userCtx.name});

值得注意的是,一个设计文档只能有一个validate_doc_update函数。因此,如果你想对不同的文档进行不同类型的验证,那么你需要做如下操作:

function(newDoc, oldDoc, userCtx) {
if (newDoc.type == "post") {
// validation logic for posts
}
if (newDoc.type == "comment") {
// validation logic for comments
}
}

我们可以用验证函数做更多的事情。事实上,我们经常使用的_users数据库通过validate_doc_update函数驱动所有用户验证和控制。

现在,让我们测试一下我们的validation函数。

  1. 打开你的浏览器,转到http://localhost/verge

  2. 以一个不同于John Doe的用户登录。

  3. 通过访问:http://localhost/verge/user/johndoe,转到John Doe的个人资料。

  4. 尝试点击(Delete)按钮。

  5. 你的浏览器将向你显示以下消息:刚刚发生了什么?

太棒了!CouchDB 为我们抛出了一个403错误,因为它知道我们没有以John Doe的身份登录,而我们试图删除他的帖子。如果你想进一步调查,你可以再次以John Doe的身份登录,并验证当你以他的身份登录时是否可以删除他的帖子。

我们可以放心地知道,无论用户使用什么接口,Sag、curl,甚至通过 Futon,CouchDB 都会确保用户必须拥有文档才能删除它。

我们可以为这个验证错误添加一个更优雅的错误消息,但这种错误很少发生,所以现在让我们继续。让我们只是在用户个人资料中添加一些简单的逻辑,这样用户就没有能力删除其他用户的帖子。

行动时间-当不在当前用户的个人资料页面时隐藏删除按钮

对用户隐藏删除按钮对我们来说实际上非常容易。虽然这种方法不能取代我们之前的验证函数,但对我们来说,这是一种友好的方式,可以防止用户意外尝试删除其他人的帖子。

  1. 在文本编辑器中打开 view/user/profile.php。

  2. 找到我们创建帖子的循环,并在我们的删除按钮周围添加这段代码。

<div class="span1">
<?php if ($is_current_user) { ?>
<a href="<?php echo $this->make_route('/post/delete/' . $post-
>
_id . '/' . $post->_rev)?>" class="delete">(Delete)
</a>
<?php } ?>
</div>

刚刚发生了什么?

我们刚刚使用了我们简单的变量$is_current_user,当用户查看其他人的个人资料时,隐藏了删除按钮,并在查看自己的个人资料时显示了它。这与我们在本章早期用于显示和隐藏创建帖子文本区域的技术相同。

如果你的用户现在去另一个用户的个人资料,他们将无法看到删除他们帖子的选项。即使他们以某种方式找到了帖子的_id_rev,并且能够触发删除帖子,validation函数也会阻止他们。

总结

在本章中,我们经历了很多,但我只能触及一些绝对值得进一步研究的要点。

想要更多例子吗?

学习MapReduce函数和设计文档的高级技术可能需要一整本书的篇幅。事实上,已经有一整本书在讲这个!如果你想了解更多关于真实用例场景以及如何处理一对多和多对多关系的内容,那就看看Bradley Holt的一本书,名为《在 CouchDB 中编写和查询 MapReduce Views》。

在 Futon 中使用设计文档太难了!

你并不是唯一一个认为在 Futon 中使用设计文档太难的人。

有一些工具可能值得一试:

  • CouchApp (couchapp.org/):这是一个实用程序,可以让你创建在 CouchDB 内部运行的完整的 JavaScript 应用程序。然而,它管理设计文档的方式也可以在开发 PHP 应用程序时让你的生活更轻松。

  • LoveSeat (www.russiantequila.com/wordpress/?p=119):这是一个轻量级的编辑器,可以在 Mono 下工作,这意味着它可以在任何操作系统上运行。它允许你非常容易地管理你的文档和设计文档。

摘要

在本章中,我们深入研究了 CouchDB,并利用了它的一些独特特性来使我们的应用程序更简单。更具体地说,我们讨论了设计文档以及 CouchDB 如何使用它们,使用 Futon 创建视图和设计文档。我们了解了视图,以及如何使用选项查询它们,例如 SQL,如何在视图中使用 MapReduce 查询我们的帖子,在我们的应用程序中使用视图动态显示每个用户的帖子列表和计数,还学习了如何在 CouchDB 中构建验证并将其用于保护我们的应用程序。

在下一章中,我们将进一步完善我们的应用程序,并添加一些有趣的功能,例如使用 JQuery 改善用户体验,添加分页,使用 Gravatars 等等!

第九章:为您的应用程序添加花里胡哨的功能

我们为我们的应用程序添加了许多实用功能。但是,还有一些缺少的功能,有些人可能认为是“很好有”的,并且对我们来说很重要,以便我们的应用程序具有良好的用户体验。

在本章中,我们将:

  • 将 jQuery 添加到项目中并使用它简化删除按钮

  • 通过使用 CouchDB 视图和 jQuery 为用户帖子添加基本分页

  • 通过使用 Gravatar 的 Web 服务为我们所有的用户添加个人资料图片

这些功能是有趣的小添加,它们也会让您看到当您将其他库与 CouchDB 和 PHP 结合使用时可能发生的事情。

将 jQuery 添加到我们的项目中

尽管这本书主要是关于在 PHP 中编写应用程序,但是在构建优秀的应用程序时,JavaScript 已经成为开发人员工具包中几乎必不可少的工具。我们已经在 CouchDB 视图中使用了 JavaScript,但是在本章中,我们将使用 JavaScript 进行其最常见的用例-改善用户体验。为了使我们能够编写更简单的 JavaScript,我们将使用一个名为jQuery的流行库。如果您以前没有使用过 jQuery,您会惊喜地发现它在简化 JavaScript 中的常见和复杂操作方面有多么简化。

安装 jQuery

幸运的是,将 jQuery 添加到任何项目中都非常简单。我们可以从www.jquery.com下载它,但是,因为我们想要专注于速度,我们实际上可以在不将任何内容安装到我们的存储库中的情况下使用它。

行动时间-将 jQuery 添加到我们的项目中

由于使用 jQuery 的人数激增,谷歌建立了一个内容传递网络,为我们提供 jQuery 库,而无需在我们的项目中需要任何东西。让我们告诉我们的layout.php文件在哪里找到 jQuery。

打开layout.php文件,并在body部分的末尾之前添加以下代码:

<script type="text/javascript" src= "//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js">
</script>
</body>
</html>

刚刚发生了什么?

我们只是在layout.php文件的body标记的末尾之前添加了一行代码。这就是使用 jQuery 与我们的项目所需的全部!您可能想知道为什么我们决定将我们的 jQuery 库放在文件的底部。最简单的解释是,当浏览器加载代码时,它会逐行加载。通过将 JavaScript 放在页面底部,它可以更快地加载其他元素,例如 CSS 和 HTML 标记,从而给用户一种快速加载的印象。

行动时间-创建 master.js 并连接 Boostrap 的 JavaScript 文件

随着我们的应用程序的增长,我们希望能够将我们的 JavaScript 添加到外部文件中。让我们创建一个名为master.js的文件,它将保存我们应用程序的所有 JavaScript,然后连接我们在第六章中下载的 Bootstrap 的 JavaScript 文件,建模用户

  1. public/js文件夹中创建一个名为master.js的新文件。

  2. 打开layout.php文件,并在body部分的末尾之前添加以下代码:

<script type="text/javascript" src= "//ajax.googleapis.com/ajax/libs/jquery /1.7.2/jquery.min.js">
**<script type="text/javascript" src= "//ajax.googleapis.com/ajax/libs/jquery /1.7.2/jquery.min.js">
</script>
<script type="text/javascript" src="<?php echo $this- >make_route('/js/bootstrap.min.js') ?>">
</script>
<script type="text/javascript" src="<?php echo $this- >make_route('/js/master.js') ?>">
</script>** 
</body>
</html>

刚刚发生了什么?

我们创建了一个名为master.js的空文件,这是我们应用程序的所有 JavaScript 将存储的地方。接下来,我们再次调整了我们的layout.php文件,允许我们包括我们在第六章中下载的boostrap.min.js文件,以及我们新创建的master.js文件。

注意

在编写 JavaScript 时,加载文件的顺序很重要。在本章后面编写 jQuery 时,对于我们的浏览器首先加载 jQuery 文件很重要,这样它就知道 jQuery 是什么以及语法是如何工作的。

使用 jQuery 改进我们的网站

现在我们有了 jQuery,让我们立即使用它来稍微改进我们的网站。您可以以许多不同的方式编写 jQuery 和 JavaScript 代码,但是在本书中,我们将坚持绝对基础知识,并尽量保持简单。

修复我们的删除帖子操作以实际使用 HTTP 删除

你可能已经在上一章的早期注意到的一件事是,当我们从用户的个人资料中编写帖子删除时,我们实际上使用了GET HTTP方法而不是DELETE方法。这是因为很难触发DELETE路由而不使用 JavaScript。因此,在接下来的部分中,我们将改进删除过程,使其按照以下方式工作:

  1. 用户点击帖子上的“删除”。

  2. 从 jQuery 到我们的应用程序发出了一个DELETE AJAX请求。

  3. 我们的应用程序将删除帖子文档,并向 jQuery 报告一切都如预期般进行。

  4. 帖子将从视图中淡出,而无需用户刷新页面。

这将是对我们用户资料的一个很好的改进,因为我们不需要每次执行操作时重新加载页面。

行动时间-通过使用 AJAX 删除帖子来改善我们的用户体验

让我们通过向我们的master.js文件添加一些代码,使我们能够使用 JavaScript 删除帖子,来初步了解一下 jQuery。如果 jQuery 的语法一开始对您不太熟悉,请不要感到不知所措;坚持下去,我相信您会对结果感到非常满意。

  1. 打开public/js/master.js,确保 jQuery 代码在页面加载完成后运行,通过在我们的文件中添加$(document).ready事件。这段代码意味着一旦页面加载完成,此函数内的任何 JavaScript 代码都将运行:
$(document).ready(function() {
});

  1. 现在,让我们添加一个事件,将click事件绑定到我们 HTML 中具有delete类的任何按钮。function(event)括号内的所有代码将在每次点击我们的删除帖子按钮时运行:
$(document).ready(function() {
**$('.delete').bind('click', function(event){** 
});
});

  1. 让我们阻止链接像通常情况下那样将我们带到新页面,使用一个叫做event.preventDefault()的代码。然后,让我们将点击链接的href属性保存到一个叫做location的变量中,这样我们就可以在我们的 AJAX 调用中使用它:
$(document).ready(function() {
$('.delete').bind( 'click', function(event){
**event.preventDefault();
var location = $(this).attr('href');** 
});
});

  1. 最后,让我们创建一个基本的 AJAX 请求,将调用我们的应用程序并为我们删除帖子:
$(document).ready(function() {
$('.delete').bind( 'click', function(){
event.preventDefault();
var location = $(this).attr('href');
**$.ajax({
type: 'DELETE',
url: location,
context: $(this),
success: function(){
$(this).parent().parent().fadeOut();
},
error: function (request, status, error) {
alert('An error occurred, please try again.'); }
});** 
});
});

刚刚发生了什么?

我们刚刚学会了如何在几行代码中使用 JavaScript 进行 AJAX 请求。我们首先将我们的代码包装在一个$(document).ready函数中,该函数在页面完全加载后运行。然后,我们添加了一个捕获我们应用程序中任何删除帖子链接点击的函数。最后,脚本中最复杂的部分是我们的 AJAX 调用。让我们通过一点来讨论一下,以便它有意义。jQuery 有一个名为$.ajax的函数,它有各种选项(所有选项都可以在这里查看:api.jquery.com/jQuery.ajax/))。让我们逐个讨论我之前给出的代码片段中使用的每个选项,并确保您知道它们的含义。

  • type: 'DELETE'表示我们要使用DELETE HTTP方法进行请求。

  • url: location表示我们将使用点击链接的href属性进行请求。这将确保正确的帖子被删除。

  • context: $(this)是将用于所有 AJAX 回调的对象。因此,在此示例中,此调用的success选项中的所有代码将使用点击链接作为所有调用的上下文。

  • success: function()在我们的 AJAX 请求完成时调用。我们将以下代码放在此函数中:$(this).parent().parent().fadeOut();这意味着我们将从点击链接的两个 HTML 级别向上查找。这意味着我们将查找帖子的<div class="post-item row">,然后将其淡出视图。

  • error: function (request, status, error)在您的代码中发生错误时运行。现在,我们只是显示一个警报框,这不是最优雅的方法,特别是因为我们没有提供发生了什么的细节给用户。这暂时对我们有效,但如果您想要一些额外的分数,可以尝试一下这个函数,看看是否可以使它更加优雅。

太棒了!我们刚刚添加了一些代码,这将真正改善用户的体验。随着您的应用程序的增长,并且您为其添加更多功能,请确保牢记 jQuery 的AJAX方法,这肯定会让事情变得更容易。

更新我们的路由以使用 DELETE HTTP 方法

现在我们正确地使用DELETE作为我们的 AJAX 调用的HTTP方法,我们需要更新我们的路由,这样我们的代码就知道如何处理路由了。

  1. 打开index.php,查找我们在上一章中创建的post/delete/:id/:rev路由:
get('/post/delete/:id/:rev', function($app) {
$post = new Post();
$post->_id = $app->request('id');
$post->_rev = $app->request('rev');
$post->delete();
$app->set('success', 'Your post has been deleted');
$app->redirect('/user/' . User::current_user());
});

  1. 让我们通过将get更改为delete来更改路由以使用delete方法。然后,删除success变量和重定向代码,因为我们将不再需要它们:
delete('/post/delete/:id/:rev', function($app) {
$post = new Post();
$post->_id = $app->request('id');
$post->_rev = $app->request('rev');
$post->delete();
});

让我们来测试一下!

在测试这个功能时,确保停下来欣赏所有技术的协同工作,以解决一个相当复杂的问题。

  1. 转到http://localhost/verge/login,并以johndoe的身份登录应用程序。

  2. 单击“我的个人资料”。

  3. 单击您的帖子旁边的“(删除)”。

  4. 删除的帖子将从视图中消失,其他帖子将在页面上升。

使用 jQuery 添加简单的分页

随着我们的应用程序的增长,帖子将开始填满用户的个人资料。如果我们的应用程序变得成功,并且人们开始使用它,会发生什么?每次加载页面时,将打印数百个帖子到个人资料视图中。这样的情况绝对会使您的应用程序陷入瘫痪。

考虑到这一点,我们将在我们的个人资料页面上创建一些分页。我们的简单分页系统将按以下方式工作:

  1. 默认情况下,我们将在页面上显示 10 个帖子。当用户想要查看更多时,他们将单击“加载更多”链接。

  2. 当单击“显示更多”链接时,jQuery 将找出要跳过多少项,并告诉 Bones 要检索哪些文档。

  3. Bones 将使用 Sag 调用 CouchDB,并通过posts_by_user视图获取更多帖子。

  4. Bones 将结果加载到包含我们帖子需要格式化的 HTML 布局的部分视图中。这个 HTML 将返回给 jQuery 在我们的页面上显示。

这里有很多事情要做,但这种功能在大多数应用程序中都很常见。所以,让我们跳进去,看看我们是否能把这一切都拼凑起来。

采取行动-将帖子从 profile.php 中取出并放入它们自己的部分视图

列出帖子的代码直接位于profile.php页面内,这在目前为止都还好。然而,在某一时刻,我们将希望能够通过Javascript回调显示帖子,如果我们不小心,这可能意味着重复的代码或不一致的布局。让我们通过将我们的代码移动到一个可以轻松重用的部分视图中来保护自己。

  1. 在 views/user 中创建一个名为_posts.php的新文件。

  2. 复制并粘贴从views/user/profile.php列出帖子的foreach代码,并将其粘贴到我们的新文件_posts.php中。_posts.php的最终结果应该如下所示:

<?php foreach ($posts as $post): ?>
<div class="post-item row">
<div class="span7">
<strong><?php echo $user->name; ?></strong>
<p>
<?php echo $post->content; ?>
</p>
<?php echo $post->date_created; ?>
</div>
<div class="span1">
<?php if ($is_current_user) { ?>
<a href="<?php echo $this->make_route('/post/delete/' . $post->_id . '/' . $post->_rev)?>" class="delete">
(Delete)
</a>
<?php } ?>
</div>
<div class="span8"></div>
</div>
<?php endforeach; ?>

  1. 现在,让我们从views/user/profile.php中删除相同的foreach语句,并将其替换为对新创建的_posts文件的include调用。然后让我们在我们的列表的h2元素内添加一个span,这样我们就可以很容易地通过 jQuery 访问它。
**<h2>
Posts (<span id="post_count"><?php echo $post_count; ?></span>)
</h2>
<div id="post_list">
<?php include('_posts.php'); ?>
</div>** 

刚刚发生了什么?

我们将profile.php中列出的所有帖子的代码移到了一个名为_posts.php的新部分中。我们在文件名前加上下划线,没有其他原因,只是为了让我们在查看源代码时知道它与普通视图不同。所谓的部分视图,是指它是要加载到另一个页面中的,单独存在时可能没有任何作用。在表面上,我们的应用程序将与我们将代码移动到部分视图之前完全相同。

然后我们修改了profile.php中的代码,以便使用 jQuery 更容易。我们在h2元素内添加了一个 ID 为post_countspan元素。这个span元素只包含总帖子数。我们很快就会用到它,以便告诉我们是否已经将我们需要的所有帖子加载到我们的列表中。然后我们用 ID 为post_listdiv包装了我们的帖子列表。我们将使用这个标识符来从我们的分页控件中将新帖子追加到列表中。

为分页添加后端支持

我们不需要另一个用于分页的函数。让我们只是改进Post类的get_posts_by_user函数。我们只需要添加skiplimit选项,然后将它们传递给 CouchDB 中的posts_by_user视图。将skip传递给此视图将使我们能够跳过结果中的某些记录,而limit将允许我们一次只显示一定数量的帖子。通过结合这两个变量,我们将支持分页!

行动时间——调整我们的 get_posts_by_user 函数以跳过和限制帖子

既然我们知道该怎么做,让我们立即进入编辑classes/post.php文件,并调整我们的get_posts_by_user函数,以便我们可以将$skip$limit作为参数添加进去。

  1. 通过打开名为classes/post.php的文件来打开Post类。

  2. 找到我们的get_posts_by_user函数,并添加带有默认值0$skip和带有默认值10$limit

**public function get_posts_by_user($username, $skip = 0, $limit = 10) {** 
$bones = new Bones();
$posts = array();
...
}

  1. 更新我们对 Sag 的get调用,以便将$skip$limit的值传递给查询。
public function get_posts_by_user($username, $skip = 0, $limit = 10) {
$bones = new Bones();
$posts = array();
**foreach ($bones->couch-> get('_design/application/_view/posts_by_user?key="' . $username . '"&descending=true&reduce=false&skip=' . $skip . '&limit=' . $limit)->body->rows as $_post) {** 
...
}

  1. 现在我们已经更新了我们的函数以包括skiplimit,让我们在index.php中创建一个类似于user/:username路由的新路由,但是接受skip的路由变量来驱动分页。在这个路由中,我们将返回部分_posts,而不是整个布局:
get('/user/:username/:skip', 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->set('posts', Post::get_posts_by_user($app-> request('username'), $app->request('skip')));
$app->set('post_count', Post::get_post_count_by_user($app-> request('username')));
$app->render('user/_posts', false);
});

刚刚发生了什么?

我们刚刚为get_posts_by_user函数添加了额外的$skip$limit选项。我们还设置了当前调用,使其在不更改任何内容的情况下也能正常运行,因为我们为每个变量设置了默认值。我们现有的用户资料中的调用现在也将显示前 10 篇文章。

然后我们创建了一个名为/user/:username/:skip的新路由,其中skip是我们在查询时要跳过的项目数。这个函数中的其他所有内容与/user/:username路由中的内容完全相同,只是我们将结果返回到我们的部分中,并且布局为false,因此没有布局包装。我们这样做是为了让 jQuery 可以调用这个路由,它将简单地返回需要添加到页面末尾的帖子列表。

让我们来测试一下!

通过直接通过浏览器玩弄它来确保我们的/user/:username/:skip路由按预期工作。

  1. 前往http://localhost/verge/user/johndoe/0(或任何有相当数量帖子的用户)。

  2. 您的浏览器将使用views/user/_posts.php作为模板返回一个大的帖子列表。请注意,它显示了 10 篇总帖子,从最近的帖子开始。让我们来测试一下!

  3. 现在,让我们尝试跳过前 10 篇文章(就像我们的分页器最终会做的那样),并通过访问http://localhost/verge/user/johndoe/10来检索接下来的 10 篇文章!让我们来测试一下!

  4. 我们的代码希望能够很好地工作。我在这个帐户上只有 12 篇帖子,所以这个视图跳过了前 10 篇帖子,显示了最后两篇。

这一切都按我们的预期进行,但是我们的代码还有一些清理工作要做。

行动时间-重构我们的代码,使其不冗余

虽然我们的代码运行良好,但您可能已经注意到我们在/user/:username/user/:username/:skip中有几乎相同的代码。我们可以通过将所有冗余代码移动到一个函数中并从每个路由中调用它来减少代码膨胀。让我们这样做,以便保持我们的代码整洁的习惯。

  1. 打开index.php,并创建一个名为get_user_profile的函数,它以$app作为参数,并将其放在/user/:username路由的上方。
function get_user_profile($app) {
}

  1. /user/:username/:skip中的代码复制到此函数中。但是,这一次,我们不仅仅传递$app->request('skip'),让我们检查它是否存在。如果存在,让我们将其传递给get_posts_by_user函数。如果不存在,我们将只传递0
function get_user_profile($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->set('posts', Post::get_posts_by_user($app-> request('username'), ($app->request('skip') ? $app-> request('skip') : 0)));
$app->set('post_count', Post::get_post_count_by_user($app-> request('username')));
}** 

  1. 最后,让我们清理我们的两个 profile 函数,使它们都只调用get_user_profile函数。
get('/user/:username', function($app) {
**get_user_profile($app);** 
$app->render('user/profile');
});
get('/user/:username/:skip', function($app) {
**get_user_profile($app);** 
$app->render('user/_posts', false);
});

刚刚发生了什么?

我们通过将大部分逻辑移动到一个名为get_user_profile的新函数中,简化了用户配置文件路由。两个路由之间唯一不同的功能是request变量skip。因此,我们在对Posts::get_posts_by_user函数的调用中放置了一个快捷的if语句,如果存在skip请求变量,就会传递skip请求变量;但如果不存在,我们将只传递0。添加这个小功能片段使我们能够在两个不同的路由中使用相同的代码。最后,我们将全新的函数插入到我们的路由中,并准备享受代码的简洁。一切仍然与以前一样工作,但现在更容易阅读,并且将来更新也更容易。

重构和持续清理代码是开发过程中要遵循的重要流程;以后你会为自己做这些而感激的!

行动时间-为分页添加前端支持

我们几乎已经完全支持分页。现在我们只需要向我们的项目添加一点 HTML 和 JavaScript,我们就会有一个很好的体验。

  1. 让我们从master.css文件中添加一行 CSS,这样我们的加载更多按钮看起来会很漂亮。
#load_more a {padding: 10px 0 10px 0; display: block; text-align: center; background: #e4e4e4; cursor: pointer;}

  1. 现在我们已经有了 CSS,让我们在profile.php视图中的post列表底部添加我们的加载更多按钮的 HTML。
<h2>
Posts (<span id="post_count"><?php echo $post_count; ?></span>)
</h2>
<div id="post_list">
<?php include('_posts.php'); ?>
</div>
**<div id="load_more" class="row">
<div class="span8">
<a id="more_posts" href="#">Load More...</a>
</div>
</div>**

  1. 现在,让我们打开master.js,并在$(document).ready函数的闭合括号内创建一个函数。这个函数将针对 ID 为more_posts的任何元素的click事件。
**$('#more_posts').bind( 'click', function(event){
event.preventDefault();
});** 
});

  1. 为了调用/user/:username/:skip路由,我们需要使用一个名为window.location.pathname的 JavaScript 函数来获取页面的当前 URL。然后,我们将在字符串的末尾添加帖子项目的数量,以便跳过当前页面上显示的帖子数量。
$('#more_posts').bind( 'click', function(event){
event.preventDefault();
**var location = window.location.pathname + "/" + $('.post-item') .size();** 
});

  1. 现在我们已经有了位置,让我们填写剩下的 AJAX 调用。这一次,我们将使用GET HTTP方法,并使用 ID 为post_list的帖子列表作为我们的上下文,这将允许我们在success事件中引用它。然后,让我们只添加一个通用的error事件,以便在发生错误时通知用户发生了错误。
$('#more_posts').bind( 'click', function(event){
event.preventDefault();
var location = window.location.pathname + "/" + $('#post_list').children().size();
**$.ajax({
type: 'GET',
url: location,
context: $('#post_list'),
success: function(html){
// we'll fill this in, in just one second
},
error: function (request, status, error) {
alert('An error occurred, please try again.');
}
});** 
});

  1. 最后,让我们用一些代码填充我们的success函数,将从我们的 AJAX 调用返回的 HTML 附加到post_list div的末尾。然后,我们将检查是否有其他帖子要加载。如果没有更多帖子要加载,我们将隐藏加载更多按钮。为了获取帖子数量,我们将查看我们使用post_count作为 ID 创建的span,并使用parseInt将其转换为整数。
$('#more_posts').bind( 'click', function(event){
event.preventDefault();
var location = window.location.pathname + "/" + $('#post_list').children().size();
$.ajax({
type: 'GET',
url: location,
context: $('#post_list'),
success: function(html){
**$(this).append(html);
if ($('#post_list').children().size() <= " parseInt($('#post_count').text())) {
$('#load_more').hide();
}** 
},
error: function (request, status, error) {
alert('An error occurred, please try again.');
}
});
});

刚刚发生了什么?

在这一部分,我们完成了分页!我们首先创建了一个快速的 CSS 规则,用于我们的加载更多链接,使其看起来更友好,并添加了在个人资料页面上出现所需的 HTML。我们通过调用一个 AJAX 函数到当前用户个人资料的 URL,并将当前存在的帖子数量附加到#post_list div中来完成分页。通过将这个数字传递给我们的路由,我们告诉我们的路由将这个数字传递并忽略所有这些项目,因为我们已经显示了它们。

接下来,我们添加了一个success函数,使用_posts部分的布局返回我们路由的 HTML。这个 HTML 将被附加到#post_list div的末尾。最后,我们检查了是否有更多的项目要加载,通过比较#post_list的大小与我们的reduce函数返回到我们个人资料顶部的帖子数量#post_count span。如果这两个值相等,这意味着没有更多的帖子可以加载,我们可以安全地隐藏加载更多链接。

行动时间-修复我们的删除帖子功能以适应分页

当我们添加分页时,我们还破坏了通过 AJAX 加载的帖子的删除功能。这是因为我们使用bind事件处理程序将click事件绑定到我们的链接,这只会在页面加载时发生。因此,我们需要考虑通过 AJAX 加载的链接。幸运的是,我们可以使用 jQuery 的live事件处理程序来做到这一点。

  1. 打开master.js,并将delete帖子代码更改为使用live而不是bind
**$('.delete').live( 'click', function(event){** 
event.preventDefault();
var location = $(this).attr('href');

  1. 如果您开始删除帖子列表中的一堆项目,它目前不会使用 JavaScript 更改与用户帐户相关联的帖子数量。在这里,让我们修改success函数,以便它还更新我们帖子列表顶部的帖子数量:
$('.delete').live( 'click', function(event){
event.preventDefault();
var location = $(this).attr('href');
$.ajax({
type: 'DELETE',
url: location,
context: $(this),
success: function(html){
$(this).parent().parent().parent().fadeOut();
**$('#post_count').text(parseInt($('#post_count').text()) - 1);** 
},
error: function (request, status, error) {
alert('An error occurred, please try again.');
}
});
});

刚刚发生了什么?

我们刚刚更新了我们的删除按钮,使用live事件处理程序而不是bind事件处理程序。通过使用live,jQuery 允许我们定义一个选择器,并将规则应用于所有当前和将来匹配该选择器的项目。然后,我们使我们的#post_count元素动态化,以便每次删除帖子时,帖子计数相应地更改。

测试我们完整的分页系统

我们的分页最终完成了。让我们回去测试一切,确保分页按预期工作。

  1. 转到http://localhost/verge/login,并以johndoe的身份登录应用程序。

  2. 点击我的个人资料

  3. 滚动到页面底部,点击加载更多。接下来的 10 篇帖子将返回给你。

  4. 如果您的帐户中帖子少于 20 篇,加载更多按钮将从页面中消失,向您显示您已经加载了所有帖子。

  5. 尝试点击通过 AJAX 加载的列表中的最后一篇帖子,它将消失,就像应该的那样!

太棒了!我们的分页系统正如我们所希望的那样工作;我们能够删除帖子,我们的帖子计数每次删除帖子时都会更新。

使用 Gravatars

在这一点上,我们的个人资料看起来有点无聊,只有一堆文本,因为我们没有支持将图像上传到我们的系统中。出于时间考虑,我们将在本书中避免这个话题,也是为了我们用户的利益。让用户每次加入服务时都上传新的个人资料图像存在相当大的摩擦。相反,有一个服务可以让我们的生活变得更轻松:Gravatarwww.gravatar.com)。Gravatar 是一个网络服务,允许用户将个人资料图像上传到一个单一位置。从那里,其他应用程序可以使用用户的电子邮件地址作为图像的标识符来获取个人资料图像。

行动时间-向我们的应用程序添加 Gravatars

通过我们的用户类添加对 Gravatars 的支持就像添加几行代码一样简单。之后,我们将在我们的应用程序中添加gravatar函数。

  1. 打开user/profile.php,并添加一个名为gravatarpublic函数,它接受一个名为 size 的参数;我们将给它一个默认值50
public function gravatar($size='50') {
}

  1. 为了获取用户的 Gravatar,我们只需要创建用户电子邮件地址的md5哈希,这将作为gravatar_id。然后,我们使用我们的$size变量设置大小,并将所有这些附加到 Gravatar 的网络服务 URL。
public function gravatar($size='50') {
return 'http://www.gravatar.com/avatar/?gravatar_id=' .md5(strtolower($this->email)).'&size='.$size;
}

  1. 就是这样!我们现在在我们的应用程序中有了 Gravatar 支持。我们只需要在任何我们想要看到个人资料图片的地方开始添加它。让我们首先在views/user/profile.php文件的用户信息部分顶部添加一个大的 Gravatar。
<div class="span4">
<div class="well sidebar-nav">
<ul class="nav nav-list">
<li><h3>User Information</h3></li>
**<li><img src="<?php echo $user->gravatar('100'); ?>" /></li>** 
<li><b>Username:</b> <?php echo $user->name; ?></li>
<li><b>Email:</b> <?php echo $user->email; ?></li>
</ul>
</div>
</div>

  1. 接下来,让我们更新views/user/_posts.php文件中的帖子列表,这样我们就可以很好地显示我们的 Gravatars。
<?php foreach ($posts as $post): ?>
<div class="post-item row">
**<div class="span7">
<div class="span1">
<img src="<?php echo $user->gravatar('50'); ?>" />
</div>
<div class="span5">
<strong><?php echo $user->name; ?></strong>
<p>
<?php echo $post->content; ?>
</p>
<?php echo $post->date_created; ?>
</div>
</div>** 
<div class="span1">
<?php if ($is_current_user) { ?>
<a href="<?php echo $this->make_route('/post/delete/' . $post->_id . '/' . $post->_rev)?>" class="deletes">(Delete)</a>
<?php } ?>
</div>
<div class="span8"></div>
</div>
<?php endforeach; ?>

刚刚发生了什么?

我们在我们的User类中添加了一个名为gravatar的函数,它接受一个名为$size的参数,默认值为 50。从那里,我们对对象的电子邮件地址和$size进行了md5哈希,并将其附加到 Gravatar 的网络服务的末尾。结果是一个链接到一个漂亮且易于显示的 Gravatar 图像。

有了我们的 Gravatar 系统,我们将它添加到了views/user/profile.phpviews/user/_posts.php页面中。

测试我们的 Gravatars

我们的 Gravatars 应该在我们的个人资料页面上运行。如果用户的电子邮件地址没有关联的图像,将显示一个简单的占位图像。

  1. 转到http://localhost/user/johndoe,你会在每篇帖子和用户信息部分看到 Gravatar 的占位符。测试我们的 Gravatars

  2. 现在,让我们通过访问www.gravatar.com并点击注册来将 Gravatar 与你的电子邮件关联起来。测试我们的 Gravatars

  3. 输入你的电子邮件,然后点击注册。你将收到一封验证邮件到你的地址,所以去检查一下,然后点击激活链接。测试我们的 Gravatars

  4. 接下来,你将被带到一个页面,显示你当前的账户和与你的账户关联的图像。你还没有任何与你的账户关联的东西,所以点击点击这里添加一个!测试我们的 Gravatars

  5. 在你上传了图像到账户并添加了你想要使用的任何电子邮件地址之后,你可以回到与你的电子邮件地址关联的个人资料(对我来说是http://localhost/user/tim),你将看到一个 Gravatar!测试我们的 Gravatars

将所有内容添加到 Git

我希望在本章的过程中,你已经将你的代码提交到了 Git;如果你还没有,这是提醒你。确保及早并经常这样做!

总结

希望你喜欢这一章!虽然这些功能对我们的应用程序的工作并不是“使命关键”的功能,但随着应用程序的发展,这些是用户会要求的功能。

具体来说,我们学会了如何安装 jQuery 并使用它来帮助创建一些基本的 JavaScript,并用它来使帖子的删除和分页更加清晰。接下来,我们添加了 Gravatar 图像到个人资料和帖子列表中,使我们的个人资料更加有趣。

就是这样!我们的应用程序已经准备好投入使用。在下一章中,我们将保护应用程序的最后部分并部署所有内容,这样世界就可以看到你所建立的东西。

第十章:部署您的应用程序

在我们的应用程序上线并准备好供用户注册并创建帖子之前,我们还有一些步骤要完成。

在本章中,我们将做以下事情来让我们的应用程序运行起来:

  • 我们将在 Cloudant 上建立一个帐户,用于存放我们应用的 CouchDB 数据库,并为我们的应用做好准备

  • 我们将在我们的项目中添加一个配置类,使用环境变量来驱动我们应用程序的设置

  • 我们将创建一个 PHP Fog 帐户来托管我们的应用程序

  • 我们将配置 Git 连接到 PHP Fog 的 Git 存储库并部署我们的应用程序

正如您可能期望的那样,在本章中,我们将进行大量的帐户设置和代码调整。

在我们开始之前

对于任何应用程序或数据库部署,都有各种各样的选择。每个选项都有其优势和劣势。我想给你一些知识,而不是立即设置服务,以防有一天你想改用其他服务。

在过去的几年里,云已经成为技术行业中最常用和滥用的术语之一。要完全理解云这个术语,您需要阅读大量的研究论文和文章。但是,简单来说,云这个术语描述了从传统的单租户专用托管转变为可扩展的多租户和多平台主机。CouchDB 本身就是一个可扩展的数据库的完美例子,可以实现云架构。我们的应用程序也是云解决方案的一个很好的候选,因为我们没有本地存储任何东西,也没有任何特殊的依赖。

考虑到这一点,我们将使用云服务来托管我们的应用程序和数据库。其中一个额外的好处是,我们将能够让我们的应用程序免费运行,并且只有在我们的应用程序成功后才需要开始付费。这一点真的不错!

让我们快速讨论一下我们将如何处理我们的应用程序和 CouchDB 托管以及我们可用的选项。

应用程序托管

在云中托管 Web 应用程序时,有无数种方法可以实现。由于我们不是服务器设置专家,我们希望使用一种设置较少但回报较高的系统。考虑到这一点,我们将使用平台即服务(PaaS)。有很多 PaaS 解决方案,但目前,对于 PHP 开发人员来说,最好的选择是 Heroku 和 PHP Fog。

Heroku (www.heroku.com) 是将 PaaS 推向聚光灯下的创新者。他们使用 Cedar 堆栈支持 PHP 应用程序。但是,由于它不是一个特定于 PHP 的堆栈,对于我们来说,可能更明智选择另一个提供商。

PHP Fog (www.phpfog.com) 在我看来,是一个非常专注于 PHP 开发的 PaaS,因为他们非常专注于 PHP。他们支持各种 PHP 应用框架,提供 MySQL 托管(如果您的应用需要),总体上,他们致力于为 PHP 开发人员提供一个稳固的开发环境。

考虑到这一切,PHP Fog 将是我们选择的应用托管解决方案。

CouchDB 托管

与应用程序托管相比,CouchDB 托管的解决方案要少得多,但幸运的是,它们都是非常稳固的产品。我们将讨论的两种服务是 Cloudant 和 IrisCouch。

Cloudant (www.cloudant.com) 是云中 CouchDB 最强大的解决方案之一。他们提供了我们在本书中使用过的熟悉工具,如 Futon 和命令行,还能够根据数据增长的需要进行扩展。Cloudant 特别独特的地方在于,当你的应用程序需要一些特殊功能时,他们提供定制解决方案,而且 Cloudant 是 CouchDB 本身的主要贡献者之一。

Iris Couch (www.iriscouch.com) 也允许在云中免费托管 CouchDB。不幸的是,他们刚刚开始提供 Couchbase 服务器作为他们的基础设施,这是建立在 CouchDB 核心之上的。虽然我非常喜欢 Couchbase 及其对核心 CouchDB 技术的增强,但我们的任务是在本书中只使用 CouchDB。但是,如果你将来需要 Couchbase 的增强功能,那么值得考虑一下 Iris Couch。

因为我以前使用过 Cloudant 并知道它能处理什么,我们将在这个项目中使用它。

总的来说,本章中我们将执行的设置与其他竞争性服务相对类似。因此,如果你决定以后转换,你应该能够很好地处理它,而不会有太多问题。

使用 Cloudant 进行数据库托管

在本节中,我们将设置一个 Cloudant 服务器,并准备让我们的应用程序连接到它。需要做的设置很少,而且希望这些步骤对我们在本书初期设置 CouchDB 数据库时所采取的步骤来说是熟悉的。

开始使用 Cloudant

创建 Cloudant 账户非常简单,但让我们一起走一遍,以确保我们都在同一页面上。

  1. 首先去cloudant.com/sign-up/,你会看到注册页面。开始使用 Cloudant

  2. Cloudant 只需要一些基本信息来创建你的账户。首先输入一个用户名。这将被用作你的唯一标识符和你的 Cloudant 账户的链接。我建议选择像你的名字或公司名这样的东西。

  3. 填写页面上的其余信息,当你准备好时,点击页面底部的注册按钮!

你已经完成了,现在你应该看到你的 Cloudant 仪表板。从这里,你可以管理你的账户并创建新的数据库。

开始使用 Cloudant

创建一个 _users 数据库

现在我们有了全新的 Cloudant 账户,但我们还没有任何数据库。更糟糕的是,我们甚至还没有我们的_users数据库。我们只需要创建一个新的_users数据库,Cloudant 会处理剩下的。我们技术上可以通过 Cloudant 的界面完成这个过程,但让我们使用命令行,因为它更加通用。

  1. 打开终端。

  2. 运行以下命令,并替换两个用户名和一个密码的实例,这样 Cloudant 就知道你是谁以及你要使用的账户是什么:

**curl -X PUT https://username:password@username.cloudant.com/_users** 

终端会通过返回成功消息来告诉你数据库已经创建:

**{"ok":true}** 

太棒了!你的_users数据库现在已经创建。记住,我们还需要另一个叫做verge的数据库来存储我们的所有数据。让我们接下来创建verge数据库。

创建一个 verge 数据库

你需要在你的账户中创建另一个数据库,这次叫做verge

来吧,英雄——自己试试看

现在,你应该很容易自己创建另一个数据库。按照我们创建_users数据库时所采取的相同步骤来尝试一下,但是将数据库名称改为verge

如果你感到困惑,我马上会向你展示命令行语句。好的,进行得怎么样?让我们回顾一下创建verge数据库所需执行的步骤。

  1. 打开终端。

  2. 你应该运行以下命令,并替换两个用户名实例和一个密码实例,这样 Cloudant 就会知道你是谁,以及你要使用的账户是什么:

**curl -X PUT https://username:password@username.cloudant.com/verge** 

当你看到一个熟悉的成功消息时,终端应该已经让你放心一切都进行得很顺利,如下所示:

**{"ok":true}** 

在 Cloudant 上使用 Futon

通过命令行管理内容可能有点繁琐。幸运的是,Cloudant 还带来了我们的老朋友——Futon。要在 Cloudant 上使用 Futon,按照以下步骤进行:

  1. 登录,并转到你的仪表板。在 Cloudant 上使用 Futon

  2. 点击你的数据库名称之一;在这个例子中,让我们使用verge在 Cloudant 上使用 Futon

  • 这是数据库详细页面——当文档出现在你的数据库中时,它们将显示在这个页面上。
  1. 点击 Futon 中的查看按钮继续。

看起来熟悉吗?这就是我们在本地一直在使用的伟大的 Futon。

在 Cloudant 上使用 Futon

配置权限

现在我们的生产数据库已经上线,非常重要的是我们要配置权限以在我们的生产服务器上运行。如果我们不保护我们的数据库,那么我们的用户很容易就能读取,这是我们不想卷入的事情。

幸运的是,Cloudant 已经为我们解决了所有这些问题,具体做法如下:

  • 因为我们已经创建了一个账户,数据库不再处于Admin Party模式

  • 默认情况下,Cloudant 使_users数据库对我们的admin账户可管理,但其他账户无法访问它

我们很幸运,Cloudant 一直支持我们!但是,如果你决定自己部署 CouchDB 实例,一定要回头看看第三章,“使用 CouchDB 和 Futon 入门”,并按照我们用来保护本地环境的步骤进行操作。

然而,我们需要更新我们的verge数据库,以便用户可以在该数据库中读取、创建和写入。

  1. 登录到你的 Cloudant 账户,并转到你的仪表板。cloudant.com/#!/dashboard

  2. 点击verge数据库。

  3. 点击权限来管理数据库权限。

  4. 通过选中读取、创建写入下的复选框来更新其他人权限。确保不要选中管理员,这样普通用户就无法更改我们的数据库结构和设计文档。最终结果应该类似于以下截图:配置权限

配置我们的项目

现在我们已经设置好了我们的生产数据库,我们的代码需要知道如何连接到它。我们可以只是修改我们在Bones库中硬编码的值,每次想要在本地开发或部署到生产环境时来回更改。但是,请相信我,你不想经历这样的麻烦,更重要的是,我们不想在我们的代码中存储任何用户名或密码;为此,我们将使用环境变量。环境变量是一组动态命名的值,允许你从应用程序的托管环境中定义变量。让我们创建一个类,使我们能够使用环境变量,这样我们的代码就不会包含敏感信息,我们的应用程序也容易配置。

行动时间——创建一个配置类

由于我们迄今为止编写的代码,插入一个简单的配置类实际上对我们来说非常容易。让我们一起来创建它。

  1. 首先在我们的lib文件夹内创建一个名为configuration.php的新配置文件(lib/configuration.php)。

  2. 现在,让我们为名为Configuration的类创建脚手架。

<?php
class Configuration {
}

  1. 让我们继续并创建一些描述性的配置变量。我们可以添加更多,但现在让我们只添加我们现在需要的。
<?php
class Configuration {
**private $db_server = ';
private $db_port = '';
private $db_database = '';
private $db_admin_user = '';
private $db_admin_password = '';** 
}

  1. 现在,复制你需要访问本地 CouchDB 实例的登录信息;我的看起来类似于以下内容:
<?php
class Configuration {
**private $db_server = '127.0.0.1';
private $db_port = '5984';
private $db_database = 'verge';
private $db_admin_user = 'tim';
private $db_admin_password = 'test';** 
}

  1. 让我们使用一个特殊的__get函数来检查并查看是否设置了环境变量,并返回该值,而不是默认值。如果没有,它将只返回我们在这个类中定义的默认值。
<?php
class Configuration {
private $db_server = '127.0.0.1';
private $db_port = '5984';
private $db_database = 'verge';
private $db_admin_user = 'tim';
private $db_admin_password = 'test';
**public function __get($property) {
if (getenv($property)) {
return getenv($property);
} else {
return $this->$property;
}
}** 
}

刚刚发生了什么?

我们刚刚创建了一个名为configuration.php的简单配置类,并创建了一个名为Configuration的类的框架。接下来,我们为数据库的配置创建了一些变量,我们将其设置为public,因为我们可能需要在各种地方使用这些变量。然后,我们用访问本地 CouchDB 实例的信息填充了这些变量的默认值。然后,我们添加了这个类的魔力。我们创建了一个__get函数,它覆盖了类的标准get操作。这个函数使用getenv函数来检查服务器,看看环境变量中是否设置了该变量(我们将很快介绍如何做到这一点)。如果有一个同名的环境变量,我们将把它返回给调用函数;如果没有,我们将简单地返回默认值。

Configuration类是一个很好而简单的类,它可以在不过分复杂的情况下完成我们需要的一切。接下来,让我们确保我们的应用程序知道如何访问和使用这个类。

行动时间-将我们的配置文件添加到 Bones

将新的配置类添加到我们的应用程序中非常容易。现在,我们只需要将它添加到 Bones 的__construct()中,然后我们就可以在整个项目中开始使用这个类了。

  1. 打开lib/bones.php,并查看文件开头,告诉我们的库在哪里查找其他lib文件。我们需要在这里添加我们的配置类。
require_once ROOT . '/lib/bootstrap.php';
require_once ROOT . '/lib/sag/src/Sag.php';
**require_once ROOT . '/lib/configuration.php';** 

  1. 让我们确保在 Bones 的公共变量中定义$config,这样我们在其他文件中也可以使用它们。
class Bones {
private static $instance;
public static $route_found = false;
public $route = '';
public $method = '';
public $content = '';
public $vars = array();
public $route_segments = array();
public $route_variables = array();
public $couch;
**public $config;** 

  1. 让我们看一下文件中稍后的__construct()方法。在这个方法中(就在实例化 Sag 之前),让我们创建一个Configuration类的新实例。
public function __construct() {
...
**$this->config = new Configuration();** 
$this->couch = new Sag('127.0.0.1','5984');
$this->couch->setDatabase('verge');
}

  1. 现在我们的代码知道了配置类,我们只需要把变量放在正确的位置,就可以运行起来了。让我们告诉 Sag 如何使用配置类连接到 CouchDB。
public function __construct() {
$this->route = $this->get_route();
$this->route_segments = explode('/', trim($this->route, '/'));
$this->method = $this->get_method();
$this->config = new Configuration();
**$this->couch = new Sag($this->config->db_server, $this->config->db_port);
$this->couch->setDatabase($this->config->db_database);** 
}

  1. 还有一些地方需要更新我们的代码,以便使用配置类。记住,我们在classes/user.php中有admin用户名和密码,用于创建和查找用户。让我们首先看一下classes/user.php中的注册函数,清理一下。一旦我们插入我们的配置类,该函数应该类似于以下内容:
public function signup($password) {
$bones = new Bones();
$bones->couch->setDatabase('_users');
**$bones->couch->login($bones->config->db_admin_user, $bones->config->db_admin_password);** 

  1. 我们需要调整的最后一个地方是classes/user.php文件末尾的get_by_username函数,以使用config类。
public static function get_by_username($username = null) {
$bones = new Bones();
**$bones->couch->login($bones->config->db_admin_user, $bones->config->db_admin_password);** 
$bones->couch->setDatabase('_users');

  1. 我们刚刚删除了index.php顶部定义的ADMIN_USERADMIN_PASSWORD的所有引用。我们不再需要这些变量,所以让我们切换到index.php,并从文件顶部删除ADMIN_USERADMIN_PASSWORD

刚刚发生了什么?

我们刚刚写完了我们应用程序的最后几行代码!在这一部分中,我们确保 Bones 完全可以访问我们最近创建的lib/configuration.php配置文件。然后,我们创建了一个公共变量$config,以确保我们在应用程序的任何地方都可以访问我们的配置类。将我们的配置类存储在$config变量中后,我们继续查看我们的代码中那些硬编码了数据库设置的地方。

将更改添加到 Git

因为我们刚刚写完了我们的代码的最后几行,我要确保你已经完全提交了我们的所有代码到 Git。否则,当我们不久部署我们的代码时,你的所有文件可能都不会到达生产服务器。

  1. 打开终端。

  2. 使用通配符添加项目中的任何剩余文件。

**git add .** 

  1. 现在,让我们告诉 Git 我们做了什么。
**git commit m 'Abstracted out environment specific variables into lib/configuration.php and preparing for launch of our site 1.0!'** 

使用 PHP Fog 进行应用程序托管

我们的代码已经更新完毕,准备部署。我们只需要一个地方来实际部署它。正如我之前提到的,我们将使用 PHP Fog,但请随意探索其他可用的选项。大多数 PaaS 提供商的设置和部署过程都是相同的。

设置 PHP Fog 账户

设置 PHP Fog 账户就像我们设置 Cloudant 账户一样简单。

  1. 首先,访问www.phpfog.com/signup设置 PHP Fog 账户

  2. 填写每个字段创建一个账户。完成后,点击“注册”。你将被引导创建你的第一个应用程序。设置 PHP Fog 账户

  3. 你会注意到有各种各样的起始应用程序和框架,可以让我们快速创建 PHP 应用程序的脚手架。我们将使用我们自己的代码,所以点击“自定义应用程序”。设置 PHP Fog 账户

  4. 我们的应用程序几乎创建完成了,我们只需要给 PHP Fog 提供更多信息。

  5. 你会注意到 PHP Fog 要求输入 MySQL 的密码。由于我们在这个应用程序中没有使用 MySQL,我们可以输入一个随机密码或其他任何字符。值得一提的是,如果将来有一天你想在项目中使用 MySQL 来存储一些关系数据,只需点击几下,就可以在同一应用程序环境中进行托管。记住,如果正确使用,MySQL 和 CouchDB 可以成为最好的朋友!

  6. 接下来,PHP Fog 会要求输入你的域名。每个应用程序都会有一个托管在phpfogapp.com上的短 URL。这对我们来说在短期内是完全可以接受的,当我们准备使用完整域名推出我们的应用程序时,我们可以通过 PHP Fog 的“域名”部分来实现。在为应用程序创建域名时,PHP Fog 要求它是唯一的,所以你需要想出自己的域名。你可以使用类似yourname-verge.phpfogapp.com的形式,或者你可以特别聪明地创建一个以你最喜欢的神话生物命名的应用程序。这是一个常见的做法,这样在你还在修复错误和准备推出时,没有人会随机找到你的应用程序。设置 PHP Fog 账户

  7. 当你准备好时,点击“创建应用程序”,你的应用程序将被创建。设置 PHP Fog 账户

  8. 就是这样!你的应用程序已经准备就绪。你会注意到 PHP Fog 会在短暂的时间内显示“状态:准备应用...”,然后会变成“状态:运行”。设置 PHP Fog 账户

创建环境变量

我们的 PHP Fog 应用程序已经启动运行,我们在将代码推送到服务器之前需要进行最后一项配置。记得我们在配置项目时设置的所有环境变量吗?好吧,我们需要在 PHP Fog 中设置它们,这样我们的应用程序就知道如何连接到 Cloudant 了。

为了管理你的环境变量,你需要首先转到你项目的“应用程序控制台”,这是你创建第一个应用程序后留下的地方。

点击“环境变量”,你将进入“环境变量管理”部分。

创建环境变量

你会注意到 PHP Fog 为我们创建的 MySQL 数据库的环境变量已经设置好了。我们只需要输入 Cloudant 的环境变量。名称需要与我们在本章前面定义的配置类中的名称相同。

让我们从添加我们的db_server环境变量开始。我的db_server位于https://timjuravich:password@timjuravich.cloudant.com,所以我将这些详细信息输入到名称文本字段中。

创建环境变量

让我们继续为配置文件中的每个变量进行此过程。回顾一下,这里是您需要输入的环境变量:

  • db_server: 这将是您的 Cloudant URL,同样,我的是https://timjuravich:password@timjuravich.cloudant.com

  • db_port: 这将设置为5984

  • db_database: 这是将存储所有内容的数据库,应设置为verge

  • db_admin_user: 这是admin用户的用户名。在我们的情况下,这是设置为 Cloudant 管理员用户名的值

  • db_admin_password: 这是上述admin用户的密码

当您完成所有操作后,点击保存更改,您的环境变量将被设置。有了这个,我们就可以部署到 PHP Fog 了。

部署到 PHP Fog

部署到 PHP Fog 是一个非常简单的过程,因为 PHP Fog 使用 Git 进行部署。很幸运,我们的项目已经使用 Git 设置好并准备就绪。我们只需要告诉 PHP Fog 我们的 SSH 密钥,这样它就知道如何识别我们。

将我们的 SSH 密钥添加到 PHP Fog

PHP Fog 使用 SSH 密钥来识别和验证我们,就像 GitHub 一样。由于我们在本书的早期已经创建了一个,所以我们不需要再创建一个。

  1. 您可以从右上角点击我的帐户开始,然后在下一页上点击SSH 密钥。您将看到以下页面,您可以在其中输入您的 SSH 密钥:将我们的 SSH 密钥添加到 PHP Fog

  2. 输入昵称的值。您应该使用简单但描述性的内容,比如Tim's Macbook。将来,您会因为保持这种组织而感激自己,特别是如果您开始与其他开发人员合作开发这个项目。

您需要获取公钥以填入公钥文本框。幸运的是,我们可以在终端中用一个简单的命令来做到这一点。

  1. 打开终端。

  2. 运行以下命令,您的公钥将被复制到剪贴板。

**pbcopy< ~/.ssh/id_rsa.pub** 

  1. 将公钥复制到剪贴板后,只需点击文本框,粘贴值进去。

  2. 最后,在表单底部有一个复选框,上面写着给此密钥写入访问权限。如果您希望计算机能够将代码推送到 PHP Fog(我们希望能够这样做),则需要勾选此复选框。

  3. 点击保存 SSH 密钥,我们就可以继续进行部署应用程序的最后步骤了。

连接到 PHP Fog 的 Git 存储库

由于我们已经设置好并准备好使用的 Git 存储库,我们只需要告诉 Git 如何连接到 PHP Fog 上的存储库。让我们通过向我们的工作目录添加一个名为phpfog的远程存储库来完成这个过程。

从 Php Fog 获取存储库

当我们在 PHP Fog 上创建应用程序时,我们还创建了一个独特的 Git 存储库,我们的应用程序由此驱动。在本节中,我们将获取此存储库的位置,以便告诉 Git 连接到它。

  1. 登录到您的 PHP Fog 帐户。

  2. 转到您的应用程序的应用控制台。

  3. 点击源代码

  4. 源代码页面上,您将看到一个部分,上面写着克隆您的 git 存储库。我的里面有以下代码(您的应该类似):

**git clone git@git01.phpfog.com:timjuravich-verge.phpfogapp.com** 

  1. 因为我们已经有一个现有的 Git 存储库,所以我们不需要克隆他们的,但是我们需要应用程序的 Git 存储库的位置来进行下一步配置。使用这个例子,存储库位置将是git@git01.phpfog.com:timjuravich-verge.phpfogapp.com。将其复制到剪贴板上。

从 Git 连接到存储库

现在我们知道了 PHP Fog 的 Git 存储库,我们只需要告诉我们的本地机器如何连接到它。

  1. 打开终端。

  2. 将目录更改为您的工作文件夹。

**cd /Library/WebServer/Documents/verge** 

  1. 现在,让我们将 PHP Fog 的存储库添加为一个名为phpfog的新远程存储库。
**git remote add phpfog git@git01.phpfog.com:verge.phpfogapp.com** 

  1. 清除跑道,我们准备启动这个应用程序!

部署到 PHP Fog

这就是我们一直在等待的时刻!让我们将我们的应用程序部署到 PHP Fog。

  1. 打开终端。

  2. 将目录更改为您的working文件夹。

**cd /Library/WebServer/Documents/verge** 

  1. 我们希望忽略 PHP Fog 的 Git 存储库中的内容,因为我们已经构建了我们的应用程序。因此,这一次,我们将在调用的末尾添加--force
**git push origin master --force** 

我希望这不会太令人失望,但恭喜,您的应用程序已经上线了!这是不是很简单?从现在开始,每当您对代码进行更改时,您只需要将其提交到 Git,输入命令git push phpfog master,并确保通过git push origin master将您的代码推送到 GitHub。

如果您开始对您的实时应用程序进行一些操作,您可能会感到沮丧,因为您的本地机器上的数据并不适合您查看。您很幸运;在下一节中,我们将使用 CouchDB 强大的复制功能将本地数据库推送到我们的生产数据库。

将本地数据复制到生产环境

复制的内部工作原理和背景信息将不会在本节中详细介绍,但您可以在 Packt Publishing 网站上的名为复制您的数据的奖励章节中找到完整的演练。

为了给您一个快速概述,复制是 CouchDB 在服务器之间传输数据的方式。复制由每个文档中的_rev字段驱动,_rev字段确保您的服务器知道哪个版本具有正确的数据可供使用。

在本节中,我们将复制_usersverge数据库,以便我们所有的本地数据都可以在生产服务器上使用。如果您的应用程序已经上线了几分钟甚至几天,您不必担心,因为复制的最大好处是,如果有人已经在使用您的应用程序,那么他们的所有数据将保持完整;我们只是添加我们的本地数据。

执行操作-将本地 _users 数据库复制到 Cloudant

让我们使用 Futon 将我们的本地_users数据库复制到我们在 Cloudant 上创建的_users数据库。

  1. 在浏览器中打开 Futon,单击复制器,或者您可以直接导航到http://localhost:5984/_utils/replicator.html

  2. 确保您以管理员身份登录;如果没有,请单击登录,以管理员身份登录。

  3. 从中复制更改的下拉列表中选择_users数据库。

  4. To部分中单击远程数据库单选按钮。执行操作-将本地 _users 数据库复制到 Cloudant

  5. 远程数据库文本字段中,输入 Cloudant 数据库的 URL 以及凭据。 URL 的格式看起来类似于https://username:password@username.cloudant.com/_users执行操作-将本地 _users 数据库复制到 Cloudant

  6. 单击复制,CouchDB 将推送您的本地数据库到 Cloudant。

  7. 您将看到 Futon 的熟悉结果。执行操作-将本地 _users 数据库复制到 Cloudant

刚刚发生了什么?

我们刚刚使用 Futon 将我们的本地_users数据库复制到了我们在 Cloudant 上托管的_users生产数据库。这个过程与我们之前做的完全相同,但是我们在To部分使用了远程数据库,并使用了数据库的 URL 以及我们的凭据。复制完成后,我们收到了一个冗长且令人困惑的报告,但要点是一切都进行得很顺利。让我们继续复制我们的verge数据库。

注意

值得一提的是,如果你尝试从命令行复制_users数据库,你将不得不在调用中包含用户名和密码。这是因为我们完全将用户数据库锁定为匿名用户。函数看起来类似于以下内容:

curl -X POST http://user:password@localhost:5984/_replicate -d '{"source":"_users","target":"https://username:password@username.cloudant.com/_users"}' -H"Content-Type: application/json"

试一试英雄——将本地的 verge 数据库复制到 Cloudant

你认为你能根据我刚给你的提示找出将本地verge数据库复制到 Cloudant 上的verge数据库的命令吗?在游戏的这个阶段几乎不可能搞砸任何事情,所以如果第一次没有搞定,不要害怕尝试几次。

试一试。完成后,继续阅读,我们将讨论我使用的命令。

一切进行得如何?希望你能轻松搞定。如果你无法让它工作,这里有一个你可以使用的命令示例:

curl -X POST http://user:password@localhost:5984/_replicate -d '{"source":"verge","target":"https://username:password@username .cloudant.com/verge"}' -H "Content-Type: application/json"

在这个例子中,我们使用我们的本地 CouchDB 实例将本地的verge数据库复制到目标 Cloudantverge数据库。对于本地数据库,我们可以简单地将名称设置为verge,但对于目标数据库,我们必须传递完整的数据库位置。

当你的所有数据都在生产服务器上并且在线时,你可以登录为你在本地创建的任何用户,并查看你创建的所有内容都已经准备好供全世界查看。这并不是你旅程的结束;让我们快速谈谈接下来的事情。

接下来是什么?

在我送你离开之前,让我们谈谈你的应用在野外的前景,以及你可以做些什么来使这个应用更加强大。

扩展你的应用

幸运的是,在利用 PHPFog 和 Cloudant 时,扩展你的应用应该是非常容易的。实际上,你唯一需要做的最紧张的事情就是登录 PHPFog 并增加我们的 Web 进程,或者登录 Cloudant 并升级到更大的计划。他们处理所有的艰苦工作;你只需要学会如何有效地扩展。这是无法超越的!

要了解更多关于有效扩展的信息,请浏览 PHPFog 和 Cloudant 的帮助文档,它们详细介绍了不同的扩展方式和需要避免的问题领域。

值得再次提到的是,我们在本章中并没有完全涵盖复制。要全面了解复制,请务必查看题为复制数据的奖励章节,该章节可在 Packt Publishing 网站上找到。

下一步

我希望你继续开发和改进 Verge,使其成为非常有用的东西,或者,如果不是,我希望你利用这本书学到的知识构建更伟大的东西。

如果你决定继续在 Verge 上构建功能,这个应用还有很多可以做的事情。例如,你可以:

  • 添加用户之间相互关注的功能

  • 允许用户过滤和搜索内容

  • 添加一个消息系统,让用户可以相互交流

  • 自定义 UI,使其成为真正独特的东西

我将继续在 GitHub 上的 Verge 存储库中逐步添加这样的功能和更多功能:github.com/timjuravich/verge。所以,请确保关注存储库的更新,并在需要时进行分叉。

再次感谢你在这本书中花费的时间,请随时在 Twitter 上联系我@timjuravich,如果你有任何问题。

开心开发!

总结

在本章中,我们学会了如何与世界分享我们的应用。具体来说,我们注册了 Cloudant 和 PHP Fog 的帐户,并成功部署了我们的应用。你所要做的就是继续编码,将这个应用变成一些了不起的东西。

附录 A. 小测验 — 答案

第二章,设置开发环境

1 当我们使用默认的 Apache 安装进行 Web 开发时,默认的工作目录在哪里? [/Library/WebServer/Documents]
2 为了在 CouchDB 中使用我们的本地开发环境,我们需要确保两个服务正在运行。它们是什么,如何在终端中使它们运行?
  • [Apache]

sudo apachetl start

  • [CouchDB]

couchdb -b

|

3 你使用什么命令行语句向 CouchDB 发出Get请求? curl http://127.0.0.1:5984/

第三章,使用 CouchDB 和 Futon 入门

1 根据couchdb.apache.org/?,CouchDB 的定义的第一句是什么? CouchDB 是一个文档数据库服务器,可通过 RESTful JSON API 访问。
2 HTTP 使用的四个动词是什么,每个动词如何匹配 CRUD?
  • [GET -> 读取]

  • [PUT -> 创建,更新]

  • [POST -> 创建]

  • [DELETE -> 删除]

|

3 访问 Futon 的 URL 是什么? http://localhost:5984/_utils/
4 对于 CouchDB 来说,什么是 Admin Party 这个术语,如何使 CouchDB 退出这种模式? Admin Party 是 CouchDB 的默认状态,其中没有服务器管理员,因此用户没有任何限制。通过点击Fix This并添加服务器管理员,我们可以使 CouchDB 退出这种模式。
5 你如何通过命令行为安全数据库验证用户? 在 URL 前面添加username:password@
posted @ 2024-05-05 00:11  绝不原创的飞龙  阅读(14)  评论(0编辑  收藏  举报