PHP 零基础初学者手册(二)

PHP 零基础初学者手册(二)你已经在相对较少的几页中涵盖了很多内容

大家好,欢迎来到IT知识分享网。

原文:PHP for Absolute Beginners

协议:CC BY-NC-SA 4.0

五、使用 JavaScript 和 CSS 给你的图片库增添趣味

这一章完全是选读!你可以把它看作是来自 PHP 的一个小弯路的邀请。本章探索了一种将 JavaScript 集成到 PHP 项目中的方法。在这个过程中,你将不得不学习一些 JavaScript 来开发一个交互式图库。你可以探索这条弯路,或者干脆跳过它。

如果你关注任何关于网页设计和网页开发的博客,你一定会遇到一些涉及 JavaScript 的东西。也许你会遇到一些你想在你的 PHP 项目中实现的东西。这并不是一本真正关于 JavaScript 的书,但是我将向您展示一些使用 JavaScript 的例子。我的目标是向您展示一种在 PHP 项目中集成 JavaScript 的方法。如果你真的想学习 JavaScript,你必须参考其他资源。

Note

想了解更多关于浏览器中 JavaScript 的知识?考虑拿一本 Rex van der Spuy 的《HTML5 和 JavaScript 基础游戏设计》(Apress,2012)。您将使用 HTML5、CSS 和 JavaScript 为浏览器构建游戏。这是一种有趣的学习方式!

客户端与服务器端编程

PHP 是一种很好的 web 开发语言。它非常受欢迎有很多原因。但是 PHP 只是众多服务器端脚本语言中的一种。服务器端语言只在您的服务器上运行;没有办法在浏览器中执行 PHP 代码。到目前为止,您已经编写了输出 HTML 的 PHP 代码,HTML 被发送到浏览器。

为了更改生成的 HTML 中的任何内容,浏览器必须向服务器发送一个 HTTP 请求,这样 PHP 就可以运行并发回一个 HTTP 响应。但是发送一个 HTTP 请求,等待 PHP 运行,最后接收一个 HTTP 响应需要时间和带宽。

在某些情况下,当系统需要时,简单地运行一些代码会更好。幸运的是,有一种方法:可以使用 JavaScript 在浏览器中以编程方式操作 HTML,JavaScript 恰好是唯一一种在浏览器中本地运行的脚本语言。您可以在服务器端选择许多不同的语言。在客户端,也就是在浏览器中,只有一个:JavaScript。

JavaScript 是一种奇妙的语言,但是不同版本的不同浏览器以不同的方式实现了 JavaScript 的不同部分。因此,在一个浏览器中运行良好的 JavaScript 可能会在另一个浏览器中引发令人尴尬的错误。

处理这些差异的一种常见方法是使用渐进式增强,这基本上意味着您以这样一种方式编写代码,即您漂亮的 JavaScript 只能在完全理解它的浏览器中运行。

JavaScript 的渐进式增强为启用了 JavaScript 的现代浏览器提供了最佳的用户体验。旧浏览器或禁用了 JavaScript 的浏览器仍然可以提供所有内容。额外的 JavaScript 特性应该对不支持的浏览器保持隐藏,这样可以避免 JavaScript 错误。本章介绍的图像库使用渐进式增强。

编码灯箱画廊

让我们为图库编写一个所谓的 lightbox,以一种美观的方式呈现图像。灯箱是一种非常常见的显示图像的方式。当用户点击网页上的一个小图片时,JavaScript 会在所有页面内容上放置一个半透明的覆盖层。点击图像的大版本将显示在覆盖图的顶部。

从服务器端的 PHP,您将继续为每个访问浏览器提供所有 JPEG 图像的列表。但是如果用户有一个顶级的浏览器,你可以提供一个更好的解决方案:所有图片的小缩略图,这样用户可以快速浏览整个图库。如果用户点击一个缩略图,你真的可以把焦点放在那个特定的图片上。您可以将所有其他内容隐藏在半透明的覆盖图后面,真正突出显示所选图片。你甚至可以显示点击图像的更大版本:这是一个灯箱画廊。

这就是渐进式改进的意义所在:为所有浏览器提供所有内容,但为有能力的浏览器提供更好的用户体验。我们开始吧!

嵌入外部 JavaScript 文件

可以直接在 HTML 中编写 JavaScript 代码,但不推荐这样做。更好的方法是保持 HTML 和 JavaScript 的分离。可以在一个 HTML 文件中嵌入几个 JavaScript 文件,就像可以将多个样式表链接到一个 HTML 文件一样。要将 JavaScript 链接到 HTML 文件,可以使用一个<script>元素:

<script src="path/to/Javascript-file.js"></script>

属性应该指向一个现有的 JavaScript 文件,所以路径正确是很重要的。您可能想知道为什么<script>元素是一个容器标签?这是因为您可以决定在 HTML 文件中的<script>元素中直接编写 JavaScript 代码。

Note

使用外部 JavaScript 文件并小心地避免 HTML 中的任何 JavaScript 代码也被称为不引人注目的 JavaScript。

从第四章的中,你已经有了一个 PHP 驱动的动态图库。向该项目添加一些 JavaScript 应该会让您对使用 JavaScript 可以做的一些事情有一个很好的了解。本章中的代码示例依赖于你拥有在第四章中开发的图库的 PHP 源代码。

为 JavaScript 文件准备 Page_Data 类

您可以更改 PHP 代码,为一个或多个 JavaScript 文件做准备。现有的Page_Data类需要一个属性来保存一个或多个<script>元素。您还可以在Page_Data类中声明一个新方法来添加新的 JavaScript 文件。它将非常类似于保存样式表引用的<link>元素的属性和添加新样式表的方法。我建议你继续做你在第四章开始的项目,所以要更新的文件是ch4/classes/Page_Data.class.php。下面是它的完整代码:

<?php

//complete code listing for classes/Page_Data.class.php

class Page_Data {

public $title = "";

public $content = "";

public $css = "";

public $embeddedStyle = "";

//declare a new property for script elements

public $scriptElements = "";

//declare a new method for adding Javascript files

public function addScript( $src ){

$this->scriptElements .= "<script src='$src'></script>";

}

public function addCSS( $href ){

$this->css .= "<link href='$href' rel='stylesheet' />";

}

}

请注意名为$scriptElements的新公共属性。它将容纳页面所需的任意数量的<script>元素。还要注意公共函数addScript()。看看它如何把一个$src作为参数。$src应该保存一个 JavaScript 文件的路径。接收到的路径将用于创建一个<script>元素。创建的<script>元素将通过增量连接的方式与任何先前添加的<script>元素一起存储。

为 JavaScript 文件准备页面模板

您必须更新页面模板文件以接受 JavaScript 的<script>元素,就像您更新页面模板以接受 CSS 的<link>元素一样。编辑template/page.php:

<?php

//complete code listing for templates/page.php

return "<!DOCTYPE html>

<html>

<head>

<title>$pageData->title</title>

<meta http-equiv='Content-Type' content='text/html;charset=utf-8' />

$pageData->css

$pageData->embeddedStyle

</head>

<body>

$pageData->content

$pageData->scriptElements

</body>

</html>";

PHP 将通过$pageData->scriptElements的方式嵌入脚本元素。请注意,任何<script>元素都将放在页面上任何其他内容之后。这样做的时候,可以确保在 JavaScript 开始执行之前,所有的 HTML 元素都被加载到浏览器内存中。这正是我们想要的!

JavaScript 经常被用来操作 HTML。在我们操作它之前,有必要将 HTML 加载到浏览器内存中。

编写和运行外部 JavaScript 文件

我喜欢把我的 JavaScript 文件放在一个指定的文件夹中,以保持一个组织良好的文件结构。我建议你也习惯这样做。创建一个名为js的新文件夹。使用您的编辑器创建一个名为lightbox.js的新 JavaScript 文件。把它保存在你的js文件夹里。

//complete code listing for js/lightbox.js

window.console.log("Hello from Javascript");

要运行 JavaScript 代码,必须告诉浏览器有一个 JavaScript 要运行。您可以从index.php指向一个外部 JavaScript 文件。您将要编写的 JavaScript 将操纵您的 HTML,因此某些属性会动态地改变。

您还需要一个外部样式表。下面是来自index.php的一小段代码,展示了如何指向外部样式表以及如何指向外部 JavaScript。这些代码行属于在新的$pageData对象被创建之后和生成的$page被回显之前的index.php:

//partial code listing for index.php

//this line of code you already have. It creates a Page_Data object

$pageData = new Page_Data();

//new code below

//add this new line to embed an external Javascript file to your index.php

$pageData->addScript("js/lightbox.js");

//no other changes in index.php

您有一个外部 JavaScript,并且您已经从index.php链接到它。您编写的任何 JavaScript 代码现在都应该可以完美运行了。如果你使用 Google Chrome 浏览器或者其他类似 JavaScript 控制台的浏览器,测试起来非常简单。我建议你使用谷歌浏览器,除非你已经习惯了另一个带有 JavaScript 控制台的浏览器。

首先,打开谷歌 Chrome。接下来,通过点击浏览器右上角的外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传打开 Chrome 菜单。选择工具➤ JavaScript 控制台。当控制台打开时,只需在 Chrome 浏览器中加载 http://localhost/ch4/index.php。您应该会在控制台中看到一条消息,如图 5-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-1。

The JavaScript Console in Google Chrome

使用 window.console.log()

如您所见,控制台中的消息与您在 JavaScript 代码中编写的完全相同:

window.console.log("Hello from Javascript");

在 JavaScript 中,window是一个表示打开的浏览器窗口的对象。在window对象中,你可以找到console对象。console对象有一个在 JavaScript 控制台窗口中输出消息的方法log()。记录到控制台通常用于检查一些 JavaScript 是否按预期工作。

在本例中,您使用它来检查 JavaScript 是否运行。如果您在控制台中没有看到消息,那么您知道您的 JavaScript 没有运行。在这种情况下,您可以检查index.php的 HTML 源代码,看看是否找到链接到现有 JavaScript 文件的<script>元素。也许没有<script>元素,或者它的src属性没有指向您的 JavaScript 文件。请确保您已经正确完成了前面的所有步骤。

从您编写的这一行 JavaScript 代码中,您可以推断出 JavaScript 类似于 PHP,因为它有对象和方法。您还可以看到 JavaScript 语法有一点不同。JavaScript 的对象操作符是一个.,而 PHP 使用的是->。如果 JavaScript 具有与 PHP 完全相同的语法,您应该编写以下代码:

//If Javascript had PHP’s object operator

window->console->log("hello");

我希望你能注意到功能上的相似之处和句法上的不同。JavaScript 在许多方面与 PHP 非常相似,但语法略有不同。在某些方面,JavaScript 和 PHP 确实不同,但那是另一回事了。

JavaScript 数组

您已经尝试在 PHP 中使用数组:$_GET$_POST是超全局数组。让我们仔细看看数组,看看如何在 JavaScript 中使用它们。打开您的lightbox.js文件并编写一些 JavaScript,如下所示:

var pets = new Array("cat", "dog", "canary");

var firstPet = pets[0];

window.console.log( "The first pet is at index 0\. It is a " + firstPet);

使用 var 声明变量

首先声明一个变量pets并分配它来保存一个新的数组对象。您使用关键字var来声明一个 JavaScript 变量。新的数组对象包含一个由三个字符串值组成的列表。这就是数组所能做的:它们可以保存一个项目列表。

为了对数组做任何有意义的事情,你必须能够在正确的时间获得正确的数组项。数组项是根据它们在列表中的位置来记忆的。这种职位的专业术语是index。数组中的第一项的index为 0,第二项的index为 1,依此类推。从数组中获取项的一般语法如下:

arrayName[index];

如果您回头看看 pets 示例的代码,您可以看到变量firstPet保存了在pets数组中的index 0 处找到的项目。再说一次,JavaScript 非常类似于 PHP。要从 PHP 数组中获取一个项目,我们可以使用完全相同的语法。

遍历数组项

循环通常与数组一起使用。通过一个简单的while循环,您可以遍历数组中的所有元素:

var pets = new Array("cat", "dog", "canary");

var index = 0;

while ( index < pets.length ) {

window.console.log( pets[index] );

index = index + 1;

}

再一次,您可以看到 JavaScript 和 PHP 是非常相似的语言:它们都可以使用while循环。前面的循环将遍历pets数组中的每一项,并将每一项输出到控制台。

在第一次迭代中,变量index将保存值 0,因此,代码输出“cat”,这是在pets[0]找到的项目。

while循环的条件规定,只要index小于pets数组的长度,换句话说,只要index小于 3,循环就会继续。

在第一次迭代结束时,index的值被改变。它开始的值是 0,现在变成了 1,因为index = 0 + 1。在下一次迭代中,代码将向控制台输出“dog ”,因为在pets[1]找到了dog。变量index变成 2,循环继续,于是“金丝雀”出现在控制台上。

现在,index变为 3,因此while循环终止,因为 3 不小于 3。

简单渐进增强

在这个 lightbox 脚本中,您希望 JavaScript 为使用新浏览器的用户提供更好的体验。您可以通过响应只有相对较新的浏览器才能理解的事件来做到这一点。您可以在js/lightbox.js中这样做,如下所示:

//complete code for js/lightbox.js

function init(){

window.console.log("Welcome, user with a new browser");

}

document.addEventListener("DOMContentLoaded", init, false);

注意上面代码中使用的document对象。document是一个本地 JavaScript 对象。浏览器中加载的每个 HTML 网页都有自己的document对象。您可以使用document对象来检索和操作加载页面中的 HTML 内容。

前面的代码指定在调度事件DOMContentLoaded时自动调用函数init。当浏览器完成加载 DOM(文档对象模型)时,将调度DOMContentLoaded事件。DOM 是页面上 HTML 的表示。只有新浏览器会调度DOMContentLoaded事件。因此,您从函数init()内部编写或调用的任何 JavaScript 代码只有在用户拥有相对较新的浏览器时才会运行。

使用事件侦听器

事件监听器是 JavaScript 自带的。这是 JavaScript 和 PHP 真正不同的一点,因为 PHP 中没有事件监听器。事件侦听器用于将事件与函数相关联。这个想法是,每次某个事件发生时,应该运行一个特定的函数。添加事件侦听器的一般语法如下:

object.addEventListener(event, event handler, useCapture);

如您所见,addEventListener有三个参数:一个事件、一个事件处理程序和 useCapture。

事件

第一个参数指定监听哪个事件。不同的对象可以响应不同的事件。在前面的例子中,您正在监听文档对象的DOMContentLoaded事件。浏览器将调度该事件,document对象可以响应该事件,但前提是您明确告诉它监听该事件。

事件处理程序

第二个参数指定在听到事件时运行哪个函数。addEventListener为特定事件注册一个事件处理函数。在您的示例中,您已经注册了函数init作为文档对象的DOMContentLoaded事件的事件处理程序。

可选的使用捕获

addEventListener 的第三个参数表示一个称为 useCapture 的高级主题。对于大多数现代浏览器,这是一个可选参数,意味着您不必指定它。如果没有明确设置为true,大多数浏览器会简单地假设它为false。但是有些浏览器和浏览器版本需要设置这个参数,所以你不妨养成设置的习惯。

根据经验,您可以声明第三个参数并将其设置为false。你可能会遇到一种特殊的情况,需要你把它设置为true,但在本书的代码示例中不会出现。这是一个可以通过参考其他资源自己探索的 JavaScript 主题。

为覆盖图和大图像创建标记

您已经为渐进式增强建立了一个非常基本的框架:init函数只有在浏览器支持的情况下才会运行。

是时候开始使用 lightbox 画廊了。首先添加一些 JavaScript 来动态创建一个小 HTML,为在透明覆盖层上显示大图像提供标记结构。您必须更新js/lightbox.js中的init()功能,如下所示:

//edit existing function

function init() {

var lightboxElements = "<div id='lightbox'>";

lightboxElements += "<div id='overlay' class='hidden'></div>";

lightboxElements += "<img class='hidden' id='big-image' />";

lightboxElements += "</div>";

document.querySelector("body").innerHTML += lightboxElements;

}

这段代码将创建一串 HTML 元素,并将它们添加到已经在<body>中找到的 HTML 元素之后。请特别注意<img>元素。它缺少一个src属性,所以此时它不显示图片。如果保存您的工作并在浏览器中加载 http://localhost/ch4,您可能会惊讶地发现,尽管您尽了所有的 JavaScript 努力,似乎什么都没有改变。如果您的 JavaScript 工作正常,您应该有一个页面,并在末尾添加一些 HTML 元素。但是它们不包含任何内容,所以您什么也看不到——到目前为止!

在前面的代码示例中,您可以看到 JavaScript 可以像 PHP 一样连接字符串。请注意,JavaScript 的增量连接运算符不同于 PHP。这是功能相同,语法不同的另一种情况。

//Javascript's incremental concatenation operator

+=

//PHP's incremental concatenation operator

.=

document.querySelector()

函数中的最后一行看起来好像没有你在书中用过的东西。querySelector()document对象的一个方法。如果你熟悉 CSS 选择器,这绝对是一个非常好的方法。通过querySelector(),您可以使用 CSS 选择器语法从加载的页面中选择 HTML 元素。

document.querySelector("body").innerHTML += lightboxElements;

前面一行使用querySelector来获取<body>元素及其所有内容。JavaScript 将存储在变量lightboxElements中的 HTML 字符串添加到<body>中的现有内容之后。要访问<body>中的 HTML 内容,可以使用innerHTML属性。

显示覆盖图

我认为用 JavaScript 创建一个<div>元素是非常值得的。但是如果你能看到它作为一个覆盖层工作,那就更有价值了。您可以通过在现有的样式表中添加一点 CSS 来实现这一点。我把我的放在css/layout.css里。

/*declare a new style rule in css/layout.css */

div#overlay{

position: absolute;

width: 100%;

height:100%;

top:0px;

left:0px;

background:black;

opacity: 0.85;

}

如果保存该文件并在浏览器中重新加载 http://localhost/ch4,应该会看到一个半透明的覆盖图,覆盖了浏览器视窗中所有可见的内容。如果向下滚动,可以看到覆盖图只覆盖了视窗,而不是整个页面内容。这是应该的。看到这个覆盖图是 JavaScript 代码正在做一些事情的视觉确认。但是您只希望当用户单击特定图像时显示覆盖图。此外,您可能希望被单击的图像显示在覆盖图的顶部。你还有一些工作要做。

隐藏覆盖图并调整缩略图大小

默认情况下,您会希望图像显示为小缩略图。当一个缩略图被点击时,覆盖图应该出现以隐藏其他缩略图,并且被点击的图像应该几乎全屏显示。要实现这一点,你需要 CSS 和 JavaScript。您可以从准备一些 CSS 规则开始,稍后您可以通过 JavaScript 使用这些规则。在css/layout.css中,你的样式表还有一些规则:

/*hide overlay and big-image*/

div#overlay.hidden, img#big-image.hidden{ opacity: 0; left:-200%; }

/*resize images and display them as a horisontal list*/

li.lightbox img{ height: 100px; }

li.lightbox{ display: inline-block; margin: 10px; }

如果刷新浏览器,可以看到覆盖图被隐藏了。您还可以看到,尽管使用了 CSS,缩略图仍未调整大小。不要太惊讶。原因很简单,因为<li>元素还没有设置为lightboxclass属性。您将使用 JavaScript 动态设置class。但是在编写更多的 JavaScript 之前,我想让你看看隐藏图像和覆盖的 CSS。您可以看到,这两个元素都被设计为完全透明,并且位于左侧很远的位置,即使它们不是完全透明的,它们也是不可见的。请记住,重要的是覆盖图和大图都有一个设置为hiddenclass属性。如果类未设置为hidden,两个元素都将显示。

默认情况下,没有属性设置为lightbox<li>元素。所以,上面写的 CSS 规则目前不适用于任何东西。您可以通过编写一些 JavaScript 来声明一个class属性,并在用于图库图像的所有<li>元素上将该属性的值设置为lightbox,但只针对有能力的浏览器。更新js/lightbox.js如下:

//complete code listing for js/lightbox.js

//edit existing function

function init(){

var lightboxElements = "<div id='lightbox'>";

lightboxElements += "<div id='overlay' class='hidden'></div>";

lightboxElements += "<img class='hidden' id='big-image' />";

lightboxElements += "</div>";

document.querySelector("body").innerHTML += lightboxElements;

//add a new function call here

prepareThumbs();

}

//declare a new function

function toggle(){

window.console.log("show or hide a big image");

}

//declare new function

function prepareThumbs() {

var liElements = document.querySelectorAll("ul#images li");

var i = 0;

var image, li;

//loop through all <li> elements

while ( i < liElements.length ) {

li = liElements[i];

//set class='lightbox'

li.setAttribute("class", "lightbox");

image = li.querySelector("img");

//register a click event handler for the <img> elements

image.addEventListener("click", toggle, false);

i += 1;

}

}

document.addEventListener("DOMContentLoaded", init, false);

保存此代码并刷新浏览器。您应该会看到小缩略图的水平列表。当您单击一个图像时,您应该会在控制台窗口中看到一条消息,提示“显示或隐藏大图像”

这是一个步骤中的一大块代码。让我们调查一下,弄清楚到底发生了什么。

仅向支持的浏览器显示缩略图

JavaScript 使用渐进式增强,并有效地隐藏了浏览器可能无法理解的 JavaScript 特性。还记得函数init()如何只在浏览器调度DOMContentLoaded事件时运行吗?只有相对较新的浏览器才会调度该事件。如前所述,当给定页面的所有 HTML 元素都加载到浏览器的内存中时,它就会被调度。

函数prepareThumbs()是从函数init()内部调用的,所以prepareThumbs()只能在新的浏览器中运行。您已经有效地隐藏了旧浏览器中的 JavaScript:您拥有渐进式改进!

用 querySelectorAll()获取 HTML 元素的数组

接下来,看看如何将class属性添加到图库的所有<li>元素上。第一个任务是选择所有的<li>元素。您已经为该任务使用了方法querySelectorAll()querySelectorAll()就像querySelector(),除了它返回的不仅仅是一个 HTML 元素,而是匹配所用 CSS 选择器的所有 HTML 元素。

var liElements = document.querySelectorAll("ul#images li");

在前一行中,变量liElements将保存在<ul>中找到的所有<li>元素的数组,并将id属性设置为images

Note

你可以在 www.kirupa.com/html5/finding_elements_dom_using_querySelector.htm 了解更多关于使用querySelector()querySelectorAll()的信息。

您已经看到了如何遍历一组宠物。遍历 HTML 元素的数组就是这样。在prepareThumbs()内部,可以看到一个while循环。只要变量i保存的值小于找到的<li>元素的数量,它就会一直循环下去。这实际上意味着您将使用图像的id属性遍历<ul>元素中的每个<li>项目。

变量i可以用作索引,从所有<li>元素的数组中获取一个特定的<li>元素。每一个<li>都会被记忆在变量li中,每一个<li>都会得到一个lightboxclass属性。此类<li>元素有一个 CSS 规则,这就是为什么缩略图在浏览器中显示为水平列表。还有另一个 CSS 规则在这样的<li>元素中选择<img>元素。该规则将缩略图的宽度调整为 100 像素。

仍然在while循环中,使用querySelector()选择<li>元素中的<img>元素。您为每个<img>元素分配一个事件监听器。所以,每当用户点击一个<img>,函数toggle()就会被调用。换句话说,您为每个<img>元素注册了一个名为toggle()的事件处理函数。

显示大图像

每当用户点击一个<img>元素,函数toggle()就会运行。在这一点上,它没有多大作用。它只是在控制台窗口中输出一条消息。你在寻找一种不同的行为。如果单击了缩略图,您希望透明覆盖图隐藏所有缩略图,并且您希望显示所单击图像的大版本。如果单击一个大图像,您希望覆盖图和大图像消失,这样所有缩略图再次变得清晰可见。您将需要一个 CSS 规则来设计大图像的样式,还需要一些 JavaScript 来操作 HTML 类属性。你可以从 CSS 开始。给css/layout.css再加一条规则:

/*partial code listing for css/layout.css*/

/*new CSS rule for showing the big-image*/

#big-image.showing{

max-width: 80%;

max-height:90%;

position:absolute;

background-color: white;

padding: 10px;

top:5%;

left: 10%;

}

要在浏览器中看到一些动作,您还必须向 js/lightbox.js 中声明的toggle()函数添加一些代码,如下所示:

//edit existing function

function toggle( event ){

//which image was clicked

var clickedImage = event.target;

var bigImage = document.querySelector("#big-image");

var overlay = document.querySelector("#overlay");

bigImage.src = clickedImage.src;

//if overlay is hidden, we can assume the big image is hidden

if ( overlay.getAttribute("class") === "hidden" ) {

overlay.setAttribute("class", "showing");

bigImage.setAttribute("class", "showing");

} else {

overlay.setAttribute("class", "hidden");

bigImage.setAttribute("class", "hidden");

}

}

您可以看到 JavaScript 并没有真正显示或隐藏大图像或覆盖图。JavaScript 所做的只是操纵#overlay#big-image的类属性。你可以在你的浏览器中检查覆盖和图像实际上是隐藏的。如果您单击缩略图,覆盖图将出现在缩略图的顶部,大图像将出现在覆盖图的顶部。

这种效果是通过联合 CSS 和 JavaScript 实现的。在 CSS 中,规则规定了如何呈现#overlay#big-image。如果这些元素上的class被设置为hidden,它们将被隐藏。如果没有设置class hidden,将显示元素。JavaScript 动态操纵class属性。CSS 声明如何呈现#big-image#overlay,这取决于class属性的当前值。

JavaScript 有一个非常简单的工作:它只负责在覆盖图和大图像上设置class属性。如果class属性当前被设置为hidden,它将被更改为showing。否则,如果类别未设置为hidden,则会设置为hidden

让我们更详细地检查一下toggle中的代码,以便更好地理解它和 JavaScript。

使用 MouseEvent 对象

首先要注意的是添加到toggle()函数中的event参数。函数toggle()被调用,因为它被注册为<img>元素上点击事件的事件处理程序。它是从事件侦听器中调用的。当这种情况发生时,一个Event对象在事件被触发时被传递。当用户点击鼠标按钮时,点击被触发,因此,被发送的Event对象是一个MouseEvent对象。

事件对象有很多非常有用的属性,可以在代码中使用。MouseEvent对象有一个target属性,它保存了对被点击的 HTML 元素的引用。

Note

如果在 toggle()函数中添加以下代码行,您可以在控制台窗口中看到其他可用的属性:window.console.log(event);

您使用MouseEvent.target属性来获取被点击的<img>元素。您可以用它来替换大图片的src属性和被点击图片的src属性。本质上,您使用它来显示单击缩略图的大版本,如以下代码所示:

bigImage.src = clickedImage.src;

棒形纽扣

toggle 的意思是在两种状态之间转换。当你开灯的时候,你拨动灯的开关,当你关掉灯的时候,你拨动同样的灯的开关。在这段 JavaScript 中,您想要切换覆盖图和大图像。

如果覆盖元素的class属性被设置为hidden,您想要隐藏覆盖和大图像。如果覆盖图的class属性设置为showing,您希望显示覆盖图和大图。如果你查看toggle函数,你可以看到用代码表达的同样的想法。

if ( overlay.getAttribute("class") === "hidden" ) {

//code to show overlay and image

} else {

//code to hide overlay and image

}

操纵属性

要读取class属性的值,可以使用getAttribute()方法。这是所有 HTML 对象都有的标准 JavaScript 方法。getAttribute()方法可以用来读取任何属性的值。一般语法如下:

element.getAttribute( whichAttribute );

getAttribute()方法将返回在指定元素中找到的请求属性的值。有一个类似的方法,叫做setAttribute(),用于改变属性值。一般语法如下:

element.setAttribute( whichAttribute, newValue );

方法可以为特定 HTML 元素的指定属性设置一个新值。在前面的toggle()函数中,您使用它来更改覆盖图和大图像的属性值。

隐藏大图像

此时,您可以单击缩略图来显示覆盖图和大图。太棒了。但是你不能再隐藏覆盖图或大图了,这不是很好。要启用隐藏,只需将toggle()函数注册为单击大图像时触发的事件处理程序。这可以通过init()函数中的以下(粗体)两行额外代码来完成:

//edit existing function

function init(){

var lightboxElements = "<div id='lightbox'>";

lightboxElements += "<div id='overlay' class='hidden'></div>";

lightboxElements += "<img class='hidden' id='big-image' />";

lightboxElements += "</div>";

document.querySelector("body").innerHTML += lightboxElements;

//new code: register toggle as event handler

var bigImage = document.querySelector("#big-image")

bigImage.addEventListener("click",toggle, false);

//end of changes

prepareThumbs();

}

自己测试一下。在这一点上,你应该能够点击一个缩略图来显示覆盖图顶部的大图像。如果你点击大图,大图和覆盖图将再次隐藏,从而显示下面的缩略图。

使用 CSS 动画

如果半透明覆盖可以淡入隐藏缩略图,那不是很好吗?这可能是使灯箱画廊更加漂亮的最后一笔。您可以通过在css/layout.css中添加以下(粗体)单行 CSS 来创建 CSS 动画:

#overlay{

position: absolute;

width: 100%;

height:100%;

top:0px;

background:black;

opacity: 0.85;

left:0px;

/*this is the animation to fade the overlay in gradually over 1 second*/

transition: opacity 1s ease-in;

}

编码挑战

灯箱画廊现在完成了。如果您浏览互联网,可以很容易地找到更多 JavaScript 驱动的图片库的例子。也许你会遇到一个你想在画廊里实施的行为。

一个非常常见的功能是在显示大图时单击覆盖图。大多数图库会在点击覆盖图时切换。这并不难实现,所以也许这是一个你可以自己解决的任务。您只需将toggle注册为覆盖图中检测到的点击事件的事件处理程序。这种方法是可行的,但会触发一个 JavaScript 错误。您可以在控制台中看到错误消息。对您来说,一个额外的编码挑战可能是找出错误消息的含义,以及如何更改代码以避免错误。

你可以在 www.webmonkey.com/2010/02/make_a_javascript_slideshow/ 找到另一个 JavaScript 库的教程,有下一个和上一个按钮。也许你可以弄清楚如何在你的 lightbox gallery 中实现这样的按钮。这可能是一个有趣的学习经历,也是你的灯箱画廊的一个很好的补充。

摘要

你已经在相对较少的几页中涵盖了很多内容。主要目标是为您提供一种在 PHP 项目中集成 JavaScript 解决方案的方法。在这个过程中,您使用了一个相对简单的 lightbox image gallery。

您已经看到 PHP 和 JavaScript 语言在许多方面都很相似。通常,只是语法不同,有时甚至不是这样。这对你来说是个好消息。一旦你学会了 PHP,你就可以相对容易地学习 JavaScript。

另一方面,您也看到了 JavaScript 和 PHP 之间的显著差异。要真正精通这两种语言,你最终会想要密切关注这些差异。

也许最重要的区别是 JavaScript 是一种客户端脚本语言,而 PHP 是一种服务器端脚本语言。您的 JavaScript 代码在用户的浏览器中运行。PHP 只在你的服务器上运行,所以浏览器永远看不到你的 PHP。浏览器只会得到 PHP 创建的结果。

Note

实际上,JavaScript 可以在服务器上运行。在互联网上搜索node.js以了解更多信息。此外,PHP 可以在没有 web 服务器的情况下运行。但是 JavaScript 多用于客户端,PHP 多用于服务器端。

到目前为止,您已经看到当 PHP 完成它的任务时,它将创建一个输出。输出通常是发送到浏览器的 HTML 文件。JavaScript 可以在浏览器中操作 HTML,而不需要联系服务器。您最终可能会想要学习更多的 JavaScript,但是这超出了本书的范围。

六、使用数据库

现代网站非常强大,这种强大的力量很大程度上来源于它们存储信息的能力。存储信息允许开发者在他们的软件和用户之间创建高度可定制的交互,从基于条目的博客和评论系统到安全处理敏感交易的高性能银行应用。

本章涵盖了 MySQL 的基础知识,这是一个强大的开源数据库。我还演示了在 PHP 项目中使用 MySQL 的面向对象方法。涵盖的主题包括以下内容:

  • MySQL 数据存储的基础
  • 操作 MySQL 表中的数据
  • 数据库表结构
  • 使用 PHP 与 MySQL 数据库交互
  • 用模型-视图-控制器方法组织 PHP 脚本
  • 为什么编码就像演奏布鲁斯

这一章有很多东西要学,其中一些可能会让你头大。但是请放心,所有涉及的主题将在后续章节中重复和详细阐述。你将会有大量的学习机会。

MySQL 数据存储的基础

MySQL 是一个关系数据库管理系统,允许您在多个表中存储数据。每个表包含一组命名列,每一行由表中的一个数据条目组成。表格通常包含关于其他表格条目的信息。这样,一个事实可以存储在一个表中,但可以在其他表中使用。例如,看看如何存储音乐艺术家的信息(见表 6-1 和 6-2 )。

表 6-2。

The Album Table

相册 id 艺术的 相册名称
one one 对艾玛来说,永远以前
Two one EP3 血库
three Two 让它死去
four Two 提醒

表 6-1。

The Artist Table

艺术的 艺术家 _ 姓名
one 好的 Iver
Two Feist

第一个表 artist 包含两列。第一列artist_id,存储每个艺术家的唯一数字标识符。第二列artist_name,存储艺术家的名字。

第二个表 album 在album_id列中存储每个专辑的唯一标识符,在album_name列中存储专辑名称。专辑表包括第三列artist_id,它与艺术家和专辑表相关联。该列存储与录制专辑的艺术家相对应的唯一艺术家标识符。

乍一看,这似乎是一种愚蠢的数据存储方式。为什么要保留一个抽象的,无法理解的数字,而不是简单的写下每张专辑的艺人名字?表 6-3 想象你做了那件事。

表 6-3。

The Badly Designed Album Table

相册 id 艺术家 相册名称
one 好的 Iver 对艾玛来说,永远以前
Two 好的 Iver EP3 血库
three Feist 让它死去
four 第一 提醒

请注意album_id 4的拼写错误。因为每张专辑的艺术家姓名都是分开拼写的,所以可以为同一位艺术家存储不同的姓名。在一个只有四个条目的小表中,就像前面的表一样,很容易发现和纠正错误。但是在现实世界中,表很少这么小。假设您正在为一家音乐商店构建数据库。你必须记录数以千计的专辑。

如果你幸运的话,你会发现这个错误——然后你必须检查 Feist 的每张专辑,检查艺术家的名字是否拼写正确。这是可能的,但这将是对时间的疯狂浪费。

对另一个表结构(列于表 6-2 中)进行同样的思考实验。如果你把 Feist 拼错成 fiest,你就更有可能发现这个错误,因为 Feist 的每张专辑都会列在 fiest 下面。此外,纠正错误不会让你跋涉数以千计的条目。你只需简单地去一个声明艺术家名字的地方,写 Feist 而不是 fiest,每张专辑就会正确地列出来。

通过设计只存储一段数据一次的表,可以设计一个具有数据完整性的健壮数据库。SQL 社区的杰出人物 Joe Celko 恰当地创造了一个口号“一个简单的事实,在一个地方,一个时间。”记住这句口号,让你的数据库表遵循这条规则。

使用 SQL 操作数据

您可以通过结构化查询语言(SQL)操作 MySQL 表中的数据。SQL 是一种小型语言,大部分都非常容易阅读和理解。在本节中,您将学习执行以下操作的 SQL 语句:

  • 创建数据库
  • 在数据库中创建一个表
  • 将数据插入表格
  • 从表中检索数据
  • 更新表中的数据

您将使用 XAMPP 提供的 phpMyAdmin 控制面板来测试这些命令。要使用 XAMPP,您必须先启动它。打开 XAMPP 控制面板(见图 6-1 )并启动 MySQL 数据库和 Apache Web 服务器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-1。

The XAMPP control panel

在 MySQL 和 Apache 运行的情况下,您可以打开浏览器并导航到 http://localhost/phpMyAdmin,以访问 phpMyAdmin 控制面板(图 6-2 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-2。

The phpMyAdmin control panel

为民意调查开发数据库

感受数据库驱动的 web 页面的最好方法是创建一个用于测试的页面。在接下来的页面中,您将创建一个数据库驱动的网站投票。这是数据库驱动开发的一个简单例子,但它足以展示基本原理。最简单的站点投票将提出一个问题,站点访问者可以回答“是”或“否”。用户的所有答复都将显示出来,因此每个站点用户都可以看到其他站点访问者是如何回答的。

虽然这个例子很简单,但是它将总结到目前为止您在书中看到的所有内容,并且它将要求您学习如何在您的 PHP 项目中集成数据库驱动的数据。

这是一个完美的学习项目,因为它非常简单。它将需要相对较少的代码行,这意味着您可以专注于相关的原则,而不是淹没在冗长的语法中。这将是你在下一章开始的个人博客系统的完美准备。

网站投票依靠数据库表来存储投票问题和投票回复。PHP 将不得不连接到 MySQL 并检索相关数据,因此它可以在浏览器中显示为 HTML。从 PHP 中,您还将输出一个 HTML 表单,允许站点访问者与站点投票进行交互。每当访问者提交表单时,PHP 应该获得提交的答案,并相应地更新 MySQL 数据库表。首先创建一个包含一个表和一些投票数据的数据库。

使用 CREATE 创建数据库

SQL 使用 CREATE 一词来表示正在创建一个表或数据库。启动 CREATE 子句后,必须指明是创建数据库还是表。在您的例子中,您使用关键字 DATABASE 来表明您实际上正在创建一个数据库。最后,您必须指明新数据库的名称。

MySQL 最初是在瑞典开发的。所以,MySQL 中使用的默认字符集是瑞典语。也许你不想在你的解决方案中使用瑞典语。我喜欢在我的解决方案中使用 utf-8。创建使用 utf-8 的数据库很容易;您只需指定 utf-8 作为要使用的字符集。完整的命令应该如下所示:

CREATE DATABASE playground CHARSET utf8

要执行 SQL 语句,必须在 phpMyAdmin 控制面板中选择 SQL 选项卡(图 6-3 )。这将弹出一个文本字段,您可以使用它来输入 SQL 语句。要实际执行 SQL,您必须单击文本字段下面的 Go 按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-3。

The SQL tab in phpMyAdmin

CREATE TABLE 语句

MySQL 将数据存储在表中。自然,开始使用 MySQL 的第一件事就是创建第一个表。要做到这一点,您必须了解更多的 SQL。幸运的是,SQL 语法非常容易阅读和理解。创建表的一般语法如下:

CREATE TABLE table_name (

column_name datatype [any constraints or default values],

column_name datatype [any constraints or default values]

)

正如您所看到的,一个SQL CREATE语句必须声明一个表名。它还应该声明每个表列或属性的名称和数据类型。SQL 语句可以为创建的属性声明约束或默认值。

通过在 phpMyAdmin 控制面板的左栏中单击数据库名称来访问数据库playground。单击屏幕顶部的 SQL 选项卡,就可以创建第一个表了。下面是您需要的 SQL:

CREATE TABLE poll (

poll_id INT NOT NULL AUTO_INCREMENT,

poll_question TEXT,

yes INT DEFAULT 0,

no INT DEFAULT 0,

PRIMARY KEY (poll_id)

)

在 phpMyAdmin 的 SQL 选项卡中输入 SQL 后,可以单击 Go 来执行 SQL(图 6-4 )。这将创建新表。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-4。

Create a new table in the playground database

您可以通过从 phpMyAdmin 左侧的面板中选择 poll 表来研究刚刚创建的新表。接下来,选择“结构”选项卡,该选项卡位于“SQL”选项卡的旁边(图 6-5 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-5。

Poll table structure

这里有很多信息供你思考。您可以看到投票表有四个属性或列:poll_idpoll_questionyesno

您可以看到每个属性都有一个类型。表的字段只能保存正确类型的数据。比如,你只能在poll_idyesno中存储整数。你可以在上图中看到,因为类型是int(11)。你可以无视 11 这个数字。它在那里是因为 11 恰好是 MySQL 整数的默认显示宽度。

Note

整数是一个非十进制数,也就是你所说的整数。

你也可以看到你只能在poll_question中存储文本。再往poll_question里看,还可以看到归类是 utf8。

Note

归类是一组规则,用于指定字符集中哪些字符先出现。很明显 a 在 b 之前,但是字符 7 呢?应该放在字母字符之前还是之后?像#”#€%&这样的特殊字符呢?归类明确规定了字符应该如何排序。

最后,您可以看到yesno属性是用默认值 0 创建的。其他属性都没有默认值。

了解主键

您可以看到,poll_id属性带有下划线。这是一个直观的指示,表明poll_id被设置为投票实体的主键。当属性被声明为主键时,它必须具有唯一的值。因此,无论投票表最终包含多少行数据,都不可能有两个相同的poll_id值。

假设您有一行poll_id为 1 的数据。如果您试图插入另一行数据,并且poll_id也是 1,MySQL 将拒绝新行并给出一个错误消息。主键用于明确地标识一行数据。您实际上可以在 MySQL 中创建没有主键的表,但是这种表是特例。大多数情况下,您会希望创建带有主键的表,因为如果您不能唯一地标识条目,数据就没有什么用处。

您可以看到,poll 表是以这样一种方式创建的,即主键poll_id必须有一个值。属性poll_id被声明为 NOT NULL,这意味着空值对于poll_id来说是不可接受的。poll_id属性必须始终保持一个INTeger值。poll_id属性不能为空或未声明。

了解自动增量

轮询表有一个自动递增的主键。这是一个简单但强大的想法:投票表中的第一行数据将得到 1 的poll_id。下一行将自动获得 2 的poll_id。下一行将得到 3 的poll _ id,以此类推。poll_id的值会自动增加。

MySQL 将跟踪已经被用作poll_id的值。这样,poll中的每一行新数据都将获得一个唯一的poll_id。从某种意义上说,自动递增的主键非常类似于世界上许多国家用来唯一标识一个公民的社会安全号:它是一个用来唯一标识一个事物的任意数字。

INSERT 语句

创建好表后,就可以开始存储数据了。轮询表中的每个新条目都将存储为单独的一行。为了简单起见,您可以从插入单行数据开始。下面是一个 SQL 语句:

INSERT INTO poll (

poll_question

) VALUES (

"Is it hard fun to learn PHP?"

)

这个 SQL 语句将在表中插入一个新的数据行,称为poll。它将为poll_question列或属性声明一个值。更具体地说,poll_question列将得到一个值 PHP 很有趣吗?还记得投票表总共有四个属性或列吗?其余的列poll_idyesno将简单地用默认值创建。所以poll_id会得到值 1,而yesno都会得到值 0。

要让 MySQL 程序执行 SQL 语句,必须首先在 phpMyAdmin 控制面板中选择 playground 数据库。接下来,单击 SQL 选项卡并输入前面的 SQL 语句。最后,单击 Go,实际执行输入的 SQL 语句。

我假设您可以推导出一些INSERT语句的通用语法。我希望您学习以下通用语法,这样您就可以很快开始制定自己的INSERT语句:

INSERT INTO table_name (

column_name, other_column_name

) VALUES (

[data for column], [data for other column]

)

当您编写 I NSERT语句时,您必须首先指出您想要将数据插入哪个表。然后,指定要将数据插入该表的哪些列。如果表中有更多的列,它们将获得默认值。

一旦指定了要添加插入内容的表和列,就必须列出要插入的实际数据。如果在INSERT语句中指定一列,则必须列出一个值。如果指定两列,则必须列出两个值。换句话说,列的数量必须与 I NSERT语句中指示的值的数量相匹配。

SELECT 语句

将一行数据插入投票表后,您可能希望看到新的一行。您可能需要一些视觉上的确认,以确认该行确实被插入了,所以您知道您有一个数据库表,其中存储了一些数据。要从数据库表中检索数据,必须使用 SQL SELECT语句。SELECT语句的一般语法非常简单。

SELECT column_name, column_name FROM table_name

需要注意的主要关键词是SELECT。它用于检索数据库中指定表的数据指定属性FROM。一个SELECT语句总是返回一个填充了任何检索到的数据的临时表。临时表将具有紧接在关键字SELECT之后的属性。您可以使用以下 SQL 语句从轮询表中检索数据:

SELECT poll_id, poll_question, yes, no FROM poll

请转到 phpMyAdmin 控制面板中的 SQL 选项卡,输入上面的SELECT语句,并查看返回的表(参见图 6-6 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-6。

Poll table with one row inserted

在图 6-6 中可以看到,SELECT语句返回一个临时的、未命名的表,该表有四列,每一列对应于SELECT语句中指示的一列。您可以看到表中有一行数据。它有一个为 1 的poll_id和一个poll_questionyesno列分别为 1 和 0。

就其目前的状态来看,这并不算什么,但也许您会意识到,这就是在您的网站上显示站点投票所需的全部数据。您的网站将显示投票问题。网站访问者可以通过 HTML 表单发布他们的回答。可能的选项是“是”或“否”。来自站点访问者的所有响应都将存储在“是”或“否”字段中。所以,用一点数学知识,你可以计算出相对的反应,并显示如下信息:79%的网站访问者认为 PHP 学习起来很难也很有趣。

更新语句

您可能会发现,每次站点访问者提交响应时,您都必须更改投票表中的yesno值。要做到这一点,您必须知道更多的 SQL 语句。你可以假装一个网站用户同意 PHP 很难学。您需要一条 SQL 语句将yes属性的存储值增加 1,如下所示:

UPDATE poll SET yes = yes + 1

WHERE poll_id = 1

如果您愿意,您可以通过在 phpMyAdmin 的 SQL 选项卡中输入并单击 Go 来运行UPDATE语句。如果这样做,可以看到poll中第一行数据的 yes 属性的值为 1。如果再次运行相同的 SQL 语句,yes将得到值 2。

请注意WHERE子句如何限制哪些行将受到更新的影响。只有poll_id为 1 的行将受到影响。由于WHERE子句,表中的任何其他行都不会被更新。

没有WHERE子句的UPDATE语句将更新轮询表中所有行的yes属性。在您的例子中,只有一行,所以WHERE子句不是绝对必要的。但是您将使用的大多数表都不止一行,所以明确指出要更新哪一行是一个好习惯。

在前面的WHERE子句中,您可以确定只有一行将被更新,因为WHERE子句通过主键标识一行数据。您总是可以相信主键可以唯一地标识一行(除非您的表设计得非常糟糕)。

编写数据库驱动的站点投票

为了学习如何从 PHP 使用 MySQL 数据库,让我们编写一个数据库驱动的站点投票。让我们使用操场数据库和投票表来存储数据。您将学习使用所谓的 PDO 对象将您的 PHP 应用连接到 MySQL 数据库。从基本的动态 PHP 站点升级到数据库驱动的动态 PHP 站点会带来一些后果。

显然,您必须从 PHP 连接到数据库,并且您的 PHP 脚本必须与数据库表通信,以获取您的站点所需的内容。PHP 是一种非常宽容的语言,您可以用许多方式来完成这项任务。但是其中一些方法比其他方法更具可扩展性。一旦你开始处理更大的项目,比如一个博客系统,一些起初看起来很容易的方法可以把你的代码变成一个完全没有组织的,混乱的,意大利面条一样的混乱。让我们采用一种经过反复试验的方法来编写可以扩展以适应复杂项目的代码架构,即使这个站点投票是一个简单的项目。

用 MVC 分离关注点

模型-视图-控制器(MVC)设计模式是一致地组织脚本的常用方法。使用一致的方法来组织脚本可以帮助您更快、更有效地开发和调试。

学习理解 MVC 背后的基本原理也可以为学习 MVC 框架做好准备。最终,您可能会遇到 CodeIgnitor、cakePHP、yii 或其他 PHP MVC 框架。这样的框架将帮助您设计和开发更复杂的 web 应用。

最基本的,MVC 将编码问题分成三类:模型、视图和控制器。模型是代表数据的一段代码。您的模型还应该包含您正在构建的系统中涉及的大多数逻辑。视图是可视化显示信息的一段代码。视图要显示的信息是从模型接收的。控制器是一段代码,它从用户那里获取输入,并向相关模型发送命令。简而言之,MVC 将用户交互从系统逻辑和数据的可视化表示中分离出来。

Note

你可以在 http://en.wikipedia.org/wiki/Model-view-controller 阅读更多关于 MVC 的内容。

您已经看到了分离模型、视图和控制器的例子。还记得你是如何为 HTML 页面制作模板的吗?您已经使用了一个包含基本 HTML 页面框架的视图。你可以在第四章中开始建造的画廊中找到它。视图在ch4/templates/page.php中。

在同一个项目中,您创建了一个与视图相关的模型:ch4/classes/Page_Data.class.php,它声明了许多与 HTML 页面内容相关的方法和属性。

模型和视图通过控制器连接起来。在ch4/index.php中,您为模型赋值,并使模型对视图可用,因此可以创建一个包含内容的格式良好的 HTML5 页面,并在浏览器中显示。所以,index.php是你的控制器。

在本书中,我的目标是使用 MVC 的一个简单实现。您将遇到的大多数其他 MVC 实现可能要复杂得多。你可以很容易地找到许多不适合初学程序员的 MVC 例子。一旦您理解了基本的 MVC 原则,并且获得了一些在简单环境中使用这些原则的经验,您会发现理解更复杂的实现要容易得多。

规划 PHP 脚本

让我们保持简单的投票。创建一个index.php来输出一个显示投票的有效 HTML5 页面。索引将是一个前端控制器。

前端控制器是 MVC web 应用中常见的一种设计模式。前端控制器是 web 应用的单一“入口”。到目前为止,您已经在项目中使用了前端控制器。还记得index.php是唯一一个直接载入浏览器的脚本吗?这是给你的前端控制器的想法。

Note

前端控制器设计模式在网上有很好的文档。你可以在 http://en.wikipedia.org/wiki/Front_Controller_pattern 开始你自己的研究。

和前面的项目一样,index.php将输出一个有效的 HTML5 页面,并加载投票控制器。投票控制器应该将poll作为 HTML 返回,这样它就可以显示在index.php上。注意每一个视图都有自己的模型和控制器(图 6-7 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-7。

Distribution of responsibilities

查看如何有一个投票模型、投票控制器和投票视图。这三者应该协同工作来显示功能性投票。您还可以看到页面有自己的模型、视图和控制器。前端控制器是页面控制器。

创建投票项目

您可以创建一个站点结构来模拟代码职责。在XAMPP/htdocs中新建一个文件夹。调用新文件夹poll。在投票文件夹中,您可以创建另外三个文件夹:modelsviewscontrollers(图 6-8 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-8。

Folder structure for the poll project

您可以从图库项目中复制ch4/templates/page.php文件。将page.php的副本保存为poll/views/page.php

同样,从图库项目中复制ch4/classes/Page_Data.class.php,并在poll/models/Page_Data.class.php中保存一份副本。

现在是时候创建poll/index.php并编写一点代码来检查到目前为止一切都很好地一起工作了:

<?php

//complete code for htdocs/poll/index.php

error_reporting( E_ALL );

ini_set( "display_errors", 1 );

//load model

include_once "models/Page_Data.class.php";

$pageData = new Page_Data();

$pageData->title = "PHP/MySQL site poll example";

$pageData->content .= "<h1>Everything works so far!</h1>";

//load view so model data will be merged with the page template

$page = include_once "views/page.php";

//output generated page

echo $page;

这段代码应该不会让你感到意外。它与您在本书的其他项目中使用的代码几乎相同。只有文件夹名称改变了!

我想你的观点也改变了。您现在正从模型-视图-控制器的角度查看这段代码。您可以通过将浏览器指向 http://localhost/poll/index . PHP 来测试代码。您可以看到前端控制器如何将模型和视图连接起来,并将一个格式良好的 HTML5 页面输出到浏览器供用户查看。

制作投票控制器

有了一个用自己的模型和视图创建的几乎空白的页面,并且设置了一个前端控制器,您就可以准备一个文件,用于在浏览器中显示您的投票示例。坚持 MVC 方法,您最终将需要一个投票模型、一个投票视图和一个投票控制器。最小的可能步骤是创建一个基本的轮询控制器,并从前端控制器index.php加载它。在controllers文件夹中创建一个新文件poll.php:

<?php

//complete code listing for controllers/poll.php

return "Poll will show here soon";

接下来,您应该从index.php加载轮询控制器。您应该在创建了$pageData对象之后、包含页面模板之前加载控制器,如下所示:

//partial code listing for index.php

//comment out or delete this line

$pageData->content .= "<h1>Everything works so far!</h1>";

//new line of code to load poll controller

$pageData->content = include_once "controllers/poll.php";

//no changes below

$page = include_once "views/page.php";

echo $page;

如果您保存文件并在浏览器中加载 http://localhost/poll/index . PHP,您应该会看到 Poll 将显示在这里。如果你不这样做,当你输入代码的时候就会出错。

制作民意测验模型

有了初步的投票控制器,您就可以继续开发初步的投票模型了。仅用一种方法定义一个投票类。在models/Poll.class.php中创建一个新文件,如下:

<?php

//complete code for models/Poll.class.php

//beginning of class definition

class Poll {

public function getPollData() {

$pollData = new stdClass();

$pollData->poll_question = "just testing...";

$pollData->yes = 0;

$pollData->no = 0;

return $pollData;

}

}

//end of class definition

注意关键字class是如何用来声明一个类名的。这个类叫做Poll,类定义中的代码定义了所有Poll对象的蓝图。Poll类只有一个方法。它将创建一个名为$pollData的硬编码的StdClass对象,并将其返回给调用者。

看看$pollData对象如何拥有poll_questionyesno的属性。$pollData对象表示显示投票所需的所有内容。换句话说,$pollData模型会对数据进行投票。

制作投票视图

一个数据对象没什么好看的。您可以创建一个简单的投票视图,以便查看投票。在views/poll-html.php中创建一个新文件,如下:

<?php

//complete code for views/poll-html.php

return "

<aside id='poll'>

<h1>Poll results</h1>

<ul>

<li>$pollData->yes said yes</li>

<li>$pollData->no said no</li>

</ul>

</aside>

";

使用投票模型查找投票视图

创建了初步的投票模型和投票视图后,您可以打开投票控制器来连接模型和视图,最后在浏览器中显示一些内容。在编辑器中打开controllers/poll.php,进行以下必要的更改:

<?php

//complete code listing for controllers/poll.php

include_once "models/Poll.class.php";

$poll = new Poll();

$pollData = $poll->getPollData();

$pollView = include_once "views/poll-html.php";

return $pollView;

就这样!你有一个 MVC 投票。如果保存文件并在浏览器中加载 http://localhost/poll/index . PHP,您应该会看到一个格式良好的 HTML5 页面,其中有一个简单的<ul>元素,显示一些初步的、硬编码的投票数据。你可以在图 6-9 中看到它应该是什么样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-9。

The initial poll, seen in Chrome

也许你很想问一个问题,比如为什么我要创建三个不同的文件来显示一个简单的<ul>元素?这是一个完全合理的问题。如果您想要的只是显示一个带有一些硬编码值的<ul>元素,那么 MVC 方法完全是多余的。最好的方法可能是手工编写一个简短的 HTML 文件。

这里的重点是用一个非常简单的例子来介绍 MVC 设计模式,这样就不会有过于复杂的代码隐藏 MVC 的基本原理。使用 MVC 方法,您可以很好地创建数据库驱动的 web 应用。MVC 架构对于这么简单的东西来说几乎是不必要的,但是它确实可以解决你在更复杂的项目中遇到的一些挑战,比如你将在下一章开始制作的博客系统。

MVC 封装了来自控制器的模型视图。这意味着您可以在不更改任何其他内容的情况下更改视图。假设您不想在投票中加入<ul>元素。您可以简单地更改在views/poll-html.php中使用的 HTML 标签,并相信您的代码的其余部分能够正确运行。您可以轻松地更改视图,而无需更改任何其他内容。

类似地,您可以更改内容,并且仍然相信您的代码会按预期运行。将no属性设置为 9 是一个简单的任务。你只需要修改models/Poll.class.php中的一点点代码。您的投票应用是用独立的、松散耦合的元素构建的。

编码就像演奏布鲁斯

一些有创造性倾向的读者可能会反对严格的代码组织。你可能会觉得编码,尤其是当你试图实现一个标准化的方法,比如 MVC,是你的创造力的监狱。你可能会得出这样的结论:它没有给创造力留下空间。

我明白这种反对的理由,但我强烈反对。编码真的很像演奏蓝调音乐,它同样富有创造性,同样需要创造性的个人表达。

是的,实现 MVC 要求你把你的代码分成三类。是的,MVC 会强迫你在特定的、定义明确的地方写代码。当你在学习的时候,这样的限制就像监狱一样。但这就像学习演奏布鲁斯一样。

布鲁斯不是任何一种音乐:布鲁斯就是布鲁斯!要获得蓝调音乐,你不能只弹奏乐器上任何一个音符。在许多蓝调音乐中,你有三个和弦,所有的蓝调即兴创作都源于五声音阶——音乐家只有五个音符可供选择。也许严格的限制对创造性表达没有反作用。也许布鲁斯音乐家如此擅长创造性地表达自己,是因为布鲁斯仅限于三个和弦和五个音符。

要成为一名伟大的蓝调音乐家,你必须熟知蓝调音乐的规则。然后你开始弯曲它们。你开始添加过渡和弦来引导这三个基本蓝调和弦之间的音乐。你开始压五声音阶的五个音符。你开始在布鲁斯的限制下创造性地表达自己。

编码就是这样。这里有严格的、限制性的规则,个人创造性表达的空间很大。在学习的过程中,你会逐渐找到自己的路,但成为一个伟大的程序员和成为一个伟大的音乐家需要付出同样多的努力。所以开始练习吧!

从 PHP 连接到 MySQL

您的 MVC 架构将使建立数据库连接和使用数据库驱动的数据进行投票成为一项相当简单的任务。一旦建立了这样的连接,就可以从数据库中检索数据,并使用 PHP 将其发布为 HTML。这就是数据库驱动网站的本质。

有几种方法可以将 PHP 连接到 MySQL。如果您在互联网或其他书籍上寻找 PHP 代码示例,您可能会遇到一些不同的方法。你很有可能遇到过时的MYSQL()和更新的MYSQLI()

PHP 数据对象(PDO)

在本书中,你将专门使用 PHP 数据对象(PDO)。这是一种从 PHP 连接到数据库的非常安全和有效的方式。PDO 支持多个数据库,并为处理大多数数据库交互提供了一套统一的方法。对于必须支持多种数据库类型的应用,如 PostgreSQL、Firebird 或 Oracle,这是一个很大的优势。

使用 PDO,从一种数据库类型转换到另一种数据库类型通常只需要重写非常少量的代码,然后继续照常工作。

使用 PDO 的一个潜在缺点是它依赖于 PHP5 的面向对象特性,这意味着运行 PHP4 的服务器不能运行使用 PDO 的脚本。这不再是一个大问题,因为很少有服务器不能访问 PHP5 然而,这仍然是你需要注意的事情。

打开连接

是时候连接到您的数据库了。为了简单起见,我建议您在index.php中编写连接代码。当数据库连接在前端控制器中可用时,将它传递给任何其他需要它的代码将会非常容易。

默认 XAMPP 安装有默认用户名 root,没有密码。您为这个学习练习创建了一个名为 playground 的数据库。因此,您可以使用这些凭证连接到本地主机上运行的 MySQL 数据库。

您的 XAMPP 可能使用不同的凭据。您必须使用有效的凭证。您可以通过在index.php中添加几行代码来创建新的数据库连接,如下所示:

<?php

//complete code for index.php

error_reporting( E_ALL );

ini_set( "display_errors", 1 );

include_once "models/Page_Data.class.php";

$pageData = new Page_Data();

$pageData->title = "PHP/MySQL site poll example";

//new code starts here

//database credentials

$dbInfo = "mysql:host=localhost;dbname=playground";

$dbUser = "root";

$dbPassword = "";

try {

//try to create a database connection with a PDO object

$db = new PDO( $dbInfo, $dbUser, $dbPassword );

$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

$pageData->content = "<h1>We're connected</h1>";

}catch ( Exception $e ) {

$pageData->content = "<h1>Connection failed!</h1><p>$e</p>";

}

//comment out loading poll controller

//$pageData->content = include_once "controllers/poll.php";

//end of code changes

$page = include_once "views/page.php";

echo $page;

前面的代码创建了一个 PDO 对象,并将其存储在$db变量中。默认情况下,PDO 将隐藏任何错误消息。您希望看到错误消息来学习。前面的代码会将任何与 PDO 相关的错误显示为所谓的异常。

Note

您还可以使用其他设置来创建到数据库的 PDO 连接。完整详细的报道可以咨询 www.php.net/manual/en/book.pdo.php 。

保存index.php中的更改,并在浏览器中加载 http://localhost/poll/index . PHP。如果您完全正确地做了所有事情,您应该会在浏览器中看到一个输出,确认您连接成功。请注意,轮询控制器不再加载。我们很快会再次加载它。这段代码只测试数据库连接是否成功建立。

使用 try-catch 语句

当您尝试连接到数据库时,许多事情都可能出错。也许您的 XAMPP MySQL 服务器没有运行,或者您提供了无效的凭据,例如拼错的用户名或数据库名称。如果您的代码尝试连接到数据库并失败,则整个脚本都会失败。这是因为 PDO 会抛出一个所谓的异常。

异常很有趣,因为如果您处理异常,您的代码可以继续执行。这就是一条try-catch语句所能做到的。它将尝试运行可能导致异常的代码块。如果抛出一个异常,它将被捕获并处理,因此剩下的脚本可以继续。一个优点是您可以为用户制定有意义的错误消息。try-catch语句的一般语法如下:

try{

//try something that can fail

} catch ( Exception $e ) {

//whoops! It did fail

}

如果您更改了index.php中的一行代码,您可以看到一条try-catch语句正在运行。您可能会尝试使用错误的凭据连接到数据库,如下所示:

//partial code from index.php

//$dbUser = "root";

//use an invalid database user name for testing purposes

$dbUser = "bla bla";

保存并运行index.ph p,您将看到 catch 块正在做它的事情。它将处理抛出的异常并输出一条错误消息。

请改回有效的数据库凭据。现在,您应该已经连接到数据库了。更改您的代码以再次加载轮询控制器:

//partial code listing for index.php

try {

$db = new PDO( $dbInfo, $dbUser, $dbPassword );

$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

//delete or comment out the connection message

//$pageData->content = "<h1>We're connected</h1>";

}catch ( Exception $e ) {

$pageData->content = "<h1>Connection failed!</h1><p>$e</p>";

}

//loading poll controller again

$pageData->content = include_once "controllers/poll.php";

你的代码已经设置好了。您将很快使用数据库驱动的内容进行调查。但是首先,稍微绕一下面向对象编程中的一个重要话题:构造函数。

使用构造函数参数

当使用对象编程时,通常需要在新创建的对象中设置一些初始值。在 PHP 中,您可以通过使用构造函数方法来实现这一点。构造函数是一种方法,当对象第一次被创建时,它在对象的生存期内只运行一次。

这是一个需要了解的重要话题,也是面向对象工具箱的一个强大工具。您可以慢慢来,从一个简单的没有构造函数的类定义的例子开始。我在poll/test.php中创建了一个 PHP 脚本。你不必这样做;您可以简单地阅读下面的代码示例:

<?php

class Greeter {

private $greeting = "Hello";

private $subject = "World";

public function greet(){

return "$this->greeting $this->subject";

}

}

$greeter = new Greeter();

echo $greeter->greet();

如果您要运行 http://localhost/poll/test.php,您将看到以下预期输出:

Hello World

您可以看到如何使用属性$greeting$subject从方法greet()中返回一个字符串。您还可以观察到如何使用$this关键字从类定义内部访问对象属性。

想象一下,你想要一个除了“你好”之外还可以用来说话的Greeter您必须能够在调用greet()之前更改$greeting属性。一种方法是在创建Greeter对象时使用参数,如下所示:

<?php

class Greeter {

private $greeting = "Hello";

private $subject = "World";

//notice the two underscore characters in __construct()

//declare a constructor method with an argument

public function __construct( $g ) {

$this->greeting = $g;

}

public function greet(){

return "$this->greeting $this->subject";

}

}

//call constructor with an argument

$greeter = new Greeter( "Good Morning" );

echo $greeter->greet();

如果您要在浏览器中重新加载 http://localhost/poll/test.php,您会看到以下内容:

Good Morning World

构造函数方法是在创建新对象时运行的方法。在技术术语中,创建新对象的过程通常称为实例化。为了声明一个类的构造函数,你声明一个必须被调用的新方法__construct()。注意,有两个下划线字符。

如果一个类有一个构造函数方法,这个构造函数方法将在一个新的对象被实例化时运行。所以,写new Greeter()会调用Greeter的构造函数。在前面的测试示例中,构造函数接受一个参数,因此在调用它时必须发送一个值。接收到的值存储在预定义的属性$greeting中。这样,只要对象存在,接收到的值就会被记住。

Note

理解论点可能很棘手。你可以重读第三章中关于函数参数的解释,或者你可以在网上搜索“理解带参数的 PHP 函数”或者“学习 PHP 方法和参数”你一定会发现许多使用各种隐喻和代码示例的解释。我希望你能找到一个对你有用的解释。

与轮询模型共享数据库连接

您已经在index.php中创建了一个数据库连接,一个 PDO 对象。您可以使用带参数的构造函数的思想来共享与轮询模型的数据库连接。在models/Poll.class.php中更新您的代码,如下所示:

<?php

//complete code listing for models/Poll.class.php

class Poll {

//new code: declare a new property

private $db;

//new code: declare a constructor

//method requires a database connection as argument

public function __construct( $dbConnection ){

//store the received conection in the $this->db property

$this->db = $dbConnection;

}

//no code changes below – keep method getPollData() as it is

public function getPollData(){

$pollData = new stdClass();

$pollData->poll_question = "just testing...";

$pollData->yes = 0;

$pollData->no = 0;

return $pollData;

}

}

前面的变化已经准备好了Poll类。它现在可以接收一个 PDO 对象。所以现在,你必须调用Poll类的构造函数并传递一个 PDO 对象作为参数。停下来思考一下。您将在代码中的什么位置加载您的投票模型?

从您的投票控制器,从poll/controllers/poll.php,如下所示:

<?php

//complete code listing for controllers/poll.php

include_once "models/Poll.class.php";

//Only change here: pass PDO object as argument

$poll = new Poll( $db );

$pollData = $poll->getPollData();

$pollView = include_once "views/poll-html.php";

return $pollView;

现在,您已经将数据库连接传递给了轮询模型,传递给了刚刚创建的新的Poll对象。您现在可以保存并测试您的工作。您应该会看到和以前完全一样的投票。您的代码共享一个数据库连接,但是它还没有使用它做任何事情。

也许你想知道controllers/poll.php中的 PHP 代码是如何知道变量$db的。毕竟,$db是在index.php中声明的,那么怎么可能在另一个文件中使用呢?这是一个很好的问题,答案一点也不明显——除非你理解它。

变量$dbindex.php中声明。在controllers/poll.php有,因为controllers/poll.php包含在index.php里。包含一个文件很像从一个文件中复制所有代码并粘贴到另一个文件中。因此,包含文件中声明的所有变量都可以在包含文件中使用,反之亦然。

使用 PDOStatement 检索数据

您已经建立了从 PHP 到运行在本地 XAMPP 上的 MySQL 的连接。PDO 对象通过 phpMyAdmin 控制面板显式地连接到您已经用 SQL 创建的操场数据库。

在操场数据库中,有一个poll表,其中有一个poll_question以及它的yesno属性值。您已经准备好从数据库中检索数据,因此可以在 PHP 中使用它。这需要 PHP 代码中的几个步骤。

处理投票数据的代码属于投票模型。所以,你要开models/Poll.class.php。可以改变现有的方法getPollData。此时,该方法返回硬编码的轮询数据。您希望它返回数据库驱动的轮询数据。

首先,您必须编写一条 SQL 语句,并使用 PDO 将它传递给 MySQL。PDO 可以告诉 MySQL 它应该执行 SQL 语句并返回一个结果:投票数据。下面是如何用 PHP 来表达:

//partial code listing for models/Poll.class.php

//udate existing method

public function getPollData () {

//the actual SQL statement

$sql = "SELECT poll_question, yes, no FROM poll WHERE poll_id = 1";

//Use the PDO connection to create a PDOStatement object

$statement = $this->db->prepare($sql);

//tell MySQL to execute the statement

$statement->execute();

//retrieve the first row of data from the table

$pollData = $statement->fetchObject();

//make poll data available to the caller

return $pollData;

}

前面的代码查询数据库并从 poll 表中检索第一行数据。您已经有了一个投票视图和一个将投票模型与投票视图联系起来的投票控制器。

保存您的工作,并在浏览器中加载 http://localhost/poll/index . PHP,查看它的运行情况。您应该看到投票表中的数据以 HTML 的形式表示和显示。只需花一分钟来惊叹您所看到的:一个数据库驱动的网页!

您可能还会欣赏 HTML 和数据之间的清晰分离。如果您不喜欢我在示例中使用的 HTML 元素,您可以很容易地将其更改为其他元素。改变 HTML 元素不会以任何方式影响 PHP 脚本。

同样,您很快就会看到,数据可以改变,而不必改变 HTML 元素。HTML 元素的内容将根据从数据库中检索到的值动态更新。

不过,有一个依赖,重要的是你要看到它。在views/poll-html.php中,您需要一个$pollData对象,并且它必须具有 yes 和 no 属性。因此,在views/poll.php中,您必须创建这样一个对象,否则轮询将失败。该对象的名称和属性是至关重要的。

PDO 和 PDOStatement 对象

我想指出的是,在 PDO 将 SQL 传递给 MySQL 之前,应该将 SQL 字符串转换成一个PDOStatement对象,然后在 MySQL 中执行 SQL。PDO 有一个叫做prepare()的方法,它将一个简单的 SQL 字符串转换成一个PDOStatement对象。

PDOStatement对象有另一个方法,叫做execute(),用于让 MySQL 执行 SQL。PDOStatement s 还有一个方法,叫做fetchObject(),从查询的数据库表中检索一行数据。

Note

可以在 www.php.net/manual/en/class.pdostatement.php 查阅PDOStatement对象的官方文档。

PDOStatement 的 fetchObject()方法

fetchObject()方法返回一个StdClass对象,代表被查询表中的一行数据。在前面的代码中,您有一个 SQL 语句,它从投票表中选择了poll_questionyesno

因此,返回的StdClass对象将被自动创建,并带有poll_questionyesno的属性。您可以通过StdClass对象属性访问投票表中的数据。在下面的内容中,您可以看到这些属性是如何在views/poll-html.php中使用的,以在网页上显示从投票表中检索到的数据:

<?php

//complete code for views/poll-html.php

//$pollData->no holds the current value of the no attribute

//$pollData->yes holds the current value of the yes attribute

return "

<aside id='poll'>

<h1>Poll results</h1>

<ul>

<li>$pollData->yes said yes</li>

<li>$pollData->no said no</li>

</ul>

</aside>

";

您已经为$pollData使用了一个StdClass。最初,您对轮询的属性和值进行了硬编码。最大的不同是,fetchObject()会自动创建一个新的StdClass对象并返回它。创建的StdClass对象的属性名与表格列名相同。

例如,$pollDatapoll_questionyesno属性,因为 SQL SELECT语句创建了一个带有poll_questionyesno列的临时表。临时表从 MySQL 返回到 PHP。PDO 将接收到的数据转换成了一个StdClass对象,因为您使用了fetchObject()方法。

显示投票表格

你展示了数据库驱动的内容,这真是太棒了,但是你的例子还不是一个很好的网站投票。网站访问者应该被允许提交他们的意见,从而对投票结果做出贡献。不管怎样,你必须为站点访问者提供一个图形用户界面:HTML 表单是显而易见的选择。花几秒钟思考一下。这种表单的 HTML 属于哪里?在模型、视图或控制器中?在你继续阅读之前,想出你的最佳答案…

HTML 表单是用户看到的东西,所以它是一个视图。更新views/poll-html.php中的投票视图代码,如下所示:

<?php

//complete code listing for views/poll-html.php

//new code below

$dataFound = isset( $pollData );

if( $dataFound === false ){

trigger_error( 'views/poll-html.php needs an $pollData object' );

}

return "

<aside id='poll'>

<form method='post' action='index.php'>

<p>$pollData->poll_question</p>

<select name='user-input'>

<option value='yes'>Yes, it is!</option>

<option value='no'>No, not really!</option>

</select>

<input type='submit' value='post' />

</form>

<h1>Poll results</h1>

<ul>

<li>$pollData->yes said yes</li>

<li>$pollData->no said no</li>

</ul>

</aside>

";

正如您可能已经想到的,这将显示一个 HTML 表单,非常类似于您在第三章中为动态测验创建的表单。你可以刷新你的浏览器看看它是什么样子。

触发自定义错误消息

你注意到那些触发错误信息的初始代码行了吗?好了,您已经确定了$pollData对象对于脚本在输出投票时进行有意义的协作是至关重要的。

如果你——或者从事同一项目的开发人员——忘记创建一个$pollData对象或者可能拼错了(例如,$pillData),站点投票将不会正常进行。你可以测试一个$postData对象是否可用。如果不是,您可以触发一个自定义的错误消息。这实际上是一种自助:如果您或其他开发人员偶然犯了这个错误,您可以相信系统会输出一个有意义的错误消息,这样就可以轻松快速地纠正错误。

当你独自开发新项目时,你可能会花大量的时间来修正错误。使用自定义错误信息,您可以通过测试您预测可能发生的错误来加快开发时间。

根据表单输入更新数据库表

完成站点投票示例还需要最后一步。您应该检索通过表单提交的任何用户输入,并用收到的输入更新投票表。如果一个站点访问者提交了一个 no,您应该增加轮询数据库表中的no属性的值。

这与动态测验非常相似:您需要检测表单是否被提交。如果是,您可以检索提交的输入并相应地更新投票表。您可以看到这里有两个关注点:第一,检测输入;接下来,更新数据库。

更新数据库是模型的任务。处理用户交互是管制员的工作。您可以从用更新数据库表的方法更新轮询模型类开始。更新models/Poll.class.php

<?php

//declare a new method for the Poll class in models/Poll.class.php

//NB. Declare the method inside the Poll class code block

public function updatePoll ( $input ) {

if ( $input === "yes" ) {

$updateSQL = "UPDATE poll SET yes = yes+1 WHERE poll_id = 1";

} else if ( $input === "no" ) {

$updateSQL = "UPDATE poll SET no = no+1 WHERE poll_id = 1";

}

$updateStatement = $this->db->prepare($updateSQL);

$updateStatement->execute();

}

//no other code changes in Poll class

新方法updatePoll(),应该相当容易理解。如果用户提交了 yes,那么您创建一个 SQL 字符串来更新 poll 表中的yes属性。如果用户提交了一个 no,您创建一个不同的 SQL 字符串来更新no属性。注意,无论提交的是 yes 还是 no,创建的 SQL 字符串都将存储在一个名为$updateSQL的变量中。

一旦创建了$updateSQL字符串,就使用 PDO 方法prepare()将 SQL 字符串转换成一个PDOStatement对象,该对象存储在变量$updateStatement中。通过PDOStatement,你可以execute()查询到实际更新的民意测验表。

在被调用之前,updatePoll()方法不会做任何事情。调用模型方法是控制器的工作…

响应用户输入

控制器负责处理用户交互。因此,poll controller是从投票表单中截取用户输入的合适位置。脚本中还需要添加一些内容。

你必须检查表格是否已经提交。如果表单已提交,您应该获取通过表单接收的提交输入。当您调用方法到updatePoll()时,接收到的输入应该被传递给模型。下面是如何在 controllers/poll.php 中用 PHP 来表达这一点:

<?php

//complete code listing for controllers/poll.php

include_once "models/Poll.class.php";

$poll = new Poll( $db );

//check if form was submitted

$isPollSubmitted = isset( $_POST['user-input'] );

//if it was just submitted...

if ( $isPollSubmitted ) {

//get input received from form

$input = $_POST['user-input'];

//...update model

$poll->updatePoll( $input );

}

//no changes here

$pollData = $poll->getPollData();

$pollAsHTML = include_once "views/poll-html.php";

return $pollAsHTML;

如果您输入此代码,您将拥有一个全功能的站点投票,允许站点访问者查看其他站点访问者对您的投票问题的看法。任何网站访问者都可以通过表单发表意见。输入将保存在数据库中,并显示在index.php上。

您可以看到前面的脚本检查是否提交了调查表。它发生在剧本的顶部附近。代码寻找一个名为user-input的 URL 变量。这样的 URL 变量将在提交投票表单时声明,因为投票表单有一个<select name='user-input'>元素。投票表单使用了POST方法,所以您的代码应该在$_POST超全局中寻找用户输入,如下所示:

//check if form was submitted

$isPollSubmitted = isset( $_POST['user-input'] );

if ( $isPollSubmitted ) {

$input = $_POST['user-input'];

$poll->updatePoll( $input );

}

如果设置了 URL 变量,就知道已经提交了poll表单。在这种情况下,您可以检索站点访问者的输入。接下来,代码调用Poll对象中的updatePoll()方法,并将用户输入作为参数传递。

摘要

现在您已经了解了全貌——您正在用面向对象的 PHP 开发数据库驱动的 web 应用!这幅图的某些部分可能还不太清楚。随着你阅读本书的其余部分,其中一些将会改变。但是真正的改变,真正有意义的学习,发生在你开始创建自己的项目的时候。

当你开始阅读这本书时,你可能对静态网站有所了解,也就是用手写 HTML 创建的网站。有了静态 HTML,你可以写一篇关于你认为 PHP 有多难/多容易的文章。用户将能够阅读你的观点——从你到你的用户的交流是单向的。

在前几章中,你学习了用 PHP 制作动态解决方案。您创建了动态测验,允许您的用户与您的内容进行交互。因为用户可能对测验问题有不同的回答,所以不同的用户会看到不同的内容。你的测验是一种交流你对学习 PHP 的看法的吸引人的方式。用户可以阅读他们的观点是否和你的一致,你可以写一个小测验,让它提供幽默的回答——交流仍然是单向的,从你到你的用户。

现在,您开始了解数据库驱动的网站。当您使用数据库存储内容时,新的可能性就出现了。用户可以向您创作的网站提供新内容。可以发布用户提交的新内容,以便所有站点访问者可以看到其他站点访问者所做的贡献。您制作了一个便于网站访问者之间交流的网页。注意现在的交流是多方向的,你不再是内容的唯一作者。随着您从静态解决方案发展到动态解决方案,再发展到数据库驱动的解决方案,这些都是向您展现的一些新的可能性。

在这一章中,你已经学习了 SQL 语句的基础知识,以及如何通过 PHP 脚本与数据库进行交互。在接下来的章节中,你将学习如何用一个基本的条目管理器来创建一个博客,它将允许你创建、修改和删除条目,以及在公共页面上显示它们。

七、构建条目管理器

至此,你已经足够了解如何开始构建你的个人博客系统了。这本书的其余部分涵盖了发展和改善个人博客。本章将带你了解如何构建你的博客应用的主干:博客条目管理器。您将构建的部分包括以下内容:

  • 一个视图,它是一个接受条目输入的 HTML 表单
  • 一个控制器,用于处理来自表单的输入
  • 一个模型,用于保存和检索数据库中的条目

本章结束时,你将拥有一个基本的个人博客系统入口管理器。在构建条目管理器的过程中,您将会重温以前讨论过的主题,例如基本的数据库设计、SQL、用 MVC 组织您的 PHP,以及连接到数据库。

创建 blog_entry 数据库表

任何新应用最重要的步骤之一是规划保存数据的表。这对以后扩展应用的容易程度有很大的影响。扩展是应用处理更多信息和/或用户的扩展,如果在开始一个新项目时没有向前看,这可能是一个巨大的痛苦。首先,您的博客需要存储几种类型的条目信息才能运行,包括以下内容:

  • 唯一标识
  • 条目标题
  • 条目文本
  • 创建日期

为条目表中的每个条目使用一个唯一的 ID,使您能够访问只包含一个数字的信息。这对于数据访问非常有帮助,尤其是如果数据集在将来发生了变化(例如,如果您向表中添加了一个“imageURL”列)。

第一步是确定条目表需要的字段。您的表必须定义每列中存储什么类型的信息,所以让我们快速看一下每列必须存储的信息。

  • entry_id:识别条目的唯一编号。这将是一个正整数,这个数字自动递增是有意义的,因为这样可以确保这个数字是唯一的。因为每个条目都有一个唯一的entry_id,所以可以用这个数字来标识一个条目。entry_id将是该表的主键。
  • title:应该相对较短的字母数字字符串。您将把字符串限制在 150 个字符以内。
  • entry_text:长度不定的字母数字字符串。你不会限制这个字段的长度(在合理的范围内)。
  • date_created:条目最初创建时自动生成的时间戳。您将使用它来按时间顺序对条目进行排序,并让您的用户知道条目最初是何时发布的。

现在是创建数据库的时候了。打开你的 XAMPP 控制面板,启动 MySQL 和 Apache。将浏览器指向 http://localhost/phpmyadmin/下面是执行此操作的 SQL:

CREATE DATABASE simple_blog CHARSET utf8

下一步是编写创建entries表的代码。确保从 phpMyAdmin 控制面板左侧的菜单中选择simple_blog数据库。接下来,打开 SQL 选项卡,输入以下 SQL 语句:

CREATE TABLE blog_entry (

entry_id INT NOT NULL AUTO_INCREMENT,

title VARCHAR( 150 ),

entry_text TEXT,

date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY ( entry_id )

)

您可以看到创建blog_entry表的语句类似于您用来创建 poll 表的CREATE语句。你也可以看到一些不同之处。

title属性有一个新的数据类型:VARCHAR(150)。因此,任何标题都必须包含长度为VARCHAR角色。博客条目标题可以是 0 到 150 个字符长的字符串。如果您要插入 151 个字符长的标题,只有前 150 个字符会保存在blog_entry表中。这就是VARCHAR(150)的作用。

属性date_created也用新的数据类型TIMESTAMP声明。一个TIMESTAMP保存着关于某一时刻的相当精确的信息。它将年、月、日、小时、分钟和秒存储为 YYYY-MM-DD HH:MM:SS

您已经看到了如何使用默认值创建 MySQL 表属性。这里再次针对date_created属性。第一次插入新条目时,MySQL 会根据服务器的时钟自动存储当前的TIMESTAMP

规划 PHP 脚本

你已经为你的博客创建了一个数据库。合乎逻辑的下一步是用 PHP 创建一个博客条目编辑器,这样您就可以创建新的博客条目。博客条目编辑器只适用于博客作者。普通网站访问者应该不能创建新的条目。普通的站点访问者应该只是看到你的博客条目,而不能编辑现有的条目或创建新的条目。

完成这项任务的一个方法是创建两个主要的网站入口:index.php供普通访问者使用,而admin.php只供你观看。在 MVC 术语中,index.phpadmin.php都是前端控制器。在本书的后面,我将向你展示如何通过登录来限制对admin.php的访问。

管理页面应该能够列出所有的博客条目,它应该给你一个条目编辑器,这样你就可以创建新的条目,编辑或删除现有的条目。您将需要单独的视图:一个用于列出所有条目,另一个用于显示编辑器。

脚本admin.php应该输出一个 HTML5 页面。它将是你的前端控制器,它将决定是显示编辑器还是列出所有条目。图 7-1 使用 MVC 思想来开发博客管理模块的示意图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-1。

Distribution of responsibilities

注意每个视图都有一个相应的控制器。还要注意,条目使用条目模型来显示所有条目,并由编辑器重用来保存新条目。让我们回顾一下 MVC 方法提出的关注点分离(见图 7-2 )。视图是用户可以看到的东西。模型包含内容。控制器负责将正确的视图与正确的模型连接起来,并将结果输出返回给用户。控制器还负责响应用户输入;通常这意味着更新模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-2。

Model-view-controller

创建博客网站

XAMPP/htdocs中新建一个文件夹。调用新文件夹blog。在blog文件夹中,你可以创建另外四个文件夹:modelsviewscontrollerscss

您可以从图库项目中复制ch4/templates/page.php文件。在blog/views/page.php中保存一份page.php的副本。同样,从图库项目中复制ch4/classes/Page_Data.class.php,并在blog/models/Page_Data.class.php中保存一个副本。在css/blog.css中创建一个空白样式表。

现在是时候创建blog/admin.php并编写一点代码来检查到目前为止一切都很好地一起工作了:

<?php

//complete code for blog/admin.php

error_reporting( E_ALL );

ini_set( "display_errors", 1 );

include_once "models/Page_Data.class.php";

$pageData = new Page_Data();

$pageData->title = "PHP/MySQL blog demo";

$pageData->addCSS("css/blog.css");

$pageData->content = "<h1>YES!</h1>";

$page = include_once "views/page.php";

echo $page;

保存文件并在浏览器中加载 http://localhost/blog/admin . PHP。如果一切按预期运行,您应该得到一个格式良好的 HTML5,带有一个 PHP/MySQL 博客演示的<title>和一个高兴地欢呼是的<h1>元素!在浏览器中看到“是”是对项目设置正确的直观确认。

创建条目管理器导航

您的条目管理器应该有两个基本视图:一个列出所有条目,另一个显示条目编辑器。让我们为条目管理器创建一个导航。在你完成之前,你可以期待这个项目包含许多 PHP 文件。我建议您创建一些文件夹,将与管理模块相关的脚本放在一起。在现有的views文件夹中创建一个名为admin的文件夹。在views/admin/admin-navigation.php新建一个文件:

<?php

//complete code for views/admin/admin-navigation.php

return "

<nav id='admin-navigation'>

<a href='admin.php?page=entries'>All entries</a>

<a href='admin.php?page=editor'>Editor</a>

</nav>";

您可以看到,条目管理器导航与您在第四章和第五章中为动态图库创建的导航非常相似,或者与第二章中为动态作品集站点创建的导航非常相似。您应该记住 URL 变量page,每当用户单击导航项目时,该变量就会被编码。

管理导航是一个静态视图,这意味着脚本中没有动态或数据库驱动的信息。导航不需要模型,因为所有内容都是硬编码到视图中的。您确实需要一个控制器来加载导航。这将非常简单,因为导航应该一直被加载和显示。你可以从admin.php开始控制:

//partial code listing for admin.php

$pageData->addCSS("css/blog.css");

//code changes below here

//comment out or delete the YES

//$pageData->content = "<h1>YES!</h1>";

//load navigation

$pageData->content = include_once "views/admin/admin-navigation.php";

//no changes below

$page = include_once "views/page.php";

echo $page;

保存您的工作,并在浏览器中重新加载 http://localhost/blog/admin . PHP。您应该会看到浏览器窗口顶部显示的导航。单击导航项目不会有任何立即可见的效果;导航只是一个视图。单击任何导航项目都会对一个名为page的 URL 变量进行编码。你可以在浏览器的地址栏里看到它。您需要创建控制器代码来响应用户交互,比如单击。

加载管理模块控制器

您可以使用admin.php来控制当导航项目被点击时要做什么。这是前端控制器主要关心的问题。前端控制器应该加载与用户单击的导航项目相关联的任何控制器。在你的导航中有两个链接,所以你需要两个控制器。

要在单击导航项目时看到浏览器中的任何可见变化,您必须创建两个初级控制器。在controllers文件夹中创建一个名为admin的新文件夹,并创建一个新文件来控制条目编辑器视图,如下所示:

<?php

//complete source code for controllers/admin/editor.php

return "<h1>editor controller loaded!</h1>";

如您所见,编辑器控制器一开始不会做很多事情。最初,您只是想检查是否连接了正确的文件。有了这些,你就可以开发更复杂的代码了。创建另一个文件来控制最终列出所有条目的视图,如下所示:

<?php

//complete source code for controllers/admin/entries.php

return "<h1>entries controller loaded!</h1>";

您可以从admin.php载入这些控制器。您必须检查导航项目是否被点击。如果是,您应该加载相应的控制器。在创建了$pageData对象之后,在echo之前插入这几行代码:

<?php

//complete code for blog/admin.php

error_reporting( E_ALL );

ini_set( "display_errors", 1 );

include_once "models/Page_Data.class.php";

$pageData = new Page_Data();

$pageData->title = "PHP/MySQL blog demo";

$pageData->addCSS("css/blog.css");

$pageData->content = include_once "views/admin/admin-navigation.php";

//new code begins here

$navigationIsClicked = isset( $_GET['page'] );

if ( $navigationIsClicked ) {

//prepare to load corresponding controller

$contrl = $_GET['page'];

} else {

//prepare to load default controller

$contrl = "entries";

}

//load the controller

$pageData->content .=include_once "controllers/admin/$contrl.php";

//end of new code

$page = include_once "views/page.php";

echo $page;

记住文件名entries.phpeditor.php。这些名字是必不可少的。它们必须与用户单击导航项时声明的 URL 变量page的相应值相匹配。让我们仔细看看导航中使用的href值:

<a href='admin.php?page=entries'>All entries</a>

<a href='admin.php?page=editor'>Editor</a>

当用户点击All entries项时,URL 变量page得到一个值entries。在admin.php中,使用$_GET获取 URL 变量page。字符串值entries存储在一个名为$contrl的变量中,随后用于包含controllers/admin/$contrl.php,这将真正转化为包含controllers/admin/entries.php,因为变量$contrl保存值entries

如果点击Editor项,将包含controllers/admin/editor.php

在前面的代码中,您应该不会感到惊讶。尽管如此,明智的做法是进行理智检查。保存文件并在浏览器中重新加载 http://localhost/blog/admin . PHP。默认情况下,您应该会看到从条目控制器返回的消息。如果您单击编辑器的导航项,您应该会看到从编辑器控制器返回的消息。

创建条目输入表单

现在你有了一个动态导航,你不妨把它用在一些有意义的事情上。您可以为编辑器显示一个 HTML 表单。最终,应该可以使用编辑器创建新的博客条目。因此,编辑器表单应该有用于创建条目标题和条目文章的字段。还应该有一个保存新条目的按钮。同时,您也可以创建一个删除条目的按钮。创建一个新文件views/admin/editor-html.php,如下所示:

<?php

//complete source code for views/admin/editor-html.php

return "

<form method='post' action='admin.php?page=editor' id='editor'>

<fieldset>

<legend>New Entry Submission</legend>

<label>Title</label>

<input type='text' name='title' maxlength='150' />

<label>Entry</label>

<textarea name='entry'></textarea>

<fieldset id='editor-buttons'>

<input type='submit' name='action' value='save' />

<input type='submit' name='action' value='delete' />

</fieldset>

</fieldset>

</form>

";

大多数前面的代码应该是熟悉的,即使您看到了一些您以前可能没有遇到过的元素。你可以看到两个<fieldset>元素。它们用于将相关的表单字段分组在一起。主<fieldset>有一个<legend>元素。一个<legend>就像一个<fieldset>元素的标题。

条目标题的<input>元素的maxlength属性设置为 150。您可能会猜到显示的文本字段只接受 150 个字符。这很好,因为数据库中的条目表接受最多 150 个字符的新title属性。

属性增强了表单的可用性,因为用户很难通过表单创建无效的标题。属性执行客户端验证,只允许提交有效的标题。需要记住的一点是,客户端验证对于增强可用性非常重要。它不会提高安全性,因为您应该预料到恶意用户能够覆盖客户端验证。

创建了新的编辑器视图后,您必须更新控制器,以便它显示视图。编辑器的控制器可以在controller/admin/editor.php中找到。更改代码,如下所示:

<?php

//complete source code for controllers/admin/editor.php

$editorOutput = include_once "views/admin/editor-html.php";

return $editorOutput;

当从admin.php加载控制器时,这几行代码应该连接视图并显示它。重装 http://localhost/blog/admin . PHP 就可以自己看了?page =浏览器中的编辑器。您应该会看到表单。

样式编辑器

您可能会同意,无样式的条目编辑器表单看起来很难看。一点点 CSS 可以带你走很长一段路,提高审美。您很可能希望在编辑器的视觉设计上做更多的工作。这里有一个小小的 CSS 让你开始:

/* code listing for blog/css/blog.css */

form#editor{

width: 300px;

margin:0px;

padding:0px;

}

form#editor label, form#editor input[type='text']{

display:block;

}

form#editor #editor-buttons{

border:none;

text-align:right;

}

form#editor textarea, form#editor input[type='text']{

width:90%;

margin-bottom:2em;

}

form#editor textarea{

height:10em;

}

您可能还记得,也可能不记得,我在admin.php中的代码期望在css/blog.css中找到一个外部样式表。如果你的admin.php和我的一样,你会希望你的样式表在那个位置。显然,您可以在不同的文件夹中用不同的名称创建样式表。记得更新admin.php,让它指向你的样式表。你可以在图 7-3 中看到最终的编辑器设计。

我喜欢保持我的 HTML 整洁,没有idclass属性,我喜欢手工编写我的 CSS。我需要能够将样式规则与正确的 HTML 元素挂钩。我发现,通过使用上下文选择器和属性选择器,我经常可以应付过去。

如果你更喜欢使用 CSS 框架,你可能会采取完全相反的方法。您将手动编写很少的 CSS,并大量使用您最喜欢的 CSS 框架使用的classid属性。你看着办吧!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-3。

The editor displayed in Google Chrome

连接到数据库

您已经完成了基本的编辑器视图。很快,您应该能够通过编辑器表单将新的博客条目插入到您的blog_entry数据库表中。为此,您需要一个从 PHP 应用到 MySQL 数据库的数据库连接。

您可以采用与轮询相同的方法:使用 PDO 建立连接,并在前端控制器中建立连接,以便与随后加载的控制器共享。

您可以在admin.php中创建一个 PDO 对象,并与您将要加载的所有控制器共享它。在创建了$pageData之后,在echo之前,您应该在admin.php中编写以下代码:

//partial code listing for admin.php

//new code starts here

$dbInfo = "mysql:host=localhost;dbname=simple_blog";

$dbUser = "root";

$dbPassword = "";

$db = new PDO( $dbInfo, $dbUser, $dbPassword );

$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

//end of new code – no changes below

$navigationIsClicked = isset( $_GET['page'] );

if ($navigationIsClicked ) {

$contrl = $_GET['page'];

} else {

$contrl = "entries";

}

$pageData->content .=include_once "controllers/admin/$contrl.php";

$page = include_once "views/page.php";

echo $page;

请注意输入正确的数据库凭证,并指明您想要连接到simple_blog数据库。您可以通过重新加载浏览器来测试连接是否正常。如果连接失败,您应该会看到一条错误消息。因此,如果您没有看到错误消息,这意味着您已经成功连接到数据库。

使用设计模式

设计模式是对一项常见任务的通用最佳实践解决方案。有些设计模式定义得相当全面。随着经验的增长,你会遇到更多的设计模式。作为一个绝对的初学者,你不需要对设计模式进行全面的处理——那可能会更令人困惑而不是更有帮助。

这本书使用了一些设计模式的简单实现。您已经看到了前端控制器设计模式的简单实现和同样简单的 MVC 实现。您可以看到在同一个项目中组合几种设计模式是可能的。

Note

你可以在 http://en.wikipedia.org/wiki/Software_design_pattern 了解设计模式。

许多设计模式对于初学者来说很难理解。但是许多常见的编码问题都有设计模式。这本书在简单的实现中使用了一些设计模式。您应该知道您在代码中使用的是哪种设计模式,以及还有其他的可能性。你可以把这样的知识当做未来发展的路线图。

Note

我可以强烈推荐 Matt Zandstra 的书《PHP 对象、模式和实践,第四版》(Apress,2013)。在 www.apress.com/18 找到它。这不是一本真正适合初学者的书,但你可以把它记在心里以备将来参考。

表格数据网关设计模式

您的代码必须与数据库表进行广泛的通信。有许多方法可以持续地管理这种沟通。一种方法是实现表数据网关设计模式。

表数据网关设计模式并不是大型 MVC 框架中处理数据库访问最常见的方法。许多流行的 PHP MVC 框架使用活动记录模式。我建议您使用表数据网关,因为这是一种相对容易理解的设计模式。表数据网关模式指定为数据库中的每一个表创建一个(PHP)类。这个想法是你的系统和那个表之间的所有通信都通过一个这样的对象发生。表数据网关封装了系统和特定数据库表之间的数据通信。

这意味着该通信所需的所有 SQL 语句都将在表数据网关类定义中声明。这有几个优点:一是您知道在哪里编写 SQL 语句。因此,您也知道在哪里可以找到与特定数据库表相关的 SQL 语句。

数据库设计和开发本身就是一种职业。如果作为一名 PHP 开发人员,您将所有的 SQL 封装在相对较少的类定义中,那么您团队中的任何数据库专家都只需要处理这几个类。与将 SQL 语句分散在代码库中相比,这是一个巨大的优势。

编写 Entry_Table 类

最初,您需要能够插入从编辑器表单接收的新博客条目。您可以为blog_entry表的表数据网关创建一个新的类定义。它将用于与您的数据库通信。

新类将需要一个 PDO 对象来与数据库通信。你可以使用你在前一章已经看到的想法。使用构造函数方法创建一个类,并将 PDO 对象作为参数传递。停下来思考一下你将要写的类。应该是模型、视图,还是控制器?答案应该是显而易见的:负责与您的数据库通信的脚本应该是一个模型!

在名为Blog_Entry_Table.class.php的新文件中的models文件夹中创建一个新的类定义。下面是它的代码:

<?php

//complete code listing for models/Blog_Entry_Table.class.php

class Blog_Entry_Table {

private $db;

//notice there are two underscore characters in __construct

public function __construct ( $db ) {

$this->db = $db;

}

public function saveEntry ( $title, $entry ) {

$entrySQL = "INSERT INTO blog_entry ( title, entry_text )

VALUES ( '$title', '$entry' )";

$entryStatement = $this->db->prepare( $entrySQL );

try{

$entryStatement->execute();

} catch (Exception $e){

$msg = "<p>You tried to run this sql: $entrySQL<p>

<p>Exception: $e</p>";

trigger_error($msg);

}

}

}

在被另一个脚本使用之前,Blog_Entry_Table不会做任何事情。在使用之前,我想看看这段代码。如您所见,Blog_Entry_Table有一个属性db和两个方法:constructorsaveEntry()。这个构造函数接受一个 PDO 对象作为参数。接收到的 PDO 对象将被存储在db属性中。这样,Blog_Entry_Table的所有方法都可以访问 PDO 对象,并通过它访问simple_blog数据库。

在面向对象的术语中,Blog_Entry_Table和 PDO 现在通过has-a关系联系在一起。Blog_Entry_Table has-a PDO 的对象。

在开发的早期阶段,您只能保存新条目。因此,Blog_Entry_Table类除了构造函数之外只有一个方法。saveEntry()方法有两个参数:titleblog_entry保存在数据库中。

变量$entrySQL保存一个 SQL 字符串,以使用接收到的titleblog_entry插入一个新的blog_entry$entryStatement是一个PDOStatement对象,然后你可以try()execute()来实际插入一个新的blog_entry

如果该操作失败,它将抛出一个Exception对象,您的代码将捕获该对象。如果发生异常,您的代码将触发一个错误,显示导致异常的 SQL 字符串以及对异常的更详细的查看。

处理表单输入并保存条目

创建了Blog_Entry_Table类后,您可以继续开发。合乎逻辑的下一步可能是处理从编辑器表单接收的输入,并使用一个Blog_Entry_Table对象在数据库中保存一个新的博客条目。哪个负责处理用户交互?模型、视图还是控制器?MVC 设计模式规定用户交互应该在相关的控制器中处理。在这种情况下,相关的控制人是controllers/admin/editor.php:

<?php

//complete code for controllers/admin/editor.php

//include class definition and create an object

include_once "models/Blog_Entry_Table.class.php";

$entryTable = new Blog_Entry_Table( $db );

//was editor form submitted?

$editorSubmitted = isset( $_POST['action'] );

if ( $editorSubmitted ) {

$buttonClicked = $_POST['action'];

//was "save" button clicked

$insertNewEntry = ( $buttonClicked === 'save' );

if ( $insertNewEntry ) {

//get title and entry data from editor form

$title = $_POST['title'];

$entry = $_POST['entry'];

//save the new entry

$entryTable->saveEntry( $title, $entry );

}

}

//load relevant view

$editorOutput = include_once "views/admin/editor-html.php";

return $editorOutput;

保存并测试您的编辑器。它应该能够插入新的博客条目。尝试通过编辑器表单创建一些新的博客条目。一旦您完成了这些,您就可以使用 phpMyAdmin 控制面板浏览您的blog_entry表的内容。您可以在您的blog_entry表中找到创建的博客条目。你的条目编辑器工作了!

哪个按钮被点击了?

条目编辑器表单中有两个按钮。最终,您会希望脚本根据单击的按钮做出不同的响应。所以,你的代码必须知道哪个按钮被点击了。让我们来看看产生按钮的 HTML:

//partial source code for views/admin/editor-html.php

<fieldset id='editor-buttons'>

<input type='submit' name='action' value='save' />

<input type='submit' name='action' value='delete' />

</fieldset>

看看这两个不同的按钮如何具有相同的name属性。按钮之间唯一的区别是它们的value属性。您可以使用该知识来识别哪个按钮被单击了。实际上,你已经在controllers/admin/editor.php做到了:

//one line of code from controllers/admin/editor.php

//don't make any changes

$buttonClicked = $_POST['action'];

变量$buttonClicked将保存被点击按钮的值。所以,如果用户点击了保存按钮,$buttonClicked将保存相应的值'save'。如果$buttonClicked的值为save,那么您知道用户正在尝试插入一个新条目。看看下面一行。变量$insertNewEntry会持有什么?

//one line of code from controllers/admin/editor.php

//don't make any changes

$insertNewEntry = ( $buttonClicked === 'save' );

在括号内,变量$buttonClicked与字符串'save'进行比较。如果$buttonClicked'save'相同,$insertNewEntry的值将为true。如果不是,它将有一个值false。在随后的代码中,您可以看到,如果$insertNewEntry为真,则会插入一个新的blog_entry。所以你的大部分代码只有在用户点击保存按钮时才会运行。

安全警报:SQL 注入攻击

saveEntry()方法展示了一个漂亮的编码实践。它使用表单输入数据动态生成 SQL 字符串。这是一个很好的技巧,因为它是将用户输入插入数据库表的关键。但这也是一个巨大的安全漏洞。SQL 注入攻击可能是对数据库驱动的 web 应用最常见的攻击。您的条目编辑器现在很容易受到这种攻击。

Note

在 http://en.wikipedia.org/wiki/SQL_injection 了解更多关于 SQL 注入的信息。

基本上,SQL 注入攻击利用了通过表单输入的文本被用来动态生成 SQL 语句这一事实。用户通过表单输入的任何内容都将用于 SQL:

//the code that makes you vulnerable

$entrySQL = "INSERT INTO blog_entry ( title, entry_text )

VALUES ( '$title', '$entry' )";

看看前面的代码。变量$title$entry是从表单接收的文本的简单占位符。通过表单输入的任何内容都将成为 SQL 字符串的一部分。因此,对 SQL 有深入了解的恶意用户可以通过表单输入恶意 SQL,从而直接访问您的数据库表。您保存在数据库中的任何数据都可能会暴露。攻击者可能会在表单的输入字段中输入类似如下的字符串:

attack'); DROP TABLE blog_entry;--

有了这个输入,$entrySQL就会变成

$entrySQL = "INSERT INTO blog_entry ( title, entry_text )

VALUES ( '$title', ' attack'); DROP TABLE blog_entry;--' )";

您可以看到,$entrySQL现在保存了两条 SQL 语句:一条用于插入一个blog_entry,另一条用于删除整个blog_entry表。很明显,您不希望用户能够在您的系统中删除表!您应该知道前面描述的特定攻击实际上不会起作用。我不想教你如何执行 SQL 注入攻击,但我想让你知道这个漏洞。

可用性警告:带有引号字符的博客条目

条目编辑器也有一个主要的可用性缺陷。(如果您的编辑器容易受到最常见类型的黑客攻击还不够的话!)如果您通过编辑器的输入字段输入以下文本,您可以自己看到问题:

Brennan's child said: "I want some breakfast!"

PDO 抛出了一个例外。罪魁祸首是单引号字符:

//the code that makes your form submission break

$entrySQL = "INSERT INTO blog_entry ( title, entry_text )

VALUES ( '$title', '$entry' )";

因为变量$title$entry包含在单引号字符中,所以您的条目编辑器表单不能处理任何包含单引号字符的内容。因此,您键入的句子会导致异常。

只需对代码做一点小小的改动,您就可以用双引号将$title$entry括起来,但是这样一来,您的条目编辑器将无法处理任何带有双引号字符的条目。所以,这个句子仍然会导致一个异常。

解决方案:准备好的报表

很明显,动态生成 SQL 字符串的代码给你带来了很多痛苦。这使你容易受到 SQL 注入的攻击,正因为如此,你的博客文章必须没有单引号或双引号字符。PDO 优雅地支持一个可以同时解决这两个问题的功能。PDO 支持事先准备好的声明。

Note

这里有一个很好的教程,可以进一步了解 PDO 和准备好的语句: http://net.tutsplus.com/tutorials/php/why-you-should-be-using-phps-pdo-for-database-access 。

预准备语句是为动态内容准备的 SQL 语句。您可以在 SQL 语句中声明占位符,然后用实际值替换这些占位符,而不是直接在 SQL 语句中删除 PHP 变量。如果您使用 PDO 预准备语句,您将不会受到 SQL 注入攻击,也不会因为单引号和双引号等特殊字符而遇到任何麻烦。下面是如何在models/Blog_Entry_Table.class.php中做到的:

//partial code for models/Blog_Entry_Table.class.php

//edit existing method

public function saveEntry ( $title, $entry ) {

//notice placeholders in SQL string. ? is a placeholder

//notice the order of attributes: first title, next entry_text

$entrySQL = "INSERT INTO blog_entry ( title, entry_text )

VALUES ( ?, ?)";

$entryStatement = $this->db->prepare( $entrySQL );

//create an array with dynamic data

//Order is important: $title must come first, $entry second

$formData = array( $title, $entry );

try{

//pass $formData as argument to execute

$entryStatement->execute( $formData );

} catch (Exception $e){

$msg = "<p>You tried to run this sql: $entrySQL<p>

<p>Exception: $e</p>";

trigger_error($msg);

}

}

有三个同样重要的变化,如下所示:

characters are used as placeholders in the SQL string.   An array is created with the dynamic data. The order of items must match the order used in the SQL string.   Pass the array with dynamic data as an argument to the execute() method.

你可以自己测试。用单引号和双引号插入条目。你将很难证实你的编辑器不再容易受到 SQL 注入的攻击。您必须能够执行 SQL 攻击,才能验证编辑器现在是安全的。我想你必须相信我的话。

摘要

这是一个相对较短的章节,只演示了几个新的编码原则。但是你已经向个人博客系统迈出了第一步,并且开发了一个超酷的条目编辑器。

在这个过程中,你有机会更加熟悉你在前面章节中学到的几乎所有东西。你有

  • 创建了一个带有表的 MySQL 数据库
  • 通过 web 表单将数据插入到表格中
  • 创建了 MVC web 应用结构
  • 使用前端控制器设计模式
  • 使用了表格数据网关设计模式

除此之外,您还了解了 SQL 注入攻击,并了解了如何使用 PDO 准备好的语句来保护您的 PHP 项目。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/141183.html

(0)
上一篇 2025-05-21 16:20
下一篇 2025-05-21 16:26

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信