PHP8-解决方案-二-

105 阅读49分钟

PHP8 解决方案(二)

原文:PHP 8 Solutions

协议:CC BY-NC-SA 4.0

五、通过以下内容减轻您的工作量

将一个文件的内容包含在另一个文件中的能力是 PHP 最强大的特性之一。这也是最容易实现的方法之一。这意味着代码可以合并到多个页面中——例如,公共元素,如页眉、页脚或导航菜单。PHP 将内容合并到服务器上的每个页面,允许您通过编辑和上传单个文件来更新菜单或其他公共元素——节省了大量时间。

在学习本章的过程中,您将了解 PHP includes 是如何工作的,PHP 在哪里寻找包含文件,以及当找不到包含文件时如何防止出现错误消息。您还将学习用 PHP 做一些很酷的事情,比如创建一个随机图像生成器。

本章涵盖以下主题:

  • 了解不同的包含命令

  • 告诉 PHP 在哪里可以找到你的包含文件

  • 对公共页面元素使用 PHP 包含

  • 保护包含文件中的敏感信息

  • 自动化“你在这里”菜单链接

  • 从文件名生成页面标题

  • 自动更新版权声明

  • 显示带有标题的随机图像

  • 处理包含文件的错误

  • 更改您的 web 服务器的include_path

包括来自外部文件的代码

包含其他文件代码的能力是 PHP 的核心部分。所有需要做的就是使用 PHP 的 include 命令,并告诉服务器在哪里可以找到该文件。

PHP 包含命令简介

PHP 有四个命令可以用来包含来自外部文件的代码,即:

  • include

  • include_once

  • require

  • require_once

都做基本相同的事情,为什么有四个?根本的区别在于,include试图继续处理您的脚本,即使它找不到指定的文件,而require在强制意义上使用:如果文件丢失,PHP 引擎停止处理并抛出致命错误。实际上,这意味着如果你的页面在没有外部文件的情况下仍然可用,你应该使用include。如果页面依赖于外部文件,使用require

另外两个命令include_oncerequire_once防止同一个文件在一个页面中被多次包含。试图在脚本中多次定义函数或类会触发致命错误。因此include_oncerequire_once确保函数和类只定义一次,即使脚本试图多次包含外部文件,如果命令在条件语句中就可能发生这种情况。

Tip

如果有疑问,总是使用require,除了定义函数和类的文件,这时你应该使用require_once。即使找不到外部文件,依赖于您的脚本仍然可以工作也会使您面临安全风险。

PHP 在哪里寻找包含文件

要包含一个外部文件,请使用四个 include 命令中的一个,后跟用引号括起来的文件路径(单引号或双引号,无所谓)。文件路径可以是绝对路径,也可以是相对于当前文档的路径。例如,只要目标文件存在,以下任何一项都将有效:

require 'includes/menu.php';
require 'C:/xampp/htdocs/php8sols/includes/menu.php';
require '/Applications/MAMP/htdocs/php8sols/includes/menu.php';

Note

PHP 接受包含命令的 Windows 文件路径中的正斜杠。

您可以选择在 include 命令中使用圆括号,这样下面的命令也可以使用:

require('includes/menu.php');
require('C:/xampp/htdocs/php8sols/includes/menu.php');
require('/Applications/MAMP/htdocs/php8sols/includes/menu.php');

当使用相对文件路径时,建议使用./表示路径从当前文件夹开始。因此,像这样重写第一个例子更有效:

require './includes/menu.php'; // path begins in current folder

不起作用的是使用相对于站点根目录的文件路径,如下所示:

require '/includes/menu.php'; // THIS WILL NOT WORK

这是行不通的,因为 PHP include 命令将前导正斜杠解释为硬盘的根目录。换句话说,PHP 将其视为绝对路径,而不是相对于站点根目录的路径。PHP 还查看 PHP 配置中定义的include_path。我将在这一章的后面回到这个主题。在此之前,让我们把 PHP 包含实际使用。

PHP 解决方案 5-1:移动菜单和页脚以包含文件

图 5-1 展示了一个页面的四个元素是如何从包含文件的 PHP 小魔术中获益的。

img/332054_5_En_5_Fig1_HTML.jpg

图 5-1

识别静态网页中可以用 PHP 改进的元素

菜单和页脚出现在 Japan Journey 网站的每个页面上,所以它们是包含文件的主要候选对象。清单 5-1 显示了页面主体的代码,菜单和页脚以粗体突出显示。(导航菜单中的第二个链接故意不同于图 5-1 。你以后会改的。)

  1. ch05文件夹中的index_01.php复制到php8sols站点根目录,并重命名为index.php。如果你正在使用一个提供更新页面链接的程序,不要更新它们。下载文件中的相关链接是正确的。通过将index.php加载到浏览器中,检查 CSS 和图像是否正常显示。它看起来应该和图 5-1 一样。

  2. blog.phpgallery.phpcontact.phpch05文件夹复制到您的站点根文件夹。这些页面还不能在浏览器中正确显示,因为还没有创建必要的包含文件。这很快就会改变。

  3. index.php中,高亮显示清单 5-1 中粗体显示的<nav>元素,然后剪切(Ctrl+X/Cmd+X)到你的电脑剪贴板。

  4. 在站点根目录下创建一个名为includes的新文件夹。然后在刚刚创建的文件夹中创建一个名为menu.php的文件。删除编辑程序插入的任何代码;该文件必须完全为空。

  5. 将剪贴板中的代码粘贴(Ctrl+V/Cmd+V)到menu.php并保存文件。menu.php的内容应该是这样的:

<header>
    <h1>Japan Journey</h1>
</header>
<div id="wrapper">
    <nav>
        <ul>
            <li><a href="index.php" id="here">Home</a></li>
            <li><a href="blog.php">Journal</a></li>
            <li><a href="gallery.php">Gallery</a></li>
            <li><a href="contact.php">Contact</a></li>
        </ul>
    </nav>
    <main>
        <h2>A journey through Japan with PHP</h2>
        <p>One of the benefits of using PHP . . .</p>
        <figure>
            <img src="img/water_basin.jpg" alt="Maiko&​mdash;trainee geishas in Kyoto"
            width="340" height="205" class="picBorder">
            <figcaption>Maiko&​mdash;trainee geishas in Kyoto</figcaption>
        </figure>
        <p>Ut enim ad minim veniam, quis nostrud . . .</p>
        <p>Eu fugiat nulla pariatur. Ut labore et dolore . . .</p>
        <p>Sed do eiusmod tempor incididunt ullamco . . .</p>
    </main>
    <footer>
        <p>&​copy; 2006&​ndash;2021 David Powers</p>
    </footer>
</div>

Listing 5-1The static version of index.php

<nav>
    <ul>
        <li><a href="index.php" id="here">Home</a></li>
        <li><a href="blog.php">Journal</a></li>
        <li><a href="gallery.php">Gallery</a></li>
        <li><a href="contact.php">Contact</a></li>
    </ul>
</nav>

不要担心你的新文件没有DOCTYPE声明或者任何<html><head>或者<body>标签。包含该文件内容的其他页面将提供这些元素。

  1. 打开index.php并在nav无序列表留下的空间插入以下内容:
<?php require './includes/menu.php'; ?>

这使用了一个到menu.php的文档相对路径。在路径的开头使用./会更有效,因为它明确指出路径从当前文件夹开始。

Tip

我使用require命令,因为导航菜单是关键任务。没有它,就无法浏览网站。

  1. 保存index.php并将页面加载到浏览器中。看起来应该和以前一模一样。尽管菜单和页面的其余部分来自不同的文件,但 PHP 在将任何输出发送到浏览器之前会将它们合并。

    注意不要忘记 PHP 代码需要由 web 服务器处理。如果您已经将文件存储在服务器文档根目录下名为php8sols的子文件夹中,您应该使用 URL http://localhost/php8sols/index.php来访问index.php。如果你需要找到服务器文件根的帮助,请参见第二章中的“在哪里定位你的 PHP 文件(Windows 和 Mac)”。

  2. footer做同样的操作。剪切清单 5-1 中粗体突出显示的行,粘贴到includes文件夹中一个名为footer.php的空白文件中。然后插入命令,将新文件包含在<footer>留下的间隙中:

<?php include './includes/footer.php'; ?>

这一次,我使用了include而不是require<footer>是页面的重要组成部分,但是如果找不到包含文件,站点仍然可用。

Caution

如果包含文件丢失,例如,如果您不小心删除了它,您应该总是替换它或删除 include 命令。不要相信include即使找不到外部文件也会尝试处理页面的其余部分。总是在你意识到问题的时候马上解决它们。

  1. 保存所有页面并在浏览器中重新加载index.php。同样,它应该看起来与原始页面相同。如果您导航到站点中的其他页面,菜单和页脚应该出现在每个页面上。包含文件中的代码现在服务于所有页面。

  2. 为了证明菜单是从单个文件中提取的,更改menu.php中日志链接的文本,如下所示:

  3. 保存menu.php并重新加载站点。这种变化反映在所有页面上。你可以对照ch05文件夹中的index_02.phpmenu_01.phpfooter_01.php来检查你的代码。

<li><a href="blog.php">Blog</a></li>

如图 5-2 所示,有问题。指示您所在页面的样式不会改变(它由<a>标签中的here ID 控制)。

img/332054_5_En_5_Fig2_HTML.jpg

图 5-2

当前页面指示器仍然指向主页

用 PHP 条件逻辑很容易解决这个问题。在此之前,让我们检查一下 web 服务器和 PHP 引擎是如何处理包含文件的。

为 Includes 选择正确的文件扩展名

当 PHP 引擎遇到 include 命令时,它会在外部文件的开头停止处理 PHP,并在结尾继续处理。这就是包含文件只包含原始 HTML 的原因。如果您希望外部文件使用 PHP 代码,那么代码必须包含在 PHP 标签中。因为外部文件是作为包含它的 PHP 文件的一部分来处理的,所以包含文件可以有任何文件扩展名。

一些开发人员使用.inc作为文件扩展名,以表明该文件将被包含在另一个文件中。然而,大多数服务器将.inc文件视为纯文本。如果文件包含敏感信息,如数据库的用户名和密码,这会带来安全风险。如果该文件存储在您网站的根文件夹中,任何发现该文件名称的人只需在浏览器地址栏中键入 URL,浏览器就会欣然显示您所有的秘密细节!

另一方面,任何带有.php扩展名的文件在发送到浏览器之前都会自动发送到 PHP 引擎进行解析。只要你的秘密信息在 PHP 代码块中,在扩展名为.php的文件中,它就不会被暴露。这就是为什么一些开发者使用.inc.php作为 PHP 包含的双重扩展。.inc部分提醒您这是一个包含文件,但是服务器只对末尾的.php感兴趣,它确保所有 PHP 代码都被正确解析。

在很长一段时间里,我遵循对包含文件使用.inc.php的惯例。但是由于我将所有包含文件存储在一个名为includes的单独文件夹中,我认为双扩展名是多余的。我现在用的只是.php

您选择哪种命名约定取决于您,但是单独使用.inc是最不安全的。

PHP 解决方案 5-2:测试包含的安全性

这个解决方案演示了使用.inc.php(或.inc.php)作为包含文件的文件扩展名之间的区别。使用上一节中的index.phpmenu.php。或者,使用ch05文件夹中的index_02.phpmenu_01.php。如果您使用下载文件,请在使用前删除文件名中的_02_01

  1. menu.php重命名为menu.inc,并相应地编辑index.php以包含它:

  2. index.php载入浏览器。你应该看不出有什么不同。

  3. 修改menu.inc中的代码,将密码存储在 PHP 变量中,如下所示:

<?php require './includes/menu.inc'; ?>

img/332054_5_En_5_Fig3_HTML.jpg

图 5-3

PHP 代码没有输出,所以只有 HTML 被发送到浏览器

  1. 重新加载页面。如图 5-3 所示,密码仍然隐藏在源代码中。虽然 include 文件没有.php文件扩展名,但是它的内容已经与index.php合并,所以 PHP 代码被处理。
<ul>
    <li><a href="index.php" id="here">Home</a></li>
    <?php $password = 'topSecret'; ?>
    <li><a href="blog.php">Blog</a></li>
    <li><a href="gallery.php">Gallery</a></li>
    <li><a href="contact.php">Contact</a></li>
</ul>

img/332054_5_En_5_Fig4_HTML.jpg

图 5-4

在浏览器中直接加载menu.inc会暴露 PHP 代码

  1. 现在直接在浏览器中加载menu.inc。图 5-4 显示了发生的情况。

服务器和浏览器都不知道如何处理一个.inc文件,所以所有的内容都显示在屏幕上:原始 HTML,你的密码,所有的一切。

  1. 将包含文件的名称更改为menu.inc.php,并通过在上一步中使用的 URL 末尾添加.php将其直接加载到您的浏览器中。这一次,您应该会看到一个无序的链接列表。检查浏览器的源代码视图。PHP 没有公开。

  2. 将名称改回menu.php,通过直接在浏览器中加载并再次查看源代码来测试包含文件。

  3. 删除您在步骤 3 中添加到menu.php的密码 PHP 代码,并将index.php中的 include 命令改回其原始设置,如下所示:

<?php require './includes/menu.php'; ?>

PHP 解决方案 5-3:自动显示当前页面

让我们解决菜单不显示当前页面的问题。解决方案包括使用 PHP 找出当前页面的文件名,然后使用条件语句在相应的<a>标签中插入一个 ID。

继续使用相同的文件。或者,使用ch05文件夹中的index_02.phpcontact.phpgallery.phpblog.phpmenu_01.phpfooter_01.php,确保删除任何文件名中的_01_02

  1. 打开menu.php。代码目前如下所示:
<nav>
    <ul>
        <li><a href="index.php" id="here">Home</a></li>
        <li><a href="blog.php">Blog</a></li>
        <li><a href="gallery.php">Gallery</a></li>
        <li><a href="contact.php">Contact</a></li>
    </ul>
</nav>

指示当前页面的样式由第 3 行突出显示的id="here"控制。如果当前页面是blog.php,你需要 PHP 将id="here"插入到blog.php <a>标签中,如果页面是gallery.php,插入到gallery.php <a>标签中,如果页面是contact.php,插入到contact.php <a>标签中。

希望你现在已经得到了提示——你需要在每个<a>标签中有一个if语句(参见第三章中的“做决定”)。第 3 行需要看起来像这样:

<li><a href="index.php" <?php if ($currentPage == 'index.php') {
    echo 'id="here"'; } ?>>Home</a></li>

其他链接应该以类似的方式进行修改。但是$currentPage是怎么得到它的值的呢?你需要找出当前页面的文件名。

  1. 暂时把menu.php放在一边,创建一个名为get_filename.php的新 PHP 页面。插入以下代码(或者,使用ch05文件夹中的get_filename.php):

  2. 保存get_filename.php并在浏览器中查看。在 Windows 系统上,您应该会看到类似下面的屏幕截图:(在ch05文件夹中的版本包含这一步和下一步的代码,以及指示哪个是哪个的文本。)

<? php echo $_SERVER['SCRIPT_FILENAME'];

img/332054_5_En_5_Figa_HTML.jpg

在 macOS 上,您应该会看到类似这样的内容:

img/332054_5_En_5_Figb_HTML.jpg

来自 PHP 的一个内置超全局数组,它总是给你当前页面的绝对文件路径。您现在需要的是提取文件名的方法。

  1. 像这样修改上一步中的代码:

  2. 保存get_filename.php并点击浏览器中的重新加载按钮。您现在应该只看到文件名:get_filename.php

<?php echo basename($_SERVER['SCRIPT_FILENAME']);

内置的 PHP 函数basename()将文件路径作为参数,并提取文件名。这就是找到当前页面文件名的方法。

  1. 像这样修改menu.php中的代码(更改以粗体突出显示):
<?php $currentPage = basename($_SERVER['SCRIPT_FILENAME']); ?>
<nav>
    <ul>
        <li><a href="index.php" <?php if ($currentPage == 'index.php') {
            echo 'id="here"';} ?>>Home</a></li>
        <li><a href="blog.php" <?php if ($currentPage == 'blog.php') {
            echo 'id="here"';} ?>>Blog</a></li>
        <li><a href="gallery.php" <?php if ($currentPage == 'gallery.php') {
            echo 'id="here"';} ?>>Gallery</a></li>
        <li><a href="contact.php" <?php if ($currentPage == 'contact.php') {
            echo 'id="here"';} ?>>Contact</a></li>
    </ul>
</nav>

Tip

我用双引号将here括起来,所以我用单引号将字符串'id="here"'括起来。它比"id=\"here\""更容易阅读。

img/332054_5_En_5_Fig5_HTML.jpg

图 5-5

包含文件中的条件代码为每个页面生成不同的输出

  1. 保存menu.php并将index.php加载到浏览器中。菜单看起来应该和以前没有什么不同。使用菜单导航到其他页面。这一次,如图 5-5 所示,当前页面旁边的边框应该是白色的,表示你在站点中的位置。如果您在浏览器中检查页面的源代码视图,您会看到here ID 已经被自动插入到正确的链接中。

  2. 如有必要,将您的代码与ch05文件夹中的menu_02.php进行比较。

PHP 解决方案 5-4:根据文件名自动生成页面标题

这个解决方案使用basename()来提取文件名,然后使用 PHP 字符串函数来格式化名称,以便插入到<title>标签中。它只对告诉你一些关于页面内容的文件名有效,但无论如何这是一个好的实践。

  1. 创建一个名为title.php的新 PHP 文件,并将其保存在includes文件夹中。

  2. 去掉脚本编辑器插入的任何代码,并键入以下代码:

    <?php $title = basename($_SERVER['SCRIPT_FILENAME'], '.php');
    
    

    提示不要在结尾添加结束 PHP 标签。当在同一个文件中 PHP 代码后面没有任何东西时,它是可选的。省略结束标记有助于避免包含文件的一个常见错误,即“标题已发送”你将在 PHP 解决方案 5-9 中了解更多关于这个错误的信息。

PHP 解决方案 5-3 中使用的basename()函数有一个可选的第二个参数:一个包含文件扩展名的字符串,前面有一个前导句点。添加第二个参数会删除文件名的扩展名。所以这段代码提取文件名,去掉扩展名.php,并将结果赋给一个名为$title的变量。

  1. 通过在DOCTYPE上方键入以下内容,打开contact.php并包含title.php:
<?php include './includes/title.php'; ?>

Note

通常情况下,在网页中的DOCTYPE声明之前不应该有任何内容。然而,如果 PHP 代码不向浏览器发送任何输出,这就不适用于 PHP 代码。title.php中的代码只给$title赋值,所以DOCTYPE声明仍然是浏览器看到的第一个输出。

  1. 像这样修改<title>标签:
<title>Japan Journey <?= $title ?></title>

注意在开始的简写 PHP 标签前有一个空格。没有它,$title的值将与“Journey”相冲突。

img/332054_5_En_5_Fig6_HTML.jpg

图 5-6

提取文件名后,您可以动态生成页面标题

  1. 保存两个页面并将contact.php加载到浏览器中。没有扩展名.php的文件名被添加到浏览器标签中,如图 5-6 所示。

  2. 如果你喜欢用首字母大写来表示从文件名中派生出来的那部分标题呢?PHP 有一个名为ucfirst()的简洁的小函数,它就是这样做的(uc代表“大写”)。向步骤 2 中的代码添加另一行,如下所示:

<?php
$title = basename($_SERVER['SCRIPT_FILENAME'], '.php');
$title = ucfirst($title);

如果您是编程新手,这可能看起来令人困惑,所以让我们来看看这里发生了什么。PHP 标签后的第一行代码获取文件名,去掉末尾的.php,并将其存储为$title。下一行将$title的值传递给ucfirst()以大写第一个字母,并将结果存储回$title。所以,如果文件名是contact.php,那么$title开始是contact,但是到了下一行的末尾,就变成了Contact

Tip

您可以通过将两行合并成一行来缩短代码,如下所示:

$title = ucfirst(basename($_SERVER['SCRIPT_FILENAME'], '.php'));

当您像这样嵌套函数时,PHP 首先处理最内层的函数,并将结果传递给外层的函数。它使你的代码更短,但是不容易阅读。

  1. 这种技术的一个缺点是文件名只由一个单词组成——至少应该是这样。URL 中不允许有空格,这也是为什么有些网页设计软件或者浏览器会用%20代替空格,在一个 URL 中显得很难看,很不专业。您可以通过使用下划线来解决这个问题。

    contact.php的文件名改为contact_us.php

  2. 像这样修改title.php中的代码:

<?php
$title = basename($_SERVER['SCRIPT_FILENAME'], '.php');
$title = str_replace('_', ' ', $title);
$title = ucwords($title);

中间一行使用一个名为str_replace()的函数来查找每一个下划线并用一个空格替换它。该函数有三个参数:要替换的字符、替换字符和要更改的字符串。

Tip

您也可以使用str_replace()删除字符,方法是使用一个空字符串(一对中间没有任何内容的引号)作为第二个参数。这将第一个参数中的字符串替换为空,实际上是删除了它。

最后一行代码没有使用ucfirst(),而是使用了相关的函数ucwords(),它给每个单词一个初始的大写字母。

img/332054_5_En_5_Fig7_HTML.jpg

图 5-7

下划线被去掉了,两个单词的首字母都被大写

  1. 保存title.php并将重命名的contact_us.php加载到浏览器中。图 5-7 显示了结果。

img/332054_5_En_5_Fig8_HTML.jpg

图 5-8

从 index.php 生成页面标题产生了不令人满意的结果

  1. 将文件名改回contact.php,并将文件重新加载到浏览器中。title.php中的脚本仍然有效。没有下划线可以替换,所以str_replace()保持$title的值不变,而ucwords()将第一个字母转换成大写,即使只有一个单词。

  2. index.phpblog.phpgallery.php重复步骤 3 和 4。

  3. 日本之旅网站的主页名为index.php。如图 5-8 所示,将当前的解决方案应用到这个页面似乎不太合适。

有两种解决方案:要么不要对这样的页面应用这种技术,要么使用条件语句(一个if语句)来处理特殊情况。例如,要显示 Home 而不是 Index,修改title.php中的代码如下:

<?php
$title = basename($_SERVER['SCRIPT_FILENAME'], '.php');
$title = str_replace('_', ' ', $title);
if ($title == 'index') {
    $title = 'home';
}
$title = ucwords($title);

条件语句的第一行使用两个等号来检查$title的值。下面一行使用一个等号将新值赋给$title。如果页面被称为除了index.php之外的任何东西,花括号内的行被忽略,$title保持其原始值。

Tip

PHP 是区分大小写的,所以这个解决方案只有在“index”全小写的情况下才有效。要进行不区分大小写的比较,请将前面代码的第四行更改如下:

if (strtolower($title) == 'index') {

函数strtolower()将一个字符串 ing 转换成小写——因此得名——并且经常用于进行不区分大小写的比较。到小写的转换不是永久的,因为strtolower($title)没有赋给变量;它只是用来做比较的。为了使更改永久,您需要将结果赋回一个变量,就像在最后一行,当ucwords($title)被赋回$title时。

要将字符串转换为大写,请使用strtoupper()

img/332054_5_En_5_Fig9_HTML.jpg

图 5-9

条件语句将 index.php 上的标题更改为 Home

  1. 保存title.php并将index.php重新加载到浏览器中。页面标题现在看起来更加自然,如图 5-9 所示。

  2. 导航回contact.php,您将看到页面标题仍然是从页面名称中正确派生出来的。

    您可以对照title.phpch05文件夹中index_03.phpblog_02.phpgallery_02.phpcontact_02.php中其他文件的更新版本来检查您的代码。

Caution

绝大多数 PHP 网站都托管在 Linux 服务器上,这些服务器将文件名和目录(文件夹)名区分大小写。但是,在 Windows 或 macOS 上进行本地开发时,文件名和文件夹名是以不区分大小写的方式处理的。为了避免在实时服务器上部署文件时路径被破坏,我建议在命名文件和文件夹时只使用小写字母。如果要混合使用大写和小写,请确保拼写一致。

PHP 解决方案 5-5:处理缺失变量

在许多情况下,预期值会丢失。例如,您可能拼错了变量名,表单中可能没有提交值,或者缺少包含文件。因此,在尝试使用外部来源的值之前,最好先检查它是否存在。在这个解决方案中,您将使用两种不同的方法来解决这个问题。

img/332054_5_En_5_Fig10_HTML.jpg

图 5-10

拼写错误的变量会在浏览器选项卡中生成警告

  1. 继续使用与上一个解决方案中相同的文件。或者,将index_03.phpblog_02.phpgallery_02.phpcontact_02.phpch05文件夹复制到您的站点根目录。还要确保title.phpmenu_02.phpfooter_01.phpincludes文件夹中。如果使用ch05文件夹中的文件,删除每个文件名中的下划线和数字。

  2. index.php中,将<title>标签中变量的第一个字母大写,将其从$title改为$Title。PHP 变量是区分大小写的,所以这不再是指由title.php生成的值。

  3. 保存文件,并将index.php加载到浏览器中。右键单击查看源代码。如果您将error_reporting设置为第二章中推荐的水平,您应该会看到如图 5-10 所示的结果。browser 选项卡包含来自 PHP 关于未定义变量的警告的原始 HTML。

Note

PHP 8 通过生成警告而不是通知,将未定义的变量视为比以前版本更严重的错误,这是最低的错误级别。

  1. 零合并操作符(参见第四章中的“用零合并操作符设置默认值”)无缝处理这种情况。像这样更改<title>标签中的 PHP 块:

  2. 保存并重新加载页面。浏览器选项卡现在应该如下所示:

<?= $Title ?? 'default' ?>

img/332054_5_En_5_Figc_HTML.jpg

忽略未定义的变量,显示 null 合并运算符后的值,而不生成错误通知。

  1. 删除引号之间的文本,留下空字符串,如下所示:

  2. 保存并重新加载页面。这一次,浏览器选项卡只显示 HTML 中的文本。空字符串只是隐藏错误提示。

  3. 通过将第一个字母小写来更正变量名,并再次测试页面。它现在看起来和之前的 PHP 解决方案的结尾一样(见图 5-9 )。

  4. 当变量不存在时,零合并操作符可以很好地设置默认值;但是如果你想修改一个变量,你就不能使用它。在这种情况下,您需要使用isset()函数来测试变量的存在。

<?= $Title ?? " ?>

打开blog.php并像这样更改<title>标签:

<title>Japan Journey<?php if (isset($title)) {echo "&​mdash;{$title}";}
    ?></title>

请注意,HTML 文本和开始的 PHP 标记之间的空格已经被删除。此外,开始的 PHP 标签不再是简写的,因为 PHP 块包含一个条件语句;它不仅仅是显示一个值。

如果变量存在,isset()函数返回true。因此,如果已经定义了$title,那么echo会显示一个双引号字符串,其中包含一个长破折号(& mdash;是 HTML 字符实体),后跟$title的值。我用花括号将变量括起来,因为实体和$title之间没有空格。这是可选的,但是它使代码更容易阅读。

Tip

如果值是一个空字符串(一对中间没有空格的引号),isset()函数返回true。它检查一个变量已经被定义并且不是null。使用empty()检查空字符串或零值。下一章的 PHP 解决方案 6–2 解释了如何检查一个字符串不仅仅由空格字符组成。

  1. 保存blog.php并在浏览器中测试。浏览器选项卡应如下所示:

img/332054_5_En_5_Figd_HTML.jpg

因为$title有一个值,isset()返回true并显示前面有一个长破折号的值。

  1. 尝试一个未定义的变量,比如$Title。条件语句中的代码将被忽略,而不会触发错误通知。

  2. 使用isset()或零合并操作符来保护gallery.phpcontact.php避免在<title>标签中使用未定义的变量。

    您可以对照ch05文件夹中的index_04.phpblog_03.phpgallery_03.phpcontact_03.php来检查您的代码。

创建内容不断变化的页面

到目前为止,您已经使用 PHP 根据页面的文件名生成了不同的输出。接下来的两个解决方案生成独立于文件名变化的内容:一个在 1 月 1 日自动更新年份的版权声明和一个随机图像生成器。

PHP 解决方案 5-6:自动更新版权声明

footer.php中的版权声明只包含静态 HTML。这个 PHP 解决方案展示了如何使用date()函数自动生成当前年份。该代码还指定了版权的第一年,并使用条件语句来确定当前年份是否不同。如果是,则显示两个年份。

继续使用 PHP 解决方案 5-5 中的文件。或者,使用ch05文件夹中的index_04.phpfooter_01.php,并删除文件名中的数字。如果使用ch05文件夹中的文件,确保在includes文件夹中有title.phpmenu.php的副本。

  1. 打开footer.php。它包含以下 HTML:
<footer>
    <p>&​copy; 2006&​ndash;2021 David Powers</p>
</footer>

使用包含文件的好处是,您可以通过更改这一个文件来更新整个站点的版权声明。但是,自动增加年份会更有效。

  1. PHP date()函数巧妙地解决了这个问题。像这样更改段落中的代码:
<p>&​copy; 2006&​ndash;<?php echo date('Y'); ?> David Powers</p>

这将替换第二个日期,并使用四位数显示当前年份。确保将大写的 Y 作为参数传递给date()

Note

第十六章中的表 16-4 列出了可以传递给date()函数来显示日期部分的最常用字符,如月、星期几等等。

  1. 保存footer.php并将index.php加载到浏览器中。页面底部的版权声明看起来应该和以前一样——当然,除非你在 2022 年或更晚的时候阅读这篇文章,在这种情况下,将显示当前年份。

  2. 像大多数版权声明一样,这涵盖了一系列的年份,表明了一个网站首次推出的时间。因为第一次约会已经过去了,所以可以硬编码。但是,如果你正在创建一个新的网站,你只需要今年。直到 1 月 1 日才需要年份范围。

    要显示一系列年份,您需要知道起始年份和当前年份。如果两个年份相同,则只显示当前年份;如果它们不一样,用中间的破折号显示它们。这是一个简单的情况。像这样更改footer.php中段落的代码:

<?php
$startYear = 2006;
$thisYear = date('Y');
if ($startYear == $thisYear) {
    $output = $startYear;
} else {
    $output = "{$startYear}&ndash;{$thisYear}";
}
?> <p>&​copy; <?= $​output ?> David Powers</p>

就像在 PHP 解决方案 5-5 中一样,我在else子句中的变量周围使用了花括号,因为它们在不包含空格的双引号字符串中。

  1. 保存footer.php并在浏览器中重新加载index.php。版权声明看起来应该和以前一样。

  2. 将传递给date()函数的参数改为小写的 y ,如下所示:

  3. 保存footer.php并点击浏览器中的重新加载按钮。第二年仅使用最后两位数字显示,如下面的屏幕截图所示:

$thisYear = date('y');

img/332054_5_En_5_Fige_HTML.jpg

Tip

这应该提醒我们 PHP 中区分大小写的重要性。大写的 Y 和小写的 ydate()函数产生不同的结果。忘记区分大小写是 PHP 中最常见的错误原因之一。

  1. 将传递给date()的参数改回大写的 Y 。将$startYear的值设置为当前年份,并重新加载页面。这一次,您应该只看到当前显示的年份。

你现在有一个完全自动化的版权声明。完成的代码在ch05文件夹的footer_02.php中。

PHP 解决方案 5-7:显示随机图像

显示一个随机图像所需要的只是一个可用图像的列表,存储在一个索引数组中(参见第四章中的“创建数组”)。因为索引数组从 0 开始编号,所以您可以通过生成一个介于 0 和小于数组长度的 1 之间的随机数来选择其中一个图像。所有这些都是通过几行代码完成的…

继续使用相同的文件。或者,使用ch05文件夹中的index_04.php,并将其重命名为index.php。由于index_04.php使用了title.phpmenu.phpfooter.php,请确保这三个文件都在您的includes文件夹中。图像已经在images文件夹中。

  1. includes文件夹中创建一个空白的 PHP 页面,并将其命名为random_image.php。插入以下代码(也在ch05文件夹的random_image_01.php中):
<?php
$images = ['kinkakuji', 'maiko', 'maiko_phone', 'monk', 'fountains',
    'ryoanji', 'menu', 'basin'];
$i = random_int(0, count($images)-1);
$selectedImage = "img/{$images[$i]}.jpg";

这是完整的脚本:一个图像名称数组减去.jpg文件扩展名(没有必要重复共享信息——它们都是 JPEG ),一个随机数生成器,以及一个为所选文件构建正确路径名的字符串。

要生成一个范围内的随机数,将最小和最大数作为参数传递给random_int()函数。因为数组中有八个图像,所以需要一个介于 0 和 7 之间的数字。简单的方法是使用random_int(0, 7)——简单,但效率低。每次更改$images数组时,都需要计算它包含多少个元素,并更改传递给random_int()的最大数量。

count()函数让 PHP 为您做这件事要容易得多,它计算数组中元素的数量。您需要一个比数组中元素数少一的数字,所以传递给random_int()的第二个参数变成了count($images)-1,结果存储在$i中。

随机数用在最后一行,为选定的文件建立正确的路径名。变量$images[$i]嵌入在一个双引号字符串中,没有空格将其与周围的字符分开,所以它被括在花括号中。数组从 0 开始,所以如果随机数是 1,$selectedImage就是img/maiko.jpg

如果您是 PHP 新手,您可能会发现很难理解这样的代码:

$i = random_int(0, count($images)-1);

所发生的是传递给random_int()的第二个参数是一个表达式而不是一个数字。如果这样能让你更容易理解,就像这样重写代码:

  1. 打开index.php并将random_image.php包含在与title.php相同的代码块中,如下所示:
$numImages = count($images); // $numImages is 8
$max = $numImages1;       // $max is 7
$i = random_int(0, $max);    // $i = random_int(0, 7)

<?php include './includes/title.php';
include './includes/random_image.php'; ?>

因为random_image.php没有向浏览器发送任何直接输出,所以把它放在DOCTYPE上面是安全的。

  1. index.php中向下滚动,找到在 figure 元素中显示图像的代码。看起来是这样的:

  2. 不使用img/maiko.jpg作为固定图像,而是用$selectedImage代替。所有的图像都有不同的尺寸,所以删除widthheight属性,使用一个通用的alt属性。同时删除figcaption元素中的文本。步骤 3 中的代码现在应该如下所示:

<figure>
    <img src="img/maiko.jpg" alt="Maiko&​mdash;trainee geishas in Kyoto"
        width="340" height="205" class="picBorder">
    <figcaption>Maiko&​mdash;trainee geishas in Kyoto</figcaption>
</figure>

<figure>
    <img src="<?= $selectedImage ?>" alt="Random image" class="picBorder">
    <figcaption></figcaption>
</figure>

Note

PHP 块只显示一个值,所以您可以使用短的echo标记<?=

img/332054_5_En_5_Fig11_HTML.jpg

图 5-11

将图像文件名存储在索引数组中可以很容易地显示随机图像

  1. 保存random_image.phpindex.php,然后将index.php加载到浏览器中。现在应该随机选择图像。单击浏览器中的重新加载按钮;你应该会看到各种各样的图像,如图 5-11 所示。

你可以对照ch05文件夹中的index_05.phprandom_image_01.php来检查你的代码。

这是显示随机图像的一种简单而有效的方式,但是如果能够动态地设置不同大小图像的宽度和高度,并添加一个标题来描述图像,效果会更好。

PHP 解决方案 5-8:给随机图像添加标题

这个解决方案使用一个多维数组——或数组的数组——来存储每个图像的文件名和标题。如果你觉得多维数组的概念很难用抽象的术语来理解,那就把它想象成一个大盒子,里面有很多信封,每个信封里面都有一张图片和它的标题。盒子是顶层数组,里面的封套是子数组。

这些图像大小不同,但是 PHP 方便地提供了一个名为getimagesize()的函数。猜猜它是做什么的。

这个 PHP 解决方案建立在前一个的基础上,所以继续使用相同的文件。

  1. 打开random_image.php,按如下方式更改代码:
<?php
$images = [
    ['file'    => 'kinkakuji',
     'caption' => 'The Golden Pavilion in Kyoto'],
    ['file'    => 'maiko',
     'caption' => 'Maiko&​mdash;trainee geishas in Kyoto'],
    ['file'    => 'maiko_phone',
     'caption' => 'Every maiko should have one&​mdash;a mobile, of course'],
    ['file'    => 'monk',
     'caption' => 'Monk begging for alms in Kyoto'],
    ['file'    => 'fountains',
     'caption' => 'Fountains in central Tokyo'],
    ['file'    => 'ryoanji',
     'caption' => 'Autumn leaves at Ryoanji temple, Kyoto'],
    ['file'    => 'menu',
     'caption' => 'Menu outside restaurant in Pontocho, Kyoto'],
    ['file'    => 'basin',
     'caption' => 'Water basin at Ryoanji temple, Kyoto']
];
$i = random_int(0, count($images)-1);
$selectedImage = "img/{$images[$i]['file']}.jpg";
$caption = $images[$i]['caption'];

Caution

你需要小心代码。每个子数组都用一对方括号括起来,后跟一个逗号,用于将它与下一个子数组隔开。如果您按如下所示对齐数组键和值,您会发现构建和维护多维数组会更容易。

尽管代码看起来很复杂,但它是一个普通的索引数组,包含八个条目,每个条目都是一个关联数组,包含对'file''caption'的定义。多维数组的定义形成了一个语句,所以在第 19 行之前没有分号。该行的右括号与第 2 行的左括号相匹配。

用于选择图像的变量也需要改变,因为$images[$i]不再包含字符串,而是一个数组。要获得图像的正确文件名,您需要使用$images[$i]['file']。所选图像的标题包含在$images[$i]['caption']中,并存储在一个较短的变量中。

  1. 您现在需要修改index.php中的代码来显示标题,如下所示:

img/332054_5_En_5_Fig12_HTML.jpg

图 5-12

长标题突出于图像之外,并使其向左移动过远

  1. 保存index.phprandom_image.php并将index.php载入浏览器。大多数图像看起来都不错,但是在拿着手机的见习艺妓图像的右边有一个难看的缺口,如图 5-12 所示。
<figure>
    <img src="<?= $selectedImage ?>" alt="Random image" class="picBorder">
    <figcaption><?= $caption ?></figcaption>
</figure>

  1. random_image.php的末尾添加以下代码:
if (file_exists($selectedImage) && is_readable($selectedImage)) {
    $imageSize = getimagesize($selectedImage);
}

if语句使用了两个函数,file_exists()is_readable(),以确保$selectedImage不仅存在,而且可以访问(它可能被破坏或具有错误的权限)。这些函数返回布尔值(truefalse),所以它们可以直接用作条件语句的一部分。

if语句中的一行代码使用了函数getimagesize(),该函数返回一组关于图像的信息,这些信息存储为$imageSize。你将在第十章中了解更多关于getimagesize()的内容。目前,您对以下两条信息感兴趣:

  1. 首先,我们来修复一下<img>标签中的代码。像这样改变它:
  • $imageSize[0]:图像的宽度,以像素为单位

  • $imageSize[3]:一个包含图像高度和宽度的字符串,被格式化以包含在<img>标签中

<img src="<?= $selectedImage ?>" alt="Random image" class="picBorder"
    <?= $imageSize[3] ?>>

这将在<img>标签中插入正确的widthheight属性。

  1. 虽然这设置了图像的尺寸,但是您仍然需要控制标题的宽度。您不能在外部样式表中使用 PHP,但是没有什么可以阻止您在index.php<head>中创建一个<style>块。在结束的</head>标签之前插入以下代码。
<?php if (isset($imageSize)) { ?>
<style>
figcaption {
     width: <?= $imageSize[0] ?>px;
}
</style>
<?php } ?>

这段代码只有短短的七行,但是它是 PHP 和 HTML 的奇怪组合。让我们从第一行和最后一行开始。如果去掉 PHP 标签,用一个注释替换 HTML <style>块,结果是:

if (isset($imageSize)) {
  // do something if $imageSize has been set
}

换句话说,如果变量$imageSize没有被设置(定义), PHP 引擎会忽略花括号之间的所有内容。大括号之间的代码大部分是 HTML 和 CSS,这没关系。如果没有设置【the PHP 引擎会跳到右括号,中间的代码不会发送到浏览器。

Tip

许多没有经验的 PHP 程序员错误地认为他们需要使用echoprint在条件语句中创建 HTML 输出。只要左大括号和右大括号匹配,就可以使用 PHP 像这样隐藏或显示 HTML 的各个部分。这比一直使用echo要整洁得多,涉及的输入也少得多。

如果已经设置了$imageSize,则创建<style>块,并使用$imageSize[0]为包含标题的段落设置正确的宽度。

img/332054_5_En_5_Fig13_HTML.jpg

图 5-13

通过创建与图像大小直接相关的样式规则来消除难看的间隙

  1. 保存random_image.phpindex.php,然后将index.php重新加载到浏览器中。点击重新加载按钮,直到出现拿着手机的见习艺妓的图像。这一次,它应该看起来像图 5-13 。如果您查看浏览器的源代码,样式规则将使用正确的图像宽度。

Note

如果标题仍然突出,确保结束 PHP 标签和<style>块中的px之间没有间隙。CSS 不允许值和度量单位之间有空格。

img/332054_5_En_5_Fig14_HTML.jpg

图 5-14

包含文件中的错误会破坏页面的外观

  1. random_image.php中的代码和您刚刚插入的代码可以防止所选图像找不到时出现错误,但是显示图像的代码没有类似的检查。暂时更改其中一幅图像的名称,可以是在random_image.php文件夹中,也可以是在images文件夹中。多次重装index.php。最终,你应该会看到两个如图 5-14 所示的警告。不仅$imageSize 未定义(因此为空);您正试图访问空对象上的数组偏移量(索引)。看起来非常不专业。

  2. 只有当选定的图像既存在又可读时,random_image.php底部的条件语句才会设置$imageSize,所以如果已经设置了$imageSize,您就知道所有系统都运行了。在index.php中显示图像的图形元素周围添加条件语句的开始和结束块,如下所示:

<?php if (isset($imageSize)) { ?>
<figure>
     <img src="<?= $selectedImage ?>" alt="Random image" class="picBorder"
         <?= $imageSize[3] ?>>
     <figcaption><?= $caption ?></figcaption>
</figure>
<?php } ?>

现有的图像将正常显示,但您可以避免在文件丢失或损坏的情况下出现任何令人尴尬的错误消息,这看起来更专业。不要忘记恢复您在上一步中更改的图像的名称。

你可以对照ch05文件夹中的index_06.phprandom_image_02.php来检查你的代码。

防止包含文件出错

使用服务器端技术(如 PHP)的页面会处理大量的未知情况,因此明智的做法是编写防御性代码,在使用它们之前检查值。本节描述了您可以采取的措施,以防止和解决包含文件的错误。

检查变量的存在

从 PHP 解决方案 5-5 和 5-8 中可以吸取的教训是,你应该总是使用空合并操作符来设置默认值,或者使用isset()来验证来自包含文件的变量的存在,并将任何依赖代码包装在条件语句中。您也可以使用带有逻辑Not运算符的isset()(参见第四章中的表 4-7 )来指定默认值,如下所示:

if (!isset($someVariable)) {
    $someVariable = default value;
}

您可能会在许多脚本中遇到这种设置默认值的结构,因为空合并操作符从 PHP 7 开始才可用。没有一个比另一个更好;但是零合并操作符使得代码更短。

检查函数或类是否已经定义

包含文件经常用于定义自定义函数或类。试图使用尚未定义的函数或类会触发致命错误。要检查函数是否已经定义,将函数名作为字符串传递给function_exists()。将函数名传递给function_exists()时,省略函数名末尾的括号。例如,您检查一个名为doubleIt()的函数是否被定义成这样:

if (function_exists('doubleIt')) {
    // use doubleIt()
}

要检查一个类是否已经被定义,以同样的方式使用class_exists(),传递一个包含类名的字符串作为参数:

if (class_exists('MyClass')) {
    // use MyClass
}

假设您想要使用函数或类,如果函数或类尚未定义,更实用的方法是使用条件语句来包含定义文件。例如,doubleIt()的定义在一个名为utilities.php的文件中:

if (!function_exists('doubleIt')) {
    require_once './includes/utilities.php';
}

抑制实时网站上的错误消息

假设您的包含文件在远程服务器上正常工作,前面几节中概述的措施可能就是您需要的所有错误检查。但是,如果您的远程服务器显示错误消息,您应该采取措施抑制它们。以下技术隐藏所有错误信息,而不仅仅是那些与包含文件相关的错误信息。

使用错误控制运算符

一种相当粗糙的技术是使用 PHP 错误控制操作符 ( @),它抑制与使用它的行相关的错误消息。您可以将@放在行首,或者直接放在您认为可能会产生错误的函数或命令的前面,如下所示:

@ include './includes/random_image.php';

Caution

这不适用于requirerequire_once,因为试图用这些命令加载丢失或损坏的文件会产生致命错误。在 PHP 8 中,错误控制操作符不再抑制致命的错误消息。

错误控制操作符的问题在于它隐藏了错误,而不是解决它们。它只有一个字符,所以很容易忘记你用过它。因此,您可能会浪费大量时间在脚本的错误部分查找错误。如果您使用错误控制操作符,那么在对问题进行故障诊断时,您应该首先删除@标记。

另一个缺点是,您需要在可能生成错误消息的每一行使用错误控制操作符,因为它只影响当前行。

关闭 PHP 配置中的显示错误

在实时网站中抑制错误消息的一个更好的方法是在 web 服务器的配置中关闭display_errors指令。最有效的方法是编辑php.ini,如果你的主机公司让你控制它的设置。找到display_errors指令,将On改为Off

如果你不能控制php.ini,许多主机公司允许你使用一个叫做.htaccess.user.ini的文件来改变有限范围的配置设置。文件的选择取决于 PHP 在服务器上的安装方式,所以请咨询您的托管公司以确定使用哪一种。

如果您的服务器支持.htaccess文件,将以下命令添加到服务器根文件夹中的.htaccess文件:

php_flag display_errors Off

在一个.user.ini文件中,命令很简单:

display_errors Off

.htaccess.user.ini都是纯文本文件。像php.ini一样,每个命令应该在一个单独的行上。如果该文件在您的远程服务器上不存在,您可以简单地在文本编辑器中创建它。确保您的编辑器不会自动在文件名末尾添加.txt。然后将文件上传到您网站的服务器根文件夹。

Tip

默认情况下,macOS 会隐藏名称以点开头的文件。在 macOS Sierra 和更高版本中,使用键盘快捷键 Cmd+Shift+。(点)来打开和关闭隐藏文件的显示。

关闭单个文件中的 display_errors

如果您无法控制服务器配置,可以通过在任何脚本的顶部添加以下行来防止显示错误消息:

<?php ini_set('display_errors', '0'); ?>

PHP 解决方案 5-9:找不到包含文件时重定向

到目前为止,所有建议的技术都只是在找不到包含文件的情况下抑制错误消息。如果一个页面没有包含文件就没有意义,那么当包含文件丢失时,您应该将用户重定向到一个错误页面。

一种方法是抛出异常,如下所示:

$file = './includes/menu.php';
if (file_exists($file) && is_readable($file)) {
    include $file;
} else {
    throw new Exception("$file can't be found");
}

当使用可能抛出异常的代码时,您需要将其包装在一个try块中,并创建一个catch块来处理异常(参见第四章中的“处理错误和异常”)。这个 PHP 解决方案展示了如何做到这一点,如果找不到包含文件,使用catch块将用户重定向到不同的页面。

如果你已经彻底地设计和测试了你的站点,这种技术在大多数使用包含文件的页面上是不必要的。然而,下面的 PHP 解决方案绝不是毫无意义的练习。它演示了 PHP 的几个重要特性:如何抛出和捕捉异常,以及如何重定向到另一个页面。正如您将从下面的说明中看到的,重定向并不总是简单明了的。这个 PHP 解决方案展示了如何克服最常见的问题。

继续使用 PHP 解决方案 5-8 中的index.php。或者,使用ch05文件夹中的index_06.php

img/332054_5_En_5_Fig15_HTML.jpg

图 5-15

如果输出已经发送到浏览器,则header()功能不起作用

  1. error.phpch05文件夹复制到站点根目录。如果您的编辑程序提示您更新页面中的链接,请不要这样做。这是一个静态页面,包含一个一般性错误消息,并链接回其他页面。

  2. 在编辑程序中打开index.php。导航菜单是最不可缺少的包含文件,所以像这样编辑index.php中的require命令:

    $file = './includes/menu.php';
    if (file_exists($file) && is_readable($file)) {
        require $file;
    } else {
        throw new Exception("$file can't be found");
    }
    
    

    提示像这样将包含文件的路径存储在一个变量中,可以避免重新键入四次,减少了拼写错误的可能性。

  3. 要将用户重定向到另一个页面,请使用header()功能。除非有语法错误,否则 PHP 引擎通常从顶部开始处理页面,输出 HTML,直到出现问题。这意味着当 PHP 引擎得到这段代码时,输出已经开始了。为了防止这种情况发生,在产生任何输出之前启动try模块。(这在一些设置中不起作用,但是请耐心等待,因为它演示了一个重要的观点。)

    滚动到页面顶部,编辑开始的 PHP 代码块,如下所示:

    <?php try {
        include './includes/title.php';
        include './includes/random_image.php'; ?>
    
    

    这将打开try块。

  4. 向下滚动到页面底部,在结束的</html>标记后添加以下代码:

    <?php } catch (Exception $e) {
        header('Location: http://localhost/php8sols/error.php');
    } ?>
    
    

    这将关闭try块并创建一个catch块来处理异常。catch块中的代码使用header()将用户重定向到error.php

    header()函数向浏览器发送一个 HTTP 头。它接受一个字符串作为参数,该字符串包含由冒号分隔的头和值。在这种情况下,它使用Location头将浏览器重定向到冒号后面的 URL 所指定的页面。如有必要,调整 URL 以匹配您自己的设置。

  5. 保存index.php并在浏览器中测试页面。它应该正常显示。

  6. 将您在步骤 2 中创建的变量$file的值改为指向一个不存在的包含文件,比如men.php

  7. 保存index.php并在浏览器中重新加载。如果您在测试环境中使用 XAMPP 或最新版本的 MAMP,您可能会被正确地重定向到error.php。不过,在一些设置中,你可能会看到图 5-15 中的信息。

图 5-15 中的错误信息可能是导致更多头部撞到键盘的原因。(我也带着伤疤。)如前所述,如果输出已经发送到浏览器,则不能使用header()功能。发生了什么事?

答案就在错误消息中,但并不明显。它说错误发生在第 55 行,这是调用header()函数的地方。您真正需要知道的是输出是在哪里生成的。这些信息埋藏在这里:

(output started at C:\xampp\htdocs\php8sols\index.php:5)

冒号后的数字 5 是行号。那么index.php的第 5 行是什么?从下面的截图可以看出,第 5 行输出了 HTML DOCTYPE 声明。

img/332054_5_En_5_Figf_HTML.jpg

因为到目前为止代码中没有错误,PHP 引擎已经输出了 HTML。一旦发生这种情况,header()就不能重定向页面,除非输出存储在一个缓冲区(web 服务器的内存)中。

Note

在许多设置中不会出现此错误消息的原因是,输出缓冲通常设置为 4096,这意味着在 HTTP 头发送到浏览器之前,有 4 KB 的输出存储在缓冲区中。尽管这很有用,但它给了您一种错误的安全感,因为您的远程服务器上可能没有启用输出缓冲。所以,即使你被正确地重定向,也要继续读下去。

img/332054_5_En_5_Fig16_HTML.jpg

图 5-16

缓冲输出使浏览器能够重定向到错误页面

  1. 编辑index.php顶部的代码块,如下所示:

    <?php ob_start();
    try {
        include './includes/title.php';
        include './includes/random_image.php'; ?>
    
    

    ob_start()函数打开输出缓冲,防止任何输出在header()函数被调用之前被发送到浏览器。

  2. PHP 引擎会在脚本结束时自动刷新缓冲区,但最好是显式地这样做。编辑页面底部的 PHP 代码块,如下所示:

    <?php } catch (Exception $e) {
        ob_end_clean();
        header('Location: http://localhost/php8sols/error.php');
    }
    ob_end_flush();
    ?>
    
    

    这里增加了两个不同的功能。当重定向到另一个页面时,您不希望 HTML 存储在缓冲区中。因此,在catch块中,调用ob_end_clean(),关闭缓冲区并丢弃其内容。

    然而,如果没有抛出异常,您希望显示缓冲区的内容,所以在页面的末尾在trycatch块之后调用ob_end_flush()。这将刷新缓冲区的内容,并将其发送到浏览器。

  3. 保存index.php并在浏览器中重新加载。这一次,您应该被重定向到错误页面,如图 5-16 所示,不管您的服务器配置中是否启用了缓冲。

  4. $file的值改回./includes/menu.php并保存index.php。当您点击错误页面上的主页链接时,index.php应正常显示。

    您可以将您的代码与ch05文件夹中的index_07.php进行比较。

为什么我不能使用 PHP 包含的站点根目录相对链接?

你可以也可以不可以。为了清楚起见,我将首先解释相对于文档的链接和相对于站点根的链接之间的区别。

文档相关链接

大多数 web 创作工具都指定了其他文件的路径,例如相对于当前文档的样式表、图像和其他网页。如果目标页面在同一个文件夹中,则只使用文件名。如果它比当前页面高一级,文件名前面会加上../。这就是所谓的文档相对路径或链接。如果你的网站有很多层次的文件夹,这种类型的链接可能很难理解——至少对人来说是这样。

相对于网站根目录的链接

网页中使用的另一种链接总是以正斜杠开头,这是站点根目录的简写。一个站点根目录相对路径的优点是当前页面在站点层次结构中有多深并不重要。开头的斜杠保证浏览器将从站点的顶层开始查看。虽然站点根目录相对链接更容易阅读,但是 PHP 包含命令不能正确解释它们。您必须使用文档相对路径或绝对路径,或者在您的include_path指令中指定includes文件夹(请参阅本章后面的“调整您的 include_path”)。

Note

只有当前导斜杠代表站点根时,PHP include 命令才能定位外部文件。Linux 和 macOS 上的绝对路径也以正斜杠开头。绝对路径是明确的,所以它们没有问题。

通过将超全局变量$_SERVER['DOCUMENT_ROOT']连接到路径的开头,可以将站点根目录相对路径转换为绝对路径,如下所示:

require $_SERVER['DOCUMENT_ROOT'] . '/includes/filename.php';

大多数服务器都支持$_SERVER['DOCUMENT_ROOT'],但是请检查远程服务器上phpinfo()显示的配置细节底部的 PHP 变量部分以确保这一点。

包含文件中的链接

这是容易让很多人困惑的一点。PHP 和浏览器对以正斜杠开头的路径有不同的解释。因此,尽管你不能使用相对于站点根目录的链接来包含一个文件,但是包含文件中的链接通常应该是相对于站点根目录的。这是因为包含文件可以包含在站点层次结构的任何级别,所以当文件包含在不同级别时,文档相关链接会断开。

Note

menu.php中的导航菜单使用相对于文档的链接,而不是相对于站点根目录的链接。它们被故意保留成那样,因为除非你创建了一个虚拟主机,否则站点根目录是localhost,而不是php8sols。这是在 web 服务器文档根目录的子文件夹中测试站点的一个缺点。本书通篇使用的 Japan Journey 站点只有一个级别,因此文档相关的链接是有效的。当开发一个使用多层文件夹的站点时,在包含文件中使用站点根目录相对链接,并考虑设置一个虚拟主机进行测试。

选择包含文件的位置

PHP 包含文件的一个有用特性是它们可以位于任何地方,只要带有包含命令的页面知道在哪里可以找到它们。包含文件甚至不需要在您的 web 服务器根目录中。这意味着您可以保护包含敏感信息(如密码)的 include 文件,这些文件位于无法通过浏览器访问的私有目录(文件夹)中。

Tip

如果你的托管公司在你的服务器根目录之外提供了一个存储区域,你应该认真考虑在那里放置一些(如果不是全部)你的包含文件。

安全注意事项包括

包含文件是 PHP 的一个非常强大的特性。随之而来的是安全风险。只要外部文件是可访问的,PHP 就会包含它并将任何代码合并到主脚本中。从技术上讲,包含文件甚至可以在不同的服务器上。然而,这被认为是一种安全风险,配置指令allow_url_include在默认情况下是禁用的,因此不可能包含来自不同服务器的文件,除非您完全控制您服务器的配置。与include_path不同的是,allow_url_include指令不能被覆盖,除非是服务器管理员。

即使您自己控制两台服务器,也不应该包含来自不同服务器的文件。攻击者有可能伪造地址并试图在您的站点上执行恶意脚本。

永远不要包含可以被公众上传或覆盖的文件。

Note

本章的其余部分相当专业。它们主要作为参考提供。请随意跳过它们。

调整您的包含路径

include 命令需要相对路径或绝对路径。如果两者都没有给出,PHP 会自动查找 PHP 配置中指定的include_path。将包含文件放在 web 服务器的include_path中指定的文件夹中的好处是,您不需要担心获得正确的相对或绝对路径。你只需要文件名。如果你使用了大量的 includes 或者你有一个几层的站点层次结构,这是非常有用的。有三种方法可以改变include_path:

  • 编辑 php.ini中的值:如果你的托管公司允许你访问php.ini,这是添加自定义包含文件夹的最佳方式。

  • 使用 .htaccess.user.ini:如果你的托管公司允许用.htaccess.user.ini文件修改配置,这是一个不错的选择。

  • 使用 set_include_path():仅在前面的选项不可用时使用,因为它只影响当前文件的include_path

当您运行phpinfo()时,web 服务器的include_path值在配置细节的核心部分列出。它通常以句点开始,表示当前文件夹,后跟要搜索的每个文件夹的绝对路径。在 Linux 和 macOS 上,每个路径由冒号分隔。在 Windows 上,分隔符是分号。在 Linux 或 Mac 服务器上,您现有的include_path指令可能如下所示:

.:/php/PEAR

在 Windows 服务器上,对等用法如下所示:

.;C:\php\PEAR

在 php.ini 或. user.ini 中编辑 include_path

php.ini中,找到include_path指令。要在您自己的站点中添加一个名为includes的文件夹,请在现有值的末尾添加一个冒号或分号,后跟includes文件夹的绝对路径。

在 Linux 或 Mac 服务器上,使用如下冒号:

include_path=".:/php/PEAR:/home/mysite/includes"

在 Windows 服务器上,使用分号:

include_path=".;C:\php\PEAR;C:\sites\mysite\includes"

对于.user.ini文件,命令是相同的。. user.ini中的值覆盖了默认值,所以确保从phpinfo()中复制了现有的值,并向其中添加了新的路径。

使用。htaccess 来更改包含路径

在一个.htaccess文件中的值覆盖了缺省值,所以从phpinfo()复制现有的值并添加新的路径。在 Linux 或 Mac 服务器上,该值应该如下所示:

php_value include_path ".:/php/PEAR:/home/mysite/includes"

除了用分号分隔路径之外,该命令在 Windows 上是相同的:

php_value include_path ".;C:\php\PEAR;C:\sites\mysite\includes"

Caution

.htaccess中,不要在include_path和路径名列表之间插入等号。

使用 set_include_path()

虽然set_include_path()只影响当前页面,但是您可以轻松地创建一个代码片段,并将其粘贴到您想要使用它的页面中。PHP 还使得获取现有的include_path并以平台中立的方式将其与新的结合起来变得容易。

将新路径存储在变量中,然后将其与现有值合并,如下所示:

$includes_folder = '/home/mysite/includes';
set_include_path(get_include_path() . PATH_SEPARATOR . $includes_folder);

看起来好像有三个参数被传递给了set_include_path(),但实际上只有一个;这三个元素由串联运算符(句点)连接,而不是逗号:

  • get_include_path()获取已有的include_path

  • PATH_SEPARATOR是一个 PHP 常量,根据操作系统自动插入冒号或分号。

  • $includes_folder添加新路径。

这种方法的问题是,新的includes文件夹的路径在您的远程和本地测试服务器上并不相同。您可以使用条件语句来解决这个问题。超全局变量$_SERVER['HTTP_HOST']包含网站的域名。如果您的域是 www.example.com ,您可以为每个服务器设置正确的路径,如下所示:

if ($_SERVER['HTTP_HOST'] == 'www.example.com') {
    $includes_folder = '/home/example/includes';
} else {
    $includes_folder = 'C:/xampp/htdocs/php8sols/includes';
}
set_include_path(get_include_path() . PATH_SEPARATOR . $includes_folder);

对于不使用很多包含文件的小型网站来说,使用set_include_path()可能不值得。但是,您可能会发现它在更复杂的项目中很有用。

嵌套包含文件

一旦一个文件包含在另一个文件中,相对路径就从父文件开始计算,而不是从包含的文件开始计算。这给需要包含另一个外部文件的外部文件中的函数或类定义带来了问题。

如果两个外部文件在同一个文件夹中,您可以包含一个只有文件名的嵌套文件,如下所示:

require_once 'Thumbnail.php';

在这种情况下,相对路径应该是而不是./开始,因为./意味着“从这个文件夹开始”对于包含文件,“此文件夹”是指父文件的文件夹,而不是包含文件的文件夹,从而导致嵌套文件的路径不正确。

当包含文件在不同的文件夹中时,您可以使用 PHP 常量__DIR__构建目标文件的绝对路径。该常量返回包含文件目录(文件夹)的绝对路径,不带尾随斜杠。连接__DIR__、正斜杠和文档相对路径将相对路径转换为绝对路径。例如,假设这是从一个包含文件到另一个包含文件的相对路径:

'../File/Upload.php'

你把它转换成这样的绝对路径:

__DIR__ . '/../File/Upload.php'

在文档相对路径的开头添加正斜杠的效果是查找包含文件的父文件夹,然后向上一级查找正确的路径。

在第十章中你会看到一个这样的例子,一个包含文件需要包含另一个在不同文件夹中的文件。

第三章回顾

本章让你一头扎进了 PHP 的世界,使用了包含、数组和多维数组。它向您展示了如何提取当前页面的名称,显示随机图像,以及获取图像的尺寸。您还学习了如何抛出和捕获异常,以及如何重定向到不同的页面。有很多东西需要吸收,所以如果第一次没有全部吸收也不用担心。你使用 PHP 越多,你对基本技术就越熟悉。在下一章中,您将学习 PHP 如何处理来自在线表单的输入,并将使用这些知识将来自网站的反馈发送到您的电子邮件收件箱。

六、赋予表单以生命

表单是使用 PHP 的核心。您使用表单登录受限页面、注册新用户、向在线商店下订单、在数据库中输入和更新信息、发送反馈……等等。所有这些用法背后都有相同的原则,所以你从本章学到的知识在大多数 PHP 应用中都有实用价值。为了演示如何处理表单中的信息,我将向您展示如何收集网站访问者的反馈并将其发送到您的邮箱。

不幸的是,用户输入可能会使您的站点遭受恶意攻击。在接受表单之前检查从表单提交的数据是很重要的。尽管 HTML5 表单元素在现代浏览器中认证用户输入,但您仍然需要检查服务器上的数据。HTML5 验证有助于合法用户避免提交有错误的表单,但是恶意用户可以很容易地避开在浏览器中执行的检查。服务器端验证不是可选的,而是必不可少的。本章中的 PHP 解决方案向你展示了如何过滤或阻止任何可疑或危险的东西。没有一个在线应用是完全防黑客的,但是要让除了最坚定的掠夺者之外的所有人远离黑客并不需要太多的努力。如果表单不完整或发现错误,最好保留用户输入并重新显示。

这些解决方案构建了一个完整的邮件处理脚本,可以在不同的表单中重用,因此按顺序阅读它们非常重要。

在本章中,您将了解以下内容:

  • 了解用户输入是如何从在线表单传输的

  • 显示错误而不丢失用户输入

  • 认证用户输入

  • 通过电子邮件发送用户输入

PHP 如何从表单中收集信息

尽管 HTML 包含了构建表单所需的所有标签,但它没有提供任何提交表单时处理表单的方法。为此,你需要一个服务器端的解决方案,比如 PHP。

日本之旅网站包含一份简单的反馈表(见图 6-1 )。其他元素—如单选按钮、复选框和下拉菜单—将在以后添加。

img/332054_5_En_6_Fig1_HTML.jpg

图 6-1

处理反馈表单是 PHP 最常见的用途之一

首先,让我们看看表单的 HTML 代码(它在ch06文件夹的contact_01.php中):

<form method="post" action="">
    <p>
        <label for="name">Name:</label>
        <input name="name" id="name" type="text">
    </p>
    <p>
        <label for="email">Email:</label>
        <input name="email" id="email" type="text">
    </p>
    <p>
        <label for="comments">Comments:</label>
        <textarea name="comments" id="comments"></textarea>
    </p>
    <p>
        <input name="send" type="submit" value="Send message">
    </p>
</form>

前两个<input>标签和<textarea>标签包含设置为相同值的nameid属性。这种重复的原因是可访问性。HTML 使用id属性将<label>元素与正确的<input>元素关联起来。然而,表单处理脚本依赖于name属性。因此,尽管id属性在 Submit 按钮中是可选的,但是对于想要处理的每个表单元素,您必须使用name属性。

Note

表单输入元素的name属性通常不应该包含空格。如果您想要组合多个单词,请用下划线将它们连接起来(如果您留下任何空格,PHP 会自动这样做)。因为本章后面开发的脚本将name属性转换为 PHP 变量,所以不要在 PHP 变量名称中使用连字符或任何其他无效字符。

另外两件需要注意的事情是开始的<form>标签中的methodaction属性。method属性决定了表单如何发送数据。可以设置为postgetaction属性告诉浏览器在单击提交按钮时将数据发送到哪里进行处理。如果该值为空,就像这里一样,页面会尝试自己处理表单。但是,空的 action 属性在 HTML5 中是无效的,因此需要解决这个问题。

Note

我有意避免使用任何新的 HTML5 表单特性,比如type="email"required属性。这使得测试 PHP 服务器端验证脚本变得更加容易。测试后,您可以更新表单以使用 HTML5 验证功能。浏览器中的验证主要是为了避免用户提交不完整的信息,所以它是可选的。永远不要跳过服务器端验证。

理解 post 和 get 之间的区别

演示postget方法之间的区别的最好方法是用一个真实的表单。如果您完成了上一章,您可以继续使用相同的文件。

否则,ch06文件夹包含日本旅程网站的一整套文件,其中包含第五章的所有代码。将contact_01.php复制到站点根目录,并将其重命名为contact.php。同时将ch06/includes文件夹中的footer.phpmenu.phptitle.php复制到站点根目录下的includes文件夹中。

  1. 找到contact.php中的开始<form>标签,将method属性的值从post更改为get,如下所示:

  2. 保存contact.php并在浏览器中加载页面。在表单中键入您的姓名、电子邮件地址和一条短信。然后单击发送消息。

<form method="get" action="">

img/332054_5_En_6_Figa_HTML.jpg

  1. 查看浏览器地址栏。您应该会看到附加在 URL 末尾的表单内容,如下所示:

img/332054_5_En_6_Figb_HTML.jpg

如果你把网址拆开,看起来是这样的:

http://localhost/php8sols/contact.php
?name=David
&email=david%40example.com
&comments=Greetings+%3A-%29
&send=Send+message

表单提交的数据已经作为一个以问号开头的查询字符串附加到基本 URL 上。来自每个字段和提交按钮的值由表单元素的name属性标识,后跟一个等号和提交的数据。来自每个输入元素的数据由一个&符号(&)分隔。URL 不能包含空格或某些字符(如感叹号或笑脸),因此浏览器用+替换空格,并将其他字符编码为十六进制值,这一过程称为 URL 编码(完整的值列表请参见 www.degraeve.com/reference/urlencoding.php )。

  1. 回到contact.php的代码,把method改回post,像这样:

  2. 保存contact.php并在浏览器中重新加载页面,确保从 URL 的末尾清除查询字符串。键入另一条消息,然后单击发送消息。您的消息应该会消失,但不会发生其他事情。它没有丢失,但是你还没有做任何事情来处理它。

  3. contact.php中,在结束</form>标签的正下方添加以下代码:

<form method="post" action="">

<pre>
<?php if ($_POST) { print_r($_POST); } ?>
</pre>

如果已经发送了任何post数据,这将显示$_POST超全局数组的内容。如第四章所述,print_r()函数允许你检查数组的内容;<pre>标签只是让输出更容易阅读。

  1. 保存页面并单击浏览器中的刷新按钮。您可能会看到类似下面的警告。这告诉你数据会被重发,这正是你想要的。确认您要再次发送信息。

img/332054_5_En_6_Figc_HTML.jpg

img/332054_5_En_6_Fig2_HTML.jpg

图 6-2

$_ POST 数组使用表单的名称属性来标识每个数据元素

  1. 步骤 6 中的代码现在应该在表单下方显示您的消息内容,如图 6-2 所示。一切都存储在 PHP 的超级全局数组之一$_POST中,其中包含使用post方法发送的数据。每个表单元素的name属性被用作数组键,这使得检索内容变得很容易。

正如您刚才看到的,get方法发送附加到 URL 上的数据,而post方法发送带有 HTTP 头的数据,因此它是隐藏的。一些浏览器将 URL 的最大长度限制在 2000 个字符左右,因此get方法只能用于少量数据。post方法可用于更大量的数据。默认情况下,PHP 允许高达 8 MB 的post数据,尽管托管公司可能会设置不同的限制。

然而,这两种方法之间最重要的区别是它们的预期用途。get方法被设计用于无论请求多少次都不会导致服务器发生变化的请求。因此,它主要用于数据库搜索;给你的搜索结果做书签很有用,因为所有的搜索标准都在 URL 中。另一方面,post方法是为导致服务器发生变化的请求而设计的。所以它被用来插入、更新或删除数据库中的记录,上传文件或发送电子邮件。

我们将在本书的后面回到get方法。这一章集中在post方法和它相关的超级全局数组$_POST

用 PHP 超级全局变量获取表单数据

$_POST超全局数组包含使用post方法发送的数据。毫不奇怪,get方法发送的数据在$_GET数组中。

要访问表单提交的值,只需将表单元素的name属性放在$_POST$_GET后面的方括号中,这取决于表单的method属性。因此,如果通过post方法发送,则email变为$_POST['email'],如果通过get方法发送,则变为$_GET['email']。么事儿啦在那里。

您可能会遇到使用$_REQUEST的脚本,这避免了区分$_POST$_GET的需要。不太安全。你应该总是知道用户信息来自哪里。$_REQUEST还包括 cookie 的值,所以您不知道您处理的是 post 方法提交的值,还是通过 URL 传输或由 cookie 注入的值。始终使用$_POST$_GET

你可能会遇到使用$HTTP_POST_VARS$HTTP_GET_VARS的旧脚本,它们与$_POST$_GET的意思相同。这些都是过时的,已经从 PHP 8 中删除了。

处理和认证用户输入

本章的最终目的是通过电子邮件将contact.php表格中的输入发送到您的收件箱。使用 PHP mail()函数相对简单。它至少需要三个参数:电子邮件要发送到的地址、包含主题行的字符串和包含邮件正文的字符串。通过将输入字段的内容连接成一个字符串来构建消息体。

大多数互联网服务提供商(ISP)实施的安全措施使得在本地测试环境中测试mail()功能变得非常困难。PHP 解决方案 6-2 到 6-5 没有直接使用mail(),而是专注于认证用户输入以确保必填字段被填写并显示错误消息。实施这些措施使您的在线表单更加用户友好和安全。

使用 JavaScript 或 HTML5 表单元素和属性来检查用户输入被称为客户端验证,因为它发生在用户的计算机(或客户端)上。它很有用,因为它几乎是即时的,可以提醒用户有问题,而无需与服务器进行不必要的往返。然而,客户端验证很容易回避。恶意用户只需从自定义脚本提交数据,您的检查就会变得毫无用处。用 PHP 检查用户输入也很重要。

Tip

客户端验证本身是不够的。总是使用 PHP 的服务器端验证来验证来自外部数据源的数据。

创建可重用的脚本

为多个网站重用同一个脚本(可能只需少量编辑)可以节省大量时间。然而,将输入数据发送到一个单独的文件进行处理,很难在不丢失用户输入的情况下提醒用户错误。为了解决这个问题,本章采用的方法是使用所谓的自处理表单

提交表单时,页面会重新加载,条件语句会运行处理脚本。如果服务器端验证检测到错误,表单可以重新显示错误消息,同时保留用户的输入。特定于表单的脚本部分将嵌入到 DOCTYPE 声明之上。通用的、可重用的部分将位于一个单独的文件中,该文件可以包含在任何需要电子邮件处理脚本的页面中。

PHP 解决方案 6-1:防止自处理表单中的跨站脚本

当提交数据时,将开始表单标签的action属性留空或完全忽略它会重新加载表单。但是,空的action属性在 HTML5 中是无效的。PHP 有一个非常方便的超级全局变量($_SERVER['PHP_SELF']),它包含当前文件的站点根目录相对路径。将它设置为action属性的值会自动为自处理表单插入正确的值——但是单独使用它会将您的站点暴露给被称为跨站脚本 (XSS)的恶意攻击。这个 PHP 解决方案解释了风险,并展示了如何安全地使用$_SERVER['PHP_SELF']

img/332054_5_En_6_Fig3_HTML.jpg

图 6-3

畸形链接嵌入了 XSS 攻击

  1. ch06文件夹中的bad_link.php加载到浏览器中。它在同一个文件夹中包含一个到form.php的链接;但是底层 HTML 中的链接已经被故意弄成畸形,以模拟 XSS 攻击。

    注意,这个 PHP 解决方案的练习文件中的链接假设它们位于本地主机服务器根目录下的一个名为php8sols/ch06的文件夹中。如有必要,调整它们以符合您的测试设置。

  2. 单击该链接。在大多数浏览器中,您应该会看到如图 6-3 所示的 JavaScript 警告对话框。

Note

谷歌 Chrome 和微软 Edge 曾用 XSS 过滤器阻止可疑攻击。但是,它已经被删除,取而代之的是使用内容安全策略(CSP)。有关 CSP 的详细信息,请参见 https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

  1. 消除 JavaScript 警告,并右键单击以查看页面源代码。第 10 行应该类似如下:

img/332054_5_En_6_Figd_HTML.jpg

bad_link.php中的错误链接在开始的<form>标签后立即向页面中注入了一段 JavaScript 代码。在这种情况下,这是一个无害的 JavaScript 警报;但在真正的 XSS 攻击中,它可能会试图窃取 cookies 或其他个人信息。这种攻击是无声的,让用户不知道发生了什么,除非他们注意到浏览器地址栏中的脚本。

这是因为form.php使用$_SERVER['PHP_SELF']来生成action属性的值。畸形链接在action属性中插入页面位置,关闭开始表单标签,然后注入<script>标签,该标签在页面加载时立即执行。

  1. 抵消这种类型的 XSS 攻击的一个简单方法是像这样将$_SERVER['PHP_SELF']传递给htmlentities()函数:
<form method="post"  action="<?= htmlentities($_SERVER['PHP_SELF']) ?>">

这将把<script>标签的尖括号转换成它们的 HTML 实体等价物,防止脚本被执行。虽然它可以工作,但它会在浏览器地址栏中留下格式错误的 URL,这可能会导致用户质疑您的站点的安全性。我认为更好的解决方案是在检测到 XSS 病毒时将用户重定向到错误页面。

  1. form.php中,在 DOCTYPE 声明上方创建一个 PHP 块,并使用当前文件的站点根目录相对路径定义一个变量,如下所示:

  2. 现在比较一下$currentPage$_SERVER['PHP_SELF']的值。如果它们不相同,使用header()函数将用户重定向到一个错误页面并立即退出脚本,如下所示:

<?php
$currentPage = '/php8sols/ch06/form.php';
?>
<!doctype html>

if ($currentPage !== $_SERVER['PHP_SELF']) {
    header('Location: http://localhost/php8sols/ch06/missing.php');
    exit;
}

Caution

传递给header()函数的位置必须是完全合格的 URL。如果使用与文档相关的链接,目标将附加到格式错误的链接上,从而阻止页面被成功重定向。

  1. 使用$currentPage作为开始表单标签中action属性的值:

  2. 保存form.php,返回bad_link.php,再次点击链接。这次你应该被直接带到missing.php

  3. 直接在浏览器中加载form.php。它应该像预期的那样加载和工作。

<form method="post"  action="<?= $currentPage ?>">

完成的版本在ch06文件夹的form_end.php中。如果你只是想测试脚本,名为bad_link_end.php的文件链接到完成的版本。

这种技术比简单地将$_SERVER['PHP_SELF']传递给htmlentities()函数涉及更多的代码;但是它有一个优点,如果用户通过一个恶意链接访问您的表单,它可以将用户无缝地引导到一个错误页面。显然,错误页面应该链接回主菜单。

PHP 解决方案 6-2:确保必填字段不为空

当必填字段为空时,您将无法获得所需的信息,并且用户可能永远得不到回复,尤其是当联系方式被省略时。

继续使用本章前面的“理解 post 和 get 之间的区别”中的文件。或者,使用ch06文件夹中的contact_02.php,并从文件名中删除_02

  1. 处理脚本使用两个名为$errors$missing的数组来存储错误的详细信息和尚未填写的必填字段。这些数组将用于控制表单标签旁边的错误消息的显示。页面第一次加载时不会有任何错误,所以在contact.php顶部的 PHP 代码块中将$errors$missing初始化为空数组,就像这样:

  2. 只有在表单提交后,电子邮件处理脚本才会运行。使用条件语句检查超全局变量$_SERVER['REQUEST_METHOD']的值。如果是 POST(全部大写),您知道表单已经使用post方法提交了。将粗体突出显示的代码添加到页面顶部的 PHP 块中。

<?php
include './includes/title.php';
$errors = [];
$missing = [];
?>

<?php
include './includes/title.php';
$errors = [];
$missing = [];
// check if the form has been submitted
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    // email processing script
}
?>

Tip

检查$_SERVER['REQUEST_METHOD']的值是否为 POST 是一个通用条件,可以用于任何表单,不管 Submit 按钮的名称是什么。

  1. 虽然您现在还不会发送电子邮件,但是可以定义两个变量来存储电子邮件的目的地址和主题行。以下代码位于您在上一步中创建的条件语句中:

  2. 接下来,创建两个数组:一个列出表单中每个字段的name属性,另一个列出所有必需的字段。为了便于演示,将email字段设为可选,这样就只需要namecomments字段。将以下代码添加到条件块中,紧接在定义主题行的代码之后:

if ( $_SERVER['REQUEST_METHOD'] == 'POST') {
    // email processing script
    $to = 'david@example.com'; // use your own email address
    $subject = 'Feedback from Japan Journey';
}

    $subject = 'Feedback from Japan Journey';
    // list expected fields
    $expected = ['name', 'email', 'comments'];
    // set required fields
    $required = ['name', 'comments'];
}

Tip

$expected数组是为了防止攻击者将其他变量注入到$_POST数组中,试图覆盖您的默认值。通过只处理那些您期望的变量,您的表单更加安全。任何虚假值都将被忽略。

  1. 下一部分代码不是特定于这个表单的,所以它应该放在一个外部文件中,可以包含在任何电子邮件处理脚本中。在includes文件夹中创建一个名为processmail.php的新 PHP 文件。然后将它包含在contact.php中,紧跟在您在上一步中输入的代码之后,就像这样:

  2. processmail.php中的代码首先检查$_POST变量中已经留空的必填字段。去掉由你的编辑器插入的任何默认代码,并将下面的代码添加到processmail.php:

    $required = ['name', 'comments'];
    require './includes/processmail.php';
}

<?php
foreach ($_POST as $key => $value) {
    // strip whitespace from $value if not an array
    if (!is_array($value)) {
        $value = trim($value);
    }
    if (!in_array($key, $expected)) {
        // ignore the value, it's not in $expected
        continue;
    }
    if (in_array($key, $required) && empty($value)) {
        // required value is missing
        $missing[] = $key;
        $$key = "";
        continue;
    }
    $$key = $value;
}

这个foreach循环处理$_POST数组,从文本字段中去除前导和尾随空格,并将字段内容赋给一个具有简化名称的变量。结果,$_POST['email']变成了$email等等。它还检查必填字段是否为空,并将它们添加到$missing数组中,将相关变量设置为空字符串。

$_POST数组是一个关联数组,所以循环将当前元素的键和值分别分配给$key$value。通过使用带有逻辑 Not 运算符(!)的is_array()函数,循环开始检查当前值是否不是一个数组。如果不是这样,trim()函数将去掉前导和尾随的空白,并将其重新分配给$value。删除开头和结尾的空白可以防止任何人多次按空格键来避免填写必填字段。

Note

该表单目前只有文本输入字段,但以后会扩展到包含以数组形式提交数据的<select>和复选框元素。有必要检查当前元素的值是否是一个数组,因为将数组传递给trim()函数会触发错误。

下一个条件语句检查当前键是否不在$expected数组中。如果不是,关键字continue会强制循环停止处理当前元素,转到下一个元素。所以任何不在$expected数组中的东西都会被忽略。

接下来,我们检查当前数组键是否在$required数组中,以及它是否没有值。如果条件返回 true,那么这个键被添加到$missing数组中,并且基于这个键的名称的一个变量被动态创建,它的值被设置为一个空字符串。注意$$key在下面一行中以两个美元符号开始:

$$key = "";

这意味着它是一个可变变量(参见第四章中的“动态创建新变量”)。所以,如果$key的值是“姓名”,$$key就变成了$name

再次,continue将循环移动到下一个元素。

但是,如果我们一直到循环的最后一行,我们知道我们正在处理一个需要处理的元素,所以基于键名的变量是动态创建的,当前值被赋给它。

  1. 保存processmail.php。稍后您将向它添加更多的代码,但是现在让我们转向contact.php的主体。开始表单标记中的 action 属性为空。为了进行本地测试,只需将其值设置为当前页面的名称:

  2. 如果缺少任何东西,您需要显示一个警告。在页面内容顶部的<h2>标题和第一段之间添加一个条件语句,如下所示:

<form method="post" action="contact.php">

<h2>Contact us</h2>
<?php if ($missing || $errors) { ?>
<p class="warning">Please fix the item(s) indicated.</p>
<?php } ?>
<p>Ut enim ad minim veniam . . . </p>

这将检查在步骤 1 中初始化为空数组的$missing$errors。正如在第四章的“PHP 的真相”中所解释的,空数组被视为false,所以当页面第一次加载时,条件语句中的段落不会显示。但是,如果提交表单时某个必填字段还没有填写,它的名称将被添加到$missing数组中。至少包含一个元素的数组被视为true||表示“或”,因此如果必填字段留空或发现错误,将显示该警告段落。($errors数组在 PHP 解决方案 6-4 中发挥作用。)

  1. 为了确保到目前为止还能工作,保存contact.php并在浏览器中正常加载(不要点击刷新按钮)。不会显示警告消息。单击发送消息,不填写任何字段。您现在应该会看到关于缺少项目的消息,如下面的屏幕截图所示:

img/332054_5_En_6_Fige_HTML.jpg

  1. 要在每个缺少的必填字段旁边显示合适的消息,使用 PHP 条件语句在<label>标记中插入一个<span>,如下所示:
<label for="name">Name:
<?php if (in_array('name', $missing)) { ?>
    <span class="warning">Please enter your name</span>
<?php } ?>
</label>

该条件使用in_array()函数来检查$missing数组是否包含值name。如果是,则显示<span>$missing在脚本顶部被定义为一个空数组,所以当页面第一次加载时,跨度不会显示。

  1. emailcomments字段插入类似的警告,如下所示:
    <label for="email">Email:
    <?php if (in_array('email', $missing)) { ?>
        <span class="warning">Please enter your email address</span>
    <?php } ?>
    </label>
    <input name="email" id="email" type="text">
</p>
<p>
    <label for="comments">Comments:
    <?php if (in_array('comments', $missing)) { ?>
        <span class="warning">Please enter your comments</span>
    <?php } ?>
    </label>

PHP 代码是相同的,除了您在$missing数组中寻找的值。它与表单元素的name属性相同。

img/332054_5_En_6_Fig4_HTML.jpg

图 6-4

通过认证用户输入,您可以显示关于必填字段的警告

  1. 保存contact.php并再次测试页面,首先在任何字段中不输入任何内容。表单标签应该如图 6-4 所示。

虽然您为email字段向<label>添加了一个警告,但是它没有显示出来,因为email还没有被添加到$required数组中。因此,它不会被processmail.php中的代码添加到$missing数组中。

  1. email添加到contact.php顶部代码块中的$required数组中,如下所示:

  2. 再次单击发送消息,不填写任何字段。这一次,您将在每个标签旁边看到一条警告消息。

  3. 在“名称”栏中键入您的姓名。在电子邮件和评论字段中,只需按几次空格键,然后单击发送消息。名称字段旁边的警告消息会消失,但其他两条警告消息会保留。processmail.php中的代码去除了文本字段中的空白,因此它拒绝通过输入一系列空格来绕过必填字段的尝试。

$required = ['name', 'comments', 'email'];

如果你有任何问题,将你的代码与ch06文件夹中的contact_03.phpincludes/processmail_01.php进行比较。

要更改必填字段,只需更改$required数组中的名称,并在表单内适当输入元素的<label>标记中添加一个合适的警告。这很容易做到,因为您总是使用表单输入元素的name属性。

当表单不完整时保留用户输入

假设你花了 10 分钟填写一张表格。单击 Submit 按钮,返回的响应是缺少一个必填字段。如果你不得不重新填写每一个字段,那就太令人恼火了。因为每个字段的内容都在$_POST数组中,所以当出现错误时很容易重新显示它。

PHP 解决方案 6-3:创建粘性表单域

这个 PHP 解决方案展示了如何使用条件语句从$_POST数组中提取用户的输入,并在文本输入字段和文本区域中重新显示。

像以前一样继续处理相同的文件。或者,使用ch06文件夹中的contact_03.phpincludes/processmail_01.php

  1. 当页面第一次加载时,您不希望任何内容出现在输入字段中,但是如果一个必填字段丢失或出现错误,您确实希望重新显示内容。这就是关键:如果$missing$errors数组包含任何值,那么每个字段的内容都应该重新显示。您使用<input>标签的value属性为文本输入字段设置了默认文本,因此将name<input>标签修改如下:

    <input name="name" id="name" type="text"
    <?php if ($missing || $errors) {
        echo 'value="' . htmlentities($name) . '"';
    } ?>>
    
    

    花括号内的行包含引号和句点的组合,这可能会让您感到困惑。首先要意识到的是只有一个分号——就在末尾——所以echo命令适用于整行。正如在第三章中所解释的,一个句点被称为连接操作符,它连接字符串和变量。您可以将该行的其余部分分为三个部分,如下所示:

    • 'value="' .

    • htmlentities($name)

    • . '"'

第一部分将value="输出为文本,并使用连接操作符将其连接到下一部分,下一部分将$name传递给一个名为htmlentities()的函数。我稍后将解释为什么这是必要的,但是第三部分再次使用连接操作符来连接最终输出,它只包含一个双引号。因此,如果$missing$errors包含任何值,而$_POST['name']包含Joe,那么在<input>标记中就会出现这个:

<input name="name" id="name" type="text" value="Joe">

$name变量包含通过$_POST数组传输的原始用户输入。你在 PHP 解决方案 6-2 的processmail.php中创建的foreach循环处理$_POST数组并将每个元素赋给一个同名的变量。这允许您简单地以$name的形式访问$_POST['name']

那么我们为什么需要htmlentities()函数呢?正如函数名所示,它将某些字符转换成等价的 HTML 字符实体。你现在关心的是双引号。假设埃里克·克拉普顿决定通过表单发送反馈。如果您单独使用$name,图 6-5 显示了当一个必填字段被省略并且您没有使用htmlentities()时会发生什么。

img/332054_5_En_6_Fig5_HTML.jpg

图 6-5

在重新显示表单域之前,需要对引号进行特殊处理

然而,将$_POST数组元素的内容传递给htmlentities(),会将字符串中间的双引号转换为&quot;。并且,如图 6-6 所示,内容不再被截断。

img/332054_5_En_6_Fig6_HTML.jpg

图 6-6

在显示之前将值传递给 htmlentities()解决了这个问题

这个比较酷的是人物实体&quot;重新提交表单时,会转换回双引号。因此,在发送电子邮件之前,无需进一步转换。

Note

如果htmlentities()破坏了你的文本,你可以在 PHP 8 中使用命名参数直接设置编码(参见第四章中的“使用命名参数”)。例如,要将编码设置为简体中文,请使用htmlentities($name, encoding: 'GB2312')

  1. 用同样的方式编辑email字段,用$email代替$name

  2. 因为<textarea>标签没有value属性,所以对comments文本区域的处理需要稍有不同。您必须将 PHP 块放在文本区域的开始和结束标记之间,就像这样(新代码以粗体显示):

<textarea name="comments" id="comments"><?php
  if ($missing || $errors) {
      echo htmlentities($comments);
  } ?></textarea>

将 PHP 的开始和结束标记放在正对着<textarea>标记的位置是很重要的。如果不这样做,就会在文本区域中出现不需要的空白。

  1. 保存contact.php并在浏览器中测试页面。如果省略了任何必填字段,表单将显示原始内容以及任何错误消息。

你可以用ch06文件夹中的contact_04.php来检查你的代码。

Caution

使用这种技术可以防止表单的重置按钮重置 PHP 脚本更改过的任何字段,因为它显式地设置了每个字段的 value 属性。

过滤掉潜在的攻击

一个被称为邮件头注入的特别令人讨厌的漏洞试图将在线表单转化为垃圾邮件中继。攻击者试图欺骗您的脚本,将带有副本的 HTML 电子邮件发送给许多人。如果您将未过滤的用户输入合并到可以作为第四个参数传递给mail()函数的附加头中,这是可能的。添加用户的电子邮件地址作为Reply-To头是很常见的。如果您在提交的值中检测到空格、换行符、回车符或任何字符串“Content-Type:"、“Cc:”或“Bcc:”时,您就是攻击的目标,因此您应该阻止该消息。

PHP 解决方案 6-4:阻止包含可疑内容的电子邮件地址

这个 PHP 解决方案检查用户的电子邮件地址输入是否有可疑内容。如果检测到,布尔变量被设置为true。这将在以后被用来阻止电子邮件被发送。

继续使用与以前相同的页面。或者,使用ch06文件夹中的contact_04.phpincludes/processmail_01.php

  1. 为了检测可疑短语,我们将使用搜索模式或正则表达式。在现有的foreach循环之前的processmail.php顶部添加以下代码:
// pattern to locate suspect phrases
$pattern = '/[\s\r\n]|Content-Type:|Bcc:|Cc:/i';
foreach ($_POST as $key => $value) {

分配给$pattern的字符串将用于执行不区分大小写的搜索:空格、回车、换行符、" Content-Type:"、" Bcc:"或" cc:"。它是以一种叫做 Perl 兼容正则表达式(PCRE)的格式编写的。搜索模式包含在一对正斜杠中,最后一个斜杠后的i使模式不区分大小写。

Tip

正则表达式是匹配文本模式的非常强大的工具。诚然,它们不容易学;但是如果你真的想使用 PHP 和 JavaScript 之类的编程语言,这是一项必不可少的技能。看看 rg Krause 的介绍正则表达式(a press,2017,ISBN 978-1-4842-2508-0)。它主要面向 JavaScript 开发人员,但是 JavaScript 和 PHP 在实现上只有很小的区别。基本语法是相同的。

  1. 您现在可以使用存储在$pattern中的 PCRE 来检测提交的电子邮件地址中任何可疑的用户输入。在步骤 1 中的$pattern变量后立即添加以下代码:
// check the submitted email address
$suspect = preg_match($pattern,  $_POST['email']);

preg_match()函数将作为第一个参数传递的正则表达式与第二个参数中的值进行比较,在本例中是来自 Email 字段的值。如果找到匹配,它将返回true。所以,如果任何可疑的内容被发现,$suspect将是真实的。但是如果没有匹配的话,那就是false

  1. 如果在电子邮件地址中检测到可疑内容,那么进一步处理$_POST数组就没有意义了。将处理$_POST变量的代码包装在一个条件语句中,如下所示:
if (!$suspect) {
    foreach ($_POST as $key => $value) {
        // strip whitespace from $value if not an array
        if (!is_array($value)) {
           $value = trim($value);
        }
        if (!in_array($key, $expected)) {
            // ignore the value, it's not in $expected
            continue;
        }
        if (in_array($key, $required) && empty($value)) {
            // required value is missing
            $missing[] = $key;
            $$key = "";
            continue;
        }
    $$key = $value;
    }
}

只有当$suspect不是true时,才会处理$_POST数组中的变量。

不要忘记用额外的花括号来结束条件语句。

  1. 编辑contact.php<h2>标题后的 PHP 块,在表单上方添加一条新的警告消息,如下所示:
<h2>Contact Us</h2>
<?php if ($_POST && $suspect) { ?>
    <p class="warning">Sorry, your mail could not be sent.
    Please try later.</p>
<?php } elseif ($missing || $errors) { ?>
  <p class="warning">Please fix the item(s) indicated.</p>
<?php } ?>

这设置了一个新的条件,该条件通过被首先考虑而优先于原始警告消息。它检查$_POST数组是否包含任何元素——换句话说,表单已经提交——以及$suspect是否为true。这一警告的语气刻意保持中立。激怒攻击者毫无意义。

  1. 保存contact.php并通过在电子邮件字段中键入任何可疑内容来测试表单。您应该会看到新的警告消息,但是您的输入不会被保存。

你可以对照ch06文件夹中的contact_05.phpincludes/processmail_02.php来检查你的代码。

发送电子邮件

在继续之前,有必要解释一下 PHP mail()函数是如何工作的,因为它将帮助您理解处理脚本的其余部分。

PHP mail()函数最多接受五个参数,都是字符串,如下所示:

  • 收件人的地址

  • 主题行

  • 邮件正文

  • 其他电子邮件标题列表(可选)

  • 附加参数(可选)

第一个参数中的电子邮件地址可以是下列格式之一:

'user@example.com'
'Some Guy <user2@example.com>'

要发送到多个地址,请使用逗号分隔的字符串,如下所示:

'user@example.com, another@example.com, Some Guy <user2@example.com>'

消息正文必须显示为单个字符串。这意味着您需要从$_POST数组中提取输入数据并格式化消息,添加标签来标识每个字段。默认情况下,mail()函数只支持纯文本。新行必须同时使用回车和换行符。还建议将行的长度限制在 78 个字符以内。虽然听起来很复杂,但是你可以用大约 20 行 PHP 代码自动构建消息体,正如你将在 PHP 解决方案 6-6 中看到的。添加其他电子邮件标题将在下一节详细介绍。

许多托管公司现在把第五个论点作为一个要求。它确保电子邮件是由可信用户发送的,通常由您自己的电子邮件地址加上前缀-f(中间没有空格)组成,全部用引号括起来。检查你的托管公司的说明,看看这是否是必需的,以及它应该采取的确切格式。

Caution

永远不要将用户输入合并到mail()函数的第五个参数中,因为它可以用来在 web 服务器上执行任意脚本。

安全使用附加电子邮件标题

你可以在 www.faqs.org/rfcs/rfc2076 找到电子邮件标题的完整列表,但是一些最著名和最有用的标题可以让你发送电子邮件的副本到其他地址(抄送和密件抄送)或者改变编码。除了最后一个标题,每个新标题都必须在一个单独的行上,以回车和换行符结束。在 PHP 的旧版本中,这意味着在双引号字符串中使用\r\n转义序列(参见第四章中的表 4-5 )。

Tip

从 PHP 7.2 开始,附加头的格式化是自动处理的。简单地定义一个关联数组,使用标题名作为每个元素的键,然后将该数组作为第四个参数传递给mail()函数。

默认情况下,mail()使用 Latin1 (ISO-8859-1)编码,它不支持重音字符。如今,网页编辑经常使用 Unicode (UTF-8 ),它支持大多数书面语言,包括欧洲语言中常用的重音符号,以及非字母文字,如中文和日文。为了确保电子邮件不会乱码,使用Content-Type头将编码设置为 UTF-8,如下所示:

$headers['Content-Type'] = 'text/plain; charset=utf-8';

您还需要将 UTF-8 作为charset属性添加到 web 页面的<head>中的<meta>标签中,如下所示:

<meta charset="utf-8">

假设您想将副本发送到其他部门,再将副本发送到另一个您不想让其他人看到的地址。由mail()发送的电子邮件通常被识别为来自nobody@yourdomain(或者分配给网络服务器的任何用户名),所以添加一个更用户友好的“发件人”地址是一个好主意。这就是你如何建立那些额外的头:

$headers['From'] = 'Japan Journey<feedback@example.com>';
$headers['Cc'] = 'sales@example.com, finance@example.com';
$headers['Bcc'] = 'secretplanning@example.com';

在定义了您想要使用的标题数组之后,您将该数组传递给mail(),就像这样(假设目的地址、主题和消息体已经存储在变量中):

$mailSent = mail($to, $subject, $message, $headers);

像这样硬编码的附加头不会带来安全风险,但是来自用户输入的任何内容在使用之前都必须经过过滤。最大的危险来自一个要求输入用户电子邮件地址的文本字段。一种广泛使用的技术是将用户的电子邮件地址合并到一个FromReply-To标题中,这使得你可以通过点击你的电子邮件程序中的回复按钮来直接回复收到的消息。这非常方便,但是攻击者经常试图在电子邮件输入字段中装入大量伪造的标题。以前的 PHP 解决方案消除了攻击者最常用的头,但是我们需要在将电子邮件地址合并到附加头之前进一步检查它。

Caution

尽管电子邮件字段是攻击者的主要目标,但是如果您允许用户更改值,目标地址和主题行都很容易受到攻击。用户输入应该总是被认为是可疑的。始终对目的地址和主题行进行硬编码。或者,提供一个可接受值的下拉菜单,并根据相同值的数组检查提交的值。

PHP 解决方案 6-5:添加标题和自动回复地址

这个 PHP 解决方案为电子邮件添加了三个标题:FromContent-Type(将编码设置为 UTF-8)和Reply-To。在将用户的电子邮件地址添加到最终的头之前,它使用一个内置的 PHP 过滤器来验证提交的值是否符合有效电子邮件地址的格式。

继续使用与以前相同的页面。或者,使用ch06文件夹中的contact_05.phpincludes/processmail_02.php

  1. 标题通常特定于特定的网站或页面,所以FromContent-Type标题将被添加到contact.php的脚本中。将以下代码添加到页面顶部的 PHP 块中,刚好在包含processmail.php之前:

  2. 验证电子邮件地址的目的是确保其格式有效,但是该字段可能为空,因为您决定不要求它,或者因为用户简单地忽略了它。如果该字段是必填的但为空,它将被添加到$missing数组中,并显示您在 PHP 解决方案 6-2 中添加的警告。如果字段不为空,但是输入无效,则需要显示不同的消息。

$required = ['name', 'comments', 'email'];
// create additional headers
$headers['From'] = 'Japan Journey<feedback@example.com>';
$headers['Content-Type'] = 'text/plain; charset=utf-8';
require './includes/processmail.php';

切换到processmail.php并将这段代码添加到脚本的底部:

// validate the user's email
if (!$suspect && !empty($email)) {
    $validemail = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
    if ($validemail) {
        $headers['Reply-To'] = $validemail;
    } else {
        $errors['email'] = true;
    }
}

首先检查是否没有发现可疑内容,并且email字段不为空。这两个条件前面都有逻辑非运算符,所以如果$suspectempty($email)都是false,它们就返回true。您在 PHP 解决方案 6-2 中添加的foreach循环将$_POST数组中所有期望的元素分配给更简单的变量,因此$email包含与$_POST['email']相同的值。

下一行使用filter_input()来验证电子邮件地址。第一个参数是 PHP 常量INPUT_POST,它指定值必须在$_POST数组中。第二个参数是您想要测试的元素的名称。最后一个参数是另一个 PHP 常量,它指定您要检查元素是否符合电子邮件的有效格式。

如果有效的话,filter_input()函数返回被测试的值。否则返回false。因此,如果用户提交的值看起来像一个有效的电子邮件地址,$validemail包含该地址。如果格式无效,$validemailfalseFILTER_VALIDATE_EMAIL常量只接受单个电子邮件地址,因此任何插入多个电子邮件地址的尝试都将被拒绝。

Note

FILTER_VALIDATE_EMAIL检查格式,而不是地址是否真实。

如果$validemail不是false,合并到Reply-To邮件头中是安全的。但是如果$validemailfalse,那么$errors['email']被添加到$errors数组中。

  1. 您现在需要修改contact.phpemail字段的<label>,如下所示:
<label for="email">Email:
<?php if (in_array('email', $missing)) { ?>
    <span class="warning">Please enter your email address</span>
<?php } elseif (isset($errors['email'])) { ?>
    <span class="warning">Invalid email address</span>
<?php } ?>
</label>

这将在第一个条件语句中添加一个elseif子句,如果电子邮件地址验证失败,将显示不同的警告。

  1. 保存contact.php并通过将所有字段留空并点击发送消息来测试表单。您将看到最初的错误消息。通过在“电子邮件”栏中输入非电子邮件地址的值或输入两个电子邮件地址来再次测试。您应该会看到无效消息。

    注意:如果在测试更新的脚本时,在多个电子邮件地址之间的逗号后面加一个空格,您将不会看到新的错误消息,因为 PHP 解决方案 6–4 中的正则表达式拒绝包含空格的电子邮件字段中的值。无论哪种方式,企图攻击被挫败。

你可以用ch06文件夹中的contact_06.phpincludes/processmail_03.php来检查你的代码。

PHP 解决方案 6-6:构建消息体并发送邮件

许多 PHP 教程展示了如何像这样手动构建消息体:

$message = "Name: $name\r\n\r\n";
$message .= "Email: $email\r\n\r\n";
$message .= "Comments: $comments";

这将添加标签来标识输入来自哪个字段,并在每个标签之间插入两个回车符和换行符。这对于少量的字段来说很好,但是对于更多的字段来说很快就变得乏味了。只要给表单字段赋予有意义的name属性,就可以用一个foreach循环自动构建消息体,这是这个 PHP 解决方案中采用的方法。

像以前一样继续处理相同的文件。或者,使用ch06文件夹中的contact_06.phpincludes/processmail_03.php

  1. processmail.php中的脚本底部添加以下代码:
$mailSent = false;

这将初始化一个变量,以便在邮件发送后重定向到感谢页面。它需要被设置为false,直到你知道mail()功能已经成功。

  1. 现在添加构建消息的代码,紧跟在:
// go ahead only if not suspect, all required fields OK, and no errors
if (!$suspect && !$missing && !$errors) {
    // initialize the $message variable
    $message = '';
    // loop through the $expected array
    foreach($expected as $item) {
        // assign the value of the current item to $val
        if (isset($$item) && !empty($$item)) {
            $val = $$item;
        } else {
            // if it has no value, assign 'Not selected'
            $val = 'Not selected';
        }
        // if an array, expand as comma-separated string
        if (is_array($val)) {
            $val = implode(', ', $val);
        }
        // replace underscores in the label with spaces
        $item = str_replace('_', ' ', $item);
        // add label and value to the message body
        $message .= ucfirst($item).": $val\r\n\r\n";
    }
    // limit line length to 70 characters
    $message = wordwrap($message, 70);
    $mailSent = true;
}

这段代码首先检查$suspect$missing$errors都是false。如果是,它通过遍历$expected数组构建消息体,将结果作为一系列标签/值对存储在$message中。

其工作原理的关键在于下面的条件语句:

if (isset($$item) && !empty($$item)) {
    $val = $$item;
}

这是使用可变变量的另一个例子(参见第四章中的“动态创建新变量”)。每次循环运行时,$item包含$expected数组中当前元素的值。第一个元素是name,所以$$item动态创建一个名为$name的变量。实际上,条件语句变成了这样:

if (isset($name) && !empty($name)) {
    $val = $name;
}

在下一次传递中,$$item创建一个名为$email的变量,依此类推。PHP 解决方案 6–2 将每个表单字段中提交的数据分配给一个简单的变量,因此这个条件语句将当前字段中的数据分配给一个临时变量$val

Caution

该脚本仅从$expected数组中的项目构建消息正文。您必须在$expected数组中列出所有表单字段的名称,它才能工作。

如果未指定为必填的字段留空,其值将设置为“未选择”该代码还处理来自多项选择元素的值,例如复选框组和<select>列表,它们作为$_POST数组的子数组传输。implode()函数将子数组转换成逗号分隔的字符串。第一个参数是要在每个数组元素之间插入的字符串。第二个参数是要处理的数组。

每个标签都是从$expected数组的当前元素中输入字段的name属性派生而来的。str_replace()的第一个参数是下划线。如果在name属性中发现了一个下划线,它将被第二个参数替换,第二个参数是一个由单个空格组成的字符串。然后第一个字母被ucfirst()设为大写。注意str_replace()的第三个参数是$item(带单美元符号),所以这次是普通变量,不是变量变量。它包含来自$expected数组的当前值。来自当前元素的数据后跟两个回车符和换行符,然后连接到标签。

将标签和字段数据组合成一个字符串后,wordwrap()函数将行长度限制为 70 个字符。

仍然需要添加发送电子邮件的代码,但是出于测试目的,$mailSent被设置为true

  1. 保存processmail.php。在contact.php的底部找到这个代码块:
<pre>
<?php if ($_POST) {print_r($_POST);} ?>
</pre>
Change it to this:
<pre>
<?php if ($_POST && $mailSent) {
    echo htmlentities($message);
    foreach ($headers as $key => $value) {
        echo htmlentities("$key: $value") . '<br>';
    }
} ?>
</pre>

这将检查表单是否已提交,邮件是否已准备好发送。然后显示$message$headers数组中的值。所有的值都被传递给htmlentities(),以确保它们在浏览器中正确显示。

img/332054_5_En_6_Fig7_HTML.jpg

图 6-7

验证邮件正文和邮件头的格式是否正确

  1. 保存contact.php,并通过输入您的姓名、电子邮件地址和简短评论来测试表单。当你点击发送消息时,你应该看到消息正文和标题显示在页面的底部,如图 6-7 所示。

假设邮件正文和标题正确显示在页面底部,您就可以添加发送电子邮件的代码了。如有必要,对照ch06文件夹中的contact_07.phpincludes/processmail_04.php检查您的代码。

  1. processmail.php中,添加发送邮件的代码。找到以下行:
$mailSent = true;
Change it to this:
$mailSent = mail($to, $subject, $message, $headers);
if (!$mailSent) {
    $errors['mailfail'] = true;
}

这会将目的地址、主题行、消息正文和标题传递给mail()函数,如果成功地将电子邮件传递给 web 服务器的邮件传输代理(MTA ),该函数将返回true。如果失败,$mailSent被设置为false,条件语句向$errors数组添加一个元素,允许您在表单重新显示时保留用户的输入。

  1. 在位于contact.php顶部的 PHP 块中,在包含processmail.php的命令之后立即添加以下条件语句:
    require './includes/processmail.php';
    if ($mailSent) {
        header('Location: http://www.example.com/thank_you.php');
        exit;
    }
}
?>

您需要在您的远程服务器上测试这一点,所以用您自己的域名替换 www.example.com 。这将检查$mailSent是否为true。如果是,header()功能重定向到thank_you.php,一个确认消息已经发送的页面。下一行中的exit命令确保脚本在页面被重定向后终止。

ch06文件夹中有一份thank_you.php的副本。

  1. 如果$mailSentfalse,则contact.php重新显示;您需要警告用户消息无法发送。编辑<h2>标题后的条件语句,如下所示:
<h2>Contact Us </h2>
<?php if (($_POST && $suspect) || ($_POST && isset($errors['mailfail']))) { ?>
    <p class="warning">Sorry, your mail could not be sent. . . .

原始条件和新条件都被括在括号中,因此每一对都被单独考虑。如果表单已提交且发现可疑短语如果表单已提交且$errors['mailfail']已设置,则显示消息未发送的警告。

  1. 删除在contact.php底部显示消息正文和标题的代码块(包括<pre>标签)。

  2. 在本地测试可能会显示感谢页面,但电子邮件不会到达。这是因为大多数测试环境没有 MTA。即使您设置了一个,大多数邮件服务器也会拒绝来自无法识别的来源的邮件。将contact.php和所有相关文件,包括processmail.phpthank_you.php上传到你的远程服务器,并在那里测试联系方式。不要忘记processmail.php需要在一个名为includes的子文件夹中。

你可以用ch06文件夹中的contact_08.phpincludes/processmail_05.php来检查你的代码。

邮件故障排除( )

重要的是要明白mail()不是一个电子邮件程序。一旦将地址、主题、消息和头传递给 MTA,PHP 的责任就结束了。它无法知道电子邮件是否被发送到了预定的目的地。通常情况下,电子邮件会瞬间到达,但是网络堵塞会延迟几个小时甚至几天。

Tip

在远程服务器上测试这个脚本时,最有可能导致失败的原因之一是 PHP 的版本。如果您的远程服务器运行的是 PHP 7.1 或更早版本,那么$headers数组将需要被转换成一个字符串,在每个头之间有一个回车符和换行符。使用带有“r\n”(双引号)的implode()作为第一个参数。

如果您在从contact.php发送邮件后被重定向到感谢页面,但您的收件箱中没有收到任何邮件,请检查以下内容:

  • 邮件被垃圾邮件过滤器拦截了吗?

  • 您是否检查过存储在$to中的目的地地址?尝试另一个电子邮件地址,看看是否有所不同。

  • 你在From头中使用了真正的地址吗?使用假的或无效的地址很可能导致邮件被拒绝。请使用与您的 web 服务器属于同一域的有效地址。

  • 请咨询您的托管公司,看看第五个参数mail()是否是必需的。如果是这样,它通常应该是一个由-f后跟您的电子邮件地址组成的字符串。比如david@example.com变成了'-fdavid@example.com'

如果您仍然没有收到来自 contact.php 的消息,请使用以下脚本创建一个文件:

<?php
ini_set('display_errors', '1');
$mailSent = mail('you@example.com', 'PHP mail test', 'This is a test email');
if ($mailSent) {
    echo 'Mail sent';
} else {
    echo 'Failed';
}

用您自己的电子邮件地址替换you@example.com。将文件上传到您的网站,并将页面加载到浏览器中。

如果您看到一条关于没有From头的错误消息,添加一个头作为mail()函数的第四个参数,如下所示:

$mailSent = mail('you@example.com', 'PHP mail test', 'This is a test email',
'From: me@example.com');

在第一个参数中使用与目的地址不同的地址通常是个好主意。

如果你的主机公司需要第五个参数,调整代码如下:

$mailSent = mail('you@example.com', 'PHP mail test', 'This is a test email', null,
'-fme@example.com');

使用第五个参数通常取代了提供一个From头的需要,所以使用null(不带引号)作为第四个参数表明它没有值。

如果你看到“邮件已发送”但没有邮件到达,或者在尝试了所有五个参数后,你看到“失败”,请咨询你的托管公司以获得建议。

如果您收到的测试邮件来自这个脚本,而不是来自contact.php,这意味着您在代码中犯了一个错误,或者您忘记上传processmail.php。临时打开错误显示,如“为什么我的页面是空白的?”在第三章中,要检查contact.php是否能够找到processmail.php

Tip

我在英国的一所大学教书,不明白为什么学生的邮件没有被投递,尽管他们的代码是完美的。原来 It 部门已经禁用了 Sendmail(MTA ),以防止服务器被用来发送垃圾邮件!

处理多选表单元素

contact.php中的表格仅使用文本输入字段和文本区。要成功使用表单,您还需要知道如何处理多选元素,即:

  • 单选按钮

  • 检查框

  • 下拉选项菜单

  • 多项选择列表

它们背后的原理与您一直在处理的文本输入字段相同:表单元素的name属性被用作$_POST数组中的键。但是,有一些重要的区别:

  • 复选框组和多选列表将选定的值存储为一个数组,因此您需要在这些类型的输入的name属性的末尾添加一对空的方括号。例如,对于一个名为interests的复选框组,每个<input>标签中的name属性应该是name="interests[]"。如果省略方括号,只有最后选择的项目通过$_POST阵列传输。

  • 复选框组或多选列表中所选项的值作为$_POST数组的子数组传输。PHP 解决方案 6-6 中的代码自动将这些子数组转换为逗号分隔的字符串。但是,当将表单用于其他目的时,您需要从子数组中提取值。您将在后面的章节中看到如何做到这一点。

  • 如果没有选择值,单选按钮、复选框和多选列表不会包含在$_POST数组中。因此,在处理表单时尝试访问它们的值之前,使用isset()检查它们的存在是至关重要的。

本章剩余的 PHP 解决方案展示了如何处理多选表单元素。我不想详细介绍每一步,我只想强调重点。在阅读本章剩余部分时,请记住以下几点:

  • 处理这些元素依赖于processmail.php中的代码。

  • 您必须将每个元素的name属性添加到$expected数组中,以便将其添加到消息体中。

  • 要使一个字段成为必填字段,将其属性添加到数组$required中。

  • 如果不需要的字段留空,processmail.php中的代码将其值设置为“未选择”

图 6-8 显示了添加到原始设计中的各种类型输入的contact.php

img/332054_5_En_6_Fig8_HTML.jpg

图 6-8

带有多项选择表单元素示例的反馈表单

Tip

HTML5 表单输入元素都使用name属性,并将值作为文本或$_POST数组的子数组发送,因此您应该能够相应地修改代码。

PHP 解决方案 6-7:处理单选按钮组

单选按钮组只允许您选择一个值。尽管在 HTML 标记中设置默认值很常见,但这不是必须的。这个 PHP 解决方案展示了如何处理这两种情况。

  1. 处理单选按钮的简单方法是将其中一个设为默认按钮。单选按钮组总是包含在$_POST数组中,因为总是选择一个值。

具有默认值的单选按钮组的代码如下所示(name属性和 PHP 代码以粗体突出显示):

<fieldset id="subscribe">
    <h2>Subscribe to newsletter?</h2>
    <p>
    <input name="subscribe" type="radio" value="Yes" id="subscribe-yes"
    <?php
    if ($_POST && $_POST['subscribe'] == 'Yes') {
        echo 'checked';
    } ?>>
    <label for="subscribe-yes">Yes</label>
    <input name="subscribe" type="radio" value="No" id="subscribe-no"
    <?php
    if (!$_POST || $_POST['subscribe'] == 'No') {
       echo 'checked';
    } ?>>
    <label for="subscribe-no">No</label>
    </p>
</fieldset>

单选按钮组的所有成员共享相同的name属性。因为只能选择一个值,所以name属性的不是以一对空括号结束。

与 Yes 按钮相关的条件语句检查$_POST以查看表单是否已经提交。如果有,并且$_POST['subscribe']的值为“是”,那么checked属性将被添加到<input>标签中。

在 No 按钮中,条件语句使用|| (or)。第一个条件是!$_POST,当表单还没有提交时是true。如果是true,当页面第一次加载时,checked属性被添加为默认值。如果false,则表示表单已经提交,因此检查$_POST['subscribe']的值。

  1. 当单选按钮没有默认值时,它不包含在$_POST数组中,所以构建$missing数组的processmail.php中的循环不会检测到它。为了确保单选按钮元素包含在$_POST数组中,您需要在表单提交后测试它是否存在。如果没有包含它,您需要将其值设置为空字符串,如下所示:

  2. 如果单选按钮组是必需的但未被选中,则需要在表单重新加载时显示一条错误消息。您还需要更改<input>标签中的条件语句来反映不同的行为。

$required = ['name', 'comments', 'email', 'subscribe'];
// set default values for variables that might not exist
if (!isset($_POST['subscribe'])) {
    $_POST['subscribe'] = '';
}

下面的清单显示了来自contact_09.phpsubscribe单选按钮组,所有 PHP 代码都以粗体突出显示:

<fieldset id="subscribe">
    <h2>Subscribe to newsletter?
    <?php if (in_array('subscribe', $missing)) { ?>
    <span class="warning">Please make a selection</span>
    <?php } ?>
    </h2>
    <p>
    <input name="subscribe" type="radio" value="Yes" id="subscribe-yes"
    <?php
    if ($_POST && $_POST['subscribe'] == 'Yes') {
        echo 'checked';
    } ?>>
    <label for="subscribe-yes">Yes</label>
    <input name="subscribe" type="radio" value="No" id="subscribe-no"
    <?php
    if ($_POST && $_POST['subscribe'] == 'No') {
        echo 'checked';
    } ?>>
    <label for="subscribe-no">No</label>
    </p>
</fieldset>

控制<h2>标签中警告消息的条件语句使用了与文本输入字段相同的技术。如果单选按钮组是必填项并且在$missing数组中,则显示该消息。

两个单选按钮中围绕checked属性的条件语句是相同的。它检查表单是否已经提交,只有当$_POST['subscribe']中的值匹配时才显示选中的属性。

PHP 解决方案 6-8:处理复选框组

复选框可以单独使用,也可以成组使用。处理它们的方法略有不同。这个 PHP 解决方案展示了如何处理名为interests的复选框组。PHP 解决方案 6-11 解释了如何处理单个复选框。

当作为一个组使用时,组中的所有复选框共享同一个name属性,该属性需要以一对空的方括号结束,以便 PHP 将选择的值作为数组传输。为了识别哪些复选框被选中,每个复选框都需要一个惟一的value属性。

如果没有选择任何项目,复选框组不包含在$_POST数组中。表单提交后,您需要检查$_POST数组,看它是否包含复选框组的子数组。如果没有,您需要创建一个空的子数组作为processmail.php中脚本的默认值。

  1. 为了节省空间,只显示该组的前两个复选框。代码的name属性和 PHP 部分以粗体突出显示:
<fieldset id="interests">
<h2>Interests in Japan</h2>
<div>
    <p>
        <input type="checkbox" name="interests[]" value="Anime/manga"
        id="anime"
        <?php
        if ($_POST && in_array('Anime/manga', $_POST['interests'])) {
            echo 'checked';
        } ?>>
        <label for="anime">Anime/manga</label>
    </p>
    <p>
        <input type="checkbox" name="interests[]" value="Arts & crafts"
        id="art"
        <?php
        if ($_POST && in_array('Arts & crafts', $_POST['interests'])) {
            echo 'checked';
        } ?>>
        <label for="art">Arts & crafts</label>
    </p>
. . .
</div>
</fieldset>

每个复选框共享相同的name属性,该属性以一对空方括号结束,因此数据被视为数组。如果省略括号,$_POST['interests']只包含选中的第一个复选框的值。另外,如果没有选择复选框,$_POST['interests']也不会存在。您将在下一步中解决这个问题。

Note

虽然对于多重选择,必须将括号添加到name属性中,但是所选值的子数组在$_POST['interests']中,而不是在$_POST['interests[]']中。

每个复选框元素中的 PHP 代码执行与单选按钮组中相同的角色,将checked属性包装在一个条件语句中。第一个条件检查表单是否已经提交。第二个条件使用in_array()函数来检查与该复选框相关联的value是否在$_POST['interests']子数组中。如果是,则意味着该复选框已被选中。

  1. 提交表单后,您需要检查是否存在$_POST['interests']。如果还没有设置,您必须创建一个空数组作为缺省值,以便脚本的其余部分进行处理。代码遵循与单选按钮组相同的模式:

  2. 要设置所需复选框的最小数量,使用count()功能确认从表单传输的值的数量。如果少于所需的最小值,将该组添加到$errors数组,如下所示:

$required = ['name', 'comments', 'email', 'subscribe', 'interests'];
// set default values for variables that might not exist
if (!isset($_POST['subscribe'])) {
    $_POST['subscribe'] = '';
}
if (!isset($_POST['interests'])) {
    $_POST['interests'] = [];
}

if (!isset($_POST['interests'])) {
    $_POST['interests'] = [];
}
// minimum number of required check boxes
$minCheckboxes = 2;
if (count($_POST['interests']) < $minCheckboxes) {
    $errors['interests'] = true;
}

count()函数返回数组中元素的数量,所以如果选择的复选框少于两个,就会创建$errors['interests']。你可能想知道为什么我用了一个变量,而不是像这样的数字:

if (count($_POST['interests']) < 2) {

这当然是可行的,而且涉及的输入更少,但是$minCheckboxes可以在错误消息中重用。将数字存储在变量中意味着这种情况和错误消息总是保持同步。

  1. 表单正文中的错误消息如下所示:
<h2>Interests in Japan
<?php if (isset($errors['interests'])) { ?>
    <span class="warning">Please select at least <?= $minCheckboxes ?></span>
<?php } ?>
</h2>

PHP 解决方案 6-9:使用下拉选项菜单

<select>标签创建的下拉选项菜单类似于单选按钮组,因为它们通常只允许用户从几个选项中选择一个。它们的不同之处在于下拉菜单中总是有一个项目被选中,即使它只是邀请用户选择其他项目的第一个项目。因此,$_POST数组总是包含一个引用<select>菜单的元素,而单选按钮组被忽略,除非预设了默认值。

  1. 下面的代码显示了contact_09.php中下拉菜单的前两项,PHP 代码以粗体突出显示。与所有多选元素一样,PHP 代码包装了指示选择了哪个项目的属性。尽管这个属性在单选按钮和复选框中都被称为checked,但在<select>菜单和列表中它被称为selected。如果提交的表单缺少必需的项目,使用正确的属性重新显示选择是很重要的。当页面第一次加载时,$_POST数组不包含任何元素,所以您可以通过测试!$_POST来选择第一个<option>。一旦表单被提交,$_POST数组总是包含一个下拉菜单中的元素,所以您不需要测试它是否存在:

  2. 尽管下拉菜单中的某个选项总是处于选中状态,但您可能希望强制用户进行非默认选择。为此,将<select>菜单的name属性添加到$required数组,然后将默认选项的value属性和$_POST数组元素设置为空字符串,如下所示:

<p>
    <label for="howhear">How did you hear of Japan Journey?</label>
    <select name="howhear" id="howhear">
        <option value="No reply"
        <?php
        if (!$_POST || $_POST['howhear'] == 'No reply') {
            echo 'selected';
        } ?>>Select one</option>
        <option value="Apress"
        <?php
        if (isset($_POST && $_POST['howhear'] == 'Apress') {
            echo 'selected';
        } ?>>Apress</option>
    . . .
    </select>
</p>

<option value=""
<?php
if (!$_POST || $_POST['howhear'] == '') {
    echo 'selected';
} ?>>Select one</option>

<option>标签中不需要value属性,但是如果省略它,表单会使用开始和结束标签之间的文本作为选择值。因此,有必要将value属性显式设置为空字符串。否则,“选择一个”作为所选值传输。

  1. 如果没有进行选择,则显示警告消息的代码遵循一种熟悉的模式:
<label for="select">How did you hear of Japan Journey?
<?php if (in_array('howhear', $missing)) { ?>
    <span class="warning">Please make a selection</span>
<?php } ?>
</label>

PHP 解决方案 6-10:处理多项选择列表

多选列表类似于复选框组:它们允许用户选择零个或多个项目,因此结果存储在一个数组中。如果没有选择任何项目,多选列表就不会包含在$_POST数组中,所以需要像添加复选框组一样添加一个空的子数组。

  1. 下面的代码显示了contact_09.php中多选列表的前两项,用粗体突出显示了name属性和 PHP 代码。附加到name属性的方括号确保它将结果存储为一个数组。该代码的工作方式与 PHP 解决方案 6-8 中的复选框组相同:

  2. 在处理消息的代码中,以与复选框数组相同的方式为多选列表设置默认值:

<p>
    <label for="characteristics">What characteristics do you associate with
    Japan?</label>
    <select name="characteristics[]" size="6" multiple="multiple"
    id="characteristics">
        <option value="Dynamic"
        <?php
        if ($_POST && in_array('Dynamic', $_POST['characteristics'])) {
            echo 'selected';
        } ?>>Dynamic</option>
        <option value="Honest"
        <?php
        if ($_POST && in_array('Honest', $_POST['characteristics'])) {
            echo 'selected';
        } ?>>Honest</option>
. . .
    </select>
</p>

  1. 要制作所需的多选列表并设置最小选择数,使用 PHP 解决方案 6-8 中用于复选框组的相同技术。
if (!isset($_POST['interests'])) {
  $_POST['interests'] = [];
}
if (!isset($_POST['characteristics'])) {
  $_POST['characteristics'] = [];
}

PHP 解决方案 6-11:处理单个复选框

处理单个复选框的方式与复选框组略有不同。对于一个单独的复选框,您不需要将方括号附加到name属性,因为它不需要作为数组来处理。另外,value属性是可选的。如果不设置value属性,则复选框被选中时默认为“开”。但是,如果复选框没有被选中,它的名字就不会包含在$_POST数组中,所以您需要测试它是否存在。

这个 PHP 解决方案展示了如何添加一个复选框来确认站点的条款已经被接受。它假设需要选中该复选框。

  1. 这段代码显示了单个复选框,用粗体突出显示了name属性和 PHP 代码。
<p>
    <input type="checkbox" name="terms" value="accepted" id="terms"
    <?php
    if ($_POST && !isset($errors['terms'])) {
        echo 'checked';
    } ?>>
    <label for="terms">I accept the terms of using this website
    <?php if (isset($errors['terms'])) { ?>
        <span class="warning">Please select the check box</span>
    <?php } ?></label>
</p>

只有当$_POST数组包含值并且$errors['terms']没有被设置时,<input>元素中的 PHP 块才会插入checked属性。这可以确保在首次加载页面时不会选中该复选框。如果用户在没有确认接受条款的情况下提交表单,它也会保持未选中状态。

如果设置了$errors['terms'],第二个 PHP 块会在标签旁边显示一条错误消息。

  1. 除了向$expected$required数组添加项之外,还需要为$_POST['terms']设置一个默认值;然后在提交表单时处理数据的代码中设置$errors['terms']:
if (!isset($_POST['characteristics'])) {
    $_POST['characteristics'] = [];
}
if (!isset($_POST['terms'])) {
    $_POST['terms'] = '';
    $errors['terms'] = true;
}

只有在复选框是必需的情况下,才需要创建$errors['terms']。对于可选的复选框,如果它不包含在$_POST数组中,只需将值设置为空字符串。

第三章回顾

构建processmail.php已经做了很多工作,但是这个脚本的美妙之处在于它可以与任何表单一起工作。唯一需要更改的部分是$expected$required数组以及特定于表单的细节,比如目标地址、标题和多选元素的默认值,如果没有选择值,这些内容将不会包含在$_POST数组中。

我避免谈论 HTML 电子邮件,因为mail()函数处理它的能力很差。位于 www.php.net/manual/en/book.mail.php 的 PHP 在线手册展示了一种通过添加额外标题来发送 HTML 邮件的方法。然而,人们普遍认为 HTML 邮件应该包含不接受 HTML 的电子邮件程序的替代文本版本。如果想发送 HTML 邮件或附件,试试 PHPMailer ( https://github.com/PHPMailer/PHPMailer/ )。

正如你将在后面的章节中看到的,在线表单是你用 PHP 做的所有事情的核心。它们是浏览器和网络服务器之间的网关。你会一次又一次地回到你在本章中学到的技术。