PHP7-Zend-认证学习指南-五-

88 阅读20分钟

PHP7 Zend 认证学习指南(五)

原文:PHP 7 ZEND CERTIFICATION STUDY GUIDE

协议:CC BY-NC-SA 4.0

十、数据库和 SQL

数据库是一种保存数据的工具,这些数据是您打算经常引用并需要长期保存的。

PHP 使用扩展与一系列数据库进行交互。例如,要与 MySQL 数据库交互,可以使用mysqli扩展提供的函数。

Note

mysqli结尾有个“我”。这是现已废弃的mysql扩展的替代品。像“预准备语句”这样的特性只在新的扩展中可用。

PHP 还提供了抽象层,在代码和数据库之间提供了一个应用层。我们将在本书中探讨 PDO (PHP 数据对象)。

在本书中,我们将重点关注关系数据库,但在传递关系数据库的替代方案时,这是值得一提的。MongoDB 是一个非常受欢迎的 NoSQL 数据库,它提供了一个 PHP 驱动程序,允许你连接到数据库。我们将关注原生关系数据库,MongoDB 驱动程序不太可能包含在您的 Zend 考试中。

Zend 考试要求你了解基本的 SQL。如果问题没有特别说明,假设的环境将是 MySQL。

数据库基础

让我们首先确定关系数据库的一些概念是清楚的。

键施加约束,比如PRIMARYUNIQUE。主键可以在一列或多列上定义。它保证数据库中的每一行对于键中的列都有唯一的值组合。行的主键不能为空值。

一个表只能有一个主键。外键也可以在一列或多列上定义。它引用了另一个表上的主键。这是一个唯一的引用,因此被引用表中只有一行链接到包含外键的表。

图 10-1 用外键category_id表示产品所属的类别。

A456636_1_En_10_Fig1_HTML.jpg

图 10-1。

Both tables have primary keys named id

指数

索引是实现键约束所需的数据结构。索引加快了检索记录的速度。数据库引擎将在磁盘或内存中创建一个结构,其中包含索引列中的数据。这种结构针对查找进行了优化,有助于数据库更快地找到表中的行。

每当向表中插入一行时,都需要更新索引。这增加了编写的开销。

没有索引就不能有键,但是可以索引没有键的列。如果您不想强制实现惟一性,但又想加速在WHERE子句中包含这些列的SELECT语句,那么您可以这样做。

键和索引之间的绑定非常紧密,在 MySQL 中它们被认为是同义词。

关系

关系是关系数据库的核心特征。通过声明表之间的关系,您可以实施参照完整性并最大限度地减少脏数据。

有几种类型的关系。

关系描述
一对一父表中的一行只能引用子表中的一行。
一对多父表中的一行可以被子表中的多行引用。
多对多父表中任意数量的行可以被子表中任意数量的行引用。

通过表之间的关系,您可以将逻辑上相关的数据存储在一个表中,与其他数据区分开来。

例如,您可以有一个products表来存储您销售的产品的信息。产品属于类别。一个类别可以包含许多不同的产品。这意味着 category 表中的一行可以被 products 表中的多行引用。

SQL 数据类型

SQL 数据库表中的列具有分配给它们的数据类型。正如 PHP 变量类型一样,SQL 类型可以存储不同格式的数据。

每个数据库管理器实现的 SQL 数据类型略有不同,并且在不同类型之间有不同的优化。

我们讨论一些常见的数据类型,避免关注任何特定的 SQL 实现。

数字类型

整数的类型在存储它们的值所用的字节数上有所不同。

下表说明了 MySQL 数据库的整数大小: 1

整数类型字节(带符号的)值(无符号)值
BIGINTeight-9223372036854775808 转+ 92233720368547758070 到 18446744073709551615
INTEGERfour-2147483648 转+21474836470 到 4294967295
MEDIUMINTthree-8388608 至+83886070 到 16777215
SMALLINTTwo-32768 至+327670 到 65535
TINYINTone-128 至+1270 到 255

Tip

MySQL 允许你为整数指定一个参数,这个参数实际上是一个显示值,不影响底层存储。这是一个相当普遍的误解,认为参数是为了精度。

非整数类型可以存储在NUMERICDECIMAL值中。SQL-92 标准规定,NUMERIC类型必须具有规定的精确精度,而DECIMAL类型必须至少同样精确。这些数据类型的实现因供应商而异。

它们都采用相同的参数:

NUMERIC(21,3)

第一个参数指定精度的总位数,第二个参数指定必须存储多少位十进制精度。

在本例中,我们将存储一个总共有 21 位的数字,其中 3 位出现在小数点后。

字符类型

SQL 允许将字符存储在固定长度或可变长度的字符串中。

固定长度的字符串在磁盘上总是被分配相同数量的字节。在某些数据库实现中,这有助于提高读取性能。权衡的结果是,如果存储在固定长度数据存储中的字符串比分配的字符数短,则存储的字符会比实际需要的多。

可变长度字符串可以膨胀到给定的限制大小。数据库引擎根据字符串的长度分配存储。数据库实现将存储被存储的字符串的长度。这将是至少一个字符来表示字符串的结尾,但是在一些引擎中,每个可变字符串将导致更大的存储开销。

一般来说,当存储一个字符串时,你知道它总是有一个特定的长度,比如一个散列,你应该把它存储在一个固定长度的字符字段中。这将提高性能,并且不会导致存储浪费。

使用 SQL

我们不会关注 SQL 的任何具体实现,而是尝试使用通用语句。Zend 考试不会测试你对特定数据库引擎的了解,但会期望你了解基本的 SQL 语法。

创建数据库和表

CREATE语句可以用来创建数据库和表格。创建数据库很简单;您只需指定数据库的名称:

CREATE DATABASE mydatabase;

创建表时,可以指定要存储在其中的列的列表。对于每一列,可以指定名称、数据类型和属性。

CREATE TABLE IF NOT EXISTS users (
  id int unsigned NOT NULL AUTO_INCREMENT,
  name varchar(255) NOT NULL,
  email varchar(255) NOT NULL,
  password varchar(60) NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY users_email_unique (email)
);

删除数据库和表

CREATE的逆运算是DROP语句。

DROP TABLE category;
DROP DATABASE mydatabase;

如果您指定了外键,数据库将不会允许您删除违反其中一个约束的表。

例如,参考我们的产品和类别示例。如果我们尝试删除 category 表,但仍有产品引用它,数据库引擎应该不允许该操作。

检索数据

SELECT语句用于检索数据。SELECT的语法可能非常复杂,是不同供应商之间差异最大的语句之一。你需要了解基本的用法和加入你的 Zend 认证。

在这个简单的伪代码查询示例中,我们从表中检索价格超过 100 个货币单位的产品名称。我们指定希望结果按价格降序返回。

SELECT name
  FROM products
  WHERE price > 100
  ORDER BY price DESC

您可以指定用逗号分隔的多个列名,或者使用通配符*来接收所有列。

PHP 接收的数据格式取决于您用来调用查询的驱动程序和函数。您通常会收到一个对象或数组,其中包含与列相对应的键/属性。

插入新数据

INSERT语句用于在数据库中创建新行。您需要提供一个列列表以及要插入其中的值。标记为NOT NULL的列是必需的,在创建行时必须指定一个值。

INSERT INTO products
  (name, price, category_id) VALUES
  ('cheeseburger', 100, 3)

如果不指定列名,SQL 将假定您按照列在表中出现的顺序提供值。如果你改变了你的表的结构,这可能是一个缺点。

否则,如示例所示,先指定列名,然后指定值。这些值按顺序分配给列。因此,在这个例子中,产品的名称被设置为'cheeseburger',它的价格是 100,它被放入 ID 值为 3 的类别中(无论它是什么)。

更新数据

UPDATE语句接受类似于INSERT语句的值列表,以及类似于SELECT语句的可选WHERE子句。

您必须指定现有数据要更新到的值,以及必须更新的行的标准。

UPDATE products
  SET price = price + 100
  WHERE category_id = 3;

汇总数据

您可以使用数据库执行计算并将结果发送给您。

声明返回
AVG数据值的平均值
SUM找到的所有数据值的总和
COUNT找到了多少条记录
DISTINCT COUNT找到了多少条唯一记录
MIN数据集中的最小值
MAX数据集中的最高值

使用这些函数如下所示:

SELECT AVG(price) FROM products;

分组数据

您可以告诉 SQL 在将数据返回给您之前,按列或列的组合对数据进行分组。这通常与聚合函数一起使用很有用。

让我们考虑一个例子,我们想找出我们每个客户购买的总销售额。

SELECT email, SUM( sales_value )
        FROM  `transactions`
        GROUP BY email

在本例中,我们对具有相同电子邮件地址的事务进行分组。SQL 数据库引擎将应用SUM语句,将每个组中的销售值相加,然后返回该值。

我在SELECT语句中包含了电子邮件地址,因此输出将包含客户的电子邮件地址,以及包含其电子邮件地址的所有交易的销售额总和。

连接

联接用于根据提供的标准连接表。这允许您从相关表中检索信息。

在产品和类别数据库中,您可以通过将categories表连接到products表来检索产品的类别名称:

SELECT *
FROM  products
JOIN categories ON categories.id = products.category_id

我们将categories表连接到products表,并向 SQL 发出如何匹配行的指令。如果categories表中的id列与products表中的category_id列相匹配,那么将包含该表中的一行。

连接类型

有几种连接表的方法。

连接类型影响
INNER JOIN选择在两个表中都有匹配值的记录,如上例所示
LEFT OUTER JOIN从左表中选择具有匹配右表记录的表
RIGHT OUTER JOIN从右表中选择与左表记录匹配的记录
FULL OUTER JOIN选择与左表或右表记录匹配的所有记录

这些连接可以用图解法表示,如图 10-2 所示。

A456636_1_En_10_Fig2_HTML.jpg

图 10-2。

Many ways to join tables

准备好的声明

当您向 SQL 引擎发出命令时,它必须解析该命令才能执行它。执行完该语句后,SQL 将丢弃编译后的代码,结果是需要单独解析使用相同 SQL 命令的重复调用。显然,这导致了重复劳动。

您可以通过使用准备好的语句来避免 SQL 重复工作,这些语句将成为 SQL 存储的已解析代码模板,以供多次重用。

预准备语句还提供了显著的安全优势。参数被绑定到准备好的语句,并且不作为代码字符串的一部分。这意味着您的参数不可能侵入代码,这意味着您不再需要担心转义代码来防止 SQL 注入。在您不再担心数据进出数据库之前,请记住存储的 XSS 攻击的可能性。

<?php
// prepare and bind
$stmt = $conn->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $password);

// set parameters
$username = "bob";
$password = password_hash("password", PASSWORD_BCRYPT);

// run the statement
$stmt->execute();

处理

事务是一组 SQL 语句,要么全部成功,要么无效。

事务完成后,数据库不能使任何表约束失效,并且必须处于所有更改都已持久化的状态。数据库必须有某种方法来确保事务可以同时运行而不会相互干扰,例如,通过增加另一个事务所依赖的主键。

总之,事务是一组 SQL 语句,必须以“全有或全无”的方式成功完成。运行后,数据库必须处于一致状态,并且必须能够从错误中恢复。

交易的语法因供应商而异,但是有三个重要的陈述。

  • 一条语句将标记事务块的开始。其后的 SQL 语句将是事务的一部分。
  • 有两个语句可以结束事务。其中一个将告诉 SQL 继续进行事务正在进行的所有更改。
  • 另一个 end 语句将告诉 SQL,无论出于什么原因,您都希望放弃事务,而是恢复到事务开始时数据库所处的状态。

以下是三种最常见的供应商陈述:

关系型数据库数据库备份方法神谕
START TRANSACTIONBEGIN TRANSACTIONSTART TRANSACTION
COMMITCOMMIT TRANSACTIONCOMMIT
ROLLBACKROLLBACK WORKROLLBACK

PHP 数据对象(PDO)

PDO 是一个数据抽象层,它为您提供了一个与多个数据源交互的接口。使用 PDO 时,无论供应商是谁,您都可以使用相同的函数与数据库进行交互。

理解 PDO 是一个访问抽象层并且不抽象 SQL 或数据类型是很重要的。您传递给PDO::query()或准备好的语句的 SQL 必须对您正在连接的供应商有效。

PDO 使用数据库适配器连接到数据库。这些适配器类实现了 PDO 接口,并将特定于供应商的函数作为常规扩展函数公开。

PDO 是在 PHP 配置文件中配置的。运行时,您可以使用PDO::setAttribute()功能更改选项。

PDO 扩展提供了几个预定义的常量。对于 Zend 考试,您不需要记住所有的内容,但是可以浏览 PHP 手册并熟悉它们。

PDO 将模拟不支持预准备语句的数据库的预准备语句,但是将使用数据库的本机预准备语句功能。

连接到 PDO

要用 PDO 连接到数据库,需要创建一个 PDO 类的实例。如果需要,构造函数接受数据库源(DSN)和用户名/密码的参数。

<?php
try {
    $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
} catch (PDOException $e) {
    echo "Error connecting to database: " . $e->getMessage();
}

如果连接到数据库时出现错误,将会抛出一个PDOException。注意异常的堆栈跟踪可能包含完整的数据库连接细节是非常重要的。一定要抓住它,不要让它被展示出来。

要在完成后关闭连接,可以将 PDO 变量设置为null

$dbh = null;

数据库连接会在运行脚本结束时自动关闭,除非您将它们持久化。持久数据库连接不会被关闭,而是被缓存以供脚本的另一个实例使用。这减少了每次 web 应用运行时需要连接到数据库的开销。

PDO 的交易

PDO 也提供了事务命令,但是没有模拟正确的事务处理。这意味着您只能在本地支持事务的数据库上使用 PDO 事务函数。这些功能是PDO::beginTransaction()PDO::commit()PDO::rollBack()

<?php
$dsn = 'mysql:host=localhost;dbname=example';
$pdo = new PDO($dsn, 'dbuser', 'dbpass');
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, TRUE);
$pdo->setAttribute(PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION);
$password = password_hash("password", PASSWORD_BCRYPT);
try {
    $pdo->beginTransaction();
    $pdo->exec("
          INSERT INTO users
              (username, password)
          VALUES
              ('bob', '{$password}'");
    // some more update or insert statements
    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollBack();
    echo 'Rolled back because: ' . $e->getMessage();
}

在这个例子中,我们连接到数据库with PDO,并启动一个事务。

我们将所有的 PDO 事务功能包装在一个try...catch块中。如果 PDO 语句运行失败,它将抛出一个PDOException。我们使用catch块回滚事务。

获取 PDO 结果

我们使用PDO::fetch()方法 2 从 PDO 结果中检索数据。PDO 将保持一个光标来遍历结果集,并使用它来确定返回给你的元素。

PDO 将以您在第一个参数fetch()中指定的格式将数据返回给您。

提取样式返回
PDO::FETCH_ASSOC返回一个以数据库列为关键字的关联数组。
PDO::FETCH_NUM返回由结果集返回的按列号索引的数组。
PDO::FETCH_BOTH返回一个数组,该数组同时包含ASSOCNUM样式提取的索引。
PDO::FETCH_BOUND返回 true 并将结果集中列的值赋给 PHP 变量,这些变量是用PDOStatement::bindColumn()方法绑定的。
PDO::FETCH_CLASS返回所请求类的新实例,该实例将结果集的列映射到该类中的命名属性。
PDO::FETCH_INTO更新所请求类的现有实例,映射为FETCH_CLASS
PDO::FETCH_OBJ返回一个匿名对象,其属性名对应于结果集中的列名。
PDO::FETCH_LAZY组合PDO::FETCH_BOTHPDO::FETCH_OBJ并在访问时创建对象变量名。
PDO::FETCH_NAMED至于PDO::FETCH_ASSOC,返回一个关联数组。如果有多个同名列,则该键引用的值将是具有该列名的行中所有值的数组。

在 PDO 准备发言稿

并非所有数据库引擎都支持预处理语句,这是 PDO 为不支持预处理语句的适配器模拟的唯一特性。

在 PDO 中,预处理语句的语法与使用本地函数非常相似。

<?php
$stmt = $dbh->prepare("INSERT INTO users (name, email) VALUES (:name, :value)");
$stmt->bindParam(':name', $name);
$stmt->bindValue(':email',’alice@example.com’);

// insert one row
$name = 'one';
$stmt->execute();

浏览这个例子,我们看到使用了prepare()方法来创建语句对象。

我们使用两种不同形式的绑定参数来演示这种差异。

在第一个例子bindParam()中,我们将一个变量绑定到语句参数。当语句执行时,参数将采用执行时变量的值。

绑定变量的第二种方法是bindValue(),将一个文字绑定到语句参数。如果您在bindValue()中使用了变量名,那么将使用绑定时的变量值。在语句执行之前对变量的更改不会影响参数值。

您也可以在调用 execute 时将要绑定的值作为数组传递,如下例所示:

<?php
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status=:status');
$stmt->execute(['email' => $email, 'status' => $status]);
$user = $stmt->fetch();

在 SQL 语句中只能绑定值,而不能绑定像表名或列这样的实体。您只能绑定标量值,而不能绑定像数组或对象这样的复合变量。

多次致电 PDO 准备陈述

您已经看到,bindParam()方法在语句执行时将变量值插入到语句参数中。您可以看到,使用bindParam()允许您重复调用准备好的语句,在每次调用中使用不同的参数值。

方法closeCursor()用于清除数据库光标,并将语句返回到可以再次执行的状态。当先前执行的语句仍有未预取的行时,一些数据库在执行准备好的语句时会出现问题。

Chapter 10 Quiz

Q1:你可以使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ 函数从数组中构建一个适合用于GETPOST的 HTTP 查询字符串。

| http_build_query() | | http_build_param() | | parse_url() | | urlencode() |

Q2:PHP 函数encodeurl()用于:

| 确保 URL 是 UTF 8 编码的。 | | 将 URL 中的保留字符转换为%编码符号。 | | 从数组中构建一个适合于GET参数的字符串。 | | 没有这个 PHP 函数。 |

Q3:使用准备好的语句有什么好处;选择尽可能多的适用选项?

| 它们比使用普通查询更安全。 | | 对于重复查询,它们更快。 | | 您可以在不同的数据库供应商中使用相同的查询。 | | 以上都不是。 |

问题 4:我将客户信息存储在一个表中。每一行都有一个account_id,它是一个名为accounts的表的外键。对于 ID 为123的客户,如何从地址表中选择postcode列?

| 从“帐户”中选择“邮政编码”,其中 customer _ id = 123 | | 从“accounts”中选择“postcode”作为“acc ”,在“cust . id ”=“ACC . id”上加入“customers”作为“cust ”,其中“c ”. id = 123 | | 在' c . account _ id = a . id '上,从' accounts '中选择'邮政编码'作为' a ',加入' customers '作为' c ',其中' c.id = 123 | | 在“c . id ”=“a . id”上,从“accounts”中选择“postcode”作为“a”完全外部连接“customers”作为“c ”,其中“c . id”= 123 |

Footnotes 1

https://dev.mysql.com/doc/refman/5.7/en/integer-types.html

  2

https://secure.php.net/manual/en/pdostatement.fetch.php

十一、错误处理

错误处理是 PHP 5.6 和 PHP 7.1 之间一些最大变化的原因,尽管我们在本书的其他相关地方已经提到了这个重要的话题,但是单独讨论它还是有意义的。

可投掷物品

在本章中,我们将会看到ErrorException类。现在,你需要知道的是它们都实现了 PHP 7 中引入的Throwable接口。

Tip

PHP 定义一个新的不扩展Exception类的Error类的原因是为了保持与 PHP5.6 代码的向后兼容性。

可抛出的界面

异常和错误异常都实现了Throwable接口,所以您可以在一个块中捕获这两种类型,如下所示:

<?php
try {
    // ...code

} catch (Throwable $e) {
    echo "A class that inherits from either Exception or ErrorException was caught";
}

一会儿我们将看到 PHP 用来比较ErrorExceptionsExceptionscatch块的匹配规则。

你可以在 PHP 手册的Throwable接口中找到定义的方法, 1 但是为了你的方便,这里把它们列出来:

Throwable {
/* Methods */
abstract public string getMessage ( void )
abstract public int getCode ( void )
abstract public string getFile ( void )
abstract public int getLine ( void )
abstract public array getTrace ( void )
abstract public string getTraceAsString ( void )
abstract public Throwable getPrevious ( void )
abstract public string __toString ( void )
}

Tip

这些方法对于记录错误非常有用。

错误

在 PHP 的旧版本中,错误和异常的处理方式非常不同。错误是引擎中产生的东西,只要它不是致命的,就可以由用户定义的函数来处理。

问题是有几个错误是致命的,无法由用户定义的错误处理程序来处理。

这意味着你不能优雅地处理 PHP5.6 中的致命错误。有几个副作用是有问题的——比如运行时上下文的丢失,析构函数不会被调用,以及处理它们很笨拙。

在 PHP 7 中,致命错误现在是异常,并且更容易处理。

Note

只有致命错误才会引发错误异常。您需要用错误处理函数来处理非致命错误。

下面是一个在 PHP 7.1 中捕捉致命错误的例子。请注意非致命错误是如何被捕获的。

<?php
try {
    // generates a notice error (not caught)
    echo $thisVariableIsNotSet;
    // this would be a fatal error (is caught)
    badFunction();
} catch (Error $e) {
    echo "Error caught: " . $e->getMessage();
}

如果试图访问无效变量,该脚本将输出一个错误通知。在 PHP 的早期版本中,试图调用一个不存在的函数会导致致命错误,但是在 PHP 7.1 中,您可以发现它。

以下是该脚本的输出:

Notice: Undefined variable: thisVariableIsNotSet in /in/lQC3F on line 5
Error caught: Call to undefined function badFunction()

误差常数

PHP 有很多与错误相关的常量 2 。这些常量在配置 PHP 来隐藏或显示某些类的错误时使用。

以下是一些更常见的错误代码:

密码描述脚本抛出错误?
E_DEPRECATED如果您使用不推荐使用的语言功能,解释器将生成这种类型的警告。继续运行
E_STRICTE_DEPRECATED类似,这表明您正在使用一种当前不标准的语言特性,将来可能无法工作。继续运行
E_PARSE无法解析您的语法,因此您的脚本无法启动。根本不会跑
E_NOTICE信息性消息。继续运行
E_WARNING这些是非致命警告。继续运行
E_ERROR脚本无法继续运行,正在被终止。中止,除非您用错误处理程序来处理它
E_RECOVERABLE_ERROR这个错误可能足够危险,足以致命,但引擎并没有处于无法继续运行的状态。中止,除非您用错误处理程序来处理它

使用错误处理函数

set_error_handler()函数 3 用于告诉 PHP 如何处理不是Error异常类实例的标准引擎错误。对于致命错误,不能使用错误处理函数;你必须抓住他们作为Error的例外。

set_error_handler()接受一个可调用的 4 作为其参数。PHP 中的可调用函数有两种指定方式:一种是表示函数名的字符串,另一种是传递包含对象和方法名的数组(按此顺序)。

Note

您可以将对象中的受保护方法和私有方法指定为可调用的。

还可以传递 null 来告诉 PHP 使用标准的错误处理机制。

如果您的错误处理程序没有终止程序并返回,那么您的脚本将在错误发生后的行继续执行。

PHP 将参数传递给错误处理函数。如果想在函数中使用它们,可以选择在函数签名中声明它们。

<?php

function myHandler(int $errNo, string $errMsg, string $file, int $line) {
    echo "Error #[$errNo] occurred in [$file] at line [$line]: [$errMsg]";
}

set_error_handler('myHandler');

try {
  // This does not throw an Error
  5 / 0;
} catch ( Throwable $e ) {
  echo 'Caught error : ' . $e->getMessage();
}

/*
  Error #[2] occurred in [/in/Xa0Td] at line [11]: [Division by zero]
*/

在前面的例子中,我们将数字五除以零。在 PHP 中,这会导致警告,所以不会抛出Error。然而,我们已经将函数myHandler()设置为客户错误处理程序,因此当 PHP 遇到警告时会调用它。

用户错误处理程序无法捕获导致脚本终止的错误;这些包括E_ERRORE_PARSEE_CORE_ERRORE_CORE_WARNINGE_COMPILE_ERRORE_COMPILE_WARNING

显示或抑制非致命错误消息

通常,您希望在生产过程中隐藏所有系统错误消息,并且您的代码应该在不生成警告或消息的情况下运行。如果您要显示一条错误消息,请确保它是您自己生成的,并且不包含可能帮助攻击者侵入您的系统的信息。

在您的开发环境中,您希望显示所有错误,以便您可以修复与它们相关的所有问题,但是在生产环境中,您希望禁止向用户发送任何系统消息。

为此,您需要在您的php.ini文件中使用以下设置来配置 PHP:

  • display_errors可设置为 false 以抑制消息
  • log_errors可用于在日志文件中存储错误信息
  • 可以设置error_reporting来配置哪些错误触发报告

最佳实践是优雅地处理应用中的错误。在生产中,您应该记录未处理的错误,而不是允许它们显示给用户。

Note

我们在第一章中看到了错误抑制运算符@。记住最好避免使用它。

error_log()函数 5 可用于向系统错误处理程序之一发送消息。您不应该将它与error_log配置选项混淆。配置选项指定如何处理日志,而函数用于发送消息。

你也可以使用error_log()函数来发送电子邮件,但就我个人而言,我不会这样做,我宁愿用代码或使用类似滚动条的服务来实现。 6

错误处理功能

PHP 有很多与错误处理相关的函数78。下表提供了它们的摘要。在这一章中,我们将会看到其中的大部分。

功能目的
debug_backtrace生成回溯。
调试 _ 打印 _ 回溯打印回溯。使用该函数时要小心,因为它会产生大量输出!
error_clear_last清除最近的错误。
error_get_last获取最后发生的错误。
error_log向定义的错误处理例程发送错误消息。
error_reporting设置报告哪些 PHP 错误。
还原错误处理程序恢复以前的错误处理函数。
还原异常处理程序恢复先前定义的异常处理函数。
设置错误处理程序设置用户定义的错误处理函数。
设置异常处理程序设置用户定义的异常处理函数。
触发器 _ 错误生成用户级错误/警告/通知消息。
用户 _ 错误trigger_error的别名。

例外

异常是面向对象编程的核心部分,最初是在 PHP 5.0 中引入的。异常是一个程序状态,它需要特殊的处理,因为它没有以预期的方式运行。

你可以使用一个异常来改变程序的流程,例如,如果不满足某些前提条件,就停止做一些事情。

如果您没有捕捉到异常,它就会在调用堆栈中冒泡。让我们看一个例子:

<?php
function A() {
  // The exception thrown in C will bubble up to A
  // because it is not handled in C or B
  try {
    B();
  } catch (Exception $e) {
    echo "Caught exception in " . __METHOD__;
  }
}

function B() {
  // we're not catching exceptions in B
  C();
}

function C() {
  // we do not catch the exception where it is thrown
  throw new Exception('Bubble');
}
A();
/*
Outputs:
  Caught exception in A and the program ends successfully
*/

这个程序调用函数 A,函数 A 调用 B,B 调用 C。函数 C 抛出一个异常,但是我们没有在 C 中捕捉到它。异常冒泡到 B,但是也没有在 B 中捕捉到。异常继续冒泡到 A,在那里我们确实捕捉到了它。

如果我们没有在 A 中捕捉到异常,那么它将会冒泡到全局范围,在那里我们将有最后一次捕捉它的机会。如果一个异常没有被捕获,那么 PHP 会寻找一个默认的异常处理程序,最终如果没有处理程序,就会导致一个致命的错误。

扩展异常类

PHP 包括几个标准的异常类型,标准 PHP 库(SPL)还包括更多。虽然您不必使用这些异常,但是这样做意味着您可以使用更细粒度的错误检测和报告。

ExceptionError类都实现了Throwable接口 9 ,并且像任何其他类一样,可以被扩展。这允许您创建灵活的错误层次结构,并定制您的异常处理。

只有实现了Throwable类的类才能与throw关键字一起使用。换句话说,你不能声明你自己的基类,然后把它作为异常抛出。

例如,让我们创建一个异常类,我们可以用它来表示出现了表单验证问题:

<?php
class ValidationException extends Exception { }

function myValidation() {
    if (empty($_POST)) {
        throw new ValidationException('No form fields entered');
    }
}

让我们看看语法,然后更详细地讨论异常:

<?php
class ParentException extends Exception {}
class ChildException extends ParentException {}

try {
    // some code
    throw new ChildException('My Message');
} catch (ParentException $e) {
    // matches this class because of inheritance
    echo "Parent Exception :" . $e->getMessage();
} catch (ChildException $e) {
    // matches this class exactly
    echo "Child Exception :" . $e->getMessage();
} catch (Exception $e) {
    // matches this class because of inheritance
    echo "Exception :" . $e->getMessage();
}

这个例子的输出是Parent Exception :My Message

在这个例子中,我们抛出了一个继承自ParentExceptionChildException,它又扩展了基类Exception

这些块按照从上到下的顺序进行评估。将执行第一个匹配的块。

被抛出的异常的类与作为参数给定给catch子句的类名相匹配。

匹配标准是这些类别是:

  • 完全一样,或者
  • 抛出的异常是catch语句中异常的祖先

在这个例子中,我们抛出了一个继承自ParentException的异常ChildException。因此,异常与第一个catch块匹配,代码被执行。

我将基Exception放在了catch块列表的底部,因为所有自定义异常都继承自它,这使得它成为一个总括。

异常层次结构

到目前为止,我们已经知道ErrorsExceptions都实现了Throwable接口。我们刚刚看到ErrorException类都可以扩展。

内置的 PHP 7 异常层次结构如下所示:

interface Throwable
    |- Exception implements Throwable
        |- ...
    |- Error implements Throwable
        |- TypeError extends Error
            |- ArgumentCountError
        |- ParseError extends Error
        |- ArithmeticError extends Error
            |- DivisionByZeroError extends ArithmeticError
        |- AssertionError extends Error

正如您所看到的,有几个预定义的错误类在Error下面形成了一个层次结构。下表总结了它们的用途:

班级目的
TypeError当传递给函数的参数与其对应的声明参数类型不匹配时,或者当函数没有返回预期的类型时,就会抛出TypeError
ArgumentCountError当传递给用户定义的函数或方法的参数太少时,抛出ArgumentCountError
ParseError当解析 PHP 代码时出现错误,例如,当您调用eval()或包含一个文件时,就会抛出一个ParseError
ArithmeticError当您尝试以负的量进行位移位或调用intdiv()时,会发生算术错误,这将导致当前系统上的值超出整数的限制。
DivisionByZeroError如果你试图除以零,就会出现一个DivisionByZeroError
AssertionError当用assert()语言构造的断言失败时,抛出一个AssertionError

处理异常

健壮的代码可以遇到错误并处理它。以明智的方式处理异常提高了应用的安全性,并使日志记录和调试更加容易。管理应用中的错误还可以让您为用户提供更好的体验。在这一节中,我们将介绍如何捕获和处理代码中出现的错误。

捕捉异常

记得在本章的前面,我们这样定义了一个ValidationException:

<?php
class ValidationException extends Exception { }

function myValidation() {
    if (empty($_POST)) {
        throw new ValidationException('No form fields entered');
    }
}

让我们从那里继续,想象我们正在调用myValidation()函数,并且想要捕捉异常。捕捉异常的语法如下:

<?php
try {
    // assume that if there is validation problem this throws a ValidationException
    myValidation();
} catch (ValidationException $e) {
    echo "Validation exception caught ";
    echo $e->getMessage();
} catch (Exception $e) {
    echo "General exception type caught";
}

注意这里有两个catch子句。异常将从上到下与子句匹配,直到异常的类型与catch子句匹配。

Note

匹配标准是这些类要么完全相同,要么抛出的异常类是catch语句中Exception类的祖先。

由于myValidation抛出了一个ValidationException,我们期望它在第一个块中被捕获,但是如果函数中抛出了任何其他类型的异常,那么它将在第二个catch块中被捕获。

还要注意方法getMessage()正在异常对象上被调用。基本Exception类中的其他方法将给出错误代码、堆栈跟踪和其他信息。关于Exceptions 11 的 PHP 手册对于异常对象的原型是最好的参考。

有可能在catch块中抛出异常。这使您可以捕捉异常,然后在需要时将其作为不同的类型重新抛出。

Tip

你应该总是把你的catch块从最具体的排在顶部,到最一般的排在底部——记住catch块是贪婪的!

一个catch块可以指定多个异常类,用管道字符(|)分隔它们。

在下面的例子中,catch块将匹配属于MyException类或AnotherException类的异常。

<?php

class MyException extends Exception {}
class AnotherException extends Exception {}

try {
    throw new AnotherException;
} catch (MyException | AnotherException $e) {
    echo "Caught : " . get_class($e);
}
/*
Caught : AnotherException
*/

Note

一个try块必须至少有一个catch块。

最终块

我们要看的最后一个子句是finally。无论是否抛出异常,这段代码都将被执行。它在try程序块完成后或异常程序块完成后执行。

finally块的一个常见用途是关闭一个文件句柄,但是finally可以用在您希望代码总是被执行的任何地方。

<?php
try {
  // perform some functions
} catch (Exception $e) {
  // handle the error
} finally {
  // always run these statements
}

设置默认异常处理程序

任何未被捕获的异常都会导致致命错误。如果您想优雅地响应没有被catch块捕获的异常,那么您需要设置一个函数作为默认的异常处理程序。

为此,您可以使用set_exception_handler()函数,该函数接受一个 callable 作为其参数。您的脚本将在 callable 执行后终止。

函数restore_exception_handler()会将异常处理程序恢复到之前的值。

Chapter 11 Quiz

Q1:这段代码会输出什么?

| 发现错误! | | 作为会话 ID 的随机字符串 | | 一个通知错误和一个警告 | | 两行格式化的行,每一行包含关于错误的信息 |

<?php
$handler = function($errorNumber, $errorMessage, $file, $line) {
  echo "Error [$errorNumber] in [$file] at line [$line]: '[$errorMessage]'\r\n";
};
set_error_handler($handler);
try {
    echo $a;
    session_start();
    echo session_id();
} catch (Throwable $e) {
    echo "Error caught!";
}

Q2:这段代码会输出什么?

| 没有,这运行没有错误 | | 一个正常的致命错误 PHP 消息 | | 关于错误的格式化信息行 | | 以上都不是 |

<?php
$handler = function($errorNumber, $errorMessage, $file, $line) {
  echo "Error [$errorNumber] in [$file] at line [$line]: '[$errorMessage]'\r\n";
};
set_error_handler($handler);
this_function_is_not_defined();

Q3:这段代码会输出什么?

| 没有,这段代码运行没有错误 | | 哎呀! | | 一个 PHP 致命错误 | | 以上都不是 |

<?php
class IndianaError extends ArithmeticError {}
define('PI', 3);
try {
    if (is_int(PI)) {
      throw new IndianaError('Oops');
    }
} catch (Exception $e) {
    echo $e->getMessage();
}

Q4:这段代码会输出什么?

| 你好世界 | | 两行信息 | | 一个 PHP 致命错误 | | 以上都不是 |

<?php
set_error_handler(function($errorNumber, $errorMessage, $file, $line) {
  debug_print_backtrace();
});
trigger_error('Hello world', E_USER_WARNING);

Q5:这段代码会输出什么?

| 捕获到异常! | | 接住了! | | 发现错误! | | DivisionByZeroError抓到了! | | 以上都不是 |

<?php
try {
    echo 50/0;
} catch (Exception $e) {
    echo "Exception caught!";
} catch (Throwable $e) {
    echo " Throwable caught!";
} catch (Error $e) {
    echo "Error caught!";
} catch (DivisionByZeroError $e) {
    echo "DivisionByZeroError caught!";
}

Footnotes 1

https://php.net/manual/en/class.throwable.php

  2

https://php.net/manual/en/errorfunc.constants.php

  3

https://php.net/manual/en/function.set-error-handler.php

  4

https://php.net/manual/en/language.types.callable.php

  5

https://php.net/manual/en/function.error-log.php

  6

https://rollbar.com/

  7

https://php.net/manual/en/errorfunc.configuration.php#ini.error-log

  8

https://php.net/manual/en/ref.errorfunc.php

  9

https://php.net/manual/en/class.throwable.php

  10

[en.wikipedia.org/wiki/Robustness_(computer_science)](en.wikipedia.org/wiki/Robust…

  11

https://php.net/manual/en/language.exceptions.php

十二、练习

这一章只讲问题。所有这些问题的目的是帮助你确定你需要复习的领域。考试包括非常注重细节的问题,你需要确保你熟悉常用的功能。

我没有把问题按章节分类。真正的考试有侧重于教学大纲多个领域知识的问题。

Q1:当你运行这段代码时会发生什么?

| 由于没有捕获到异常,将会出现致命错误 | | 当您试图抛出异常时,将会出现致命错误 | | 脚本运行时没有任何输出,因为您没有对异常使用getMessage() | | 以上都不是 |

<?php
class CustomException { }
throw new CustomException('Error!');

Q2:这个脚本会输出什么?

| 语法错误;它根本不会跑 | | 这取决于你使用的 PHP 版本 | | 致命的错误 | | one |

<?php
function addOne($arg) {
$arg++;
}

$a = 0;
addOne(&$a);
echo $a;

Q3:这个脚本的输出会是什么?

| 语法错误;它不会运行 | | True | | False | | 以上都不是 |

<?php
$a = function($a) {
return is_callable($a);
};
$b = function($b) use ($a) {
return $a($b);
};
echo $b($a) ? 'True' : 'False';

Q4:这个脚本的输出是什么?

| 句法误差 | | Zero | | one | | Two | | nine |

<?php
$a = 3;
echo $a >> 1;

q5:display_error配置设置的推荐生产设置为On

| True | | False |

q6:session_generate_id()函数用于创建一个会话标识符,当一个人登录时应该调用它来帮助减轻会话固定攻击。

| True | | False |

Q7:当你对一个对象调用json_encode函数进行序列化时,PHP 会调用哪个神奇的方法?

| __sleep | | __wake | | __get | | __clone | | 这些都不是 |

问题 8:这个脚本会输出什么?

| 语法错误;它根本不会跑 | | array | | object | | string | | 以上都不是 |

<?php
$emptyArray = [];
$encode = json_encode($emptyArray, JSON_FORCE_OBJECT);
$decode = json_decode($encode);
echo gettype($decode);

问题 9:以下脚本的输出是什么?

| 语法错误;这不会跑 | | 数组是[Equal][Identical] | | 数组是[Equal][Not Identical] | | 数组是[Not Equal][Not Identical] | | 数组是[Not Equal][Identical] | | 以上都不是 |

<?php
$arr1 = [1,2,3];
$arr2 = array("1" => 2, 0 => "1", 2 => 3 );
$equal = $arr1 == $arr2 ? 'Equal' : 'Not Equal';
$identical = $arr1 === $arr2 ? 'Identical' : 'Not Identical';
echo "The arrays are [$equal] and [$identical]";

Q10:这个函数的输出是什么?

| One thousand two hundred and thirty-five | | One thousand two hundred and thirty-four point five six eight | | 1.234,57 | | 以上都不是 |

<?php
$number = 1234.5678;
echo number_format($number, 2, ',', '.') . PHP_EOL;

Q11:在将字符串传递到数据库之前,应该使用类似addslashes()的函数对字符串进行转义,这样就不可能在您的网站上使用 SQL 注入攻击。

| True | | False |

Q12:这段代码的输出是什么?

| 是 | | 不 | | 语法错误;这不会跑 |

<?php
class A
{
    public $name = '0';
    private $surname = '0';
    public function __isset($property)
    {
        return true;
    }
}
$a = new A;
$empty = empty($a->name);
$set = isset($a->surname);
if ($empty === $set) {
  echo "Yes";
} else {
  echo "No";
}

Q13:如果你没有指定可见性修饰符,PHP 默认选择 private,这样你的代码是安全的。

| True | | False |

Q14:您可以使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 函数来确保变量适合显示并且不包含任何空格。

| ctype_alpha | | ctype_print | | ctype_graph | | filter_var |

Q15:这段代码会输出什么?

| 语法错误;它不会跑 | | 它永远不会结束运行 | | 它会产生一个错误,因为函数nest()在全局范围内不存在 | | hello world | | 以上都不是 |

<?php
function bird($message) {
    function nest($string) {
        echo $string;
    }
    nest($message);
}
bird('hello');
echo " ";
nest('world');

Q16:代码会输出什么?

| 将会出现错误 | | Zero | | one | | Two |

<?php
$a = 0;
$b = $a++;
$a = $a + 1;
echo --$a;

问题 17:您可以使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 功能来确保文件确实上传了,并且不是您操作系统上的不同文件。

| check_file_uploaded() | | finfo_file() | | is_uploaded_file() | | 以上都不是 |

Q18:这段 PHP 代码的输出是什么?

| 语法错误 | | a 未设置,B 已设置 | | a 未设置,B 未设置 | | a 被设置,B 被设置 | | 将产生一个警告并输出"A is not set and B is not set" | | 它将显示一个致命错误,说明变量未找到 |

<?php
echo (isset($a)) ? "A is set" : "A is not set";
echo " and ";
echo (empty($b)) ? "B is not set" : "B is set";

q19:PUTPOST都是幂等的。

| True | | FalsePOST是幂等的,但PUT不是 | | FalsePUT是幂等的,但POST不是 | | False;也不是幂等的 | | FalseREST是无状态的,所以没有幂等的 |

问题 20:您可以在定义类之前对其进行实例化,如下例所示:

| True | | False |

<?php
$foo = new ExampleClass();
echo $foo;
class ExampleClass {}

问题 21:如果您没有使用预准备语句,并且希望在使用 MySQL 数据库时对字符串进行转义,您可以使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 函数。

| mysql_real_escape_string() | | real_escape_string() | | mysqli_real_escape_string() | | addslashes() | | mysqli_escape_string() | | 以上都不是 |

问题 22:这个脚本会输出什么?

| 语法错误;这不会跑 | | foo | | bar | | Hello world |

<?php
$a = "foo";
$$a = "bar";
$a = "Hello world";
echo ${"foo"};

问题 23:考虑下面的代码,输出会是什么?

| 语法错误;这不会跑 | | True | | False |

<?php
$a = "0.0";
$b = (int)$a;
if ( (boolean)$a === (bool)$b) {
echo "True";
} else {
echo "False";
}

问题 24:这个脚本会输出什么?

| foo | | bar | | 以上都不是 |

echo "Apples"<=>"bananas" ? 'foo' : 'bar';

问题 25:这是一个棘手的问题,所以请仔细阅读并预测代码的输出。记住,md5()的第二个参数导致散列以原始二进制格式返回,而不是十六进制字符串。

| 语法错误;这不会跑 | | 16 | | 32 | | 这将导致一个错误,因为您不能定义一个与 PHP 函数名同名的函数 | | 以上都不是 |

<?php
namespace A;
function md5($value) {
return \md5($value . ' Extra saltiness');
}
echo strlen(md5('Hi', true));

问题 26:以下哪种类型的错误会阻止脚本执行?

| 通知;注意 | | 警告 | | 语法错误 | | 致命错误 |

问题 27:这个脚本的输出是什么?

| bool(true) | | bool(false) |

<?php
$a = true;
$b = false;
$truth = $a && $b;
$pravda = $a and $b;
var_dump($truth == $pravda);

Q28:如果你想让 PHP 显示除通知之外的所有错误,你会使用php.ini中的哪个设置?

| error_reporting= -E_NOTICE | | error_reporting=E_ALL - E_NOTICE | | error_reporting= ∼E_NOTICE | | error_reporting= E_ALL & ∼E_NOTICE |

问题 29:私有方法只能被定义它们的类访问,所以这段代码将输出一个空数组。

| True | | False |

<?php
class Mirror {
    private function showMeGorgeous($me) {
        echo $me;
    }
}
$refObj = new ReflectionClass('Mirror');
print_r($refObj->getMethods());

问题 30:假设您从命令行运行这个脚本,而不是在 web 浏览器中。输出会是什么?

| hello <strong>hello</strong> | | <strong>hello</strong> <strong>hello</strong> | | hello hello | | 以上都不是 |

<?php
class SetMissing {
    public function __set($name, $value) {
        $this->$name = filter_var($value, FILTER_SANITIZE_STRING);
    }
}
$obj = new SetMissing();
$obj->example = "<strong>hello</strong>";
echo $obj->example . PHP_EOL;
$obj->example = "<strong>hello</strong>";
echo $obj->example;

Q31:如果你实现了__sleep()函数,你需要确保它返回一个包含你想要序列化的实例变量的名字和值的关联数组。

| True | | False |

问题 32:这段代码的输出会是什么?

| Yes please! | | No thanks! | | Woof | | Purr | | 以上都不是 |

<?php
trait Dog {
public function makeNoise() {
echo "Woof";
}
  public function wantWalkies() {
echo "Yes please!";
}
}

trait Cat {
public function makeNoise() {
echo "Purr";
  }
public function wantWalkies() {
    echo "No thanks!";
}
}

class DomesticPet
{
use Dog, Cat {
Cat::makeNoise insteadof Dog;
Cat::wantWalkies as kittyWalk;
Dog::wantWalkies insteadof Cat;
}
}
$obj = new DomesticPet();
$obj->kittyWalk();

Q33:这段代码的输出是什么?

| 语法错误;这不会跑 | | 是 | | 不 | | 致命错误 |

<?php
class A
{
    public $name = '0';
    private $surname = '0';
    public function __isset($property)
    {
        return true;
    }
}
$a = new A;
$empty = empty($a->name);
$set = isset($a->surname);
if ($empty === $set) {
  echo "Yes";
} else {
  echo "No";
}

q34:PHP 键区分大小写吗?这个脚本的输出会是什么?

| 它们不区分大小写;这将输出2 | | 它们区分大小写;这将输出4 | | PHP 密钥转换成整数;这输出2 | | 以上都不是 |

<?php
$arr1 = ["A" => "apple", "B" => "banana"];
$arr2 = ["a" => "aardvark", "b" => "baboon"];
echo count($arr1 + $arr2);

问题 35:这个脚本会输出什么?

| Zero | | 1 | | 2 | | 语法错误;你不能在一行上赋值两个变量 | | 以上都不是 |

<?php
$a = "0";
$c = $b = empty($a);
$d = ++$b + $a;
echo $d;

问题 36:这段代码会输出什么?

| 5 | | 6 | | 5.79 | | 以上都不是 |

<?php
$a = 1.23;
$b = 4.56;
$c = (int)$a + (int)$b;
echo (double)$c;

问题 37:这个脚本会输出什么?

| one | | two | | three | | 以上都不是 |

<?php
$arr = ["one", "two", 1.5 => "three"];
echo $arr[1];

问题 38:接口只能指定公共方法,但是你的类可以按照你喜欢的方式实现它们。

| True | | False |

问题 39:通过使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 配置设置,您可以强制会话仅包含在 cookies 中。

| session.cookie_secure | | session.use_cookies | | session.use_trans_sid | | 以上都不是 |

Q40:这段代码的输出是什么?

| 印第安纳 | | Three hundred | | Nine hundred | | 986.962699801 | | four hundred | | One thousand six hundred |

<?php
define('PI', 3.14159625);
define('_PI', "3.1459625");
$radius = 10;
if (PI == _PI) {
    $area = (int)PI * $radius ** 2;
    echo $area;
} else {
    echo "Indiana";
}

Q41:这个脚本的输出是什么?

| 句法误差 | | Hello World! | | PHP 注意:使用未定义的常量HELLO | | 以上都不是 |

<?php
function HelloWorld() {
echo HELLO;

}
const HELLO = "Hello World!";
HelloWorld();

Q42:这段代码会输出什么?

| 语法错误 | | 1 | | 1.1 | | 以上都不是 |

<?php
function add(int $a, int $b): integer {
    return $a + $b;
}
echo add(5.7, -4.6);

Q43:这个脚本的输出是什么?

| 语法错误;这不会跑 | | undefined | | integer | | double | | boolean |

<?php
$a = 0b0010;
$b = 0b0001;
echo gettype($a & $b);

Q44:这段代码的输出会是什么?

| 空 | | 1NULL | | Hello world!NULL | | A syntax error |

<?php
$result = echo print("Hello world!");
var_dump($result);

Q45:您已经在命令行上调用了 PHP,并且想要访问您在 PHP 脚本中传递的参数。哪个超级全球将包含这些?

| $GLOBALS | | $_ARGV | | $_SERVER | | $_ARGUMENTS |

Q46:函数gc_collect_cycles()用于执行以下哪项操作?

| 没有这个功能 | | 将脚本暂停一定数量的处理器周期 | | 启动垃圾收集以释放内存 | | 刷新opcode缓存 |

Q47:这段代码会输出什么?

| 这会产生一个通知错误 | | 这会产生致命的错误 | | 捕捉到错误 | | 以上都不是 |

<?php
try {
    // generates a notice error (not caught)
    echo $thisVariableIsNotSet;
} catch (Error $e) {
    echo "Error caught";
}

Q48:这段代码会输出什么?

| Ron loves Justin | * | | Ron adores Justin |   | | $name $emotion Justin |   | | 以上都不是 |   |

<?php
$emotion = "loves";
$theBeeb = function($name) use ($emotion) {
    echo "$name $emotion Justin";
};
$emotion = "adores";
$theBeeb("Ron");

Q49:假设PlutoGrumpy类都在class.definitions.php文件中声明,这个文件和这个脚本在同一个目录下。运行这个脚本会发生什么?

| 该脚本将生成一个致命错误,因为您需要在类名后面加上括号 | | 当您第二次尝试包含该脚本时,该脚本将生成一个错误 | | 该文件将被包括一次,脚本不会输出任何东西 | | 脚本将失败,因为您不能对spl_autoload_register使用变量;只有字符串文字 | | 以上都不是 |

<?php
$a = function() {
include('class.definitions.php');
};
spl_autoload_register($a);
// Class Pluto is defined in class.definitions.php
$planet = new Pluto;
// Class Grumpy is defined in class.definitions.php
$dwarf = new Grumpy;

问题 50:以下哪些与请求相关的信息不能被客户轻易更改?

| 远程 IP 地址 | | 会话数据 | | Cookie 数据 | | 用户代理人 | | 所有这些都很容易被客户更改 |