发现从核心中删除了什么
在本节中,我们不仅要考虑 PHP 8 中已删除的函数和类,还要看看已删除的用法。然后,我们将看看由于 PHP 8 中的其他变化而仍然存在但不再有用的类方法和函数。了解哪些函数已被移除对于防止 PHP 8 迁移后可能出现的代码断裂极为重要。
让我们从检查 PHP 8 中删除的函数开始。
检查 PHP 8 中删除的函数
为了保持向后兼容性,迄今为止 PHP 语言中保留了许多函数。然而,维护这些函数耗费了核心语言开发的资源。此外,在大多数情况下,这些函数已经被更好的编程结构所取代。因此,随着越来越多的证据表明这些命令不再被使用,这些命令也就慢慢从语言中消失了。
PHP 核心团队偶尔会对基于 GitHub 的 PHP 代码库进行统计分析。这样,他们就能确定 PHP 核心中各种命令的使用频率。 |
下表总结了 PHP 8 中已删除的函数以及替代它们的函数:
移除的函数 | 建议的替换 |
---|---|
__autoload() |
spl_autoload_register() |
convert_cyr_string() |
mb_convert_encoding() |
create_function() |
function() {} fn () |
each() |
ArrayIterator |
ezmlm_hash() |
除了运行操作系统命令外,别无其他 |
fgetss() |
strip_tags(fgets()) |
get_magic_quotes_gpc() get_magic_quotes_runtime() |
无:魔术引号功能本身已从 PHP 中删除 |
hebrevc() |
hebrev() |
is_real() |
is_float() |
money_format() |
|
restore_include_path() |
ini_restore('include_path') |
在本节的剩余部分,我们将介绍几个更重要的移除函数,并就如何重构代码以实现相同的效果提出建议。让我们从检查 each()
开始。
使用 each()
each()
是在 PHP 4 中引入的,它是一种在数组中穿行的方法,每次迭代都会产生键/值对。each()
的语法和用法非常简单,面向过程式使用。我们将展示一个简短的代码示例来演示 each()
的用法,如下所示:
-
在本代码示例中,我们首先打开一个与数据文件的连接,该文件包含来自 GeoNames ( https://geonames.org ) 项目的城市数据,如下所示:
// /repo/ch08/php7_each.php $data_src = __DIR__ . '/../sample_data/cities15000_min.txt'; $fh = fopen($data_src, 'r'); $pattern = "%30s : %20s\n"; $target = 10000000; $data = [];
-
然后,我们使用
fgetcsv()
函数将一行数据提取到$line
,并将纬度和经度信息打包到$data
数组中。请注意,在下面的代码片段中,我们过滤掉了人口少于$target
的城市数据行(在本例中,人口少于 1000 万):while ($line = fgetcsv($fh, '', "\t")) { $popNum = $line[14] ?? 0; if ($popNum > $target) { $city = $line[1] ?? 'Unknown'; $data[$city] = $line[4]. ',' . $line[5]; } }
-
然后,我们关闭文件句柄,并按城市名称对数组进行排序。为了显示输出结果,我们使用
each()
遍历数组,生成键/值对,其中城市是键,经纬度是值。代码示例见以下代码段:fclose($fh); ksort($data); printf($pattern, 'City', 'Latitude/Longitude'); printf($pattern, '----', '--------------------'); while ([$city, $latLon] = each($data)) { $city = str_pad($city, 30, ' ', STR_PAD_LEFT); printf($pattern, $city, $latLon); }
以下是 PHP 7 中的输出:
root@php8_tips_php7 [ /repo/ch08 ]# php php7_each.php
City : Latitude/Longitude
---- : --------------------
Beijing : 39.9075,116.39723
Buenos Aires : -34.61315,-58.37723
Delhi : 28.65195,77.23149
Dhaka : 23.7104,90.40744
Guangzhou : 23.11667,113.25
Istanbul : 41.01384,28.94966
Karachi : 24.8608,67.0104
Mexico City : 19.42847,-99.12766
Moscow : 55.75222,37.61556
Mumbai : 19.07283,72.88261
Seoul : 37.566,126.9784
Shanghai : 31.22222,121.45806
Shenzhen : 22.54554,114.0683
São Paulo : -23.5475,-46.63611
Tianjin : 39.14222,117.17667
不过,由于 each()
已被删除,该代码示例在 PHP 8 中将无法运行。最好的做法是采用面向对象编程 (OOP) 方法:使用 ArrayIterator
代替 each()
。下一个代码示例产生的结果与之前的完全相同,但使用了对象类而不是过程函数:
-
我们没有使用
fopen()
,而是创建了一个SplFileObject
实例。你还会注意到,在下面的代码片段中,我们没有创建数组,而是创建了一个ArrayIterator
实例来保存最终数据:// /repo/ch08/php8_each_replacements.php $data_src = __DIR__ . '/../sample_data/cities15000_min.txt'; $fh = new SplFileObject($data_src, 'r'); $pattern = "%30s : %20s\n"; $target = 10000000; $data = new ArrayIterator();
-
然后,我们使用
fgetcsv()
方法在数据文件中循环检索一行,并使用offsetSet()
添加到迭代中,如下所示:while ($line = $fh->fgetcsv("\t")) { $popNum = $line[14] ?? 0; if ($popNum > $target) { $city = $line[1] ?? 'Unknown'; $data->offsetSet($city, $line[4]. ',' . $line[5]); } }
-
最后,我们按键排序,倒退到顶部,并在迭代仍有更多值时循环。我们使用
key()
和current()
方法来检索键/值对,如下所示:$data->ksort(); $data->rewind(); printf($pattern, 'City', 'Latitude/Longitude'); printf($pattern, '----', '--------------------'); while ($data->valid()) { $city = str_pad($data->key(), 30, ' ', STR_PAD_LEFT); printf($pattern, $city, $data->current()); $data->next(); }
本代码示例实际上适用于任何版本的 PHP,从 PHP 5.1 到 PHP 8,包括 PHP 8!输出结果与前面的 PHP 7 输出结果完全相同,这里不再重复。
现在让我们来看看 create_function()
。
使用 create_function()
在 PHP 5.3 之前,将函数赋值给变量的唯一方法是使用 create_function()
。从 PHP 5.3 开始,首选的方法是定义匿名函数。匿名函数虽然在技术上属于过程编程应用程序接口(API)的一部分,但实际上是 Closure
类的实例,因此也属于 OOP 的范畴。
如果您需要的功能可以浓缩成一个表达式,在 PHP 8 中您还可以选择使用 箭头函数。 |
在执行 create_function()
定义的函数时,PHP 在内部执行了 eval()
函数。然而,这种结构的结果是语法笨拙。匿名函数的性能相当,使用起来也更直观。
下面的示例演示了 create_function()
的用法。该示例的目的是扫描网络服务器访问日志,并按互联网协议 (IP) 地址对结果进行排序:
-
我们首先以微秒为单位记录启动时间。之后,我们将使用该值来确定性能。以下是您需要的代码:
// /repo/ch08/php7_create_function.php $start = microtime(TRUE);
-
接下来,使用
create_function()
定义一个回调函数,将每行开头的 IP 地址重组为每段三位数的统一段落。我们需要这样做才能执行正确的排序(稍后定义)。create_function()
的第一个参数是表示参数的字符串。第二个参数是要执行的实际代码。代码示例如下:$normalize = create_function( '&$line, $key', '$split = strpos($line, " ");' . '$ip = trim(substr($line, 0, $split));' . '$remainder = substr($line, $split);' . '$tmp = explode(".", $ip);' . 'if (count($tmp) === 4)' . ' $ip = vsprintf("%03d.%03d.%03d.%03d", $tmp);' . '$line = $ip . $remainder;' );
请注意字符串的大量使用。这种笨拙的语法很容易导致语法或逻辑错误,因为大多数代码编辑器都无法解释嵌入字符串中的命令。
-
接下来,我们定义一个与
usort()
一起使用的排序回调,如下所示:$sort_by_ip = create_function( '$line1, $line2', 'return $line1 <=> $line2;' );
-
然后,我们使用
file()
函数将访问日志的内容提取到一个数组中。我们还将$sorted
移到一个文件中,以保存已排序的访问日志条目。代码示例见以下代码段:$orig = __DIR__ . '/../sample_data/access.log'; $log = file($orig); $sorted = new SplFileObject(__DIR__ . '/access_sorted_by_ip.log', 'w');
-
然后,我们就可以使用
array_walk()
对 IP 地址进行归一化处理,并使用usort()
进行排序,如下所示:array_walk($log, $normalize); usort($log, $sort_by_ip);
-
最后,我们将排序后的条目写入备用日志文件,并显示开始和停止之间的时间差,如下所示:
foreach ($log as $line) $sorted->fwrite($line); $time = microtime(TRUE) - $start; echo "Time Diff: $time\n";
我们不会显示完整的备用访问日志,因为它太长了,不适合收录在书中。下面是从列表中间抽出的十几行,让你了解输出结果:
094.198.051.136 - - [15/Mar/2021:10:05:06 -0400] "GET /
courses HTTP/1.0" 200 21530
094.229.167.053 - - [21/Mar/2021:23:38:44 -0400]
"GET /wp-login.php HTTP/1.0" 200 34605
095.052.077.114 - - [10/Mar/2021:22:45:55 -0500]
"POST /career HTTP/1.0" 200 29002
095.103.103.223 - - [17/Mar/2021:15:48:39 -0400]
"GET /images/courses/php8_logo.png HTTP/1.0" 200 9280
095.154.221.094 - - [25/Mar/2021:11:43:52 -0400]
"POST / HTTP/1.0" 200 34546
095.154.221.094 - - [25/Mar/2021:11:43:52 -0400]
"POST / HTTP/1.0" 200 34691
095.163.152.003 - - [14/Mar/2021:16:09:05 -0400]
"GET /images/courses/mongodb_logo.png HTTP/1.0" 200 11084
095.163.255.032 - - [13/Apr/2021:15:09:40 -0400]
"GET /robots.txt HTTP/1.0" 200 78
095.163.255.036 - - [18/Apr/2021:01:06:33 -0400]
"GET /robots.txt HTTP/1.0" 200 78
在 PHP 8 中,为了完成同样的任务,我们要定义匿名函数,而不是使用 create_function()
。下面是改写后的示例代码在 PHP 8 中的显示效果:
-
与刚才描述的 PHP 7 代码示例一样,我们再次从记录开始时间开始。下面是实现这一目的所需的代码:
// /repo/ch08/php8_create_function.php $start = microtime(TRUE);
-
接下来,我们定义一个回调,将 IP 地址规范化为四块,每块三位数。我们使用了与上一个示例完全相同的逻辑;不过,这次我们以匿名函数的形式定义命令。这样就利用了代码编辑器的帮助,代码编辑器会将每一行都视为一条实际的 PHP 命令。代码示例如下:
$normalize = function (&$line, $key) { $split = strpos($line, ' '); $ip = trim(substr($line, 0, $split)); $remainder = substr($line, $split); $tmp = explode(".", $ip); if (count($tmp) === 4) $ip = vsprintf("%03d.%03d.%03d.%03d", $tmp); $line = $ip . $remainder; };
由于匿名函数中的每一行都被当作正常的 PHP 函数来处理,因此不容易出现错别字或语法错误。
-
同样,我们以箭头函数的形式定义排序回调,如下所示:
$sort_by_ip = fn ($line1, $line2) => $line1 <=> $line2;
代码示例的其余部分与前面描述的完全相同,此处不再显示。同样,输出结果也完全相同。运行时间也大致相同。
现在我们来看看 money_format()
。
使用 money_format()
在 PHP 4.3 中首次引入的 money_format()
函数旨在使用国际货币显示货币价值。如果您正在维护一个有金融交易的基于 PHP 的国际网站,在 PHP 8 更新后可能会受到这一变化的影响。
后者是在 PHP 5.3 中引入的,因此不会导致代码崩溃。让我们来看一个涉及 money_format()
的简单例子,以及如何重写它以便在 PHP 8 中运行,如下所示:
-
我们首先将一个金额赋值给
$amt
变量。然后,我们将货币区域设置为 en_US(美国),并使用money_format()
回传数值。我们使用%n
格式代码表示国内格式,然后使用%i
代码表示国际格式。在后一种情况下,将显示国际标准化组织(ISO)的货币代码(美元或 USD)。代码说明见以下代码段:// /repo/ch08/php7_money_format.php $amt = 1234567.89; setlocale(LC_MONETARY, 'en_US'); echo "Natl: " . money_format('%n', $amt) . "\n"; echo "Intl: " . money_format('%i', $amt) . "\n";
-
然后,我们将货币地区改为
de_DE
(德国),并以国内和国际格式回传相同的金额,如下所示:
setlocale(LC_MONETARY, 'de_DE');
echo "Natl: " . money_format('%n', $amt) . "\n";
echo "Intl: " . money_format('%i', $amt) . "\n";
下面是 PHP 7.1 的输出结果:
root@php8_tips_php7 [ /repo/ch08 ]# php php7_money_format.php
Natl: $1,234,567.89
Intl: USD 1,234,567.89
Natl: 1.234.567,89 EUR
Intl: 1.234.567,89 EUR
从输出结果中可以看出,money_format()
没有显示欧元符号,只显示了 ISO 代码 (EUR)。不过,它确实正确地格式化了金额,在 en_US 本地语言中使用逗号作为千位分隔符,使用句号作为小数分隔符,而在 de_DE 本地语言中则相反。
最佳做法是使用 NumberFormatter::formatCurrency()
代替 money_format()
。下面是根据 PHP 8 重写的示例。请注意,同样的示例也适用于 PHP 5.3 及以后的任何版本!下面我们继续:
-
首先,我们将金额赋值给
$amt
,然后创建一个NumberFormatter
实例。在创建该实例时,我们会提供一些参数来说明数字的地区和类型—本例中是货币。然后,我们使用formatCurrency()
方法生成该金额的国家表示法,如下代码片段所示:// /repo/ch08/php8_number_formatter_fmt_curr.php $amt = 1234567.89; $fmt = new NumberFormatter('en_US',NumberFormatter::CURRENCY ); echo "Natl: " . $fmt->formatCurrency($amt, 'USD') . "\n";
-
为了生成 ISO 货币代码(本例中为美元),我们需要使用
setSymbol()
方法。否则,默认情况下将生成$
货币符号,而不是美元 ISO 代码。然后,我们使用format()
方法渲染输出。请注意以下代码片段中美元后面的空格。这是为了防止 ISO 代码在回传时与数字发生冲突:$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL,'USD '); echo "Intl: " . $fmt->format($amt) . "\n";
-
然后,我们使用 de_DE 区域设置格式化相同的数量,如下所示:
$fmt = new NumberFormatter( 'de_DE', NumberFormatter::CURRENCY ); echo "Natl: " . $fmt->formatCurrency($amt, 'EUR') . "\n"; $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, 'EUR'); echo "Intl: " . $fmt->format($amt) . "\n";
这是 PHP 8 的输出:
root@php8_tips_php8 [ /repo/ch08 ]#
php php8_number_formatter_fmt_curr.php
Natl: $1,234,567.89
Intl: USD 1,234,567.89
Natl: 1.234.567,89 €
Intl: 1.234.567,89 EUR
从输出结果中可以看到,在 en_US 和 de_DE 两种本地语言中,逗号小数点的位置是相反的。您还可以看到,货币符号和 ISO 货币代码都已正确呈现。
现在您已经知道如何替换 money_format()
,让我们来看看 PHP 8 中删除的其他编程代码用法。
发现其他 PHP 8 使用变化
在 PHP 8 中,有许多程序代码用法上的变化需要注意。我们先来看看两种不再允许的类型转换。
移除类型转换
开发人员经常使用强制类型转换,以确保变量的数据类型适合特定用途。举例来说,在处理超文本标记语言(HTML)表单提交时,我们假设其中一个表单元素代表一个货币金额。对该数据元素进行消毒的快速而简单的方法是将其类型转换为浮点数据类型,如下所示:
$amount = (float) $_POST['amount'];
不过,有些开发人员更喜欢使用 real
或 double
,而不是类型转换为 float
。有趣的是,这三种方法产生的结果完全相同!在 PHP 8 中,删除了 real
类型转换。如果您的代码使用这种类型转换,最佳做法是将其改为 float
。
unset
类型转换也已删除。该类型转换的目的是取消变量的设置。在下面的代码片段中,$obj
的值变成了 NULL
:
$obj = new ArrayObject();
/* some code (not shown) */
$obj = (unset) $obj;
PHP 8 中的最佳做法是使用以下任一种:
$obj = NULL;
// or this:
unset($obj);
现在让我们来看看匿名函数。
从类方法生成匿名函数的变化
在 PHP 7.1 中,添加了一个新的 Closure::fromCallable()
方法,允许将一个类方法作为 Closure
实例(例如匿名函数)返回。此外,还引入了 ReflectionMethod::getClosure()
,它也能将类方法转换为匿名函数。
为了说明这一点,我们定义了一个类,它返回的 Closure
实例可以使用不同的算法执行散列。我们的步骤如下:
-
首先,我们定义一个类和一个公共
$class
属性,如下所示:// /repo/src/Services/HashGen.php namespace Services; use Closure; class HashGen { public $class = 'HashGen: ';
-
然后,我们定义了一个方法,它可以产生三个回调中的一个,每个回调的目的都是产生不同类型的哈希值,如下所示:
public function makeHash(string $type) { $method = 'hashTo' . ucfirst($type); if (method_exists($this, $method)) return Closure::fromCallable( [$this, $method]); else return Closure::fromCallable( [$this, 'doNothing']); } }
-
接下来,我们定义了三种不同的方法,每种方法都会产生不同形式的哈希值(未显示):hashToMd5()、hashToSha256() 和 doNothing()。
-
为了使用该类,需要设计一个调用程序,首先包含该类文件并创建一个实例,如下所示:
// /repo/ch08/php7_closure_from_callable.php require __DIR__ . '/../src/Services/HashGen.php'; use Services\HashGen; $hashGen = new HashGen();
-
然后执行回调,接着执行
var_dump()
,以查看 Closure 实例的信息,如下面的代码片段所示:$doMd5 = $hashGen->makeHash('md5'); $text = 'The quick brown fox jumped over the fence'; echo $doMd5($text) . "\n"; var_dump($doMd5);
-
为了结束这个示例,我们创建了一个匿名类并将其绑定到
Closure
实例,如下代码片段所示。从理论上讲,如果匿名类真的绑定到了$this
,输出显示应该以Anonymous
开始:$temp = new class() { public $class = 'Anonymous: '; }; $doMd5->bindTo($temp); echo $doMd5($text) . "\n"; var_dump($doMd5);
以下是在 PHP 8 中运行的代码示例的输出:
root@php8_tips_php8 [ /repo/ch08 ]#
php php7_closure_from_callable.php
HashGen: b335d9cb00b899bc6513ecdbb2187087
object(Closure)#2 (2) {
["this"]=> object(Services\HashGen)#1 (1) {
["class"]=> string(9) "HashGen: "
}
["parameter"]=> array(1) {
["$text"]=> string(10) "<required>"
}
}
PHP Warning: Cannot bind method Services\HashGen::hashToMd5()
to object of class class@anonymous in /repo/ch08/php7_closure_
from_callable.php on line 16
HashGen: b335d9cb00b899bc6513ecdbb2187087
object(Closure)#2 (2) {
["this"]=> object(Services\HashGen)#1 (1) {
["class"]=> string(9) "HashGen: "
}
["parameter"]=> array(1) {
["$text"]=> string(10) "<required>"
}
从输出中可以看到,Closure
忽略了绑定另一个类的尝试,并产生了预期输出。此外,Closure
还生成了一条警告(Warning)信息,提醒您进行了非法绑定尝试。
现在让我们看看注释处理的不同之处。
注释处理的差异
传统上,PHP 支持许多符号来表示注释。散列符号 () 就是其中之一。但是,由于引入了一种称为属性(Attributes)的新语言结构,紧随开头方括号(
[
)之后的散列符号不再允许用来表示注释。但散列符号后不紧跟开头方括号,仍可作为注释分隔符。
下面是一个在 PHP 7 及更早版本中有效,但在 PHP 8 中无效的简短示例:
// /repo/ch08/php7_hash_bracket_ comment.php
test = new class() {
# This works as a comment
public $works = 'OK';
#[ This does not work in PHP 8 as a comment]
public $worksPhp7 = 'OK';
};
var_dump($test);
在 PHP 7 中运行此示例时,输出结果与预期一致,如图所示:
root@php8_tips_php7 [ /repo/ch08 ]#
php php7_hash_bracket_comment.php
/repo/ch08/php7_hash_bracket_comment.php:10:
class class@anonymous#1 (2) {
public $works => string(2) "OK"
public $worksPhp7 => string(2) "OK"
}
然而,在 PHP 8 中的相同示例会出现致命的错误信息,如图所示:
root@php8_tips_php8 [ /repo/ch08 ]#
php php7_hash_bracket_comment.php
PHP Parse error: syntax error, unexpected identifier "does",expecting "]" in /repo/ch08/php7_hash_bracket_comment.php on
line 7
请注意,如果我们正确地编写了属性实例,这个示例在 PHP 8 中可能会意外成功。然而,由于使用的语法与注释的语法一致,代码失败了。
在了解了 PHP 8 中已删除的函数和用法之后,我们现在来看看核心弃用。