PHP8 快速脚本参考(三)
二十五、会话
一个会话提供了一种跨多个网页访问变量的方法。与 cookies 不同,会话数据存储在服务器上。
开始会话
使用session_start功能开始一个会话。该函数必须在任何输出发送到网页之前出现。
<?php session_start(); ?>
session_start函数在客户端的计算机上设置一个 cookie,包含一个用于将客户端与会话相关联的 id。如果客户端已经有一个正在进行的会话,该函数将恢复该会话,而不是开始一个新的会话。
会话数组
会话启动后,$_SESSION数组用于存储会话数据以及检索会话数据。例如,页面视图计数用以下代码存储。第一次查看页面时,session 元素被初始化为 1。
if(isset($_SESSION['views']))
$_SESSION['views'] += 1;
else
$_SESSION['views'] = 1;
现在,只要在页面顶部调用session_start,就可以从域中的任何页面上检索这个元素。
echo 'Views: ' . $_SESSION['views'];
删除会话
保证会话持续到用户离开网站。之后,垃圾收集器可以删除该会话。要手动删除会话变量,可使用unset功能。为了删除所有会话变量,有一个session_destroy函数。
unset($_SESSION['views']); // destroy session variable
session_destroy(); // destroy session
会话和 Cookies
会话和 cookies 都用于跨站点页面存储数据。通常,会话用于在单次访问期间临时存储用户数据。另一方面,Cookies 用于长期存储数据,例如,保存用户的登录状态。请记住,cookies 存储在客户端,而会话数据存储在服务器端。因此,Cookie 数据可能会被恶意用户篡改,因此不应在 cookie 中存储任何密码或其他敏感数据。
二十六、命名空间
命名空间提供了一种避免命名冲突并将命名空间成员分组到一个层次结构中的方法。任何代码都可以包含在一个名称空间中,但是只有四种代码结构受到影响:类、接口、函数和常量。
创建名称空间
不包含在名称空间中的构造属于全局名称空间。
// Global code/namespace
class MyClass {}
要将该构造分配给另一个名称空间,需要定义一个名称空间指令。namespace 指令下的任何代码构造都属于该命名空间。名称空间的一个常见命名约定是全部使用小写字母。
namespace my;
// Belongs to my namespace
class MyClass {}
包含命名空间代码的脚本文件必须在任何其他代码、标记或空白之前,在文件顶部声明命名空间。Declare 语句是一个例外,因为它们必须放在名称空间声明之前。
<?php
namespace my;
class MyClass {}
?>
<html><body></body></html>
嵌套命名空间
名称空间可以嵌套任意多级,以进一步定义名称空间层次结构。像 Windows 中的目录和文件一样,名称空间及其成员用反斜杠字符分隔。
namespace my\sub;
class MyClass {} // my\sub\MyClass
替代语法
或者,可以用其他编程语言中常用的括号语法来定义名称空间。正如常规语法一样,在名称空间之外不能存在任何文本或代码。
<?php
namespace my
{
class MyClass {}
?>
<html><body></body></html>
<?php }?>
可以在同一个文件中声明多个名称空间,尽管这被认为不是良好的编码实践。如果全局代码与命名空间代码结合使用,那么必须使用括号中的语法。然后,全局代码被封装在一个未命名的命名空间块中。
// Namespaced code
namespace my
{
const PI = 3.14;
}
// Global code
namespace
{
echo my\PI; // "3.14"
}
与其他 PHP 构造不同,同一个名称空间可以在多个文件中定义。这允许名称空间成员跨多个文件拆分。
引用命名空间
命名空间成员有三种引用方式:完全限定、限定和非限定。完全限定名总是可以使用的。它由全局前缀运算符(\)组成,后跟名称空间路径和成员。全局前缀运算符指示路径相对于全局名称空间。
namespace my
{
class MyClass {}
}
namespace other
{
// Fully qualified name
$obj = new \my\MyClass();
}
限定名包括命名空间路径,但不包括全局前缀运算符。因此,只有在层次结构中当前命名空间之下的命名空间中定义了所需成员时,才能使用它。
namespace my
{
class MyClass {}
}
namespace
{
// Qualified name
$obj = new my\MyClass();
}
成员名或非限定名只能在定义该成员的命名空间中使用。
namespace my
{
class MyClass {}
// Unqualified name
$obj = new MyClass();
}
非限定类名和接口名只能解析为当前命名空间。相反,如果当前名称空间中不存在未限定的函数或常量,它们将尝试解析为同名的任何全局函数或常量。
namespace
{
function myPrint() { echo 'global'; }
}
namespace my
{
// Falls back to global namespace
myPrint(); // "global"
}
或者,全局前缀运算符可用于显式引用全局成员。如果当前名称空间包含同名的函数,这将是必要的。
namespace my
{
function myPrint() { echo 'local'; }
\myPrint(); // "global"
myPrint(); // "local"
}
命名空间别名
别名缩短了限定名以提高源代码的可读性。类、接口和命名空间的名称可以缩短。别名是用一个use指令定义的,该指令必须放在文件最顶层范围的名称空间名称之后。
namespace my;
class MyClass {}
namespace foo;
use my\MyClass as MyAlias;
$obj = new MyAlias();
使用带括号的语法,任何use指令都放在最顶层作用域中的左花括号之后。
namespace foo;
{
use my\MyClass as MyAlias;
$obj = new MyAlias();
}
可以选择省略as子句,以当前名称导入成员。
namespace foo;
use \my\MyClass;
$obj = new MyClass();
不可能批量导入另一个命名空间的成员。但是,在同一个use语句中导入多个成员有一个语法上的捷径。
namespace foo;
use my\Class1 as C1, my\Class2 as C2;
PHP 7 进一步简化了这种语法,允许将use声明放在花括号中。
namespace foo;
use my\{ Class1 as C1, Class2 as C2 };
除了类、接口和名称空间,PHP 5.6 还扩展了use结构来支持函数和常量别名。这些分别用use函数和use const构造导入。
namespace my\space {
const C = 5;
function f() {}
}
namespace {
use const my\space\C;
use function my\space\f;
}
请记住,别名仅适用于定义它们的脚本文件。因此,导入的文件不会继承父文件的别名。
名称空间关键字
在全局代码中,namespace关键字可以用作计算当前名称空间的常量或空字符串。它可以用于显式引用当前的名称空间。
namespace my\name
{
function myPrint() { echo 'Hi'; }
}
namespace my
{
namespace\name\myPrint(); // "Hi"
name\myPrint(); // "Hi"
}
命名空间指南
随着 web 应用中组件数量的增长,名称冲突的可能性也在增加。解决这个问题的一个方法是在名称前加上组件的名称。但是,这会产生长名称,降低源代码的可读性。出于这个原因,PHP 5.3 引入了名称空间,允许开发人员将每个组件的代码分组到单独命名的容器中。
二十七、引用
引用是一个别名,允许两个不同的变量写入相同的值。可以对引用执行三种操作:按引用赋值、按引用传递和按引用返回。
通过引用分配
通过在要绑定的变量前放置一个&符号(&)来分配引用。
$x = 5;
$r = &$x; // r is a reference to x
$s =& $x; // alternative syntax
然后,该引用成为该变量的别名,可以完全像原始变量一样使用。
$r = 10; // assign value to $r/$x
echo $x; // "10"
通过引用传递
在 PHP 中,默认情况下函数参数通过值传递。这意味着传递了变量的本地副本;所以如果副本被更改,不会影响原始变量。
function myFunc($x) { $x .= ' World'; }
$x = 'Hello';
myFunc($x); // value of x is passed
echo $x; // "Hello"
要允许函数修改参数,它必须通过引用传递。这是通过在函数定义中的参数名称前添加一个&符号来实现的。
function myFunc(&$x) { $x .= ' World'; }
$x = 'Hello';
myFunc($x); // reference to x is passed
echo $x; // "Hello World"
默认情况下,对象变量也通过值传递。但是,实际传递的是指向对象数据的指针,而不是数据本身。因此,对对象成员的更改会影响原始对象,但用赋值运算符替换对象变量只会创建一个局部变量。
class MyClass { public $x = 1; }
function modifyVal($o)
{
$o->x = 5;
$o = new MyClass(); // new local object
}
$o = new MyClass();
modifyVal($o); // pointer to object is passed
echo $o->x; // "5"
相反,当对象变量通过引用传递时,不仅可以更改其属性,还可以替换整个对象,并将更改传播回原始对象变量。
class MyClass { public $x = 1; }
function modifyRef(&$o)
{
$o->x = 5;
$o = new MyClass(); // new object
}
$o = new MyClass();
modifyRef($o); // reference to object is passed
echo $o->x; // "1"
通过引用返回
通过引用返回函数,可以从函数中为变量赋值引用。返回引用的语法是在函数名前放置&符号。与通过引用传递相反,当调用函数绑定引用时,也使用&符号。
class MyClass
{
public $val = 10;
function &getVal()
{
return $this->val;
}
}
$obj = new MyClass();
$myVal = &$obj->getVal();
请记住,引用不应该仅仅出于性能原因而使用,因为 PHP 引擎会自行处理这种优化。仅当需要引用类型的行为时才使用引用。
二十八、高级变量
除了作为数据的容器之外,PHP 变量还有其他的特性,这将在本章中讨论。这些都是不常用的功能,但了解这些功能是有好处的。
卷曲语法
变量名可以用花括号括起来显式指定。这就是所谓的卷曲或复杂语法。为了说明这一点,即使变量出现在单词中间,下面的代码也会输出该变量。
$fruit = 'Apple';
echo "Two {$fruit}s"; // "Two Apples"
更重要的是,对于从表达式中形成变量名来说,花语法很有用。考虑下面的代码,它使用 curly 语法来构造三个变量的名称。
for ($i = 1; $i <= 3; $i++)
${'x'.$i} = $i;
echo "$x1 $x2 $x3"; // "1 2 3"
这里需要使用花语法,因为需要对表达式求值以形成有效的变量名。如果表达式只有一个变量,则不需要花括号。
for ($i = 'a'; $i <= 'c'; $i++)
$$i = $i;
echo "$a $b $c"; // "a b c"
这种语法在 PHP 中被称为变量变量。
变量变量名
可变变量是一个可以通过代码改变名称的变量。例如,考虑下面的常规变量。
$a = 'foo';
这个变量的值可以用作变量名,方法是在它前面加一个美元符号。
$$a = 'bar';
$a的值foo现在成为了$$a变量的另一个名字。
echo $foo; // "bar"
echo $$a; // "bar"
这种用法的一个例子是从数组中生成变量。
$arr = array('a' => 'Foo', 'b' => 'Bar');
foreach ($arr as $key => $value)
{
$$key = $value;
}
echo "$a $b"; // "Foo Bar"
可变函数名
通过在变量后放置括号,其值被计算为函数的名称。
function myPrint($s) { echo $s; }
$func = 'myPrint';
$func('Hello'); // "Hello"
这种行为不适用于内置语言结构,比如echo。
echo('Hello'); // "Hello"
$myecho = 'echo';
$myecho('Hello'); // error
可变类名
类似于变量函数名,类可以使用字符串变量来引用。这个功能是 PHP 5.3 中引入的。
class MyClass {}
$classname = 'MyClass';
$obj = new $classname();
通过字符串和字符串变量访问代码实体的机制也适用于类或实例的成员。
class MyClass
{
public $myProperty = 10;
}
$obj = new MyClass();
echo $obj->{'myProperty'}; // "10"
二十九、错误处理
错误是开发人员需要修复的代码中的错误。当 PHP 中出现错误时,默认行为是在浏览器中显示错误消息。此消息包括文件名、行号和错误描述,以帮助开发人员纠正问题。
虽然编译和解析错误通常很容易检测和修复,但运行时错误可能更难发现,因为它们可能只在某些情况下发生,并且原因超出了开发人员的控制。考虑下面的代码,它试图使用fopen函数打开一个文件进行读取。
$handle = fopen('myfile.txt', 'r');
它依赖于这样一个假设,即所请求的文件将一直存在。如果由于某种原因,文件不在那里或者不可访问,该函数将生成一个错误。
Warning: fopen(myfile.txt):
failed to open stream: No such file or directory in C:\xampp\htdocs\mypage.php on line 2
一旦检测到错误,就应该纠正它,即使它只发生在异常情况下。
纠正错误
有两种方法可以纠正这个错误。第一种方法是在尝试打开文件之前进行检查,以确保文件可以被读取。PHP 方便地为这个任务提供了is_readable函数,如果指定的文件存在并且可读,这个函数将返回true。
if (is_readable('myfile.txt'))
$handle = fopen('myfile.txt', 'r');
第二种方法是使用错误控制操作符(@)。当前置到一个表达式时,该运算符禁止显示可能由该表达式生成的错误信息。PHP 8 改变了这个操作符的行为,因此只有非致命的错误消息会被屏蔽。
$handle = @fopen('myfile.txt', 'r');
要确定文件是否成功打开,需要检查返回值。查看文档, 1 可以发现fopen在出错时返回false。
if ($handle === false)
{
echo 'File not found.';
}
如果不是这样,那么可以用fread函数读取文件的内容。该函数从第一个参数中给出的文件处理程序中读取第二个参数中指定的字节数。
else
{
// Read the content of the whole file
$content = fread($handle, filesize('myfile.txt'));
// Close the file handler
fclose($handle);
}
一旦不再需要文件处理程序,最好用fclose关闭它;虽然,PHP 会在脚本完成后自动关闭文件。
误差等级
PHP 提供了几个内置常量来描述不同的错误级别。表 29-1 包括一些更重要的。
表 29-1
误差等级
|名称
|
描述
|
| --- | --- |
| E_ERROR | 致命的运行时错误。执行停止。 |
| E_WARNING | 非致命运行时错误。 |
| E_NOTICE | 关于可能错误的运行时通知。 |
| E_USER_ERROR | 用户生成的致命错误。 |
| E_USER_WARNING | 用户生成的非致命警告。 |
| E_USER_NOTICE | 用户生成的通知。 |
| E_COMPILE_ERROR | 致命的编译时错误。 |
| E_PARSE | 编译时分析错误。 |
| E_STRICT | 建议更改以确保向前兼容。 |
| E_ALL | PHP 5.4 之前的所有错误,除了E_STRICT。 |
前三个级别代表 PHP 引擎生成的运行时错误。以下是触发这些错误的一些操作示例。
// E_NOTICE (<PHP8) – Use of undefined variable
$a = $x;
// E_WARNING – Missing file
$b = fopen('missing.txt', 'r');
// E_ERROR – Missing function
$c = missing();
PHP 8 将几个警告和通知重新分类为更严重的异常,这将在下一章讨论。例如,从 PHP 8 开始,使用未定义的变量会导致Error类型的异常。
错误处理环境
PHP 为设置错误处理环境提供了一些配置指令。error_reporting函数设置 PHP 通过内部错误处理程序报告哪些错误。错误级别常量具有位掩码值。这允许使用按位运算符对它们进行组合和相减,如下所示。
error_reporting(E_ALL | ~E_NOTICE); // all but E_NOTICE
错误报告级别也可以在php.ini中永久更改。从 PHP 8 开始,php.ini中的缺省值是E_ALL,所以所有的错误信息都会显示出来。这是开发过程中的一个很好的设置,可以通过在脚本的开头放置下面一行代码以编程方式进行设置。注意,为了向后兼容,可以添加E_STRICT,因为这个错误级别直到 PHP 5.4 才包含在E_ALL中。
// During development
error_reporting(E_ALL | E_STRICT);
当 web 应用上线时,原始的错误消息应该对用户隐藏起来。这是通过display_errors指令完成的。它确定内部错误处理程序是否将错误打印到网页上。默认值是打印它们,但是当网站运行时,最好隐藏任何潜在的原始错误消息。
// During production
ini_set('display_errors','0');
另一个与错误处理环境相关的指令是log_errors指令。它设置是否在服务器的错误日志中记录错误消息。默认情况下,该指令是禁用的,但是在开发过程中启用它来跟踪错误是一个好主意。
// During development
ini_set('log_errors','1');
ini_set功能设置配置选项的值。或者,这些选项都可以在php.ini配置文件中永久设置,而不是在脚本文件中。
自定义错误处理程序
内部错误处理程序可以用自定义错误处理程序重写。这是处理错误的首选方法,因为它允许您抽象原始错误,并向最终用户提供友好的自定义错误消息。
使用set_error_handler函数定义自定义错误处理程序。该函数接受两个参数:一个在出现错误时调用的回调函数,以及该函数处理的错误级别(可选)。
set_error_handler('myError', E_ALL | E_STRICT);
如果没有指定错误级别,错误处理器被设置为处理所有错误,包括E_STRICT。然而,用户定义的错误处理程序实际上只能处理运行时错误,并且只能处理除E_ERROR之外的运行时错误。请记住,对error_reporting设置的更改不会影响自定义错误处理程序,只会影响内部错误处理程序。
回调函数需要两个参数:错误级别和错误描述。可选参数包括文件名、行号和错误上下文,错误上下文是一个数组,包含触发错误的范围内的每个变量。
function myError($errlvl, $errdesc, $errfile, $errline)
{
switch($errlvl)
{
case E_USER_ERROR:
error_log("Error: $errdesc", 1, 'me@example.com');
require_once('my_error_page.php');
return true;
}
return false;
}
此示例函数处理级别为E_USER_ERROR的错误。出现这种错误时,会向指定的地址发送一封电子邮件,并显示一个自定义错误页面。通过从函数中返回其他错误的false,它们将由内部错误处理程序来处理。
引发错误
PHP 提供了用于引发错误的trigger_error函数。它有一个必需的参数,即错误消息,还有一个可选的参数,用于指定错误级别。误差等级必须是三个E_USER等级之一,默认等级为E_USER_NOTICE。
if( !isset($myVar) )
trigger_error('$myVar not set'); // E_USER_NOTICE
当您有一个定制的错误处理程序时,触发错误是很有用的,它允许您将定制错误和 PHP 引擎引发的错误的处理结合起来。
Footnotes 1www.php.net/manual/en/function.fopen.php
三十、异常处理
PHP 5 引入了异常,这是一种内置机制,用于在程序失败发生的上下文中处理程序失败。与通常需要由开发人员修复的错误不同,异常由脚本处理。它们代表了一种不规则的运行时情况,这种情况应该是有可能发生的,并且脚本应该能够自己处理。
Try-catch 语句
为了处理一个异常,必须使用一个try-catch语句来捕获它。该语句由一个包含可能导致异常的代码的try块和一个或多个catch子句组成。
try
{
$div = invert(0);
}
catch (LogicException $e) {}
如果try块成功执行,程序将在try-catch语句后继续运行。然而,如果出现异常,执行将传递给第一个能够处理该异常类型的catch块。
抛出异常
当出现函数无法恢复的情况时,它可以生成一个异常,通知调用者函数已经失败。这是通过使用关键字throw完成的,后跟一个新的类实例Throwable或者它的一个子类。在下面的例子中,抛出了LogicException类型。它继承自异常类,而异常类又继承自Throwable。1
function invert($x)
{
if ($x == 0) {
throw new LogicException('Division by zero');
}
return 1 / $x;
}
捕捉块
在前面的例子中,catch块被设置为处理内置的LogicException类型。如果try块中的代码可以抛出更多种类的异常,那么可以使用多个catch块,允许以不同的方式处理不同的异常。
catch (LogicException $e) {}
catch (RuntimeException $e) {}
// ...
为了捕捉更具体的异常,需要将catch块放在更一般的异常之前。比如LogicException继承自Exception,所以需要先抓。
catch (LogicException $e) {}
catch (Exception $e) {}
catch子句可以定义一个异常对象。这个对象用于获取关于异常的更多信息,比如使用getMessage方法对异常的描述。
catch (LogicException $e)
{
echo $e->getMessage(); // "Division by zero"
}
从 PHP 8 开始,如果不需要使用异常对象,可以选择省略该对象。
catch (LogicException) {}
最终阻止
PHP 5.5 引入了finally块,它可以作为try-catch语句的最后一个子句添加。该块用于清理try块中分配的资源。无论是否有异常,它总是会执行。
$resource = myopen();
try { myuse($resource); }
catch(Exception $e) {}
finally { myfree($resource); }
再次引发异常
有时一个异常不能在第一次被捕获的地方被处理。然后可以使用关键字throw后跟异常对象来重新抛出它。
try { $div = invert(0); }
catch (LogicException $e) { throw $e; }
然后,异常沿着调用方堆栈向上传播,直到被另一个try-catch语句捕获。如果异常从未被捕获,它将成为一个级别为E_ERROR的错误,这会暂停脚本,除非定义了一个未被捕获的异常处理程序。
PHP 8 把throw从语句变成了表达式。这使得在任何允许表达式的地方抛出异常成为可能,比如在下面的语句中。
$name = $_GET['name'] ?? throw new Exception('name missing');
未捕获的异常处理程序
set_exception_handler函数允许捕获任何未被捕获的异常。它采用单个参数,即针对此类事件引发的回调函数。
set_exception_handler('myException');
回调函数只需要一个参数,即抛出的异常对象。
function myException($e)
{
$file = 'exceptionlog.txt';
file_put_contents($file, $e->getMessage(), FILE_APPEND);
require_once('my_error_page.php');
exit;
}
因为这个异常处理程序是在发生异常的上下文之外调用的,所以从异常中恢复会很困难。相反,此示例处理程序将异常写入日志文件并显示错误页面。为了停止脚本的进一步执行,使用了内置的exit构造。它与die构造同义,并且可以选择接受一个字符串参数,该参数在脚本暂停之前打印。
错误和异常
尽管抛出异常是为了由脚本处理,但会生成错误来通知开发人员代码中有错误。当涉及到运行时出现的问题时,异常机制通常被认为是优越的。然而,由于它直到 PHP 5 才被引入,所以直到 PHP 8,所有内部函数都继续使用错误机制。从 PHP 8 开始,许多错误被重新归类为Error异常。例如,在 PHP 8 中被零除会抛出一个DivisionByZeroError异常,而在以前的 PHP 版本中它会触发一个警告。
try {
echo 1/0; // throws exception in PHP 8
}
catch(DivisionByZeroError $e){
echo $e->getMessage();
}
Throwable有两个内置子类,Exception 和Error。从 PHP 8 开始,大多数内部函数抛出Error类型的异常,比如TypeError、ValueError或DivisionByZeroError。
对于用户定义的函数,开发人员可以自由选择异常或错误处理机制,但更现代的异常机制是首选。请记住,错误不能被try-catch语句捕获。同样,异常不会触发错误处理程序。
www.php.net/manual/en/spl.exceptions.php
三十一、断言
Assert 是一个调试特性,可以在开发过程中使用,以确保条件始终为真。任何表达式都可以被断言,只要它的计算结果是true或false。
// Make sure $myVar is set
assert(isset($myVar));
像这样的代码断言有助于验证没有违反指定假设的执行路径。如果发生这种情况,就会显示一个错误(或 PHP 8 之前的警告),显示断言的文件和行号,这使得定位和修复代码中的错误变得很容易。
Fatal error: Assertion failed in C:\xampp\htdocs\mypage.php on line 3
可以包括断言的描述,如果断言失败,则显示该描述。PHP 5.4.8 中增加了对第二个参数的支持。
assert(isset($myVar), '$myVar not set');
从 PHP 7 开始,第二个参数也可以是在断言失败时抛出的异常对象。默认情况下,当断言失败时会抛出一个AssertionError。
assert(false, new AssertionError('Assert failed'));
资产绩效
通过将ASSERT_ACTIVE选项设置为零,可以使用assert_options函数关闭断言。这意味着在调试完成并且开发代码变成生产代码之后,不需要从代码中移除断言。
// Disable assertions
assert_options(ASSERT_ACTIVE, 0);
在 PHP 7 之前,传递给断言的条件总是被求值,即使断言被关闭。为了避免生产代码中的额外开销,条件可以作为字符串传递,然后由assert进行评估。
assert('isset($myVar)');
在 PHP 7 中,assert 变成了一种语言结构,而不是一种函数,允许在产品代码中包含断言而不会造成任何性能损失。在 PHP 7 中完全跳过断言的方法是在php.ini配置文件中将zend.assertions配置指令设置为-1。在 PHP 8 中不推荐使用带有字符串参数的 assert,也不应该再使用了。