PHP-MySQL-和-JavaScript-学习指南第六版-二-

56 阅读37分钟

PHP、MySQL 和 JavaScript 学习指南第六版(二)

原文:zh.annas-archive.org/md5/4aa97a1e8046991cb9f8d5f0f234943f

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:PHP 函数和对象

任何编程语言的基本要求包括存储数据的地方,指导程序流的方法,以及一些细节,如表达式评估、文件管理和文本输出。PHP 具备所有这些功能,还有像 elseelseif 这样的工具,使生活更轻松。但即使在你的工具包中拥有所有这些,编程仍然可能笨拙和乏味,特别是在每次需要它们时必须重新编写非常相似的代码片段时。

函数和对象应运而生。正如你可能猜到的那样,函数是执行特定功能的一组语句,还可以选择性地返回一个值。当你需要使用多次的一段代码时,你可以将其放入一个函数中,并在需要时通过函数名调用该函数。

函数比连续的内联代码有很多优势。例如:

  • 减少输入量

  • 减少语法和其他编程错误

  • 减少程序文件的加载时间

  • 减少执行时间,因为每个函数只编译一次,无论调用多少次

  • 接受参数,因此可以用于一般以及特定的情况

对象进一步推进了这个概念。对象将一个或多个函数及其使用的数据合并到一个称为的单一结构中。

在本章中,你将学习如何使用函数,从定义和调用到来回传递参数。掌握这些知识后,你将开始创建函数,并在自己的对象中使用它们(在这里它们被称为方法)。

注意

现在使用低于 PHP 5.4 的任何版本已经非常不常见(而且绝对不推荐)。因此,本章假设您将使用此版本作为最低版本。一般建议使用版本 5.6,或者新版本 7.0 或 7.1(没有版本 6)。您可以从 AMPPS 控制面板中选择其中任何一个,如第二章所述。

PHP 函数

PHP 提供了数百个内置函数,使其成为一个非常丰富的语言。要使用函数,只需通过名称调用它。例如,你可以在这里看到 date 函数的运行效果:

echo date("l"); // Displays the day of the week

括号告诉 PHP 你正在引用一个函数。否则,它会认为你在引用一个常量或变量。

函数可以接受任意数量的参数,包括零个。例如,如下所示的 phpinfo 显示有关当前 PHP 安装的大量信息,并且不需要参数:

phpinfo();

调用该函数的结果可以在图 5-1 中看到。

图 5-1. PHP 内置函数 phpinfo 的输出
警告

phpinfo 函数非常有用,可以获取有关当前 PHP 安装的信息,但这些信息对潜在的黑客也可能非常有用。因此,在任何 Web 可用的代码中绝不要留下对此函数的调用。

一些使用一个或多个参数的内置函数出现在示例 5-1中。

示例 5-1. 三个字符串函数
<?php
  echo strrev(" .dlrow olleH"); // Reverse string
  echo str_repeat("Hip ", 2);   // Repeat string
  echo strtoupper("hooray!");   // String to uppercase
?>

此示例使用三个字符串函数输出以下文本:

Hello world. Hip Hip HOORAY!

正如您所见,strrev函数颠倒了字符串中字符的顺序,str_repeat重复了字符串"Hip "两次(根据第二个参数的要求),而strtoupper"hooray!"转换为大写。

定义一个函数

函数的一般语法如下:

function *`function_name`*([*`parameter`* [, ...]])
{
  // *`Statements`*
}

语法的第一行指示如下:

  • 定义以function开头。

  • 名称紧随其后,必须以字母或下划线开头,后跟任意数量的字母、数字或下划线。

  • 括号是必需的。

  • 一个或多个用逗号分隔的参数是可选的(如方括号所示)。

函数名称不区分大小写,所以以下所有字符串都可以引用print函数:PRINTPrintPrInT

开放大括号开始执行调用函数时会执行的语句;匹配的大括号必须结束它。这些语句可以包括一个或多个return语句,它们会强制函数停止执行并返回到调用代码。如果return语句附有值,调用代码可以检索它,我们将在接下来看到。

返回一个值

让我们看一个简单的函数,将一个人的全名转换为小写,然后将每个部分的第一个字母大写。

我们已经在示例 5-1中看到了 PHP 内置的strtoupper函数的示例。对于我们当前的函数,我们将使用它的对应函数,strtolower

$lowered = strtolower("aNY # of Letters and Punctuation you WANT");
echo $lowered;

此实验的输出如下:

any # of letters and punctuation you want

尽管我们不希望名字全部小写;我们希望将句子中每个部分的第一个字母大写。(对于这个示例,我们不打算处理 Mary-Ann 或 Jo-En-Lai 等细微情况。)幸运的是,PHP 还提供了一个ucfirst函数,可以将字符串的第一个字符设为大写:

$ucfixed = ucfirst("any # of letters and punctuation you want");
echo $ucfixed;

输出如下:

    Any # of letters and punctuation you want

现在我们可以进行第一步程序设计:将单词首字母大写,我们先调用strtolower函数将字符串转为小写,然后再调用ucfirst。这样做的方法是在ucfirst中嵌套调用strtolower。让我们看看为什么,因为理解代码评估顺序很重要。

假设你简单调用print函数:

print(5-8);

表达式5-8首先被评估,输出是–3。(如前一章所示,PHP 将结果转换为字符串以便显示。)如果表达式包含一个函数,那么该函数也将首先被评估:

print(abs(5-8));

PHP 在执行这个简短语句时做了几件事情:

  1. 评估5-8以生成–3

  2. 使用abs函数将–3转换为3

  3. 将结果转换为字符串并使用print函数输出它。

这一切都有效是因为 PHP 从内到外评估每个元素。当我们调用以下内容时,同样的过程正在进行:

ucfirst(strtolower("aNY # of Letters and Punctuation you WANT"))

PHP 将我们的字符串传递给strtolower,然后传递给ucfirst,生成的结果如我们分别使用这些函数时所见:

    任意数量的字母和标点符号

现在让我们定义一个函数(显示在示例 5-2 中),该函数接受三个名称并将每个名称转换为小写,并以大写字母开头。

示例 5-2. 清理完整姓名
<?php
  echo fix_names("WILLIAM", "henry", "gatES");

  function fix_names($n1, $n2, $n3)
  {
    $n1 = ucfirst(strtolower($n1));
    $n2 = ucfirst(strtolower($n2));
    $n3 = ucfirst(strtolower($n3));

    return $n1 . " " . $n2 . " " . $n3;
  }
?>

你可能会发现自己写这种类型的代码,因为用户经常不小心将 Caps Lock 键保持打开状态,错误地插入大写字母,甚至完全忘记大写。此示例的输出如下所示:

**  William Henry Gates**

返回一个数组

我们刚刚看到一个函数返回单个值。还有从函数中获取多个值的方法。

第一种方法是在一个数组中返回它们。正如你在第 3 章中看到的那样,数组就像一排粘在一起的变量。示例 5-3 展示了如何使用数组返回函数值。

示例 5-3. 在数组中返回多个值
<?php
  $names = fix_names("WILLIAM", "henry", "gatES");
  echo $names[0] . " " . $names[1] . " " . $names[2];

  function fix_names($n1, $n2, $n3)
  {
    $n1 = ucfirst(strtolower($n1));
    $n2 = ucfirst(strtolower($n2));
    $n3 = ucfirst(strtolower($n3));

    return array($n1, $n2, $n3);
  }
?>

这种方法的好处是保持所有三个名称分开,而不是将它们连接成一个字符串,因此你可以仅仅通过名字或姓氏来引用任何用户,而不必从返回的字符串中提取任何一个名称。

按引用传递参数

在 PHP 5.3 之前的版本中,你可以在调用函数时用&符号作为变量的前缀(例如,increment(&$myvar);)告诉解析器传递变量的引用,而不是变量的值。这使函数可以访问变量(允许将不同的值写回到它)。

注意

在 PHP 5.3 中弃用了调用时传递引用,而在 PHP 5.4 中移除了这个特性。因此,除了在旧版网站上,你不应该使用这个特性,即使在那里也建议重新编写传递引用的代码,因为在较新版本的 PHP 上会产生致命错误。

但是,在函数定义内部,你仍然可以通过引用访问参数。这个概念可能很难理解,所以让我们回到第 3 章中的火柴盒比喻。

想象一下,不是从火柴盒里拿出一张纸,读取它,将上面的内容复制到另一张纸上,把原始的放回去,然后将复印件传递给一个函数(呼!),你可以简单地将一根线连接到原始的纸上,并将其一端传递给函数(参见图 5-2)。

将引用想象成连接到变量的线程

图 5-2. 想象一个参考作为附加到变量的线索

现在函数可以跟随线索找到要访问的数据。这样可以避免为函数使用的变量创建副本的所有开销。更重要的是,函数现在可以修改变量的值。

这意味着您可以重写 示例 5-3,将所有参数的引用传递,然后函数可以直接修改这些参数(参见 示例 5-4)。

示例 5-4. 通过引用将值传递给函数
<?php
  $a1 = "WILLIAM";
  $a2 = "henry";
  $a3 = "gatES";

  echo $a1 . " " . $a2 . " " . $a3 . "<br>";
  fix_names($a1, $a2, $a3);
  echo $a1 . " " . $a2 . " " . $a3;

  function fix_names(&$n1, &$n2, &$n3)
  {
    $n1 = ucfirst(strtolower($n1));
    $n2 = ucfirst(strtolower($n2));
    $n3 = ucfirst(strtolower($n3));
  }
?>

与其直接将字符串传递给函数,不如先将它们分配给变量并打印出它们的“before”值。然后像以前一样调用函数,但在函数定义内部,您在每个参数前面放置一个 & 符号以按引用传递。

现在变量 $n1$n2$n3 都附加到通向 $a1$a2$a3 值的“线索”。换句话说,有一个值组,但允许两组变量名访问它们。

因此,函数 fix_names 只需要为 $n1$n2$n3 分配新值即可更新 $a1$a2$a3 的值。此代码的输出是:

    威廉·亨利·盖茨·威廉·亨利·盖茨

正如您所见,echo 语句只使用了 $a1$a2$a3 的值。

返回全局变量

给函数访问外部创建的不作为参数传递的变量更好的方法是通过在函数内部声明具有全局访问权限。在变量名后面跟着 global 关键字可以让代码的每个部分都完全访问它(参见 示例 5-5)。

示例 5-5. 在全局变量中返回值
<?php
  $a1 = "WILLIAM";
  $a2 = "henry";
  $a3 = "gatES";

  echo $a1 . " " . $a2 . " " . $a3 . "<br>";
  fix_names();
  echo $a1 . " " . $a2 . " " . $a3;

  function fix_names()
  {
    global $a1; $a1 = ucfirst(strtolower($a1));
    global $a2; $a2 = ucfirst(strtolower($a2));
    global $a3; $a3 = ucfirst(strtolower($a3));
  }
?>

现在您不必将参数传递给函数,函数也不必接受它们。一旦声明,这些变量保留全局访问权限,并且可以在程序的其余部分(包括其函数)中使用。

变量作用域回顾

快速回顾从 第三章 中所知的内容:

  • 局部变量 只能从定义它们的代码部分访问。如果它们在函数外部,则可以被所有函数外部的代码访问,包括类等。如果一个变量在函数内部,只有该函数可以访问该变量,并且当函数返回时其值会丢失。

  • 全局变量 可以从代码的所有部分访问,无论是在函数内部还是外部。

  • 静态变量 只能在声明它们的函数内部访问,但在多次调用之间保留它们的值。

包含和需要文件

在您使用 PHP 编程时,您可能会开始构建您认为将来会再次使用的函数库。您还可能开始使用其他程序员创建的库。

没有必要将这些函数复制粘贴到你的代码中。你可以将它们保存在单独的文件中,并使用命令将它们拉入。有两个命令可以执行此操作:includerequire

include 语句

使用 include,你可以告诉 PHP 获取一个特定文件并加载其所有内容。就好像你把包含的文件粘贴到当前文件的插入点一样。示例 5-6 展示了如何包含一个名为 library.php 的文件。

示例 5-6. 包含一个 PHP 文件
<?php
  include "library.php";

  // Your code goes here
?>

使用 include_once

每次使用 include 指令,即使已经插入过,也会再次包含请求的文件。例如,假设 library.php 包含许多有用的函数,你将它包含在你的文件中,但你还包含了另一个包含 library.php 的库。通过嵌套,你无意中包含了 library.php 两次。这将产生错误消息,因为你尝试多次定义相同的常量或函数。所以,你应该使用 include_once 替代(参见 示例 5-7)。

示例 5-7. 仅包含一次 PHP 文件
<?php
  include_once "library.php";

  // Your code goes here
?>

然后,任何进一步尝试包含同一文件(使用 includeinclude_once)都将被忽略。要确定请求的文件是否已执行,需要在解析所有相对路径(到它们的绝对路径)并在你的 include 路径中找到文件后,匹配绝对文件路径。

注意

一般来说,最好坚持使用 include_once,忽略基本的 include 语句。这样,你就永远不会遇到多次包含文件的问题。

使用 require 和 require_once

includeinclude_once 的一个潜在问题是,PHP 只会 尝试 包含请求的文件。即使找不到文件,程序执行也会继续。

当绝对需要包含文件时,使用 require。出于同样的原因,我建议你在需要 require 文件时通常坚持使用 require_once(参见 示例 5-8)。

示例 5-8. 仅需要一次引入 PHP 文件
<?php
  require_once "library.php";

  // Your code goes here
?>

PHP 版本兼容性

PHP 正在持续开发中,有多个版本。如果需要检查特定函数是否可用于你的代码,可以使用 function_exists 函数,该函数检查所有预定义和用户创建的函数。

示例 5-9 检查 array_combine,这是仅适用于某些 PHP 版本的特定函数。

示例 5-9. 检查函数是否存在
<?php
  if (function_exists("array_combine"))
  {
    echo "Function exists";
  }
  else
  {
    echo "Function does not exist - better write our own";
  }
?>

使用这样的代码,你可以利用新版本 PHP 的功能,同时使你的代码在旧版本上运行,只要你复制了任何缺失的功能。你的函数可能比内置函数慢,但至少你的代码将更加可移植。

你也可以使用phpversion函数来确定你的代码运行在哪个版本的 PHP 上。返回的结果将类似于以下内容,具体取决于版本:

**  8.0.0**

PHP 对象

就像函数代表着编程力量在计算机早期时代的巨大增长一样,在那些时代,有时候最好的程序导航只是一个非常基本的GOTOGOSUB语句一样,面向对象编程(OOP)将函数的使用带入了不同的方向。

一旦你掌握了将可重用的代码片段压缩成函数的技巧,将这些函数及其数据打包成对象就不是那么大的飞跃了。

让我们以一个有很多部分的社交网络站点为例。其中一个部分处理所有用户功能——也就是说,用于启用新用户注册和现有用户修改其详细信息的代码。在标准的 PHP 中,你可能会创建一些函数来处理这些,并嵌入一些调用 MySQL 数据库的代码来跟踪所有用户。

要创建一个表示当前用户的对象,你可以创建一个类,也许叫做User,其中包含处理用户所需的所有代码以及操作类内部数据所需的所有变量。然后,每当需要操作用户数据时,你可以简单地使用User类创建一个新对象。

你可以将这个新对象视为实际的用户。例如,你可以向对象传递姓名、密码和电子邮件地址;询问它是否已存在这样一个用户;如果不存在,让它使用这些属性创建一个新用户。你甚至可以有一个即时消息对象,或者用于管理两个用户是否是朋友的对象。

术语

当创建一个使用对象的程序时,你需要设计一个称为的数据和代码的复合体。基于这个类创建的每个新对象称为该类的实例(或发生)。

与对象相关联的数据称为其属性;它所使用的函数称为方法。在定义一个类时,你提供其属性的名称和方法的代码。参见图 5-3 关于一个对象的自动唱机比喻。想象一下它在旋转碟盘中保存的 CD,就像是它的属性;播放它们的方法是在前面板上按按钮。还有一个插入硬币的槽口(用于激活对象的方法),以及一个激光唱片阅读器(用于从 CD 中检索音乐或属性的方法)。

一个自包含对象的绝佳例子:自动唱机

图 5-3. 一个自包含对象的绝佳例子:自动唱机

当你创建对象时,最好使用封装,或者以这样一种方式编写类,即只有其方法才能用于操作其属性。换句话说,你拒绝外部代码直接访问其数据。你提供的方法被称为对象的接口

这种方法使得调试变得容易:你只需修复类内的错误代码。 另外,当你想要升级程序时,如果使用了适当的封装并保持了相同的接口,你只需开发新的替代类,彻底调试它们,然后替换旧类。 如果它们不起作用,你可以重新替换旧类,立即修复问题,然后进一步调试新类。

一旦你创建了一个类,你可能会发现你需要另一个类,它与之类似但不完全相同。 最快最简单的方法是使用继承定义一个新类。 当你这样做时,你的新类拥有从它继承的所有属性。 原始类现在称为父类(或偶尔是超类),而新类则是子类(或派生类)。

在我们的点唱机示例中,如果你发明了一个新的点唱机,它可以同时播放视频和音乐,你可以继承原始点唱机超类的所有属性和方法,并添加一些新属性(视频)和新方法(电影播放器)。

这个系统的一个显著优点是,如果你提高了超类的速度或任何其他方面,其子类将获得同样的好处。 另一方面,对父/超类进行的任何更改可能会破坏子类。

声明类

在你可以使用对象之前,你必须用class关键字定义一个类。 类定义包含类名(区分大小写),其属性和其方法。 示例 5-10 定义了User类,具有两个属性$name$password(由public关键字指示—参见“属性和方法范围”)。 它还创建了这个类的新实例(称为$object)。

示例 5-10. 声明一个类并检查一个对象
<?php
  $object = new User;
  print_r($object);

  class User
  {
    public $name, $password;

    function save_user()
    {
      echo "Save User code goes here";
    }
  }
?>

在这里,我还使用了一个称为print_r的宝贵函数。 它要求 PHP 以人类可读的形式显示关于变量的信息。 (_r代表人类可读。) 在新对象$object的情况下,它显示如下内容:

`User` `Object`
`(`
  `[``name``]`     `=>`
  `[``password``]` `=>`
`)`

然而,浏览器会压缩所有空白字符,因此在浏览器中输出稍微难以阅读:

    用户对象([name] => [password] => )

无论如何,输出显示$object是一个具有属性namepassword的用户定义对象。

创建对象

要创建一个具有指定类的对象,请使用new关键字,如此:$object = new Class。 这里有几种我们可以这样做的方式:

$object = new User;
$temp   = new User('name', 'password');

在第一行中,我们只是将一个对象分配给User类。 在第二行,我们向调用传递参数。

一个类可能需要或禁止参数; 它也可能允许不明确需要的参数。

访问对象

让我们添加几行到示例 5-10,并检查结果。示例 5-11 通过设置对象属性并调用方法扩展了先前的代码。

示例 5-11. 创建并与对象交互
<?php
  $object = new User;
  print_r($object); echo "<br>";

  $object->name     = "Joe";
  $object->password = "mypass";
  print_r($object); echo "<br>";

  $object->save_user();

  class User
  {
    public $name, $password;

    function save_user()
    {
      echo "Save User code goes here";
    }
  }
?>

正如您所见,访问对象属性的语法是*$object->property。同样,调用方法的方式是这样的:$object->method()*。

您应该注意到,示例中的propertymethod并没有在它们前面加上$符号。如果您在它们前面加上$符号,代码将无法工作,因为它会尝试引用变量中的值。例如,表达式$object->$property会尝试查找分配给名为$property的变量的值(假设该值是字符串brown),然后尝试引用属性$object->brown。如果$property未定义,则会尝试引用$object->NULL并导致错误。

使用浏览器的查看源代码工具查看示例 5-11 的输出如下:

`User` `Object`
`(`
  `[``name``]`     `=>`
  `[``password``]` `=>`
`)`
`User` `Object`
`(`
  `[``name``]`     `=>` `Joe`
  `[``password``]` `=>` `mypass`
`)`
`Save` `User` `code` `goes` `here`

再次使用print_r通过在属性分配之前和之后提供$object的内容来展示其实用性。从现在开始,我将省略print_r语句,但是如果您在开发服务器上跟随本书工作,可以添加一些语句以准确了解发生了什么。

您还可以看到,在方法save_user中的代码通过调用该方法而执行。它打印了一个提醒我们创建一些代码的字符串。

注意

您可以将函数和类定义放置在代码的任何位置,无论是在使用它们的语句之前还是之后。不过,通常认为将它们放置在文件的末尾是一种良好的实践。

克隆对象

一旦创建了对象,在将其作为参数传递时,它将按引用传递。用火柴盒的比喻来说,这就像将几根线固定在一个放在火柴盒中的对象上,这样您就可以跟随任何附加的线来访问它。

换句话说,对象分配并不复制对象的整体内容。您将在示例 5-12 中看到这是如何工作的,我们定义了一个非常简单的User类,没有方法,只有属性name

示例 5-12. 复制对象
<?php
  $object1       = new User();
  $object1->name = "Alice";
  $object2       = $object1;
  $object2->name = "Amy";

  echo "object1 name = " . $object1->name . "<br>";
  echo "object2 name = " . $object2->name;

  class User
  {
    public $name;
  }
?>

在这里,我们首先创建对象$object1并将值Alice分配给name属性。然后,我们创建$object2,将其赋值为$object1的值,并仅将Amy赋值给$object2name属性——或者我们可能会这样认为。但是,此代码输出如下:

object1 name = Amy
object2 name = Amy

发生了什么?$object1$object2都指向同一个对象,因此将$object2name属性更改为Amy也会为$object1设置该属性。

为了避免混淆,可以使用clone操作符,它创建类的新实例,并从原始实例复制属性值到新实例中。示例 5-13 展示了这种用法。

示例 5-13. 克隆对象
<?php
  $object1       = new User();
  $object1->name = "Alice";
  $object2       = clone $object1;
  $object2->name = "Amy";

  echo "object1 name = " . $object1->name . "<br>";
  echo "object2 name = " . $object2->name;

  class User
  {
    public $name;
  }
?>

瞧!这段代码的输出正是我们最初想要的:

object1 name = Alice
object2 name = Amy

构造函数

当创建一个新对象时,你可以向被调用的类传递一系列参数。这些参数传递给类中的一个特殊方法,称为构造方法,它初始化各种属性。

要做到这一点,你使用函数名__construct(即,construct前面加上两个下划线字符),就像在示例 5-14 中一样。

示例 5-14. 创建构造方法
<?php
  class User
  {
    function __construct($param1, $param2)
    {
      // Constructor statements go here
    }
  }
?>

析构函数

你还可以创建析构方法。当代码已经引用了一个对象或脚本达到结尾时,这个功能非常有用。示例 5-15 展示了如何创建析构方法。析构函数可以进行清理工作,例如释放数据库连接或类中保留的其他资源。因为你在类中保留了资源,所以必须在这里释放它,否则它将无限期存在。许多系统-wide 问题是由程序保留资源并忘记释放它们引起的。

示例 5-15. 创建析构方法
<?php
  class User
  {
    function __destruct()
    {
      // Destructor code goes here
    }
  }
?>

编写方法

正如你所见,声明方法类似于声明函数,但有一些区别。例如,以双下划线 (__) 开头的方法名是保留的(例如,__construct__destruct),你不应该创建这种形式的任何方法。

你还可以访问一个特殊的变量称为$this,它可以用来访问当前对象的属性。要了解它是如何工作的,请看示例 5-16,其中包含了User类定义中不同的方法get_password

示例 5-16. 在方法中使用变量$this
<?php
  class User
  {
    public $name, $password;

    function get_password()
    {
      return $this->password;
    }
  }
?>

get_password使用$this变量来访问当前对象,然后返回该对象的password属性的值。请注意,当我们使用->操作符时,前面的$符号在$password属性中被省略了。在首次使用此功能时,保留$可能是一个典型的错误。

下面是如何使用在示例 5-16 中定义的类:

$object           = new User;
$object->password = "secret";

echo $object->get_password();

这段代码打印出密码secret

声明属性

在类中显式声明属性并不是必需的,因为它们可以在首次使用时隐式定义。为了说明这一点,在示例 5-17 中,User类没有属性和方法,但是这是合法的代码。

示例 5-17. 隐式定义属性
<?php
  $object1       = new User();
  $object1->name = "Alice";

  echo $object1->name;

  class User {}
?>

这段代码正确输出字符串Alice,没有问题,因为 PHP 会隐式地为你声明属性$object1->name。但这种编程方式可能会导致极难发现的错误,因为name是从类外部声明的。

为了帮助自己和任何将来维护你代码的人,我建议你养成在类内部始终显式声明属性的习惯。你会为此感到高兴的。

此外,当你在类内声明一个属性时,你可以为其分配一个默认值。你使用的值必须是一个常量,而不是函数或表达式的结果。示例 5-18 展示了一些有效和无效的赋值。

示例 5-18. 有效和无效的属性声明
<?php
  class Test
  {
    public $name  = "Paul Smith"; // Valid
    public $age   = 42;           // Valid
    public $time  = time();       // Invalid - calls a function
    public $score = $level * 2;   // Invalid - uses an expression
  }
?>

声明常量

就像你可以使用define函数创建全局常量一样,你也可以在类内部定义常量。普遍接受的做法是使用大写字母以突出它们的重要性,如示例 5-19 所示。

示例 5-19. 在类内定义常量
<?php
  Translate::lookup();

  class Translate
  {
    const ENGLISH = 0;
    const SPANISH = 1;
    const FRENCH  = 2;
    const GERMAN  = 3;
    // ...

    static function lookup()
    {
      echo self::SPANISH;
    }
  }
?>

你可以直接引用常量,使用self关键字和双冒号操作符。请注意,此代码在第 1 行直接调用类,而不是先创建实例。运行此代码时打印的值将是1

请记住,一旦定义了常量,就不能更改它。

属性和方法的作用域

PHP 提供了三个关键字来控制属性和方法(成员)的作用域:

public

公共成员可以在任何地方引用,包括其他类和对象的实例。这是使用varpublic关键字声明变量时的默认值,或者在第一次使用变量时隐式声明的默认值。varpublic关键字是可互换的,因为尽管var已被弃用,但为了与 PHP 的旧版本兼容,它仍然保留。方法默认被假定为public

protected

这些成员只能通过对象的类方法及其任何子类的方法引用。

private

这些成员只能通过同一类内的方法引用,而不能通过子类引用。

下面是如何决定使用哪个:

  • 当外部代码应该访问此成员且扩展类也应该继承它时,请使用public

  • 当外部代码不应该访问此成员但扩展类应该继承它时,请使用protected

  • 当外部代码不应该访问此成员且扩展类也不应该继承它时,请使用private

示例 5-20 说明了这些关键字的使用。

示例 5-20. 改变属性和方法的作用域
<?php
  class Example
  {
    var $name   = "Michael"; // Same as public but deprecated
    public $age = 23;        // Public property
    protected $usercount;    // Protected property

    private function admin() // Private method
    {
      // Admin code goes here
    }
  }
?>

静态方法

您可以将方法定义为static,这意味着它是在类上调用而不是在对象上调用。静态方法无法访问任何对象属性,并且如在示例 5-21 中所示创建和访问。

示例 5-21. 创建和访问静态方法
<?php
  User::pwd_string();

  class User
  {
    static function pwd_string()
    {
      echo "Please enter your password";
    }
  }
?>

注意如何使用双冒号(也称为作用域解析运算符)而不是->来调用类本身以及静态方法。静态函数用于执行与类本身相关的操作,而不是特定类的实例。您可以在示例 5-19 中看到另一个静态方法的示例。

注意

如果尝试从静态函数内部访问$this->property或其他对象属性,将收到错误消息。

静态属性

大多数数据和方法适用于类的实例。例如,在User类中,您希望执行设置特定用户密码或检查用户注册时间等操作。这些事实和操作分别适用于每个用户,因此使用实例特定的属性和方法。

但偶尔您可能需要维护关于整个类的数据。例如,要报告注册用户的数量,您将存储一个适用于整个User类的变量。PHP 提供了用于此类数据的静态属性和方法。

如示例 5-21 中简要显示的那样,声明类成员为static使它们可以在不实例化类的情况下访问。声明为static的属性无法在类的实例内直接访问,但静态方法可以。

示例 5-22 定义了一个名为Test的类,其中包含一个静态属性和一个公共方法。

示例 5-22. 定义具有静态属性的类
<?php
  $temp = new Test();

  echo "Test A: " . Test::$static_property . "<br>";
  echo "Test B: " . $temp->get_sp()        . "<br>";
  echo "Test C: " . $temp->static_property . "<br>";

  class Test
  {
    static $static_property = "I'm static";

    function get_sp()
    {
       return self::$static_property;
    }
  }
?>

运行此代码时,将返回以下输出:

 Test A: I'm static
Test B: I'm static
Notice: Undefined property: Test::$static_property
Test C: 

此示例显示,通过 Test A 中的双冒号运算符,可以直接从类本身引用属性$static_property。另外,Test B 可以通过从类Test创建的对象$temp调用get_sp方法来获取其值。但是,Test C 失败了,因为对象$temp无法访问静态属性$static_property

注意方法get_sp如何使用关键字self访问$static_property。这是在类内部直接访问静态属性或常量的方式。

继承

定义类后,可以从中派生子类。这可以节省大量费力的代码重写:您可以取一个与您需要编写的类类似的类,将其扩展为子类,并仅修改不同的部分。您可以使用extends关键字来实现这一点。

在示例 5-23 中,类Subscriber通过extends关键字声明为User的子类。

示例 5-23. 继承和扩展类
<?php
  $object           = new Subscriber;
  $object->name     = "Fred";
  $object->password = "pword";
  $object->phone    = "012 345 6789";
  $object->email    = "fred@bloggs.com";
  $object->display();

  class User
  {
    public $name, $password;

    function save_user()
    {
      echo "Save User code goes here";
    }
  }

  class Subscriber extends User
  {
    public $phone, $email;

    function display()
    {
      echo "Name:  " . $this->name     . "<br>";
      echo "Pass:  " . $this->password . "<br>";
      echo "Phone: " . $this->phone    . "<br>";
      echo "Email: " . $this->email;
    }
  }
?>

原始User类具有两个属性,$name$password,以及一个将当前用户保存到数据库的方法。 Subscriber通过添加另外两个属性$phone$email来扩展此类,并包括使用变量$this显示当前对象属性的方法,该变量引用正在访问的对象的当前值。 此代码的输出如下:

    名称:  弗雷德     密码:  密码     电话: 012 345 6789     电子邮件: fred@bloggs.com

父关键字

如果您在子类中编写与父类中同名的方法,其语句将覆盖父类的语句。 有时这不是您想要的行为,您需要访问父方法。 为此,可以使用parent运算符,如示例 5-24。

示例 5-24. 覆盖方法并使用parent运算符
<?php
  $object = new Son;
  $object->test();
  $object->test2();

  class Dad
  {
    function test()
    {
      echo "[Class Dad] I am your Father<br>";
    }
  }

  class Son extends Dad
  {
    function test()
    {
      echo "[Class Son] I am Luke<br>";
    }

    function test2()
    {
      parent::test();
    }
  }
?>

此代码创建了一个名为Dad的类和一个名为Son的子类,后者继承了其属性和方法,然后覆盖了方法test。 因此,当第 2 行调用方法test时,将执行新方法。 要执行Dad类中重写的test方法的唯一方法是使用parent运算符,如Son类的test2函数所示。 代码输出如下:

**    [类儿子] 我是卢克**

**    [类爸爸] 我是你的父亲**

如果希望确保您的代码调用当前类的方法,可以使用self关键字,如下所示:

self::method();

子类构造函数

当您扩展一个类并声明自己的构造函数时,应该意识到 PHP 不会自动调用父类的构造函数。 如果希望确保执行所有初始化代码,子类应始终调用父类构造函数,如示例 5-25。

示例 5-25. 调用父类构造函数
<?php
  $object = new Tiger();

  echo "Tigers have...<br>";
  echo "Fur: " . $object->fur . "<br>";
  echo "Stripes: " . $object->stripes;

  class Wildcat
  {
    public $fur; // Wildcats have fur

    function __construct()
    {
      $this->fur = "TRUE";
    }
  }

  class Tiger extends Wildcat
  {
    public $stripes; // Tigers have stripes

    function __construct()
    {
      parent::__construct(); // Call parent constructor first
      $this->stripes = "TRUE";
    }
  }
?>

此示例以典型方式利用继承。 Wildcat类创建了属性$fur,我们希望重用该属性,因此我们创建了Tiger类来继承$fur并额外创建了另一个属性$stripes。 为了验证已调用两个构造函数,程序输出如下:

    老虎有...     毛皮: TRUE     条纹: TRUE

最终方法

当你希望防止子类覆盖超类方法时,可以使用final关键字。 示例 5-26 演示了如何使用。

示例 5-26. 创建一个final方法
<?php
  class User
  {
    final function copyright()
    {
      echo "This class was written by Joe Smith";
    }
  }
?>

一旦您消化了本章的内容,您应该对 PHP 能为您做什么有很强的感觉。 您应该能够轻松使用函数,并且如果希望,编写面向对象的代码。 在第六章中,我们将通过查看 PHP 数组的工作方式来完成我们对 PHP 的初步探索。

问题

  1. 使用函数的主要好处是什么?

  2. 函数可以返回多少个值?

  3. 通过名称和引用访问变量之间有什么区别?

  4. 作用域在 PHP 中的含义是什么?

  5. 如何在一个 PHP 文件中引用另一个?

  6. 对象与函数有何不同?

  7. 如何在 PHP 中创建一个新对象?

  8. 使用什么语法可以从现有类创建一个子类?

  9. 如何在创建对象时使其初始化?

  10. 明确在类中声明属性是个好主意的原因是什么?

参见“第五章答案”,可以找到这些问题的答案。

第六章:PHP 数组

在第三章中,我对 PHP 数组进行了非常简要的介绍——仅仅足够让你略知一二它们的强大。在本章中,我将向你展示更多可以用数组做的事情,其中一些——如果你曾经使用过像 C 这样的强类型语言——可能会因其优雅和简洁而让你感到惊讶。

数组不仅消除了编写处理复杂数据结构的代码的枯燥感,而且提供了许多访问数据的方式,同时保持惊人的快速性。

基本访问

我们已经看过数组,就好像它们是粘在一起的火柴盒群。另一种思考数组的方法是把它看作是一串珠子,每个珠子代表一个可以是数字、字符串或甚至其他数组的变量。它们像珠串一样,因为每个元素都有自己的位置,并且(除了第一个和最后一个)每个元素两侧都有其他元素。

有些数组通过数字索引进行引用;其他允许使用字母数字标识符。内置函数允许你对它们进行排序、添加或移除部分,并通过一种特殊的循环来遍历它们处理每个项目。通过在另一个数组内放置一个或多个数组,你可以创建二维、三维或任意维数的数组。

数字索引数组

假设你被委托为当地一家办公用品公司创建一个简单的网站,目前正在处理与纸张相关的部分。管理该类别库存中各种物品的一种方式是将它们放入一个数字数组中。你可以在例子 6-1 中看到最简单的做法。

例子 6-1. 向数组添加项目
<?php
  $paper[] = "Copier";
  $paper[] = "Inkjet";
  $paper[] = "Laser";
  $paper[] = "Photo";

  print_r($paper);
?>

在这个例子中,每当你给数组$paper赋值时,都会使用该数组内的第一个空位置来存储值,并且 PHP 内部的指针会递增指向下一个空位置,为将来的插入做准备。熟悉的print_r函数(用于打印变量、数组或对象的内容)用于验证数组是否已正确填充。它打印出以下内容:

Array
(
  [0] => Copier
  [1] => Inkjet
  [2] => Laser
  [3] => Photo
)

之前的代码也可以像在例子 6-2 中展示的那样书写,其中指定了数组中每个项目的确切位置。但是,正如你所看到的,这种方法需要额外的输入,并且如果你想要在数组中插入或删除项目,会使得你的代码更难维护。所以,除非你希望指定不同的顺序,通常最好让 PHP 处理实际的位置编号。

例子 6-2. 使用显式位置向数组添加项目
<?php
  $paper[0] = "Copier";
  $paper[1] = "Inkjet";
  $paper[2] = "Laser";
  $paper[3] = "Photo";

  print_r($paper);
?>

这些例子的输出是相同的,但在开发网站时你不太可能使用print_r,所以例子 6-3 展示了如何使用for循环打印出网站提供的各种类型的纸张。

示例 6-3. 向数组添加项目并检索它们
<?php
  $paper[] = "Copier";
  $paper[] = "Inkjet";
  $paper[] = "Laser";
  $paper[] = "Photo";

  for ($j = 0 ; $j < 4 ; ++$j)
    echo "$j: $paper[$j]<br>";
?>

此示例打印出以下内容:

 0: Copier
  1: Inkjet
  2: Laser
  3: Photo

到目前为止,你已经看到了几种向数组添加项目和引用它们的方式。PHP 还提供了更多方法,稍后会详细介绍。但首先,我们将看看另一种类型的数组。

关联数组

通过索引跟踪数组元素可以很好地工作,但可能需要额外的工作来记住哪个数字指代哪个产品。这也可能使得其他程序员难以理解代码。

这就是关联数组发挥作用的地方。使用它们,您可以通过名称而不是数字引用数组中的项目。示例 6-4 扩展了先前的代码,为数组中的每个元素赋予了一个标识名称和一个更长、更详细的字符串值。

示例 6-4. 向关联数组添加项目并检索它们
<?php
  $paper['copier'] = "Copier & Multipurpose";
  $paper['inkjet'] = "Inkjet Printer";
  $paper['laser']  = "Laser Printer";
  $paper['photo']  = "Photographic Paper";

  echo $paper['laser'];
?>

现在,每个项目都有一个您可以在其他地方引用的唯一名称,例如 echo 语句—它仅仅打印出 激光打印机。这些名称(如 复印机喷墨打印机 等)称为索引,而分配给它们的项目(如 激光打印机)称为

PHP 的这一强大功能经常用于从 XML 和 HTML 中提取信息。例如,像搜索引擎使用的 HTML 解析器可以将网页的所有元素放入一个关联数组中,其名称反映了页面的结构:

$html['title'] = "My web page";
$html['body']  = "... body of web page ...";

该程序还可能会将页面中找到的所有链接分解为另一个数组,并将所有标题和副标题分解为另一个数组。当您使用关联数组而不是数值数组时,引用所有这些项的代码变得易于编写和调试。

使用 array 关键字进行赋值

到目前为止,你已经学会了如何通过逐个添加新项目来向数组赋值。无论你指定键、指定数字标识符还是让 PHP 隐式分配数字标识符,这都是一种冗长的方法。使用 array 关键字,有一种更紧凑和更快速的赋值方法。示例 6-5 展示了使用这种方法分配的数字和关联数组。

示例 6-5. 使用 array 关键字向数组添加项目
<?php
  $p1 = array("Copier", "Inkjet", "Laser", "Photo");

  echo "p1 element: " . $p1[2] . "<br>";

  $p2 = array('copier' => "Copier & Multipurpose",
              'inkjet' => "Inkjet Printer",
              'laser'  => "Laser Printer",
              'photo'  => "Photographic Paper");

  echo "p2 element: " . $p2['inkjet'] . "<br>";
?>

此片段的前半部分将旧的、简短的产品描述分配给数组 $p1。有四个项目,因此它们将占据 0 到 3 的位置。因此,echo 语句打印出以下内容:

    p1 元素:激光

第二部分使用格式*key* => value 将关联标识符和伴随的较长产品描述分配给数组$p2=>的使用类似于常规的=赋值运算符,不同之处在于你正在为索引而不是变量赋值。索引因此与该值紧密联系,除非它被赋予新值。因此,echo命令打印出以下内容:

    p2 元素: 墨水喷墨打印机

你可以验证$p1$p2是不同类型的数组,因为将以下两个命令附加到代码后,都会导致Undefined indexUndefined offset错误,因为每个数组标识符都不正确:

echo $p1['inkjet']; // Undefined index
echo $p2[3];        // Undefined offset

foreach...as循环

PHP 的创建者为了使语言易于使用,做出了很大努力。因此,他们不满足于已经提供的循环结构,特别为数组添加了另一个循环:foreach...as循环。使用它,你可以逐个遍历数组中的所有项,并对它们进行操作。

这个过程从第一项开始,直到最后一项结束,因此你甚至不需要知道数组中有多少项。示例 6-6 展示了如何使用foreach...as重写示例 6-3。

示例 6-6. 使用foreach...as遍历数值数组
<?php
  $paper = array("Copier", "Inkjet", "Laser", "Photo");
  $j = 0;

  foreach($paper as $item)
  {
    echo "$j: $item<br>";
    ++$j;
  }
?>

当 PHP 遇到foreach语句时,它将数组的第一个项放入跟随as关键字的变量中;每次控制流返回到foreach时,下一个数组元素将被放入as关键字中。在这种情况下,变量$item依次设置为数组$paper中的每个值。一旦所有值都被使用,循环的执行结束。这段代码的输出与示例 6-3 完全相同。

现在让我们看看如何通过查看示例 6-7,了解foreach如何与关联数组一起使用,这是示例 6-5 的后半部分重写。

示例 6-7. 使用foreach...as 遍历关联数组
<?php
  $paper = array('copier' => "Copier & Multipurpose",
                 'inkjet' => "Inkjet Printer",
                 'laser'  => "Laser Printer",
                 'photo'  => "Photographic Paper");

  foreach($paper as $item => $description)
    echo "$item: $description<br>";
?>

请记住,关联数组不需要数值索引,因此在此示例中未使用变量$j。相反,数组$paper的每个项被传递到变量$item$description的键/值对中,从中打印出它们。该代码的显示结果如下:

    复印机: 复印机和多用途     墨水喷墨: 墨水喷墨打印机     激光: 激光打印机     照片: 摄影纸

在 PHP 7.2 版本之前,作为foreach...as的替代语法,您可以将list函数与each函数结合使用。然而,each随后被弃用,因此不建议使用,因为它可能会在未来的版本中删除。对于需要更新的 PHP 程序员来说,这是一场噩梦,尤其是each函数非常有用。因此,我编写了一个名为myEach的替代品,它的功能与each完全相同,并且允许您轻松更新旧代码,如示例 6-8。

示例 6-8. 使用myEachlist遍历关联数组
<?php
  $paper = array('copier' => "Copier & Multipurpose",
                 'inkjet' => "Inkjet Printer",
                 'laser'  => "Laser Printer",
                 'photo'  => "Photographic Paper");

  while (list($item, $description) = myEach($paper))
    echo "$item: $description<br>";

  function myEach(&$array) // Replacement for the deprecated each function
  {
    $key    = key($array);
    $result = ($key === null) ? false :
              [$key, current($array), 'key', 'value' => current($array)];
    next($array);
    return $result;
  }
?>

在这个示例中,设置了一个while循环,并且将继续循环,直到myEach函数(等同于旧版 PHP 的each函数)返回FALSE为止。myEach函数类似于foreach,它返回一个包含数组$paper中的键/值对的数组,然后将其内置指针移动到该数组中的下一个对。当没有更多的对要返回时,myEach返回FALSE

list函数接受一个数组作为其参数(在本例中,是函数myEach返回的键/值对),然后将数组的值分配给括号内列出的变量。

在示例 6-9 中,您可以更清楚地看到list函数的工作原理,其中一个数组由两个字符串AliceBob创建,然后传递给list函数,该函数将这些字符串分配为变量$a$b的值。

示例 6-9. 使用list函数
<?php
  list($a, $b) = array('Alice', 'Bob');
  echo "a=$a b=$b";
?>

这段代码的输出如下所示:

    a=Alice b=Bob

因此,在处理数组时,您可以根据需要选择。使用foreach...as创建一个循环,将值提取到as后面的变量中,或者使用myEach函数创建自己的循环系统。

多维数组

PHP 数组语法中的一个简单设计特性使得可以创建多维数组。事实上,它们可以是任意多维(尽管很少有应用程序会超过三维)。

这个特性是将整个数组作为另一个数组的一部分包含进来,并且可以继续这样做,就像那首古老的韵文:“大跳蚤背上有小跳蚤来咬它们。小跳蚤有更小的跳蚤,继续蚤类,无穷尽。”

让我们通过扩展前面示例中的关联数组来看看它是如何工作的;参见示例 6-10。

示例 6-10. 创建一个多维关联数组
<?php
  $products = array(

    'paper' => array(

      'copier' => "Copier & Multipurpose",
      'inkjet' => "Inkjet Printer",
      'laser'  => "Laser Printer",
      'photo'  => "Photographic Paper"),

    'pens' => array(

      'ball'   => "Ball Point",
      'hilite' => "Highlighters",
      'marker' => "Markers"),

    'misc' => array(

      'tape'   => "Sticky Tape",
      'glue'   => "Adhesives",
      'clips'  => "Paperclips"
    )
  );

  echo "<pre>";

  foreach($products as $section => $items)
    foreach($items as $key => $value)
      echo "$section:\t$key\t($value)<br>";

  echo "</pre>";
?>

现在代码开始增长,为了使事情更清晰,我已经重新命名了一些元素。例如,因为前面的数组$paper现在只是更大数组的一个子部分,所以主数组现在称为$products。在这个数组内部,有三个条目——paperpensmisc——每个条目都包含另一个带有键/值对的数组。

如果必要的话,这些子数组可能还包含更进一步的数组。例如,在 ball 下面可能有许多不同类型和颜色的圆珠笔可以在在线商店中获取。但是现在,我将代码限制在了只有两层的深度。

一旦数组数据被赋值,我使用一对嵌套的 foreach...as 循环来打印出各种值。外部循环从数组的顶层提取主要部分,内部循环提取每个部分内的键/值对。

只要记住每个数组级别都以相同方式工作(它是一个键/值对),你可以轻松编写代码来访问任何级别的任何元素。

echo 语句利用 PHP 转义字符 \t 输出制表符。尽管制表符通常对网页浏览器来说不重要,但我让它们被 <pre>...</pre> 标签使用,这告诉浏览器将文本格式化为预格式化和等宽字体,并且 忽略空白字符如制表符和换行符。从这段代码的输出看起来,如下所示:

paper:  copier  (Copier & Multipurpose)
paper:  inkjet  (Inkjet Printer)
paper:  laser   (Laser Printer)
paper:  photo   (Photographic Paper)
pens:   ball    (Ball Point)
pens:   hilite  (Highlighters)
pens:   marker  (Markers)
misc:   tape    (Sticky Tape)
misc:   glue    (Adhesives)
misc:   clips   (Paperclips)

你可以通过使用方括号直接访问数组的特定元素。

echo $products['misc']['glue'];

这输出值为 胶粘剂

你也可以创建数值型多维数组,通过索引而不是通过字母数字标识符进行访问。示例 6-11 创建了一个象棋游戏的棋盘,其中各个棋子处于起始位置。

示例 6-11. 创建一个多维数值数组
<?php
  $chessboard = array(
    array('r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'),
    array('p', 'p', 'p', 'p', 'p', 'p', 'p', 'p'),
    array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    array('P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'),
    array('R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R')
  );

  echo "<pre>";

  foreach($chessboard as $row)
  {
    foreach ($row as $piece)
      echo "$piece ";

    echo "<br>";
  }

  echo "</pre>";
?>

在这个例子中,小写字母表示黑色棋子,大写字母表示白色棋子。键为 r = 车,n = 马,b = 象,k = 王,q = 后,p = 兵。同样,一对嵌套的 foreach...as 循环遍历数组并显示其内容。外部循环将每一行处理为变量 $row,它本身是一个数组,因为 $chessboard 数组为每一行使用一个子数组。这个循环内有两条语句,所以用大括号将它们括起来。

然后内部循环处理每行中的每个方块,输出存储在其中的字符($piece),后跟一个空格(以使打印输出更加整齐)。这个循环只有一条语句,所以不需要用大括号将其括起来。<pre></pre> 标签确保输出正确显示,如下所示:

r n b q k b n r
p p p p p p p p

P P P P P P P P
R N B Q K B N R

你可以通过使用方括号直接访问这个数组中的任何元素。

echo $chessboard[7][3];

这个语句输出大写字母 Q,在第八行向下和第四列向右的位置(请记住数组索引从 0 开始,不是从 1 开始)。

使用数组函数

你已经看到了 listeach 函数,但是 PHP 还配备了许多其他处理数组的函数。你可以在文档中找到完整的列表。然而,其中一些函数非常基础,值得花时间在这里仔细研究它们。

is_array

数组和变量共享相同的命名空间。这意味着你不能同时有一个名为 $fred 的字符串变量和一个名为 $fred 的数组。如果你不确定并且你的代码需要检查一个变量是否是数组,你可以使用 is_array 函数,如下所示:

echo (is_array($fred)) ? "Is an array" : "Is not an array";

注意,如果 $fred 还没有被赋值,将生成一个 Undefined variable 的消息。

count

虽然 each 函数和 foreach...as 循环结构是遍历数组内容的优秀方式,但有时你确实需要知道数组中有多少元素,特别是如果你将直接引用它们的话。要计算顶层数组中的所有元素数,使用如下命令:

echo count($fred);

如果你想知道多维数组中总共有多少元素,可以使用如下语句:

echo count($fred, 1);

第二个参数是可选的,设置要使用的模式。它应该是 0 以限制仅计算顶层,或者 1 以强制递归计算所有子数组元素。

排序

排序是如此常见,PHP 为此提供了一个内置函数。在其最简单的形式中,你可以像这样使用它:

sort($fred);

需要记住的是,与其他一些函数不同,sort 会直接对提供的数组进行操作,而不是返回已排序元素的新数组。它在成功时返回 TRUE,在出错时返回 FALSE,并且还支持一些标志——你可能希望使用的主要两个标志,强制按数字或字符串对项目进行排序,如下所示:

sort($fred, SORT_NUMERIC);
sort($fred, SORT_STRING);

你也可以使用 rsort 函数按照反向顺序对数组进行排序,如下所示:

rsort($fred, SORT_NUMERIC);
rsort($fred, SORT_STRING);

洗牌

有时候你可能需要将数组的元素随机排序,比如在创建一副扑克牌游戏时:

shuffle($cards);

sort 类似,shuffle 直接作用于提供的数组,并在成功时返回 TRUE,在出错时返回 FALSE

explode

explode 是一个非常有用的函数,你可以用它将包含多个由单个字符(或字符串)分隔的项目的字符串转换成一个数组。一个方便的例子是将句子拆分成一个包含所有单词的数组,如 示例 6-12 所示。

示例 6-12. 使用空格将字符串分割成数组
<?php
  $temp = explode(' ', "This is a sentence with seven words");
  print_r($temp);
?>

此示例在浏览器中打印出以下内容(单行显示):

Array
(
  [0] => This
  [1] => is
  [2] => a
  [3] => sentence
  [4] => with
  [5] => seven
  [6] => words
)

第一个参数,分隔符,不一定是空格,甚至不一定是单个字符。示例 6-13 展示了一个略有不同的变体。

示例 6-13. 将用 *** 分隔的字符串分割成数组
<?php
  $temp = explode('***', "A***sentence***with***asterisks");
  print_r($temp);
?>

示例 6-13 中的代码将打印出以下内容:

Array
(
  [0] => A
  [1] => sentence
  [2] => with
  [3] => asterisks
)

extract

有时候将数组中的键/值对转换为 PHP 变量可能会很方便。一个这样的时机可能是当你处理由表单发送到 PHP 脚本的 $_GET$_POST 变量时。

当通过网络提交表单时,Web 服务器将变量解包到 PHP 脚本的全局数组中。如果使用 GET 方法发送变量,则会放入一个名为$_GET的关联数组;如果使用 POST 发送,则会放入一个名为$_POST的关联数组。

当然,您可以按照迄今为止的示例中所示的方式遍历这样的关联数组。但是,有时您只想将发送的值存储到变量中以供以后使用。在这种情况下,您可以让 PHP 自动执行这项工作:

extract($_GET);

因此,如果将查询字符串参数q发送到 PHP 脚本以及相关联的值Hi there,将创建一个名为$q的新变量,并为其分配该值。

虽然这种方法很有用,但要小心,因为如果提取的任何变量与您已经定义的变量冲突,您现有的值将被覆盖。为了避免这种可能性,您可以使用该函数提供的许多附加参数之一,就像这样:

extract($_GET, EXTR_PREFIX_ALL, 'fromget');

在这种情况下,所有新变量将以给定前缀字符串开始,后跟下划线,因此$q将变为$fromget_q。我强烈建议您在处理$_GET$_POST数组或任何其他可能由用户控制键的数组时使用此函数版本,因为恶意用户可能会提交有意选择的键来覆盖常用变量名并危害您的网站。

compact

有时您可能想要使用compactextract的反向操作)来创建一个包含变量及其值的数组。示例 6-14 展示了您可能如何使用此函数。

示例 6-14. 使用compact函数
<?php
  $fname         = "Doctor";
  $sname         = "Who";
  $planet        = "Gallifrey";
  $system        = "Gridlock";
  $constellation = "Kasterborous";

  $contact = compact('fname', 'sname', 'planet', 'system', 'constellation');

  print_r($contact);
?>

运行示例 6-14 的结果如下:

Array
(
  [fname] => Doctor
  [sname] => Who
  [planet] => Gallifrey
  [system] => Gridlock
  [constellation] => Kasterborous
)

请注意,compact要求以引号提供变量名称,而不是以$符号开头。这是因为compact正在寻找变量名列表,而不是它们的值。

另一个使用该函数的情况是调试时,您希望快速查看几个变量及其值,例如示例 6-15。

示例 6-15. 使用compact函数进行调试
<?php
  $j       = 23;
  $temp    = "Hello";
  $address = "1 Old Street";
  $age     = 61;

  print_r(compact(explode(' ', 'j temp address age')));
?>

这通过使用explode函数从字符串中提取所有单词到数组中实现,然后将其传递给compact函数,后者再返回一个数组给print_r,最终显示其内容。

如果您复制并粘贴print_r代码行,则只需修改那里命名的变量即可快速打印一组变量的值。在本例中,输出如下所示:

Array
(
  [j] => 23
  [temp] => Hello
  [address] => 1 Old Street
  [age] => 61
)

重置

foreach...as 结构或 each 函数遍历数组时,它会保持一个内部的 PHP 指针,记录应该返回的数组元素。如果你的代码需要返回数组的开头,你可以使用 reset,它也会返回那个元素的值。以下是如何使用此函数的示例:

reset($fred);         // Throw away return value
$item = reset($fred); // Keep first element of the array in $item

end

reset 类似,你可以使用 end 函数将 PHP 内部数组指针移动到数组的最后一个元素,并返回该元素的值。以下是一些示例:

end($fred);
$item = end($fred);

本章结束了你对 PHP 的基础介绍,现在你应该能够使用所学的技能编写相当复杂的程序。在下一章中,我们将讨论使用 PHP 处理常见实际任务的方法。

问题

  1. 数值数组和关联数组有什么区别?

  2. array 关键字的主要优势是什么?

  3. foreacheach 有什么区别?

  4. 如何创建多维数组?

  5. 如何确定数组中元素的数量?

  6. explode 函数的目的是什么?

  7. 如何将 PHP 内部指针设置回数组的第一个元素?

请参阅“第六章答案”,位于附录 A 中,以获取这些问题的答案。

第七章:PHP 实用指南

前面的章节介绍了 PHP 语言的各个元素。本章将基于你的新编程技能,教你如何执行一些常见但重要的实际任务。你将学习如何处理字符串,以实现清晰简洁的代码,并在网页浏览器中显示你想要的效果,包括高级的日期和时间管理。你还将了解如何创建和修改文件,包括用户上传的文件。

使用 printf

你已经看到了 printecho 函数,它们仅仅是将文本输出到浏览器。但是一个更强大的函数 printf 可以通过在字符串中添加特殊的格式化字符来控制输出的格式。对于每个格式化字符,printf 都希望你传递一个参数,它将使用该格式显示。例如,以下示例使用 %d 转换说明符来显示值 3 的十进制表示:

printf("There are %d items in your basket", 3);

如果你用 %b 替换 %d,那么值 3 将以二进制 (11) 形式显示。表 7-1 显示了支持的转换说明符。

表 7-1. printf 转换说明符

指示符对参数arg的转换操作示例(对于参数为 123)
%显示 % 字符(不需要arg%
b以二进制整数显示arg1111011
c显示arg的 ASCII 字符{
d以有符号十进制整数显示arg123
e使用科学计数法显示arg1.23000e+2
f以浮点数显示arg123.000000
o以八进制整数显示arg173
s以字符串形式显示arg123
u以无符号十进制显示arg123
x以小写十六进制显示arg7b
X以大写十六进制显示arg7B

printf 函数中,你可以使用任意数量的转换说明符,只要你传递相匹配的参数,并且每个说明符前面都有 % 符号。因此,以下代码是有效的,并将输出 "我的名字是 Simon。我今年 33 岁,这在十六进制中是 21"

printf("My name is %s. I'm %d years old, which is %X in hexadecimal",
  'Simon', 33, 33);

如果省略任何参数,你将收到一个解析错误,提示意外遇到右括号 ) 或参数不足。

更实际的 printf 示例是使用十进制值在 HTML 中设置颜色。例如,假设你想要一个颜色,其中红色为 65、绿色为 127、蓝色为 245,但不想自己将其转换为十六进制。这里有一个简单的解决方案:

printf("<span style='color:#%X%X%X'>Hello</span>", 65, 127, 245);

仔细检查撇号('')之间的颜色规范格式。颜色规范首先是井号 (#)。然后是三个 %X 格式说明符,分别对应你的数字。这个命令的输出如下所示:

<span style='color:#417FF5'>Hello</span>

通常,使用变量或表达式作为 printf 的参数会更方便。例如,如果您将颜色值存储在三个变量 $r$g$b 中,可以通过以下方式创建更深的颜色:

printf("<span style='color:#%X%X%X'>Hello</span>", $r-20, $g-20, $b-20);

精度设置

不仅可以指定转换类型,还可以设置显示结果的精度。例如,通常货币金额只显示两位小数。但是,在计算后,值可能具有更高的精度,例如 123.42 / 12,得到 10.285. 为确保这些值在内部正确存储但仅显示两位小数,可以在 % 符号和转换说明符之间插入字符串 ".2"

printf("The result is: $%.2f", 123.42 / 12);

此命令的输出如下:

The result is $10.29

实际上,你拥有比这更多的控制权,因为你还可以指定输出是用零还是空格填充,通过在转换说明符前面加上特定的值。示例 7-1 展示了四种可能的组合。

示例 7-1. 精度设置
<?php
  echo "<pre>"; // Enables viewing of the spaces

  // Pad to 15 spaces
  printf("The result is $%15f\n", 123.42 / 12);

  // Pad to 15 spaces, fill with zeros
  printf("The result is $%015f\n", 123.42 / 12);

  // Pad to 15 spaces, 2 decimal places precision
  printf("The result is $%15.2f\n", 123.42 / 12);

  // Pad to 15 spaces, 2 decimal places precision, fill with zeros
  printf("The result is $%015.2f\n", 123.42 / 12);

  // Pad to 15 spaces, 2 decimal places precision, fill with # symbol
  printf("The result is $%'#15.2f\n", 123.42 / 12);
?>

此示例的输出如下:

The result is $      10.285000
The result is $00000010.285000
The result is $          10.29
The result is $000000000010.29
The result is $##########10.29

它的工作方式很简单,如果你从右到左看(见 表 7-2)。请注意:

  • 最右边的字符是转换说明符:在本例中为浮点数 f

  • 如果在转换说明符之前有一个句点和一个数字在一起,则输出的精度被指定为该数字的值。

  • 无论是否有精度说明符,如果有数字,则表示输出应填充到该字符数。在前面的示例中,这是 15 个字符。如果输出已等于或大于填充长度,则忽略此参数。

  • % 符号之后允许的最左边的参数是 0,除非设置了填充值,否则会被忽略,如果需要的是除零或空格之外的填充字符,则可以使用任意你选择的一个,并在其前面加上一个单引号,例如 '#

  • 左边是 % 符号,表示开始转换。

表 7-2. 转换说明符组件

开始转换填充字符填充字符数显示精度转换说明符示例
% 15 f10.285000
%015.2f000000000010.29
%'#15.4f########10.2850

字符串填充

您还可以像处理数字一样,将字符串填充到所需的长度,选择不同的填充字符,甚至可以选择左对齐或右对齐。示例 7-2 展示了各种示例。

示例 7-2. 字符串填充
<?php
  echo "<pre>"; // Enables viewing of the spaces

  $h = 'Rasmus';

  printf("[%s]\n",         $h); // Standard string output
  printf("[%12s]\n",       $h); // Right justify with spaces to width 12
  printf("[%-12s]\n",      $h); // Left justify with spaces
  printf("[%012s]\n",      $h); // Pad with zeros
  printf("[%'#12s]\n\n",   $h); // Use the custom padding character '#'

  $d = 'Rasmus Lerdorf';        // The original creator of PHP

  printf("[%12.8s]\n",     $d); // Right justify, cutoff of 8 characters
  printf("[%-12.12s]\n",   $d); // Left justify, cutoff of 12 characters
  printf("[%-'@12.10s]\n", $d); // Left justify, pad with '@', cutoff 10 chars
?>

请注意,在 Web 页面布局方面,我使用了 <pre> HTML 标签来保留所有空格和每行后的 \n 换行字符。该示例的输出如下所示:

[Rasmus]
[      Rasmus]
[Rasmus      ]
[000000Rasmus]
[######Rasmus]

[    Rasmus L]
[Rasmus Lerdo]
[Rasmus Ler@@]

当您指定填充值时,长度等于或大于该值的字符串将被忽略,除非提供了截断值,将字符串缩短至小于填充值。

表 7-3 显示了可用于字符串转换说明符的组件。

表 7-3. 字符串转换说明符组件

开始转换左/右对齐填充字符填充字符数截断转换说明符示例(使用“Rasmus”)
%    s[Rasmus]
%- 10 s[Rasmus ]
% '#8.4s[####Rasm]

使用 sprintf

通常情况下,您不希望输出转换的结果,但需要将其用于代码中的其他地方。这就是 sprintf 函数的用武之地。使用它,您可以将输出发送到另一个变量,而不是直接发送到浏览器。

您可以使用它进行转换,就像以下示例中为 RGB 颜色组 65, 127, 245 返回十六进制字符串值 $hexstring 一样:

$hexstring = sprintf("%X%X%X", 65, 127, 245);

或者您可能希望将输出存储在变量中以供其他用途或显示:

$out = sprintf("The result is: $%.2f", 123.42 / 12);
echo $out;

日期和时间函数

PHP 使用标准的 Unix 时间戳来跟踪日期和时间,即从 1970 年 1 月 1 日开始的秒数。要确定当前时间戳,可以使用 time 函数:

echo time();

因为值以秒为单位存储,要获得下周此时刻的时间戳,您可以使用以下方法,将 7 天× 24 小时× 60 分钟× 60 秒添加到返回值中:

echo time() + 7 * 24 * 60 * 60;

如果您希望为给定日期创建时间戳,可以使用 mktime 函数。对于 2022 年 12 月 1 日的第一秒的时间戳是 1669852800

echo mktime(0, 0, 0, 12, 1, 2022);

传递的参数从左到右依次为:

  • 小时数(0–23)

  • 分钟数(0–59)

  • 秒数(0–59)

  • 月份数(1–12)

  • 日数(1–31)

  • 年份(1970–2038,或者在 32 位有符号系统上的 PHP 5.1.0+中为 1901–2038)

注意

您可能会问为什么要将年份限制在 1970 到 2038 年之间。这是因为 Unix 的原始开发者选择了 1970 年作为基准日期,没有程序员需要在此之前使用!

幸运的是,从版本 5.1.0 开始,PHP 支持使用有符号 32 位整数的系统进行时间戳,支持 1901 年到 2038 年的日期。然而,这引入了一个比原问题更严重的问题,因为 Unix 的设计者还决定,在大约 70 年后没有人会继续使用 Unix,因此他们认为可以用 32 位值存储时间戳,而这将在 2038 年 1 月 19 日耗尽!

这将产生所谓的 Y2K38 问题(类似于千年虫问题,由于将年份存储为两位数值引起,也必须解决)。PHP 在 5.2 版本中引入了DateTime类以解决这个问题,但仅适用于 64 位架构,这在今天的大多数计算机上都是(但使用前请检查)。

要显示日期,请使用date函数,它支持众多格式选项,使您能够按任意方式显示日期。格式如下:

date($format, $timestamp);

参数$format应为包含格式说明符的字符串,如表 7-4 所详述,并且$timestamp应为 Unix 时间戳。有关所有说明符的完整列表,请参阅文档。以下命令将以格式"Monday February 17th, 2025 - 1:38pm"输出当前日期和时间:

echo date("l F jS, Y - g:ia", time());

Table 7-4. 主要日期函数格式说明符

格式描述返回值
日期说明符
d日期,两位数,前导零0131
D星期几,三个字母MonSun
j日期,无前导零131
l星期几,全称SundaySaturday
N星期几,数字形式,星期一至星期日17
S日期后缀(与j说明符一起使用)stndrdth
w星期几,数字形式,星期日至星期六06
z年内的第几天0365
周说明符
W年的周数0152
月份说明符
F月份名称JanuaryDecember
m月份,带前导零0112
M月份名称,三个字母JanDec
n月份,无前导零112
t给定月份的天数2831
年份说明符
L闰年1 = 是,0 = 否
y年份,2 位数0099
Y年份,4 位数00009999
时间格式说明符
a上午或下午,小写ampm
A上午或下午,大写AMPM
g小时,12 小时制,无前导零112
G小时,24 小时制,无前导零023
h小时,12 小时制,前导零0112
H小时,24 小时制,前导零0023
i分钟,前导零0059
s秒,前导零0059

日期常量

有许多有用的常量可以与 date 命令一起使用,以返回特定格式的日期。例如,date(DATE_RSS) 返回符合 RSS 订阅格式的当前日期和时间。一些常用的常量如下:

DATE_ATOM

这是 Atom 订阅的格式。PHP 的格式是"Y-m-d\TH:i:sP",示例输出是"2025-05-15T12:00:00+00:00"

DATE_COOKIE

这是从 Web 服务器或 JavaScript 设置的 cookie 的格式。PHP 的格式是"l, d-M-y H:i:s T",示例输出是"Thursday, 15-May-25 12:00:00 UTC"

DATE_RSS

这是用于 RSS 订阅的格式。PHP 的格式是"D, d M Y H:i:s O",示例输出是"Thu, 15 May 2025 12:00:00 UTC"

DATE_W3C

这是用于万维网联盟的格式。PHP 的格式是"Y-m-d\TH:i:sP",示例输出是"2025-05-15T12:00:00+00:00"

可以在文档中找到完整的列表。

使用 checkdate

您已经看到如何以多种格式显示有效日期。但是如何检查用户是否向程序提交了有效日期?答案是将月、日和年传递给 checkdate 函数,如果日期有效则返回TRUE,否则返回FALSE

例如,如果输入任意年份的 9 月 31 日,将始终是一个无效日期。示例 7-3 显示了您可以用于此目的的代码。目前,它会发现给定日期无效。

示例 7-3. 检查日期的有效性
<?php
  $month = 9;    // September (only has 30 days)
  $day   = 31;   // 31st
  $year  = 2025; // 2025

  if (checkdate($month, $day, $year)) echo "Date is valid";
  else echo "Date is invalid";
?>

文件处理

尽管 MySQL 强大,但并非在 Web 服务器上存储所有数据的唯一(或者说是最佳)方法。有时,直接访问硬盘上的文件可能更快、更便捷。这种情况包括修改用户上传头像或处理日志文件。

不过,首先要注意文件命名:如果您在编写可能在不同 PHP 安装中使用的代码,就无法确定这些系统是否区分大小写。例如,Windows 和 macOS 的文件名不区分大小写,但 Linux 和 Unix 的文件名区分大小写。因此,您应始终假定系统区分大小写,并坚持使用全小写文件名等约定。

检查文件是否存在

要确定文件是否已存在,可以使用 file_exists 函数。该函数返回TRUEFALSE,用法如下:

if (file_exists("testfile.txt")) echo "File exists";

创建文件

此时,testfile.txt 不存在,让我们创建它并向其中写入几行内容。键入 示例 7-4 并将其保存为 testfile.php

示例 7-4. 创建一个简单的文本文件
<?php // testfile.php
  $fh = fopen("testfile.txt", 'w') or die("Failed to create file");

  $text = <<<_END
Line 1
Line 2
Line 3
_END;

  fwrite($fh, $text) or die("Could not write to file");
  fclose($fh);
  echo "File 'testfile.txt' written successfully";
?>

如果程序调用die函数,打开的文件将作为终止程序的一部分自动关闭。

当你在浏览器中运行这个程序时,如果一切顺利,你将会收到消息File 'testfile.txt' written successfully。如果你收到错误消息,可能是你的硬盘已满,或者更可能的是你没有权限创建或写入文件,这时你应该根据你的操作系统修改目标文件夹的属性。否则,文件testfile.txt现在应该存储在你保存testfile.php程序的相同文件夹中。尝试用文本编辑器或程序打开文件——内容将会像这样:

Line 1
Line 2
Line 3

这个简单的例子展示了所有文件处理所需的顺序:

  1. 总是通过调用fopen来开始打开文件。通过调用fopen函数来实现这一点。

  2. 然后你可以调用其他函数;这里我们向文件写入(fwrite),但你也可以从现有文件中读取(freadfgets)以及执行其他操作。

  3. 最后通过关闭文件(fclose)来完成。尽管程序在结束时会自动关闭文件,但在你完成后手动关闭文件也是必要的。

每个打开的文件都需要一个文件资源,以便 PHP 可以访问和管理它。前面的例子将变量$fh(我选择用来表示文件句柄的变量名)设置为fopen函数返回的值。此后,每个访问已打开文件的文件处理函数,如fwritefclose,都必须将$fh作为参数传递,以标识正在访问的文件。不用担心$fh变量的内容;它是 PHP 用来引用有关文件的内部信息的数字——你只需将该变量传递给其他函数。

失败时,fopen会返回FALSE。前面的例子展示了捕获并响应失败的简单方法:调用die函数来结束程序并向用户显示错误消息。Web 应用程序绝不会以这种粗暴的方式中止(你应该创建一个带有错误消息的网页),但这对于我们的测试目的来说是可以接受的。

注意fopen调用的第二个参数。它只是字符w,告诉函数以写入模式打开文件。如果文件不存在,函数会创建该文件。在使用这些函数时要小心:如果文件已经存在,w模式参数会导致fopen调用删除旧内容(即使你没有写入任何新内容!)。

在这里可以使用几种不同的模式参数,详细说明见表 7-5。包含+符号的模式在“更新文件”部分有进一步解释。

表 7-5. 支持的fopen模式

模式动作描述
'r'从文件开头读取只读模式打开;将文件指针放在文件开头。如果文件不存在,则返回FALSE
---------
'r+'从文件开头读取并允许写入可读写打开;将文件指针放在文件开头。如果文件不存在,则返回FALSE
---------
'w'从文件开头写入并截断文件只写打开;将文件指针放在文件开头并截断文件为零长度。如果文件不存在,则尝试创建。
---------
'w+'从文件开头写入、截断文件并允许读取可读写打开;将文件指针放在文件开头并截断文件为零长度。如果文件不存在,则尝试创建。
---------
'a'追加到文件末尾只写打开;将文件指针放在文件末尾。如果文件不存在,则尝试创建。
---------
'a+'追加到文件末尾并允许读取可读写打开;将文件指针放在文件末尾。如果文件不存在,则尝试创建。
---------

从文件中读取

从文本文件中读取的最简单方法是通过fgets获取整行(最后的s可以理解为string),如示例 7-5。

示例 7-5. 使用fgets读取文件
<?php
  $fh = fopen("testfile.txt", 'r') or
    die("File does not exist or you lack permission to open it");

  $line = fgets($fh);
  fclose($fh);
  echo $line;
?>

如果你按照示例 7-4 创建文件,你会得到第一行:

Line 1

你可以通过fread函数检索多行或行的部分,如示例 7-6。

示例 7-6. 使用fread读取文件
<?php
  $fh = fopen("testfile.txt", 'r') or
    die("File does not exist or you lack permission to open it");

  $text = fread($fh, 3);
  fclose($fh);
  echo $text;
?>

我在fread调用中请求了三个字符,因此程序显示如下内容:

Lin

fread函数通常用于二进制数据。如果在跨越多行的文本数据上使用它,请记得计算换行符。

复制文件

让我们尝试 PHP copy 函数来创建testfile.txt的克隆。将其保存为copyfile.php,然后在浏览器中调用该程序,如示例 7-7。

示例 7-7. 复制文件
<?php // copyfile.php
  copy('testfile.txt', 'testfile2.txt') or die("Could not copy file");
  echo "File successfully copied to 'testfile2.txt'";
?>

如果再次检查你的文件夹,你会看到现在有了新文件testfile2.txt。顺便说一句,如果不希望程序在复制失败时退出,你可以尝试示例 7-8 中的替代语法。它使用!NOT)运算符作为一个快捷简便的缩写。放在表达式前面,它会对其应用NOT运算符,所以这里的等效语句在英语中会以“如果无法复制...”开头。

示例 7-8. 复制文件的替代语法
<?php // copyfile2.php
  if (!copy('testfile.txt', 'testfile2.txt')) echo "Could not copy file";
  else echo "File successfully copied to 'testfile2.txt'";
?>

移动文件

要移动文件,请使用rename函数重命名,如示例 7-9。

示例 7-9. 移动文件
<?php // movefile.php
  if (!rename('testfile2.txt', 'testfile2.new'))
    echo "Could not rename file";
  else echo "File successfully renamed to 'testfile2.new'";
?>

你也可以在目录上使用rename函数。为了避免在原始文件不存在时出现任何警告消息,可以先调用file_exists函数进行检查。

删除文件

删除文件只需使用unlink函数从文件系统中删除,如示例 7-10 中所示。

示例 7-10. 删除文件
<?php // deletefile.php
  if (!unlink('testfile2.new')) echo "Could not delete file";
  else echo "File 'testfile2.new' successfully deleted";
?>
警告

每当直接访问硬盘上的文件时,您必须始终确保文件系统不会被破坏。例如,如果根据用户输入删除文件,则必须确保它是可以安全删除的文件,并且用户被允许删除它。

与移动文件一样,如果文件不存在,将显示警告消息,您可以通过首先使用file_exists检查其是否存在,然后再调用unlink来避免这种情况。

更新文件

通常,您可能希望向已保存的文件添加更多数据,有多种方法可以实现。您可以使用追加写模式之一(参见表 7-5),或者您可以简单地使用支持写入的其他模式之一打开文件进行读写,并将文件指针移动到希望写入或读取的文件中的正确位置。

文件指针是文件中下一个文件访问将发生的位置,无论是读取还是写入。它与文件句柄(在示例 7-4 中存储在变量$fh中)不同,后者包含有关正在访问的文件的详细信息。

您可以通过输入示例 7-11 并将其保存为update.php来查看其操作。然后在浏览器中调用它。

示例 7-11. 更新文件
<?php // update.php
  $fh   = fopen("testfile.txt", 'r+') or die("Failed to open file");
  $text = fgets($fh);

  fseek($fh, 0, SEEK_END);
  fwrite($fh, "\n$text") or die("Could not write to file");
  fclose($fh);

  echo "File 'testfile.txt' successfully updated";
?>

此程序通过使用'r+'设置模式同时打开testfile.txt进行读取和写入,将文件指针置于开头。然后使用fgets函数从文件中读取一行(直到第一个换行符)。之后,调用fseek函数将文件指针直接移动到文件末尾,在此时,从文件开头提取的文本行(存储在$text中)将以\n换行符开头附加到文件末尾,然后关闭文件。结果文件现在如下所示:

Line 1
Line 2
Line 3
Line 1

第一行已成功复制并附加到文件的末尾。

此处使用的$fh文件句柄外,还向fseek函数传递了另外两个参数,0SEEK_ENDSEEK_END告诉函数将文件指针移动到文件末尾,0告诉它从那一点开始向后移动多少位置。在示例 7-11 的情况下,使用0是因为需要保持指针在文件的末尾。

fseek函数还有两个其他的寻址选项:SEEK_SETSEEK_CURSEEK_SET选项告诉函数将文件指针设置为前面参数给出的确切位置。因此,以下示例将文件指针移动到位置 18:

fseek($fh, 18, SEEK_SET);

SEEK_CUR将文件指针设置为当前位置加上给定偏移量的值。因此,如果文件指针当前位于位置 18,则以下调用将其移动到位置 23:

fseek($fh, 5, SEEK_CUR);

为多次访问锁定文件

Web 程序经常被许多用户同时调用。如果多个人尝试同时写入文件,可能会导致文件损坏。如果一个人在读取文件时另一个人在写入它,文件是没问题的,但读取它的人可能会得到奇怪的结果。为了处理同时使用者,你必须使用文件锁定flock函数。这个函数将所有其他访问文件的请求排队,直到你的程序释放锁。因此,每当你的程序对可能被多个用户同时访问的文件执行写入访问时,你还应该为它们添加文件锁定,如示例 7-12 中所示,这是示例 7-11 的更新版本。

示例 7-12. 使用文件锁更新文件
<?php
  $fh   = fopen("testfile.txt", 'r+') or die("Failed to open file");
  $text = fgets($fh);

  if (flock($fh, LOCK_EX))
  {
    fseek($fh, 0, SEEK_END);
    fwrite($fh, "$text") or die("Could not write to file");
    flock($fh, LOCK_UN);
  }

  fclose($fh);
  echo "File 'testfile.txt' successfully updated";
?>

对于文件锁定有一个技巧,以保持网站访问者最佳的响应时间:在对文件进行更改之前直接执行锁定操作,然后立即解锁。将文件锁定时间超过此时间将不必要地减慢应用程序。这就是为什么在示例 7-12 中的flock调用直接在fwrite调用之前和之后的原因。

第一次调用flock使用LOCK_EX参数在由$fh引用的文件上设置独占文件锁:

flock($fh, LOCK_EX);

从此时起,直到使用LOCK_UN参数释放锁为止,没有其他进程可以写入(甚至读取)该文件,如下所示:

flock($fh, LOCK_UN);

一旦释放锁定,其他进程就可以再次访问文件。这也是为什么每次需要读取或写入数据时都应重新定位到文件中希望访问的位置的原因之一——另一个进程可能在上次访问后更改了文件。

但是,你是否注意到请求独占锁的调用嵌套在一个if语句中?这是因为并非所有系统都支持flock;因此,明智的做法是检查是否成功获取了锁,以防无法获取锁。

另一件你必须考虑的事情是,flock被称为建议性锁定。这意味着它只锁定调用该函数的其他进程。如果你有任何直接修改文件而没有实现flock文件锁定的代码,它将始终覆盖锁定,并可能对你的文件造成严重破坏。

顺便说一句,实现文件锁定然后意外地在某个代码部分中留下它可能会导致一个极其难以定位的错误。

警告

flock 在 NFS 和许多其他网络文件系统上无法工作。 此外,当使用像 ISAPI 这样的多线程服务器时,您可能无法依赖 flock 来保护文件免受同一服务器实例的并行线程中运行的其他 PHP 脚本的影响。 另外,flock 不支持使用旧的 FAT 文件系统的任何系统,例如较旧版本的 Windows,尽管您不太可能遇到这些系统(希望如此)。

如果不确定,可以尝试在程序开始时快速锁定一个测试文件,看看是否可以锁定该文件。 检查后不要忘记解锁它(如果不需要,可能还要删除它)。

还要记住,任何对 die 函数的调用都会自动解锁锁定并关闭文件,作为结束程序的一部分。

读取整个文件

一个方便的函数用于读取整个文件,而无需使用文件句柄是 file_get_contents。 它非常容易使用,就像您可以在 Example 7-13 中看到的那样。

Example 7-13. 使用 file_get_contents
<?php
  echo "<pre>";  // Enables display of line feeds
  echo file_get_contents("testfile.txt");
  echo "</pre>"; // Terminates <pre> tag
?>

但是这个函数实际上比这更有用,因为您还可以使用它从 Internet 上的服务器获取文件,就像 Example 7-14 中请求 O’Reilly 主页的 HTML 并将其显示为用户已经浏览到页面本身一样。 结果类似于 Figure 7-1。

Example 7-14. 抓取 O’Reilly 主页
<?php
  echo file_get_contents("http://oreilly.com");
?>

Figure 7-1. 使用 file_get_contents 抓取的 O’Reilly 主页

文件上传

将文件上传到 Web 服务器是许多人看起来令人生畏的主题,但实际上却没有那么困难。 要从表单中上传文件,您需要选择一种特殊的编码类型称为 multipart/form-data,您的浏览器会处理其余部分。 要查看其工作原理,请输入 Example 7-15 中的程序并将其保存为 upload.php。 运行时,您将在浏览器中看到一个表单,让您上传您选择的文件。

Example 7-15. 图像上传程序 upload.php
<?php // upload.php
  echo <<<_END
 <html><head><title>PHP Form Upload</title></head><body>
 <form method='post' action='upload.php' enctype='multipart/form-data'>
 Select File: <input type='file' name='filename' size='10'>
 <input type='submit' value='Upload'>
 </form>
_END;

  if ($_FILES)
  {
    $name = $_FILES['filename']['name'];
    move_uploaded_file($_FILES['filename']['tmp_name'], $name);
    echo "Uploaded image '$name'<br><img src='$name'>";
  }

  echo "</body></html>";
?>

让我们逐段检查这个程序。 多行 echo 语句的第一行开始一个 HTML 文档,显示标题,然后开始文档的主体。

接下来我们来到表单,它选择表单提交的 POST 方法,将发布数据的目标设置为程序 upload.php(程序本身),并告诉 Web 浏览器应使用 multipart/form-data 的内容类型来编码发布的数据,这是文件上传的 MIME 类型。

设置好表单后,下一行显示提示 Select File:,然后请求两个输入。 第一个请求是文件,它使用 file 类型的输入,名称为 filename,并且输入字段宽度为 10 个字符。 第二个请求的输入只是一个提交按钮,标签为 Upload(取代默认的提交按钮文本 Submit Query)。 然后关闭表单。

这个简短的程序展示了 Web 编程中的一个常见技术,即一个程序被调用两次:用户首次访问页面时和用户点击提交按钮时。

接收上传数据的 PHP 代码非常简单,因为所有上传的文件都放置在关联数组$_FILES中。因此,仅需快速检查$_FILES是否包含任何内容即可确定用户是否已上传文件。这通过语句if ($_FILES)完成。

用户首次访问页面时,在上传文件之前,$_FILES是空的,所以程序会跳过此代码块。当用户上传文件时,程序再次运行,并在$_FILES数组中发现一个元素。

一旦程序意识到文件已上传,就会从 PHP 存储它的临时位置检索并将实际名称读取到变量$name中。现在只需要使用move_uploaded_file函数将上传的文件从 PHP 存储的临时位置移动到更永久的位置即可。我们通过将文件的原始名称传递给它来执行此操作,文件将保存到当前目录中。

最后,上传的图像将显示在一个IMG标签中,结果应该看起来像图 7-2。

警告

如果运行此程序并收到类似move_uploaded_file函数调用时Permission denied的警告消息,那么您可能没有为程序运行的文件夹设置正确的权限。

以表单数据形式上传图像

图 7-2. 以表单数据形式上传图像

使用 $_FILES

当上传文件时,$_FILES 数组中存储了五个东西,如表 7-6 所示(其中*file*是提交表单提供的文件上传字段名)。

表 7-6. $_FILES数组的内容

数组元素内容
$_FILES['*file*']['*name*']上传文件的名称(例如,smiley.jpg
$_FILES['*file*']['*type*']文件的内容类型(例如,image/jpeg
$_FILES['*file*']['*size*']文件的字节大小
$_FILES['*file*']['*tmp_name*']服务器上存储的临时文件名
$_FILES['*file*']['*error*']文件上传引起的错误代码

以前称为MIME(多用途互联网邮件扩展)类型的内容类型,但由于它们后来的使用扩展到整个互联网,现在通常称为互联网媒体类型。表 7-7 展示了在$_FILES['file']['type']中经常出现的一些类型。

表 7-7. 一些常见的互联网媒体内容类型

application/pdfimage/gifmultipart/form-datatext/xml
application/zipimage/jpegtext/cssvideo/mpeg
audio/mpegimage/pngtext/htmlvideo/mp4
audio/x-wavapplication/jsontext/plainaudio/webm

验证

我希望现在毋庸置疑(尽管我还是会这么说),表单数据验证是非常重要的,因为用户可能试图入侵你的服务器。

除了恶意构造的输入数据之外,你还需要检查一些其他事情,例如是否实际接收到了文件,以及如果有的话,是否发送了正确类型的数据。

考虑到所有这些因素,示例 7-16,upload2.php,是upload.php的更安全的重写。

示例 7-16. upload.php的更安全版本
<?php // upload2.php
  echo <<<_END
 <html><head><title>PHP Form Upload</title></head><body>
 <form method='post' action='upload2.php' enctype='multipart/form-data'>
 Select a JPG, GIF, PNG or TIF File:
 <input type='file' name='filename' size='10'>
 <input type='submit' value='Upload'></form>
 _END;

  if ($_FILES)
  {
    $name = $_FILES['filename']['name'];

    switch($_FILES['filename']['type'])
    {
      case 'image/jpeg': $ext = 'jpg'; break;
      case 'image/gif':  $ext = 'gif'; break;
      case 'image/png':  $ext = 'png'; break;
      case 'image/tiff': $ext = 'tif'; break;
      default:           $ext = '';    break;
    }
    if ($ext)
    {
      $n = "image.$ext";
      move_uploaded_file($_FILES['filename']['tmp_name'], $n);
      echo "Uploaded image '$name' as '$n':<br>";
      echo "<img src='$n'>";
    }
    else echo "'$name' is not an accepted image file";
  }
  else echo "No image has been uploaded";

  echo "</body></html>";
?>

非 HTML 代码部分从示例 7-15 的半打行扩展到超过 20 行,从if ($_FILES)开始。

就像之前的版本一样,这个if行检查是否实际上有任何数据被发布,但是现在在程序底部附近有一个匹配的else,当没有上传任何内容时向屏幕输出一条消息。

if语句内部,变量$name被赋予从上传计算机检索到的文件名的值(就像以前一样),但是这一次我们不依赖于用户发送给我们有效数据。相反,switch语句根据此程序支持的四种图像类型检查上传的内容类型。如果找到匹配项,则变量$ext设置为该类型的三字母文件扩展名。如果没有找到匹配项,则上传的文件不属于接受的类型,变量$ext设置为空字符串""

注意

在这个例子中,文件类型仍然来自浏览器,并且可以被上传文件的用户修改或更改。在这种情况下,此类用户操作并不令人担忧,因为这些文件只被视为图像。但是,如果文件可能是可执行的,你不应依赖于你未确认绝对正确的信息。

代码的下一部分然后检查变量$ext是否包含一个字符串,并且如果是这样的话,创建一个新的文件名叫做$n,基本名称为image,扩展名存储在$ext中。这意味着程序对要创建的文件类型有完全的控制,因为它只能是其中之一:image.jpgimage.gifimage.pngimage.tif

有了程序未被破坏的保证,PHP 代码的其余部分与之前的版本大致相同。它将上传的临时图像移动到新位置,然后显示它,同时显示旧图像和新图像的名称。

注意

不用担心必须删除 PHP 在上传过程中创建的临时文件,因为如果文件没有被移动或重命名,当程序退出时它会自动删除。

if语句之后,有一个匹配的else语句,只有在上传了不受支持的图像类型时才会执行(此时显示适当的错误消息)。

当编写自己的文件上传例程时,我强烈建议您使用类似的方法,并为上传的文件预先选择名称和位置。这样,无法通过您使用的变量尝试添加路径名和其他恶意数据。如果这意味着多个用户可能使用相同的名称上传文件,则可以在这些文件前加上其用户的用户名前缀,或将它们保存到为每个用户单独创建的文件夹中。

但是,如果您必须使用提供的文件名,您应该通过允许仅包含字母数字字符和句点的方式进行清理,可以使用以下命令使用正则表达式(参见第十八章)对$name进行搜索和替换:

$name = preg_replace("/[^A-Za-z0-9.]/", "", $name);

这样,字符串$name中仅留下 A-Z、a-z、0-9 和句点,并删除所有其他内容。

更好的是,为了确保您的程序在所有系统上都能正常工作,无论它们是区分大小写还是不区分大小写,您可能应该改用以下命令,同时将所有大写字母改为小写字母:

$name = strtolower(preg_replace("[^A-Za-z0-9.]", "", $name));
注意

有时您可能会遇到image/pjpeg的媒体类型,表示渐进式 JPEG,但您可以安全地将其添加到代码中作为image/jpeg的别名,如下所示:

case 'image/pjpeg':
case 'image/jpeg': $ext = 'jpg'; break;

系统调用

有时 PHP 可能没有您执行某个操作所需的函数,但运行它的操作系统可能会有。在这种情况下,您可以使用exec系统调用来完成工作。

例如,要快速查看当前目录的内容,您可以使用诸如示例 7-17 的程序。如果您使用的是 Windows 系统,它将直接使用 Windows dir命令运行。在 Linux、Unix 或 macOS 上,请注释或删除第一行,并取消注释第二行以使用ls系统命令。您可能希望键入此程序,将其保存为exec.php,并在浏览器中调用它。

示例 7-17. 执行系统命令
<?php // exec.php
  $cmd = "dir";   // Windows, Mac, Linux
  // $cmd = "ls"; // Linux, Unix & Mac

  exec(escapeshellcmd($cmd), $output, $status);

  if ($status) echo "Exec command failed";
  else
  {
    echo "<pre>";
    foreach($output as $line) echo htmlspecialchars("$line\n");
    echo "</pre>";
  }
?>

调用htmlspecialchars函数用于将系统返回的任何特殊字符转换为 HTML 可以理解和正确显示的字符,整理输出。根据您使用的系统,运行此程序的结果将类似于以下内容(来自 Windows dir命令):

Volume in drive C is Hard Disk
 Volume Serial Number is DC63-0E29

 Directory of C:\Program Files (x86)\Ampps\www

11/04/2025  11:58    <DIR>          .
11/04/2025  11:58    <DIR>          ..
28/01/2025  16:45    <DIR>          5th_edition_examples
08/01/2025  10:34    <DIR>          cgi-bin
08/01/2025  10:34    <DIR>          error
29/01/2025  16:18            1,150 favicon.ico
              1 File(s)      1,150 bytes
              5 Dir(s)  1,611,387,486,208 bytes free

exec接受三个参数:

  • 命令本身(在前一种情况下为$cmd

  • 系统将从命令中获取输出的数组(在前一种情况下为$output

  • 一个变量,用于包含调用的返回状态(在前一种情况下是$status

如果愿意,可以省略$output$status参数,但将无法知道调用生成的输出,甚至不知道是否成功完成。

您还应注意escapeshellcmd函数的使用。在发出exec调用时,始终使用此函数是一个好习惯,因为它清理命令字符串,防止执行任意命令,如果您向调用提供用户输入。

警告

共享网络主机通常会禁用系统调用功能,因为它们存在安全风险。如果可能的话,你应该始终尝试在 PHP 内部解决问题,只有在确实必要时才直接访问系统。此外,访问系统相对较慢,如果你的应用程序预计在 Windows 和 Linux/Unix 系统上运行,你需要编写两种实现。

XHTML 还是 HTML5?

因为 XHTML 文档需要良好的格式,你可以使用标准的 XML 解析器解析它们——与 HTML 不同,后者需要宽松的专用 HTML 解析器。因此,XHTML 从未真正流行起来,当制定新标准时,万维网联盟选择支持 HTML5 而不是较新的 XHTML2 标准。

HTML5 同时具有 HTML4 和 XHTML 的一些特性,但使用起来更简单,验证时更宽松。幸运的是,现在你只需要在 HTML5 文档的头部放置一个单一的文档类型声明(而不是之前需要的多种 strict、transitional 和 frameset 类型):

<!DOCTYPE html>

只需简单的单词html即可告诉浏览器你的网页是为 HTML5 设计的,因为自 2011 年左右,大多数流行浏览器的最新版本都支持大部分 HTML5 规范,所以这种文档类型通常是你唯一需要的,除非你选择支持旧版浏览器。

就 HTML 文档编写而言,Web 开发人员可以安全地忽略旧的 XHTML 文档类型和语法(例如使用<br />而不是更简单的<br>标签)。但如果你发现自己需要支持非常旧的浏览器或依赖于 XHTML 的特殊应用程序,你可以在http://xhtml.com获取更多信息。

问题

  1. 哪个printf转换说明符用于显示浮点数?

  2. 什么printf语句可用于将输入字符串"Happy Birthday"输出为字符串"**Happy"

  3. 要将printf的输出发送到变量而不是浏览器,你会使用哪个替代函数?

  4. 如何为 2025 年 5 月 2 日上午 7:11 创建 Unix 时间戳?

  5. 使用fopen以写入和读取模式打开文件,并将文件截断,并将文件指针置于起始位置时,会使用哪种文件访问模式?

  6. 删除文件file.txt的 PHP 命令是什么?

  7. 哪个 PHP 函数用于一次性从网络上读取整个文件?

  8. 哪个 PHP 超全局变量包含有关上传文件的详细信息?

  9. 哪个 PHP 函数启用运行系统命令?

  10. HTML5 中更倾向于使用以下哪种标签风格:<hr>还是<hr />

请参阅“第七章答案”,位于附录 A 中,以获取这些问题的答案。