PHP8-揭秘-全-

PHP8 揭秘(全)

原文:PHP 8 Revealed

协议:CC BY-NC-SA 4.0

一、JIT 编译器

日期:2019-01-28

作者:德米特里·斯托戈夫,泽夫·苏拉斯基

投票:50/2

我对当前 RFC 的一个担忧是缺乏一个很好的理由来说明为什么它是必要的;JIT 的理由是基于性能优势,但是提供的例子对我来说没有说服力,因为它们看起来太做作了。bench.php 和绘制分形都代表了 JIT 的一个最好的例子,JIT 是一个小程序,它只做大量的算术,不做其他事情。也许 PHP 能够用于这种软件会很酷,但它不会证明向 PHP 添加 JIT 所增加的复杂性(以及安全问题)是合理的,因为 C、C++、FORTRAN 等已经存在,并且更适合于它。

安德里亚·福尔兹

自亨利·福特和他的生产线时代以来,准时制(JIT)的概念在哲学上并没有太大的变化,但它的实现却发生了变化。JIT 从 20 世纪 60 年代开始使用,指的是程序执行后在程序中动态执行的任何翻译。13 年前,拉斯马斯·勒德尔夫满怀深情地写下了这篇关于将 JIT 引入 PHP 的文章。

这个问题每年会出现一两次。你编译的机器码最终看起来很像当前的执行器,因为你没有强类型来帮助你优化任何东西。您仍然需要传递联合,进行运行时类型杂耍,以及随之而来的所有开销。从第一天起,PHP 背后的理念就是它是一个包装编译代码的环境。对性能至关重要的东西用 C/C++编写,不重要的东西留在 PHP 模板中。无论您是从 PHP 还是从编译过的 C 程序发出 SQL 查询,都不会影响系统的整体性能,所以您还不如从 PHP 发出。如果你正在计算一个分形,你用 C 写它,用一个 get_fractal($args) 函数调用把它暴露给 PHP,这样你就可以标记它,并容易地改变传递给底层函数的参数。对于 PHP 来说,尽可能减少自身和速度关键代码之间的开销是非常重要的,而用户空间执行器的速度则不那么重要。这并不意味着它应该很慢。它应该尽可能快,但不能以方便为代价。

直到今天,这种观点仍然站得住脚,并且有很大的希望和承诺,PHP 8 中的 JIT 实现将增加而不是减少 PHP 的语言和目的。

自从 PHP 在 21 世纪初出现并统治 PERL 以来,PHP 中的优化一直处于语言的最前沿,我们今天仍然看到这种趋势。有明显的例子如三元表达式。

if ($pants) {
    $output = 'foo'
} else {
    $output = 'bar'
}

变成

$output = $pants ? 'foo' : 'bar';

关于 JIT 的讨论已经在 PHP 内部流传多年了。在 PHP 5 到 7 的更新之后,PHP 8 被认为是向世界展示这一新的 PHP 范例的完美版本。正如 Suraski 在征求意见稿(RFC)中所述,Dmitry Stogov 和 Zeev Suraski 自 2011 年以来一直致力于此。然而,进展总是受到阻碍。Suraski 指出了两个必须克服的主要障碍,以证明这一重大代码更新的合理性。

  1. 在标准网络应用中没有实质性的性能提升

    如果不是标准 web 应用的语言,PHP 是什么?PHP 是一切,从 WordPressand 和 Drupal 到 Laravel 和用 IONCube 编码的企业应用。PHP 是当今最有形的编程语言,几乎可以在任何 web 服务器上使用,只需很少的配置。如果没有实质性的性能提升,我们为什么会在这里?这可能是真的,但在某些地区速度明显加快。该团队使用 Mandelbrot 基准测试程序显示,与 PHP 7.4(0.046 秒)和 PHP 8 Beta(0.011 秒)相比,性能提高了 4 倍。尼基塔·波波夫(Nikita Popov)在 PHP 邮件列表中表示,尽管 JIT 的速度提高了约 1.3 倍,但现实生活中的应用(如 WordPress)的速度仅从每秒 315 个请求增加到每秒 326 个请求。这一收获可能不是惊天动地的,但它是一个开始。JIT 和 PHP 的真正优势在于数学计算,就像大数据和机器学习领域一样。将 JIT 添加到 PHP 中引入了一个新的途径,开发人员可以利用它,同时最小化对“标准 web 应用”的整体影响。Suraski 说“标准 web 应用没有实质性的性能提升”,实际上是说他们引入了新的计算能力和功能,整体上不会干扰 PHP。设法在机器码级别上更新 PHP 充其量是有风险的,最终可能是有害的。归根结底,任何转换 php ->操作码并直接在 CPU 而不是虚拟机(VM)上运行的特性都是朝着更优化的代码迈出的一大步。尽管大部分代码不是数学或 CPU 密集型的,但总体意见是,这是一个将在现在和未来提高性能的附加功能。

    Stogov 已经声明使用 JIT 为开发人员提供了一个独特的机会,可以进入 PHP 还不是主要参与者的领域。他设想 PHP 被用于非 Web、CPU 密集型的场景,在这些场景中,性能直接受所用语言的能力影响,RUST 或 Python 是首选。对 Stogov 和他的团队来说,PHP 8 是采取必要措施用 JIT 增强 PHP 的正确时机。为此,他们选择了为 LuaJit 创建的动态汇编程序(DynAsm)。DynAsm 目前支持 POSIX 平台和 Windows 上的 x86、x86 _ 64 CPUs 以及 ARM、ARM 64、MIPS、MIPS64 和 PPC。这使得 PHP 的 JIT 能够被当今部署 PHP 的最流行的平台所支持。

  2. 开发和维护的复杂性太高

    这个话题从 PHP 7.4 之前就开始来回争论了。Popov 提出的担忧表明,在 PHP 中包含 JIT 基本上会将用户群分成两个阵营:启用 JIT 的用户和未启用 JIT 的用户。用户会这样做主要是因为 JIT 组件可能会产生未知的错误,即使他们的应用不一定依赖于 JIT。启用 JIT 后,凡是 JIT 能做的事情,JIT 都会做。如果我们的用户群中只有一定比例的人启用了 JIT,那么专门为 JIT 编写的 PHP 代码呢?对于 PHP 团队来说,“它确实引入了一些危险。例如,如果我们决定将部分 C 代码转移到 PHP 中,因为 PHP 使用 JIT 已经足够快了,那么这当然只适用于真正支持 JIT 的平台。”Popov 说,“因为这个 JIT 是基于 DynAsm 的,所以核心的 JIT 代码本质上是用原始的 x86 汇编编写的。这将进一步限制能够做出重要引擎更改的人员。”PHP 团队将处理机器代码级别的新错误,不可否认,只有少数成员知道如何处理。Popov 接着说,“增加一个 JIT 编译器增加了一个新的稳定性问题。除了“简单的”执行程序错误和优化程序错误,我们现在还会在 JIT 中发现额外的错误。这些可能很难调试,因为我们将不得不从通常的 C 级工具下降到检查装配。”我们稍后将进一步研究这个问题。

JIT php.ini 设置

如前所述,用户将能够启用或禁用 JIT。像 PHP 的大多数其他添加一样,这是在php.ini中完成的。JIT 将有三个 INI 设置用于定制。前两个是opcache.jit_buffer_sizeopcache.jit,第三个是opcache.jit_debug,可选用于调试。

opcache.jit_buffer_size

此设置确定为本机代码生成保留的共享内存缓冲区的大小(以字节为单位;支持 k,M,后缀)。

opcache.jit_buffer_size=100M

opcache . JIT-快取记忆体、快取记忆体、快取记忆体、快取记忆体、快取记忆体、快取记忆体、快取记忆体、快取记忆体、快取记忆体、快取记忆体、快取记忆体、快取记忆体

该设置指定 JIT 控件选项。该值由四个十进制数字组成,CRTO。

c 决定特定于 CPU 的优化。

|

|

意义

|
| --- | --- |
| Zero | 没有任何优化 |
| one | 启用 AVX 指令生成 |

R 的值设置寄存器分配模式。

|

|

意义

|
| --- | --- |
| Zero | 从不执行寄存器分配 |
| one | 使用局部线性扫描寄存器分配 |
| Two | 使用全局线性扫描寄存器分配 |

T 的值决定了 JIT 触发器。

|

|

意义

|
| --- | --- |
| Zero | 在第一次脚本加载时 JIT 一切 |
| one | JIT 函数在执行时 |
| Two | 分析第一个请求并在第二个请求时编译热函数 |
| three | 始终剖析和编译热函数 |
| four | 在 doc 块中用@jit 编译函数 |

最后,O 的设置表示优化级别。

|

|

意义

|
| --- | --- |
| Zero | 从不 JIT |
| one | 最少的 JIT(使用常规的 VM 处理程序) |
| Two | 选择性虚拟机处理程序内联 |
| three | 基于个体函数静态类型推理的优化 JIT |
| four | 基于静态类型推理和调用树的优化 JIT |
| five | 基于静态类型推理和内部过程分析的优化 JIT |

以下是一些设置示例:

  • 1205 (JIT 一切)

  • 1235(基于相对使用的 JIT 热代码)

  • 1255(跟踪热代码的 JITability)。

opcache.jit_debug

该设置指定 JIT 调试控制选项,其中每个位启用一些调试选项。默认值为 0。

  • (1<<0):打印生成的汇编代码。

  • (1<<1):打印用于代码生成的中间静态单一分配(SSA)表单。

  • (1<<2):注册分配信息。

  • (1<<3):打印存根汇编代码。

  • (1<<4):生成perf.map文件,在 Linux 性能报告中列出 JITed 函数。

  • (1<<5):生成perf.dump文件,显示 Linux perf 报告中 JITed 函数的汇编代码。

  • (1<<6):提供关于 Linux Oprofile 的 JITed 代码的信息。

  • (1<<7):提供有关英特尔 VTune 的 JITed 代码的信息。

  • (1<<8):允许使用 GNU 调试器调试 JITed 代码(GDB)。

JIT 调试

Popov 对实现 JIT 的关注是新的未知错误的产生和团队(更不用说用户)处理这些的能力。他在 RFC 中承认了这一事实:“修复这些新类型的错误将会更加困难,因为我们必须抓住失败的地方,获取并分析为伪造函数生成的汇编代码,找到错误,并理解 JIT 编译器为什么要这么做。”GNU 项目调试器是调查涉及 JIT 的新错误的首选方法。波波夫在 RFC 中举了一个例子:

“在崩溃的情况下,我们可以只运行 gdb 下的 app,直到崩溃,检查 JIT 是否参与崩溃回溯并找到位置”:

$ gdb php
(gdb) r app.php
...
(gdb) bt

#1  0xe960dc11 in ?? ()
#2  0x08689524 in zend_execute (op_array=0xf4074460, return_value=0x0) at Zend/zend_vm_execute.h:69122
#3  0x085cb93b in zend_execute_scripts (type=8, retval=0x0, file_count=3) at Zend/zend.c:1639
#4  0x0855a890 in php_execute_script (primary_file=0xffffcbfc) at main/main.c:2607
#5  0x0868ba25 in do_cli (argc=2, argv=0x9035820) at sapi/cli/php_cli.c:992
#6  0x0868c65b in main (argc=2, argv=0x9035820) at sapi/cli/php_cli.c:1384

zend_execute()调用的未知函数??是 JITed 代码。我们可以通过分析执行上下文来确定故障位置。

(gdb) p (char*)executor_global.current_execute_data.func.op_array.filename.val
(gdb) p executor_global.current_execute_data.opline.lineno

行号可能不准确,因为 JIT 没有保持opline的一致性。我们可以反汇编伪指令周围的代码来理解真正的opline

(gdb) disassemble 0xe960dc00,0xe960dc30

此外,分析假 JITed 函数的字节码和汇编程序转储可能也很有用。

$ php --opcache.jit_debug=1 app.php
$ php --opcache.jit_debug=2 app.php

为了捕捉错误,我们可能需要跟踪 JIT 代码生成器(当它生成伪代码时),或者引导它生成一个断点(int3 x86 指令),然后跟踪生成的代码。

PHP JIT 可能使用 GDB API 向调试器提供有关生成代码的信息。然而,它只适用于相当小的脚本。在大量 JIT 代码的情况下,GDB 只是注册函数。如果我们能隔离伪代码,我们就能以更舒适的方式调试 JIT。

$ gdb php
(gdb) r -dopcache.jit_debug=0x100 test.php
...
(gdb) bt
#1  0xe960dc11 in JIT$foo () at test.php:2
#2  0x08689524 in zend_execute (op_array=0xf4074460, return_value=0x0) at Zend/zend_vm_execute.h:69122
#3  0x085cb93b in zend_execute_scripts (type=8, retval=0x0, file_count=3) at Zend/zend.c:1639
#4  0x0855a890 in php_execute_script (primary_file=0xffffcbfc) at main/main.c:2607
#5  0x0868ba25 in do_cli (argc=2, argv=0x9035820) at sapi/cli/php_cli.c:992
#6  0x0868c65b in main (argc=2, argv=0x9035820) at sapi/cli/php_cli.c:1384
(gdb) disassemble
...
(gdb) layout asm

这无疑给 PHP 调试增加了一层复杂性。尽管 PHP 可能通过了 lints 和语法检查,但问题可能出在通过 JIT 的汇编(ASM)中。调试代码和寻找解决方案已经成为学习曲线的一部分。接下来我们将看到 PHP 社区如何应对这个潜在的潘多拉魔盒问题。

二、联合类型 V2

日期:2019-09-02

作者:尼基塔·波波夫

表决结果:61 票赞成,5 票反对

虽然联合类型会导致更高的类型检查成本,但它们也提供了更强大的方法来帮助类型推断和提高性能。随着 opcache 的不断改进,我认为我们可以预期成本会降低,而收益会增加。

-粗枝

联合类型接受多种不同类型的值,而不是单一类型的值。PHP 已经支持两种特殊的联合类型:

  • 使用特殊的?Type语法键入或空。

  • 数组或可遍历的,使用特殊的可迭代类型。

在 PHP 的早期版本中,联合类型只能在 phpdoc 注释中定义,如 RFC:

class Number {
    /**
     * @var int|float $number
     */
    private $number;

    /**
     * @param int|float $number
     */
    public function setNumber($number) {
        $this->number = $number;
    }

    /**
     * @return int|float
     */
    public function getNumber() {
        return $this->number;
    }
}

union types 2.0 的主要目的是移除这些内联规范,并将功能引入 PHP 代码。波波夫在 RFC 中解释说:

语言中对联合类型的支持允许我们将更多的类型信息从 phpdoc 转移到函数签名中,这通常会带来以下好处:

  • 类型实际上是强制执行的,因此可以及早发现错误。

  • 因为它们是强制的,所以类型信息不太可能变得过时或错过边缘情况。

  • 在继承过程中检查类型,执行 Liskov 替换原则。

  • 通过反射可以获得类型。

  • 语法比 phpdoc 少了很多样板。

这个 RFC 去掉了 PHPDoc 注释中对@var@param@return的需求,并将这个功能返回给 PHP。这意味着 PHP 按照计算机编程标准(即 Liskov 替换原则[LSP])保持强制的类型继承。LSP 声明一个已定义超类的对象应该可以被它们子类的对象替换,而不会抛出错误。换句话说,你的子类的对象需要和你的超类的对象有相同的行为方式。将这些声明从 PHPDoc 注释中移出,可以在执行前进行编程检查和调试。我们将在以后继续看到这种趋势。

在这里我们可以看到如何使用它。

class Number {
    private int|float $number;

    public function setNumber(int|float $number): void {
        $this->number = $number;
    }

    public function getNumber(): int|float {
        return $this->number;
    }
}

类型处理

RFC 继续解释了对空类型、可空联合类型、假伪类型以及重复和冗余类型的处理

联合类型支持 PHP 当前支持的所有类型,下面列出了一些警告。

Void 和 Null 类型

void 类型永远不能是联合的一部分。因此,像T|void这样的类型在所有位置都是非法的,包括返回类型。这是因为 void 类型表明函数没有返回值,这就强制使用无参数的return;从函数返回。它从根本上与非 void 返回类型不兼容。

取而代之的可能是?T,它允许返回 T 或 null。null 类型作为联合的一部分受到支持,因此T1|T2|null可以用来创建一个可空的联合。现有的?T符号被认为是常见的T|null的简写。联合类型和?T可空类型符号不能混合。不支持写入?T1|T2, T1|?T2?(T1|T2),需要使用T1|T2|null来代替。波波夫确实说过,“尽管我对允许使用?(T1|T2)语法持开放态度,如果这被认为是可取的,”所以我们可能会在未来看到一些交叉。

尽管鼓励使用null而不是false作为错误或缺席返回值,但由于历史原因,许多内部函数(如strpos()返回int|false)继续使用false来代替,因此这个问题需要解决。如统计部分所示,内部函数的绝大多数联合返回类型包括false

虽然有可能不太准确地将其建模为int|bool,但这给人一种错误的印象,即该函数也可以返回一个true值,这使得这种类型信息对人类和静态分析人员都没有多大用处。出于这个原因,对false伪类型的支持包含在这个提议中。一个true伪类型不是提议的一部分,因为不存在其必要性的类似历史原因。false伪类型不能用作独立类型(包括可空的独立类型)。因此,falsefalse|null?false是不允许的。

冗余类型

为了捕捉联合类型声明中的一些简单错误,可以在不执行类加载的情况下检测到的冗余类型将导致编译时错误。这包括以下事实:

  • 每个名称解析类型只能出现一次。像int|string|INT这样的类型会导致错误。

  • 如果使用了bool,则false不能额外使用。

  • 如果使用了object,则class类型不能额外使用。

  • 如果使用iterable,则arrayTraversable不能另外使用。

以下是这些规则的一些例子。

function foo(): int|INT {} // Disallowed
function foo(): bool|false {} // Disallowed

use A as B;
function foo(): A|B {} // Disallowed ("use" is part of name resolution)

class_alias('X', 'Y');
function foo(): X|Y {} // Allowed (redundancy is only known at runtime)

这个 RFC 修改了 PHP 中类型的语法,不包括特殊的 void 类型。

type: simple_type
    | "?" simple_type
    | union_type
    ;

union_type: simple_type "|" simple_type
          | union_type "|" simple_type
          ;

simple_type: "false"          # only legal in unions
           | "null"           # only legal in unions
           | "bool"
           | "int"
           | "float"
           | "string"
           | "array"
           | "object"
           | "iterable"
           | "callable"       # not legal in property types
           | "self"
           | "parent"
           | namespaced_name
           ;

变化

联合类型遵循现有的差异规则:

  • 返回类型是协变的(子类型必须是子类型)。

  • 参数类型是逆变的(子类型必须是超类型)。

  • 属性类型是不变的(子类型必须是子类型和父类型)。

唯一的变化是联合类型与子类型的交互方式,增加了三条规则:

  • 如果对于每个U_i都存在一个V_j,使得U_iV_j的子类型,那么联合U_1|...|U_n就是V_1|...|V_m的子类型。

  • 可迭代类型被认为与array|Traversable相同(即子类型和超类型)。

  • false伪类型被认为是bool的子类型。

这里有一个例子:

class A {}
class B extends A {}

class Test {
    public A|B $prop;
}
class Test2 extends Test {
    public A $prop;
}

在这个例子中,union A|B实际上表示与 just A相同的类型,这种继承是合法的,尽管类型在语法上不相同。逻辑流程如下:

首先,AA|B的一个子类型,因为它是A的一个子类型。

第二,A|BA的子类型,因为AA的子类型,BA的子类型。

添加和移除联合类型

在返回位置删除联合类型和在参数位置添加联合类型是合法的:

class Test {
    public function param1(int $param) {}
    public function param2(int|float $param) {}

    public function return1(): int|float {}
    public function return2(): int {}
}

class Test2 extends Test {
    public function param1(int|float $param) {} // Allowed: Adding extra param type
    public function param2(int $param) {}       // FORBIDDEN: Removing param type

    public function return1(): int {}           // Allowed: Removing return type
    public function return2(): int|float {}     // FORBIDDEN: Adding extra return type
}

单个联合成员的差异

同样,可以在返回位置限制联合成员,或者在参数位置扩大联合成员:

class A {}
class B extends A {}

class Test {
    public function param1(B|string $param) {}
    public function param2(A|string $param) {}
    public function return1(): A|string {}
    public function return2(): B|string {}
}

class Test2 extends Test {
    public function param1(A|string $param) {} // Allowed: Widening union member B -> A
    public function param2(B|string $param) {} // FORBIDDEN: Restricting union member A -> B

    public function return1(): B|string {}     // Allowed: Restricting union member A -> B
    public function return2(): A|string {}     // FORBIDDEN: Widening union member B -> A
}

强制打字模式

strict_types未启用时,标量类型声明受到有限的隐式类型强制。这些在联合类型中是有问题的,因为输入应该转换成哪种类型并不总是显而易见的。例如,当将一个boolean传递给一个int|string参数时,0 和“”都是可行的强制候选值。

如果值的确切类型不是联合的一部分,则按以下优先顺序选择目标类型:

  1. int

  2. float

  3. string

  4. bool

如果该类型既存在于联合中,又可以根据 PHP 现有的类型检查语义将值强制为该类型,则选择该类型。否则尝试下一种类型。

作为一个例外,如果值是一个string并且intfloat都是联合的一部分,那么首选类型由现有的“数字字符串”语义决定。例如,对于42,我们选择int,而对于42.0,我们选择float

不属于上述首选项列表的类型不是隐式强制的合格目标。特别是,不会发生对nullfalse类型的隐式强制。

表 2-1 显示了不同输入类型的优先顺序,假设确切的类型不是联合的一部分。

表 2-1

偏好顺序

|

原始类型

|

首次尝试

|

第二次尝试

|

第三次尝试

|
| --- | --- | --- | --- |
| 弯曲件 | (同 Internationalorganizations)国际组织 | 浮动 | 线 |
| (同 Internationalorganizations)国际组织 | 漂浮物 | 线 | 弯曲件 |
| 漂浮物 | (同 Internationalorganizations)国际组织 | 线 | 弯曲件 |
| 线 | int/float | 弯曲件 |   |
| 目标 | 线 |   |   |

以下是一些例子:

// int|string
42    --> 42          // exact type
"42"  --> "42"        // exact type
new ObjectWithToString --> "Result of __toString()"
                      // object never compatible with int, fall back to string
42.0  --> 42          // float compatible with int
42.1  --> 42          // float compatible with int
1e100 --> "1.0E+100"  // float too large for int type, fall back to string
INF   --> "INF"       // float too large for int type, fall back to string
true  --> 1           // bool compatible with int
[]    --> TypeError   // array not compatible with int or string

// int|float|bool
"45"    --> 45        // int numeric string
"45.0"  --> 45.0      // float numeric string
"45X"   --> 45 + Notice: Non well formed numeric string
                      // int numeric string
""      --> false     // not numeric string, fall back to bool
"X"     --> true      // not numeric string, fall back to bool
[]      --> TypeError // array not compatible with int, float or bool

可供选择的事物

本提案使用的基于偏好的方法有两个主要替代方案。首先是指定联合类型总是使用严格类型,从而完全避免任何复杂的强制语义。除了这在语言中引入的不一致性之外,它还有两个主要的缺点。首先,从像float这样的类型到int|float实际上会减少有效输入的数量,这是非常不直观的。其次,它打破了联合类型的方差模型,因为我们不能再说floatint|float的子类型。

第二种选择是根据类型的顺序执行强制。这意味着int|stringstring|int是不同的类型,前者倾向于整数,后者倾向于字符串。根据精确类型匹配是否仍然优先,string类型将总是用于后一种情况。再一次,这是不直观的,并且对于变异所基于的分型关系有非常不清楚的含义。

属性类型和引用

对具有联合类型的类型化属性的引用遵循类型化属性 RFC 中概述的语义:如果类型化属性是引用集的一部分,则针对每个属性类型检查值。如果类型检查失败,则生成一个TypeError,参考值保持不变。

还有一个额外的警告:如果类型检查需要强制赋值,那么所有类型检查都可能成功,但会产生不同的强制值。由于引用只能有一个值,这种情况也导致了一个TypeError

已经考虑了与联合类型的交互,因为它会影响详细的引用语义。重复这里给出的例子:

class Test {
    public int|string $x;
    public float|string $y;
}
$test = new Test;
$r = "foobar";
$test->x =& $r;
$test->y =& $r;

// Reference set: { $r, $test->x, $test->y }
// Types: { mixed, int|string, float|string }

$r = 42; // TypeError

基本问题是,最终赋值(在执行类型强制之后)必须与属于引用集的所有类型兼容。然而,在这种情况下,属性Test::$x的强制值将是int(42),而属性Test::$y的强制值将是float(42.0)。因为这些值不相同,所以这被认为是非法的,并抛出一个TypeError

另一种方法是将该值强制转换为唯一的通用类型字符串,主要缺点是这与您从直接属性赋值中获得的值都不匹配。

反射

为了支持联合类型,添加了一个新类Reflection UnionType:

class ReflectionUnionType extends ReflectionType {
    /** @return ReflectionType[] */
    public function getTypes();

    /* Inherited from ReflectionType */
    /** @return bool */
    public function allowsNull();

    /* Inherited from ReflectionType */
    /** @return string */
    public function __toString();
}

getTypes()方法返回属于联合的一部分的ReflectionTypes的数组。这些类型可能以与原始类型声明不匹配的任意顺序返回。这些类型也可以进行等价转换。

例如,类型int|string可以返回顺序为["string", "int"]的类型。型号iterable|array|string可能会改成iterable|string或者Traversable|array|string。对反射 API 的唯一要求是最终表示的类型是等价的。

allowsNull()方法返回联合是否包含类型null

__toString()方法返回该类型的字符串表示,它在非命名空间上下文中构成该类型的有效代码表示。它不一定与原始代码中使用的相同。

出于向后兼容的原因,只包含null和一个其他类型(写成?TT|null,或者通过隐式参数为空性)的联合类型将改为使用ReflectionNamedType

以下是一些例子:

// This is one possible output, getTypes() and __toString() could
// also provide the types in the reverse order instead.
function test(): float|int {}
$rt = (new ReflectionFunction('test'))->getReturnType();
var_dump(get_class($rt));    // "ReflectionUnionType"
var_dump($rt->allowsNull()); // false
var_dump($rt->getTypes());   // [ReflectionType("int"), ReflectionType("float")]
var_dump((string) $rt);      // "int|float"

function test2(): float|int|null {}
$rt = (new ReflectionFunction('test2'))->getReturnType();
var_dump(get_class($rt));    // "ReflectionUnionType"
var_dump($rt->allowsNull()); // true
var_dump($rt->getTypes());   // [ReflectionType("int"), ReflectionType("float"),
                             //  ReflectionType("null")]
var_dump((string) $rt);      // "int|float|null"

function test3(): int|null {}
$rt = (new ReflectionFunction('test3'))->getReturnType();
var_dump(get_class($rt));    // "ReflectionNamedType"
var_dump($rt->allowsNull()); // true
var_dump($rt->getName());    // "int"
var_dump((string) $rt);      // "?int"

统计数据和结论

为了说明联合类型在野外的使用,我们分析了 PHPDoc 注释中的@param@return注释中联合类型的使用。

在排名前 2,000 的 composer 软件包中,有:

  • 25,000 个参数联合类型。

  • 14,000 个返回联合类型。

在内部函数的 PHP 存根中(目前这些还不完整,所以实际数字应该至少是两倍大)有 336 个联合返回类型,其中 312 个包含值false。这说明 unions 中的false伪类型对于表达许多现有内部函数的返回类型是必要的。

三、命名参数

日期:2013 年 9 月 6 日,大幅更新于 2020 年 5 月 5 日

作者:尼基塔·波波夫

表决:57 票/18 票

一般来说,命名参数确实改变了什么是好的 API,什么不是。像函数的布尔标志这样的东西被认为是糟糕的设计,因为我们没有命名的参数。如果我选择一些随机的 Python API,比如说subprocess.run() ...

subprocess.run(args, *, stdin=None, input=None, stdout=None,
stderr=None, capture_output=False, shell=False, cwd=None, timeout=None,
check=False, encoding=None, errors=None, text=None, env=None,
universal_newlines=None)

向 PHP 开发人员展示这一点,他们可能会告诉我这是可怕的 API 设计。当然,他们是错的。这是合理的 API 设计,只是在一种支持命名参数的语言中。

—Nikita Popov

PHP 8 中的命名参数允许根据参数名而不是位置将变量传递给函数。首先,这使得传递给函数的参数与顺序无关。这也使得参数的含义不言自明,并允许任意跳过默认值。正如 Popov 所说,这也是我们思考调用函数和方法的方式的改变。

参数不必是可选的,也能从命名语法中受益;它们只需要很难被人类书写(和阅读)解析!)的称呼。一个明显的例子是 PHP 的针/干草堆不一致,每个人都喜欢恨。如果所有的参数都被命名为, strpos(haystack: $content, needle: 'hello') 就可以了,我就不必查看手册来检查它们的顺序是否正确。一个有趣的例子是源自参数命名上下文的网络协议。

以 RabbitMQ 使用的消息协议 AMQP 为例。在 PHP 中,你可以像这样写一个无等待模式的队列: $channel->queue_declare('hello', false, true, false, false, true);

在 Python(或者 Ruby,或者 C#,或者 Elixir,或者……)中,你可以让它更具可读性,甚至在提供所有参数的时候,也不需要记住事情的顺序:

channel.queue_declare(queue='hello', durable=True, nowait=True, passive=False, exclusive=False, auto_delete=False)

这些名字也不是 Python 客户端随意选择的;它们列在协议规范中。

——rowan tommins

// Using positional arguments:
array_fill(0, 311, 42);
// Using named arguments:
array_fill(indexStart: 0, theBest: 311, theMeaning: 42);
or
array_fill(theBest: 311, theMeaning: 42 indexStart: 0,);

有序参数和命名参数也可以组合使用。

htmlspecialchars($string, double_encode: false);
// Same as
htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);

利益

跳过默认值

命名参数的一个好处是不必定义默认值,除非你想改变它;例如:

htmlspecialchars($string, default, default, false);
// vs
htmlspecialchars($string, double_encode: false);

这个例子的第一行不清楚什么值被设置为false。使用命名参数,我们现在看到false被分配给了double_encode

自我记录代码

自记录代码也是自解释的,除非你已经记住了array_slice()的参数,知道preserve_keys的第四个参数是未知的。

array_slice($array, $offset, $length, true);
// vs
array_slice($array, $offset, $length, preserve_keys: true);

对象初始化

命名参数的副产品是初始化对象时的一个好处。与一般方法相比,对象构造函数方法通常有许多参数,其中大多数也是默认的。现在,对象可以在不知道参数顺序的情况下被初始化。

new dogPants("test", null, null, false, true);
// becomes:
new dogPants("test", goodboi: true);

new dogPants($name, null, null, $isGoodBoi, $getsTreat);
// or was it?
new dogPants($name, null, null, $getsTreat, $isGoodBoi);
// becomes
new dogPants($name, goodBoi: $isGoodBoi, treat: $getsTreat);
// or
new dogPants($name, treat: $getsTreat, goodBoi: $isGoodBoi);
// and it no longer matters!

限制

当您查看命名参数时,它们附带的几个约束是有意义的。

命名参数和位置参数可以一起使用,但是命名参数必须在位置参数之后。

// Legal
getDog($pants, param: $dog);
// Compile-time error
getDog(param: $dog, $pants);

多次传递相同的参数会导致Error异常。

function getDog($param) { ... }

// Error: Named parameter $param overwrites previous argument
getDog(param: 1, param: 2);
// Error: Named parameter $param overwrites previous argument
getDog(1, param: 2);

变量函数和参数解包

可变函数,或者使用(...$args)语法的函数,将继续收集未知的命名参数到$args中。让我们快速回顾一下什么是变元函数。变元函数是期望一些变量的函数。通常一个接受多个变量的函数会将传入的数据强制放入一个数组或对象中。通常这是一个$data$userContent类型的变量。这些也将专门创建;相反,阵列的结构将是已知的和预期的。这意味着“额外的”或“丢失的”数据可能会破坏代码。基本上,这是在创造一种不灵活的结构化情况,这取决于使用情况。对于变量函数,预期的参数就是:预期的。该函数知道有一些数据即将到来,但不知道具体有多少。

<?php
function sum(...$numbers) {
    $acc = 0;
    foreach ($numbers as $n) {
        $acc += $n;
    }
    return $acc."\n";
}

print sum(1, 2, 3, 4);
print sum(1, 0, 9, 323, 2, 3, 4);
print sum(1, 3, 4);
----
10
342
8

在变量函数中使用命名参数时,未知的命名变量将跟在任何位置变量后面,并保持它们被传递的顺序。

function pants(...$args) { var_dump($args); }
pants(1, 2, 3, a: 'a', b: 'b');
// [1, 2, 3, "a" => "a", "b" => "b"]

pants(...$args)的解包也支持命名参数:

$params = ['start_index' => 0, 'num' => 100, 'value' => 50];
array_fill(...$params);

函数处理函数

统称为func_*,这些函数将透明地处理命名参数,将所有参数视为按位置传递,同时用默认值替换任何缺失的值。属性的工作原理是一样的。

function pants($a = 2, $b = 3, $c = 4) {
    var_dump(func_get_args());
}
pants(c: 311);
// specifying one value will result the same as:
pants(0, 1, 311);
// Which is:
// array(3) { [0] => 0, [1] => 1, [2] =>311 }

call_user_func( )call_user_func_array( )

内部函数支持命名函数,并遵守相同的限制,例如将位置参数放在命名参数之前。对call_user_func_array()的一个警告是增加了数组键的使用,作为字符串键,被解释为参数名。

_ _ 调用( )

两个神奇的方法,__call()__callStatic(),都有没有指定正确方法的签名;因此,使用变量来确定预期行为是不可能的。这些函数将未知的命名参数收集到$args数组中,以获得最大的兼容性。

参数名称在继承过程中更改

当前的 PHP 在继承的签名契约中不考虑参数名,签名是方法的函数。当只有位置用于价值确定时,这是有意义的。当然,使用命名参数会改变这种情况。现在我们已经声明了一个我们期望数据交换的特定顺序。在继承期间更改参数的名称可能会导致失败,违反 LSP。PHP 承认他们正在将命名参数改造成一种旧语言(PHP 就是那种旧语言),他们认为无条件地诊断参数名不匹配是不明智的。他们认为,这最好留给静态分析器和集成开发环境(ide)来完成。PHP 将在运行时默默接受参数名的更改,并在调用时抛出异常。

什么事?移动预期的数据点很可能会导致错误,甚至失败。然而,我们不会处理它,而是把它留给 ide 和运行时编译器去处理。RFC 继续澄清这一点:

这是一种实用的方法,它承认命名的参数与许多方法无关,重命名的参数在实践中通常不会成为问题。无法想象为什么像 offsetGet() 这样的方法会被命名参数调用,因此要求 offsetGet() 实现者使用相同的参数名没有任何好处。

如前所述,这种方法也被一些现有的语言使用,最著名的是 Python,它是使用命名参数最多的语言之一。这有力地证明了这种方法在实践中确实相当有效,尽管情况当然有些不同。

所以这是基于某种逻辑意义上的。对于很可能没有名称参数的方法和函数,没有必要更改 PHP 的核心函数,我们可以指向 Python 来说明它们也这样做。很公平。

四、重新分类引擎警告,或者我如何学会停止担心并记录错误

日期:2019-08-27

作者:尼基塔·波波夫

投票:投票是按照特定的方法和警告

尽管 JIT 可能是 PHP 8 中的头条新闻,但是对引擎警告进行重新分类还是值得注意的。警告一点也不特别性感或有趣。PHP 8 的这一增加引起了很多讨论,主要集中在记录错误上:错误是 bug 吗?我们应该用代码保存这些吗?我们需要强迫人们编写没有 bug 的代码吗?这件事很快就变得复杂了。从表面上看,这只是几个函数以及它们如何报告错误的提升和转移。担心的原因是,现在我们有可能让已经在生产中运行的代码用“不必要的”(那是另一个蠕虫的罐子)错误填满日志文件,在最坏的情况下导致 PHP 停止曾经工作的代码。当然,通过设计,我们可以关闭日志和错误报告,随心所欲地编码。但是代码呢?如果有错误,那就意味着有 bug。为什么你不能修复这个错误,这样你的代码就不会再“中断”了?PHP 的基础在于不太严格的开发风格。一些成员认为这是对 PHP 创造的文化的攻击。不管怎样,这正在发生,我们需要看一看。以下是三个有问题的发动机警告:

  • E_ERROR:不可恢复,将停止编码。

  • E_WARNING:非致命错误,代码将继续。

  • E_NOTICE:非致命,表示可能存在问题。

然而,这种情况的发生方式是,如果你的生活有赖于去哪里吃午饭,从议会交流变成了群聊辩论。一方面,我们有善意的波波夫,他正在游说发起一场运动来“清理”PHP 在错误日志中发出的警告和错误通知。进入苏拉斯基和抵抗组织,站在 PHP 发展的历史和基础理解上。Suraski 也是创建通知系统的主要部分。许多其他的声音也支持严格的语言和编程最佳标准。这些声音指出,这种变化将把一个普通的通知变成一个更严重的警告,通过将可能的错误与非关键的错误结合起来,把水搅浑。结局很值得一读,看看我们的结局如何。不过,首先让我们看看 RFC。

在 RFC 中,Popov 指出,“我们有许多旧的错误条件,由于历史原因,它们使用了不适当的严重级别。例如,访问一个未定义的变量,虽然是一个非常严重的编程错误,但只会产生一个通知。”他确实认识到,在 PHP 的新版本中,错误得到了更好的处理,PHP 中错误“指导原则”的现实是“我们没有任何关于此事的现有规则,下面是我在下面的重新分类中尝试遵循的一些通用指导原则。

|

消息

|

当前电平

|

建议水平

|
| --- | --- | --- |
| 尝试递增/递减非对象的属性“%s” | 警告 | 错误异常 |
| 试图修改非对象的属性“%s” | 警告 | 错误异常 |
| 试图分配非对象的属性“%s” | 警告 | 错误异常 |
| 从空值创建默认对象 | 警告 | 错误异常 |
| 基本原理:当在写入上下文中访问非对象的属性时,会产生这些错误。如果非对象为“真”,则生成警告并忽略操作,如果为“假”,则创建一个空的stdClass对象。虽然自动验证是数组语言的核心部分,但对象却不是这样,在非对象上创建属性几乎肯定是编程错误,而不是有意为之。 |
| 试图获取非对象的属性“%s” | 通知;注意 | 警告 |
| 未定义的属性:%s::\(%s | 通知;注意 | 警告 | | 基本原理:第一个警告是针对与上面相同的情况,但是是针对读上下文。这被归类为警告,因为它通常表明一个编程错误(在现代代码中,所有非神奇的属性往往是已知的和固定的)。然而,对象属性也可以是动态的(例如,对象形式的 JSON),在这种情况下,访问未定义的属性可能是不太严重的问题。一般来说,PHP 对“丢失”数据的读访问有些宽容。 | | 无法将元素添加到数组中,因为下一个元素已被占用 | 警告 | 错误异常 | | 基本原理:当试图推入已经使用了`PHP_INT_MAX`键的数组时,会出现这种错误情况。这种错误情况在巧尽心思构建的代码之外几乎不会发生,如果发生,则意味着数据丢失。因此,它被更改为异常。 | | 无法在非数组变量中取消设置偏移量 | 警告 | 错误异常 | | 不能将标量值用作数组 | 警告 | 错误异常 | | 试图访问类型为`%s`的值的数组偏移量 | 通知;注意 | 警告 | | 基本原理:这些诊断是在试图将标量用作数组时生成的。前两者发生在写上下文中,后者发生在读上下文中。后者是在 PHP 7.4 中作为一个通知引入的,其明确意图是提升 PHP 8.0 中的严重性。与对象的对称情况一致,写情况在这里被更严格地对待,因为它通常意味着数据丢失。 | | 只有数组和可遍历对象可以解包 | 警告 | TypeError exception | | 为`foreach()`提供的参数无效 | 警告 | TypeError exception | | 基本原理:这些是简单的类型错误,应该这样处理。 | | 非法偏移类型 | 警告 | TypeError exception | | isset 中的非法偏移量类型或为空 | 警告 | TypeError exception | | 未设置中的非法偏移类型 | 警告 | TypeError exception | | 基本原理:如果一个数组或对象被用作数组键,就会产生这些错误。这又是一个简单的类型错误。 | | `%s`重载元素的间接修改没有效果 | 通知;注意 | (通知) | | 重载属性`%s::\)%s的间接修改无效 | 通知;注意 | (通知) | | 基本原理:如果__get()offsetGet()返回一个非引用,就会出现这些通知,但是是在写上下文中使用的。因为我们对写上下文的检测现在有误报,所以在我们能够确定诊断始终是合法的之前,这些应该是值得注意的。 | | 无法将类%s的对象转换为 int/float/number | 通知;注意 | (通知) | | 基本原理:对象和标量之间的比较目前是通过将对象转换为适当的类型来实现的,这就是为什么像\(obj == 1`这样的比较目前也会引起注意,尽管它们不应该引起注意。在这一问题得到解决之前,通知的分类应保持不变。 | | 遇到非数字值 | 警告 | (警告) | | 遇到格式不正确的数值 | 通知;注意 | (通知) | | 基本原理:这两个警告的区别在于一个字符串是完全非数字的,还是有数字前缀。这是一个基于操作中涉及的特定字符串值的运行时问题,可能是用户控制的。因此,我们不提倡例外。 | | 将静态属性`%s::\)%s作为非静态属性访问 | 通知;注意 | (通知) | | 基本原理:这个通知在它所做的事情上有些混乱:它是在访问$obj->staticProp时抛出的,但实际上并不读取静态属性。相反,它将回退到使用名为staticProp的动态属性。在这方面有更多的不一致性,因为访问对象上受保护的静态属性将生成一个错误异常,即使它实际上不会访问该属性。我不知道该怎么办,但我倾向于不去管它。 | | 数组到字符串的转换 | 通知;注意 | 警告 | | 基本原理:这通常是一个错误(你得到的“数组”字符串是没有意义的),但在很多情况下也不是特别严重。既然现在支持[字符串转换异常](https://wiki.php.net/rfc/tostring_exceptions),我们也可以将其升级为错误异常,我通常对此持开放态度。 | | 资源ID#%d用作偏移量,转换为整数(%d) | 通知;注意 | 警告 | | 基本原理:原则上,这是一个有意义的操作,但也非常奇怪,应该用显式的整数强制转换来表明意图。 | | 出现字符串偏移转换 | 通知;注意 | 警告 | | 非法字符串偏移量'%s' | 警告 | (警告) | | 基本原理:前者在使用 null/bool/float 作为字符串偏移量时抛出,后者在字符串不是整数时抛出。这两者应该使用相同的严重性。 | | 未初始化的字符串偏移量:%d | 通知;注意 | 警告 | | 非法字符串偏移量:%d | 警告 | (警告) | | 基本原理:前者在读取越界字符串偏移量时使用,后者在写入越界负字符串偏移量时使用(对于正偏移量,字符串被扩展)。与未定义的索引/属性一致,我们在这里始终生成一个警告。 | | 不能将空字符串赋给字符串偏移量 | 警告 | 错误异常 | | 基本原理:该操作没有意义,表明存在某种逻辑错误。 | | 只有变量应该通过引用传递 | 通知;注意 | (通知) | | 只有变量引用应该通过引用返回 | 通知;注意 | (通知) | | 只有变量引用应该通过引用产生 | 通知;注意 | (通知) | | 只有变量应该通过引用来赋值 | 通知;注意 | (通知) | | 试图将引用设置为不可引用的值 | 通知;注意 | (通知) | | 无法通过解包可遍历对象来传递引用参数%s%s%s()%d`,而是传递值 | 警告 | (警告) |
| 基本原理:在需要引用的地方使用值目前有些不一致,根据具体情况,从编译器错误、错误异常、警告和通知都有可能。将非变量传递给引用参数通常是一个编程错误,因为无法修改传递的值,引用也无法达到其目的。然而,这由于可选的引用参数或可选的引用返回值而变得复杂。在这两种情况下,警告都可能是误报。这里不太清楚要做什么,所以我现在保留当前的分类。 |

  • 错误异常应该是指示编程错误的错误条件的基线。

  • 如果期望某个错误条件通常被有意抑制,特别是在遗留代码中,则不应该使用异常。

  • 如果错误条件是数据相关的,最好不要使用异常。

  • 对于已知误报的错误情况,应使用通知。

  • 避免从通知直接升级到错误异常。我只是针对未定义变量的情况提出这个建议,因为现在它被严重地错误分类了。

虽然这个列表看起来很全面,但是有三个错误条件在 PHP 团队中引起了争议:未定义的变量、未定义的数组索引和被零除。波波夫以此开头:“我认为是时候看看我们现有的引擎中的警告&通知,并思考它们当前的分类是否仍然合适。像‘未定义变量’这样的错误条件只会产生一个通知,这真的令人难以置信。”引发了这次交流。

是否会包含 $$foo等动态声明的变量?

——Lynn van der Berg

专门针对未定义的变量,我们处理它们的方式与 register_globals 关系不大。您可以在其他动态语言(例如 Perl)中找到这种行为,并允许特定的代码模式(在写入上下文中使用时,依赖于变量的自动创建,在读取上下文中使用时,依赖于预先知道的默认值)。不喜欢这种行为或者通常依赖于它的代码模式(例如 @$foo++ )是可以的,但是这是有意的,与任何历史原因都没有关系

-沙瓦尼亚语

这个论点对数组和对象有意义(出于这个原因,我不会将未定义的索引/属性提升为异常),但我不认为它对简单变量有任何意义。编写 @$counts[$key]++ 是一种懒惰的方式来计算值,避免 if (isset($counts[$key])) { $counts[$key]++; } else { $counts[$key] = 1; } 的难看的样板文件。但是 @$foo++ 无论是 $foo++ 还是 $foo = 1 都只是一种非常糟糕的写法。在可变变量之外,条件定义变量的概念没有太多意义

—Nikita Popov

我所要求的只是一个清晰的升级路径,这样我的代码就可以随着时间安全地迁移,而不是在下一个版本中崩溃。相比<?php vs <?,这将打破了很多,不容易修复。通过让它抛出我们可以轻松记录到特定文件中的反对意见,当我们有时间花在技术债务上时,我们可以收集并修复案例,等到警告变成错误异常时,我们可能已经修复了 90%以上的案例,使升级成为可能。

——Lynn van der Berg

我们经常使用 $a .= "xy" $a[] = "xy" 的模式,要求初始化会导致通常非常简单的函数中的 boiler-plate 代码,或者更糟糕的是将第一个代码写成 $a = "xy" $a = ["xy"] ,这两者都损害了对称性和可扩展性(如果要添加新的第一个条目,需要更改两个位置)。我们已经取消了 E_NOTICE ,因为这个模式对填充我们的日志太有用了。

静态分析器可以很好地捕捉到变量名中的简单输入错误,我们的 git 提交钩子中有一个简单的错误,它为我们做了很多工作。在一般情况下,它甚至做得更好,因为它还捕获了测试中不执行的罕见代码路径中的错别字。不,不要说“确保你有 100%的代码覆盖率”,那是一个神话。

总结:将 E_NOTICE 提升为 E_WARNINGS ,罚款,让他们中止代码执行:不要做,不要破坏我们的代码

——基督教裁缝

这个例子与数组无关。在许多代码模式中,依赖这种行为对于那些思想不那么严格的人来说非常有意义。例如: foreach (whatever) { if (sth) { @$whCount++; } }

是的,对于许多人来说, $whCount 没有显式初始化可能是痛苦的,但上面的代码是完全合法的,永远没有警告通知的代码。此外——这不是遗产——有很多人欣赏这种精确的行为,这种行为在过去 20 多年里被记录在案并按预期工作。

称之为“清理”是固执己见的,通过将这种观点强加给每个人来避免分歧对于那些有其他观点的人来说不是一个很好的解决方案。虽然在初始化之前不能使用变量的观点显然是一个有效的观点——仅仅是一个有效的观点——还有其他的观点。

-沙瓦锡

这很有可能是多年来给 PHP 带来坏名声的心态。

Suraski 然后把情况分解成一个开发者的权利问题。这是交流中最有说服力和最基本的 PHP 论点之一。

PHP 从未将这一观点视为公理化的要求(并不是因为register_globals)——相反,其意图是为未初始化的变量提供默认值——这是自该语言诞生以来一致的、有据可查的行为。这在某些情况下会有问题吗?绝对的。在其他情况下能有用吗?当然(这就是为什么它很常见)。很多人都依赖这种行为,并且喜欢它。那些不这样做的人(当然也有很多这样的人)总是有一个合理的解决方案,即启用 E_STRICT 并强制执行 E_STRICT 兼容的代码。我仍然认为拥有严格的模式(可以包含严格的类型、严格的操作、更严格的错误行为等等。)很有意义,对于许多喜欢更严格语言的人来说,这可能是一个更好的选择——但是我们根本没有办法改变这种语言最基本的行为之一,并强迫人们接受它——不仅因为它破坏了兼容性,还因为它破坏了许多人用来编写 PHP 代码的习惯。几十年前,Perl 以 'use strict;' 的形式为喜欢更严格的人提供了一个解决方案;JS 最近也做了类似的事情。这两者都没有产生任何分歧——这是一个简单、明智的解决方案,几乎没有任何缺点。

-沙瓦尼亚语

这是 PHP 的一个基本原则:让开发人员选择他们想要的编码方式。让代码的交付不受其他更严格语言的约束。在波波夫回来并试图达成一个解决方案之前,谈话又回到了针锋相对的状态。

不。破坏兼容性的事情就是兼容性破坏。不管它们是错误还是时尚,如果代码会崩溃,它就会崩溃。我们不能通过争论工作流和工具来改变这一点。我们的工作是决定是否以及如何实现这些突破。

—罗文·柯林斯

这不是破坏所有的东西——这是破坏本应被破坏的代码,但不知何故没有被破坏

—马修·布朗

我用许多不同的语言写过代码。这些语言中的许多语言(最著名的是标准 ML)迫使我思考数据到底是如何流经我的程序的。另一方面,PHP 不需要这么多的工作。这意味着它的开发人员通常也不会有很大的改进,这最终会损害该语言的声誉,因为以前的 PHP 开发人员发现他们的坏习惯不能很好地移植到其他语言。有了这个改变,我们可以让人们更难写出糟糕的代码,我认为这会让现有的 PHP 用户成为更好的开发者。

—马修·布朗

对我来说,“未定义变量”错误的问题是有争议的。因此,我决定在提案中把它分成一个单独的部分,与其他部分分开投票。我将提供抛出错误异常(如最初提议的那样)、抛出警告或保留现有通知的选择。阅读这个讨论令人失望,有些幻灭。我可以理解并欣赏对遗留代码的关注。但是看到使用未定义变量被认为是合法的编程风格,这让我很难过。

—Nikita Popov

真的很尴尬,任何人都会有这样的错觉,认为这种语言一直以来的行为方式,从一开始就是一致的和有据可查的,是一个每个人都同意的错误,只是在等待有人来修复它。

PHP 可以为那些发现它不足的人而改进,而不会伤害那些实际上对它目前的方式感到满意的人。我们应该开始寻找这样的解决方案,而不是每个人都试图“为自己的阵营赢得胜利”,将事情付诸表决,希望征服另一方。

-沙瓦尼亚语

很抱歉,但是如果你真的认为引起注意(或者警告,或者错误……)的事情不是 bug,那你就错了。这就是 bug 和通知/警告/错误等的定义。是该语言用来向开发人员报告这些错误的机制。如果做 X 已经产生了 20 年的通知,那么做 X 是错误的,是一个 bug,句号。如果语言本身不认为你所做的是错误的,为什么会有通知呢?那么通知的目的是什么呢?我真的不明白怎么会有人质疑这个。

—通过内部构件的埃吉尔立特

嘘!

对我和我所认识的每一个开发人员来说,通知和警告之间的唯一区别是错误的严重性。但是它们都被认为是错误——您在代码中犯下的需要修复的错误。我很确定这是现实世界中大多数开发者对待他们的方式。

不管怎样,如果你想要一种不太严格的语言,那种语言已经存在了:它是 PHP 的当前版本,你和其他喜欢它工作方式的人可以继续使用它。

与此同时,我认为大多数目前从事严肃 PHP 工作的人会喜欢更严格一些,我不认为让你的旧代码在全新版本的语言上运行是一个足够好的理由让这个特性不在 8.0 中出现。如果每一个潜在的 BC 中断都被邮件列表上的这三个人否决了,那么即使有主要版本又有什么意义呢?

—埃吉尔·李特

我反对这两项重新分类:

未定义偏移: %d —通知→警告

未定义指标: %d —通知→警告

根据经验,必须尽职尽责地初始化数组中的每一个键是很麻烦的。我理解在一些编码标准中强制执行的基本原理;但是那些特定的缺失索引是否应该被认为是意外的(因此值得警告)主要是编码风格的问题。

原则上,这是一个与使用未初始化变量类似的问题,正如在这个帖子中提到的,这是一些语言中完全接受的编码模式(这个问题不同于未声明的变量)。我说“原则上”,因为完全合理的编码风格可能会选择强制变量初始化,但不会强制数组键初始化。

——克劳德·帕奇

最讽刺评论奖颁给了这颗宝石:

我见过很多人担心 PHP 会抛出实际错误(而不是通知),因为他们试图使用一个不存在的变量/偏移量,当然也经常有人提议使用 declare 语句或类似的东西,以允许他们的“代码风格”运行时不出错。

因此,我对这种情况的建议是引入一个单一声明,一劳永逸地解决这个问题。

declare(sloppy=1);

这将抑制任何关于未定义变量、数组偏移量的错误,当遇到未定义常量时,将逆转“裸字”的变化,等等。见鬼,这种模式甚至可以重新实现 register_globals magic_quotes ,为什么不呢?

如果你想写潦草的代码,那完全是你的特权,但是请你承认它是什么,不要假装它是某种艰巨的任务,要么首先定义变量/偏移量;或者检查它们是否被定义;或者使用适当的方法访问它们,特别考虑到未定义的变量/偏移量(即 ?? ??=)。

—斯蒂芬·雷伊

*波波夫最终提出了一个解决方案,将两个主要的反对意见分开,并除以零,形成他们自己的投票。

我已经把未定义数组键的问题拆分成一个单独的部分,将单独投票。我大体上同意忽略未定义的数组键通知是一种合理的编码风格选择(虽然我非常不同意未定义的变量也是如此),尽管最终我认为最好用自定义错误处理程序(它可以检查错误是否源于您自己的代码库)来处理这个问题,而不是通过全面禁止通知。本着这种想法,我认为这是一个通知还是出于压制目的的警告并没有太大的关系。但是我也没有强烈地感觉到这个案例是一个警告,特别是 PHP 8 中的 error_reporting=E_ALL 默认。

—Nikita Popov

结果如下:

| 将未定义的变量严重性更改为? | | 错误异常:36 | 警告:18 | 请注意:10 | | 将未定义的数组索引严重性更改为? | | 警告:42 | 请注意:21 |   | | 将除以零严重性更改为? | | division byzero error exception:52 | 保持警告:8 |   |

对于所有的喧嚣,RFC 的通过给开发者留下了阴影。*

五、空安全运算符

日期:2020 年 6 月 2 日

作者:Ilija Tovilo

票数:56 票赞成、2 票反对

目前有几种检查空值的方法,但是我们仍然需要一种检查方法。为了做到彻底,我们必须创建一个嵌套的检查结构,这很繁琐,例如:

$pizza =  null;
 if ($session !== null) {
    $user = $session->user;
    if ($user !== null) {
        $order = $user->getOrder();
        if ($order !== null) {
            $pizza = $order->pizza;
        }
    }
}

PHP 8 为我们提供了一种更简单的检查空值的方法。同样的代码可以写成:

$pizza = $session?->user?->getOrder()?->pizza;

这与前面给出的链接方法是一样的,其中左侧被计算,当值为 null 时将停止。当值不为 null 时,使用->操作符的行为与预期的一样。Tovilo 在邮件列表中解释道,“在我看来,你应该只在你期望空值的地方使用?->。使用??将忽略任何子表达式中的空值(以及未定义的值),即使是意外的。”

短路

PHP 8 给了开发人员一个选项,让他们根据特定条件“短路”或跳过,就像&&||操作符一样。PHP 8 提供了“完全短路”,如果链中一个元素的计算失败,剩余链的执行将被中止,整个链的计算结果为 null。在下面的代码中,函数pants()和方法pizza()都没有被调用。不会出现“空时调用成员函数”错误。

null?->$dogs(pants())->pizza();
  $foo = $a?->b();
// --------------- chain 1
//        -------- chain 2
// If $a is null then chain 2 is not evaluated, method b() is not called, $foo returns null

   $a?->b($c->d());
// --------------- chain 1
//        -------  chain 2
// If $a is null, then chain 1 is stopped, method b() is not called,`$c->d()` is not evaluated

   $a->b($c?->d());
// --------------- chain 1
//       --------  chain 2
// If $c is null, then chain 2 halts, method d() is not called,`$a->b()` is set to null

以下元素被视为链的一部分:

  • 数组访问([])

  • 属性访问(->)

  • Nullsafe 属性访问(?->)

  • 静态属性访问(::)

  • 方法调用(->)

  • Nullsafe 方法调用(?->)

  • 静态方法调用(::)

因为我们可以在链上创建链,这些元素将启动一个新的逻辑链:

  • 函数调用中的参数

  • 数组访问的[]中的表达式

  • 访问属性(->{})时{}中的表达式

好处

使用 nullsafe 运算符和短路的好处可以从几个方面来看:

  1. 避免意外

    $pants = null;
    $pants?->dogs(getMeasurements());
    
    

    如果$pants为空,调用getMeasurements()是不可取的,因为它的结果将被浪费,同时可能引发其他系统和方法,导致意想不到的后果。假设在调用getMeasurements()时,我们初始化了几个分配给内存的数组或对象,减少了我们的计算资源。与此同时,这个函数构建了数百行数据库(DB)信息,比如说,准备订购一条狗裤。这是一种巨大的资源浪费,并且不容易被开发人员发现,除非用批判的眼光来看待数据库的大小。

  2. 清除零回波的光学元件

    $pants = null;
    $pants?->dog()->breed();
    
    

    如果没有短路,必须在链的每个方面进行检查,以测试哪里返回空值。短路允许我们很容易地看到哪个属性或方法导致了空值。

  3. 与其他操作员混合

    $pants = null;
    $poodlePants = $pants?->dog()[‘poodle’];
    var_dump($poodlePants);
    
    // Without short circuiting:
    // Notice: Trying to access array offset on value of type null
    // NULL
    
    // With short circuiting
    // NULL
    
    

因为通过短路,数组访问[‘poodle’]将被完全跳过,所以不会发出通知。

禁止使用

笔迹

有关于在“写作”的上下文中使用 nullsafe 操作符的讨论,但是由于技术上的困难,这被排除在 PHP 8 之外。以下例子是不允许的。

$pants?->dog->breed = 'poodle';
// Can't use nullsafe operator in write context

foreach ([1, 2, 3] as $pants?->dog->breed) {}
// Can't use nullsafe operator in write context

unset($pants?->dog->breed);
// Can't use nullsafe operator in write context

[$pants?->dog->breed] = 'poodle';
// Assignments can only happen to writable values

参考

不允许在 nullsafe 链中分配引用。引用需要 l 值(像变量或属性这样的内存位置),nullsafe 操作符能够返回 r 值(比如 null)。这是不可能的。

// 1
$x = &$pants?->dog;
// Compiler error: Cannot take reference of a nullsafe chain

// 2
takes_ref($pants?->dog);
// Error: Cannot pass parameter 1 by reference

// 3
function &return_by_ref($pants) {
    return $pants?->dpg;
    // Compiler error: Cannot take reference of a nullsafe chain
}

六、属性 V2

日期:2020 年 3 月 9 日

作者:本杰明·埃伯雷,马丁·施罗德

票数:41 票/ 12 票

月复一月的来来回回已经进入了 PHP 的这个附加部分。主要的症结不在于它是否应该实现,也不在于如何处理某些边缘情况。引发所有争论的问题是用户界面(UI);比如输入<<>>或者@:(作为备选方法)的用户体验。总结 PHP 邮件列表中的大部分对话,引用 Mike Schinkel 的话:

看到这一幕,我非常激动。我在 PhpDoc 和使用类常量中大量使用伪属性,拥有真正的属性将是相当大的好处。

我确实有些担心。

  1. 语法让我眼睛出血

现在,让我们深入 RFC。

PHP 中的属性由<< >>@:定义,可以应用于该语言的许多元素:

  • 功能

  • 类、接口和特征

  • 课程内容

  • 类别属性

  • 类方法

  • 函数和方法参数

<<ExampleAttribute>>
class Pants
{
    <<ExampleAttribute>>
    public const PANTS = 'pants';

    <<ExampleAttribute>>
    public $pants;

    <<ExampleAttribute>>
    public function pants(<<ExampleAttribute>> $dog) { }
}
$object = new <<ExampleAttribute>> class () { };
<<ExampleAttribute>>
function pants1() { }
$pants2 = <<ExampleAttribute>> function () { };
$pants3 = <<ExampleAttribute>> fn () => 1;

属性明显优于 docblock 注释,因此它们值得作为一种新的语言结构引入,原因如下:

  • 命名空间防止了使用相同文档注释标签的不同库之间的冲突。

  • 与不可预测的 strstr 性能相比,检查属性存在是一个 O(1)散列键测试,甚至是解析文档块。

  • 将属性映射到类确保了属性的正确类型,减少了运行时依赖于文档块的主要错误来源。

  • 基于注释在许多不同工具和社区中的普遍使用,对注释之类的东西有明显的需求。但是,这对于新人来说,在评论中看到的永远是一件令人困惑的事情。另外,/*/* *的区别还是一个非常细微的 bug 来源。

虽然可以让 PHP 解析现有的文档注释并将信息作为结构化属性保存,但是我们需要为每个文档注释调用一个额外的解析器。文档注释可能不符合上下文语法,我们必须决定如何处理语法错误。最后,这是 PHP 内部的另一种语言。这种解决方案比引入属性要复杂得多,因此并不理想。

有了 RFC 提出的属性,我们可以重用表达式和常量表达式的现有语法。该功能的核心补丁很小。

类似于 docblock 注释,可以在附加属性的声明之前添加属性。也可以在 docblock 注释之前或之后添加属性。每个声明(函数、类、方法、属性、参数或类常量)可以有一个或多个属性。每个属性可能有也可能没有附加值,类似于类构造函数。

因为语法是目前为止关于这个 RFC 讨论最多的一点,我们还考虑了一个替代方法,为属性(T_ATTRIBUTE)引入一个新的标记,定义为解析器可以寻找的@:

@:WithoutArgument
@:SingleArgument(0)
@:FewArguments('Hello', 'World')
function foo() {}

属性名解析为类

为了命名空间并避免属性的意外重用,在编译期间,属性名称将根据所有当前导入的符号进行解析。

use My\Attributes\thePants;
use My\Attributes\otherPants;

<<thePants("Hello")>>
<<Another\thePants("World")>>
<<\My\Attributes\otherPants("foo", "bar")>>
function foo() {}

为此示例声明一个属性类,如下所示:

namespace My\Attributes;
use PhpAttribute;
<<PhpAttribute>>
class thePants
{
    public $value;
    public function __construct(string $value)
    {
        $this->value = $value;
    }
}

使用属性类的好处可能感觉有些过头,但是它们是切实可见的。

  • 反射 API 可以转换属性。

  • 通过静态分析工具进行属性验证。

  • 通过 ide 的自动完成支持。

属性提取

反射 API 主要用于调试,用于检索属性并存储它们以供使用。除非请求属性的实例,否则不会对任何代码执行或调用构造函数。属性作为属性名和可选参数(如果可用)的字符串返回。关于反射 API 及其用途的更多信息可以在 https://www.php.net/manual/en/book.reflection.php 找到。

编译器和用户域属性

PHP 中使用了两种不同类型的属性。恰如其名,编译器和用户域属性用在它们各自的领域。编译器属性是与PhpCompilerAttribute一起使用的内部类,用户域属性与PhpAttribute一起使用。这两者都在 PHP 的 Zend 内部。如果您从未涉足 php-src 或 Zend 函数领域,那也没关系。Zend 函数和 php-src 并不常见,但对于调试和 xdebug 之类的应用很有用。

属性用例

RFC 给出了一些可能的属性用例的例子。

  1. <<jit>>

    正如我们前面提到的,JIT 附带了某些规则和设置。一个这样的设置是 Opcache JIT 中现有的对@jit的检查,它指示 JIT 总是优化一个函数或方法。

    PHP 核心或扩展想要检查某些声明是否有属性。

    一个这样的例子是 Opcache JIT 中现有的对@jit的检查,它指示 JIT 总是优化一个函数或方法。可以创建一个扩展来利用哈希表中关联的内置*op_array值来检查属性中 JIT 的手动声明。

    static int zend_needs_manual_jit(const zend_op_array *op_array)
       return op_array->attributes &&
            zend_hash_str_exists(op_array->attributes, "opcache\\jit", sizeof("opcache\\jit")-1));
    }
    
    

    开发人员可以使用属性来代替文档注释:

    use Opcache\Jit;
    <<Jit>>
    function foo() {}
    
    
  2. 结构化折旧

    目前使用属性的大多数语言已经能够在代码库中弃用类、属性或常量。这对于遗留或企业代码的维护者来说可能是有用的。

    // an idea, not part of the RFC
    use Php\Attributes\Deprecated;
    <<Deprecated("Use bar() instead")>>
    function foo() {}
    ------ or
    class Foo
    {
        <<Deprecated()>>
        const BAR = 'BAR';
    }
    echo Foo::BAR;
    // PHP Deprecated:  Constant Foo::BAR is deprecated in test.php on line 7
    
    
  3. 在对象上声明事件侦听器挂钩

    在 Symfony 中,有一个名为EventSubscribers的方法,要求用户声明哪个事件由getSubscribedEvents()方法中的类上的哪个方法处理。用户可以重构代码,使用属性来标记方法。

    // current code without attributes
    class RequestSubscriber implements EventSubscriberInterface
    {
        public static function getSubscribedEvents(): array
        {
            return [RequestEvent::class => 'onKernelRequest'];
        }
    
        public function onKernelRequest(RequestEvent $event)
        {
        }
    }
    
    // refactor to:
    <<PhpAttribute>>
    class Listener
    {
        public $event;
    
        public function __construct(string $event)
        {
            $this->event = $event;
        }
    }
    
    class RequestSubscriber
    {
        <<Listener(RequestEvent::class)>>
        public function onKernelRequest(RequestEvent $event)
        {
        }
    }
    
    // and the EventDispatcher to register listeners based on attributes:
    
    class EventDispatcher
    {
        private $listeners = [];
    
        public function addSubscriber(object $subscriber)
        {
            $reflection = new ReflectionObject($subscriber);
    
            foreach ($reflection->getMethods() as $method) {
                // Does this method has Listener attributes?
                $attributes = $method->getAttributes(Listener::class);
    
                foreach ($attributes as $listenerAttribute) {
                    /** @var $listener Listener */
                    $listener = $listenerAttribute->newInstance();
    
                    // with $listener instanceof Listener attribute,
                    // register the method to the given Listener->event
                    // as a callable
                    $this->listeners[$listener->event][] = [$subscriber, $method->getName()];
                }
    
            }
        }
    
        public function dispatch($event, $args...)
        {
            foreach ($this->listeners[$event] as $listener) {
                // invoke the listener callables registered to an event name
                $listener(...$args);
            }
        }
    }
    $dispatcher = new EventDispatcher();
    $dispatcher->addSubscriber(new RequestSubscriber());
    $dispatcher->dispatch(RequestEvent::class, $payload);
    
    

七、match表达式 V2

作者:Ilija Tovilo

首先是“如果…那么”,然后(没有双关的意思)是“交换”令人欣喜的是,switch 为开发人员提供了一种格式,可以提供多种“if … then”场景,而没有繁琐的重复 if … then 语法。PHP 8 给我们带来了“匹配”,它提供了定义好的返回值,没有类型强制,没有错误,以及穷举。

// PHP 7x
switch ($this->pants->mine['type']) {
    case pants::T_SELECT:
        $result = $this->SelectPants();
        break;
    case Pants::T_UPDATE:
        $result = $this->UpdatePants();
        break;
    case pants::T_DELETE:
        $result = $this->DeletePants();
        break;
    default:
        $this->syntaxError('SELECT, UPDATE or DELETE');
        break;
}
// After
$result = match ($this->pants>mine['type']) {
    Pants::T_SELECT => $this->SelectPants(),
    Pants::T_UPDATE => $this->UpdatePants(),
    Pants::T_DELETE => $this->DeletePants(),
    default => $this->syntaxError('SELECT, UPDATE or DELETE'),
};

返回值

开关与匹配

switch (1) {

    case 0:
        $result = 'Pants';
        break;
    case 1:
        $result = 'Pants2';
        break;
    case 2:
        $result = 'Pants3';
        break;
}
echo $result;
//> Pants2

您可以看到 match 是如何让这变得简单明了的。

echo match (1) {
    0 => 'Pants',
    1 => 'Pants2',
    2 => 'Pants3',
};
//> Pants2

无类型强制

PHP 在评估 switch 语句中的值时使用非严格(==)比较。这可能会导致意想不到的结果。

switch ('Pants') {
    case 0:
      $result = "Not Pants!\n";
      break;
    case 'Pants':
      $result = "This is Pants!\n";
      break;
}
echo $result;
//> Not Pants!

Match 使用严格比较(===)来代替。无论是否使用strict_types,都会发生这种情况。

echo match ('Pants') {
    0 => "Not Pants!\n",
    'Pants' => "This is Pants!\n",
};
//> This is Pants!

没有失败

在任何语言中,switch 的一个失败之处是必须“中断”语句;否则,执行将一直持续到结束。Match 在每一行之后都有一个隐含的分隔符,这样可以简化代码。

match ($anyKeypress) {
    Key::RETURN_ => saveIt(),
    Key::DELETE => deleteIt(),
};

可以使用逗号将条件链接在一起。

echo match ($pants) {
    1, 2 => 'Pants 1 or 2',
    3, 4 => 'Pants 3 or 4',
};

穷尽性

使用 switch 语句的另一个错误来源是没有考虑到可能遇到的每一种情况。Match 将抛出一个UnhandledMatchError,允许更早地捕捉错误。

$result = match ($option) {
    BinaryOperator::ADD => $lhs + $rhs,
};

// Throws when $option is BinaryOperator::SUBTRACT

八、混合类型 V2

版本:0.9

日期:2020 年 3 月 23 日

作者:梅特·科西,达纳克

实现: https://github.com/php/php-src/pull/5313

投票:50 / 11

PHP 7 给了我们标量类型,7.1 带来了可空类型,7.2 引入了对象,现在在 8.0 中又有了联合类型。可以声明大多数函数参数、函数返回和类属性的类型信息,但是仍然需要混合类型。历史上,PHP 允许不指定类型信息,在编程高度指定的时代,这为混合类型的清晰化提供了机会。

混合类型的目的比它的名字所暗示的更具体。这意味着将类型添加到参数、类属性和函数返回中,以确认类型信息没有被忘记,但是该信息不能被进一步澄清。这也可以说明程序员只是决定不这么做。混合类型将显示为(array|bool|callable|int|float|null|object|resource|string).

有趣的是,混合类型用法的例子可以在 PHP 自己的文档中作为伪类型看到。

var_dump ( mixed $expression [, mixed $... ] ) : void

这些评论来自内部:

我无法想象什么样的代码会接受这种混乱。😃

—拉里·加菲尔德

var_dump() 就是一个例子。另外,可以存储任何用户土地值的缓存也是另一个合理的常见用途

一般的模式是当你处理别人的“数据”时,对数据的类型没有任何限制。这实际上是促使我帮助起草这个 RFC 的用例。

—丹确认

现在我们有了联合类型,还有哪些地方是当前可用的类型声明不足的呢?

—拉里·加菲尔德

显式允许任何类型的类型比写出联合类型更向前兼容。对比一下这个:

致:

var_dump( mixed $expression [, mixed $... ] ) : void

如果/当我们添加一个不是任何当前类型的子类型的“枚举”类型时,如果它使用混合类型,而不是写出的联合类型,则该函数的签名不需要改变。还有“线上拟合”问题。

—丹确认

var_dump( null|bool|int|float|string|array|object|resource $expression [, null|bool|int|float|string|array|object|resource $... ] ) : void

子类可以将参数类型从特定的值类型扩展到混合类型。根据 LSP,从混合到特定的反向操作是无效的。

// Valid example
class A
{
    public function pants(int $value) {}
}
class B extends A
{
    // Parameter type widened from int to mixed is allowed
    public function pants(mixed $value) {}
}

// Invalid example
class A
{
    public function pants(mixed $value) {}
}
class B extends A
{
    // Parameter type narrowed from mixed to int
    // Fatal error
    public function pants(int $value) {}
}

混合返回类型可以缩小到一个子类中。

// Valid example
 class A
{
    public function foo(): mixed {}
}
 class B extends A
{
    // return type from mixed to int is allowed
    public function pants(): int {}
}

特定的返回类型不能使用 mixed 扩展。

// Invalid example
class C
{
    public function pants(): int {}
}
class D extends C
{
    // return type cannot be widened from int to mixed
    // Fatal error thrown
    public function pants(): mixed {}
}

属性类型是不变的。

// Invalid example
class A
{
    public mixed $foo;
    public int $bar;
    public $baz;
}

class B extends A
{
    // property type cannot be narrowed from mixed to int
    // Fatal error thrown
    public int $foo;
}

class C extends A
{
    // property type cannot be widened from int to mixed
    // Fatal error thrown
    public mixed $bar;
}

class D extends A
{
    // property type cannot be added
    // Fatal error thrown
    public mixed $baz;
}

class E extends A
{
    // property type cannot be removed
    // Fatal error thrown
    public $foo;

}

九、弱映射

日期:2019-11-04

作者:尼基塔·波波夫

票数:25 票/ 0 票

弱映射不仅仅是孩子们用来描述 MapQuest 的。它们必须处理垃圾收集和内存管理。我们先来看看什么是弱引用。弱引用用于引用符合垃圾回收条件的对象。垃圾收集发生在不再有引用指向它们的对象上。弱映射是弱引用的集合,它不会阻止对象上的垃圾收集,并且在对象被移除后也会被释放。已经有SplObjectStorage允许创建从对象到任意值的贴图,但这与弱贴图不同。稍后将对此进行更多介绍。现在,我已经在互联网上搜索了一些例子(有好几个例子)来证明这一点。这些情况绝对是一次性的。这是一个大多数开发人员很可能不会使用的特性,但是了解您的选择将使您成为顶级的编码人员。

所以,这怎么不像SplObjectStorage?很高兴你问了。波波夫在 RFC 中指出了多重原因。首先,spl_object_id()值不会消失,在对象被销毁后会被重用。多个对象可以在不同时间拥有相同的对象 ID。第二,没有从对象 ID 到对象的反向标识,因此防止了映射上的迭代。最后,销毁对象不会释放 ID 下的存储值。

弱引用是在 PHP 7.4 中引入的,与 PHP 8 的弱映射相似,但有一些不同。弱映射可以被克隆,而弱引用不能,因为对象是唯一的。当使用非对象键$map[$key]offset*()方法时,弱映射中会出现类型错误异常。类似地,使用$map[]添加到弱映射或读取不存在的键会导致错误异常。也支持弱映射的引用迭代。弱引用和弱映射的相似之处在于,它们都是不可序列化的,并且禁止对它们设置动态属性。波波夫谈到了连载(或缺乏连载)。

您能提供一些背景信息,说明为什么您认为 WeakMap 的序列化支持很重要吗?由于弱映射本质上是缓存结构,序列化它们似乎并不是特别有用,但是当结合序列化可能具有的非常不直观的行为时,我觉得最好将此留给用户(与 WeakReference 相同)。

具体来说,我所说的无效是指:当你执行 $s = serialize($weakMap) 时,你会得到一个大的有效载荷字符串,但是当你试图执行 unserialize($s) 时,你会得到一个空的 WeakMap(或者更糟:一个弱 Map,它只会在下一次 GC 时变空),因为一旦取消序列化完成,所有这些对象都会被删除。这“有效”,但似乎并不十分有用,而且很可能成为一个 wtf 时刻

—Nikita Popov

这是一个WeakMap类的样子。

$map = new WeakMap;
$obj = new stdClass;
$map[$obj] = 311;
var_dump($map);
// object(WeakMap)#1 (1) {
//   [0]=>
//   array(2) {
//     ["key"]=>
//     object(stdClass)#2 (0) {
//     }
//     ["value"]=>
//     int(311)
//   }
// }

// The key is removed from the map once the object is deleted.
unset($obj);
var_dump($map);
// object(WeakMap)#1 (0) {
// }

十、新的str_begins_with()str_ends_with()

日期:2020 年 3 月 25 日(更新时间:2020 年 5 月 5 日)

作者:威尔·哈金斯

票数:51 / 4

我们通常不会为那些可以用简单的一行程序完成的事情编写内置函数。

——rasmus lerdorf(2008)

12 年后,这两个函数在 PHP 8 中找到了归宿。他们很简单,但是他们到达这里的路却不简单。主要问题是,PHP 中其他已经“免费”的函数可以用来确定相同的解决方案。另一个关键点是是否考虑大小写敏感性。这最终导致函数严格区分大小写,就像str_contains()一样。也像str_contains()一样,一个空格或" "被认为是包含在字符串中的,因此,总是返回true

str_starts_with ( string $haystack , string $needle ) : bool
str_ends_with ( string $haystack , string $needle ) : bool

十一、str_contains

日期:2020 年 2 月 17 日

作者:菲利普·坦拉克

票数:43 票/ 9 票

我想知道你们当中有多少人正在阅读这篇文章,他们的代码库中有一个临时的函数,名字和功能都很相似。在研究这本书的时候,我告诉了一个同事这个功能,他接着向我展示了我日常使用的代码库中的一个str_contains功能!不用说,PHP 需要这一点已经有一段时间了。顾名思义,如果一个字符串包含在另一个给定的字符串中,这个函数返回true/false。和许多其他 PHP 函数一样,这是以($haystack, $needle)的形式。

<?php

str_contains("hive", "v"); // true
str_contains("brodels", "z"); // false

// using an empty string as $needle
str_contains("hive", "");  // true
str_contains("", "");     // true

尽管有九张反对票,内部邮件列表上的讨论非常少,而且大多是积极的。

十二、修正错误

算术/按位运算符的更严格的类型检查

日期:2020 年 4 月 2 日

作者:尼基塔·波波夫

票数:57 票/ 0 票

这个添加看起来非常简单。目前,PHP 评估这段代码没有问题:

var_dump([] % [311]);
// int(0)

每当对数组、资源或(非重载)对象应用算术或位操作符时,PHP 8 都会抛出类型错误。以这种方式应用时,所有算术或位运算符(+-*/**%<<>>&|^~++--)的新行为将导致抛出 TypeError。唯一的例外是对两个数组操作数使用加法。这仍然是合法的。这将使其他操作数(nullboolintfloatstring)的行为与之前相同。

更改默认 PDO 错误模式

版本:1.0

日期:2020 年 3 月 28 日

作者:AllenJB

票数:49 票/ 2 票

默认情况下,PHP 不报告 PHP 数据对象(PDO)错误。这使得许多开发人员一无所知,除非他们知道如何显式地处理这些错误的报告。会为开发人员产生有意义的错误,而无需他们自己去管理。

将命名空间名称视为单个标记

日期:2020 年 6 月 15 日

作者:尼基塔·波波夫

票数:38 票/ 4 票

目前,像Foo\Bar这样的命名空间名称被视为一系列标识符和命名空间分隔符标记。

Foo\Bar;
// T_STRING T_NS_SEPARATOR T_STRING

PHP 8 会将这些名称视为一个单独的令牌。这一变化背后的主要动机是开发人员使用保留的名称空间名称来体验他们的库。建议的解决方案将确保未来的开发人员不会在名称空间名称中使用保留字方面遇到同样的困难。

PHP 承认四种命名空间名称:

  • Unqualified names like Pants, which coincide with identifiers.

  • Qualified names like Pants\Dog.

  • Fully qualified names like \Pants.

  • Namespace-relative names like namespace\Pants.

这些类型中的每一种都将由一个不同的令牌来表示。

Pants;
// Before: T_STRING
// After:  T_STRING
// Rule:   {LABEL}

Pants\Dog;
// Before: T_STRING T_NS_SEPARATOR T_STRING
// After:  T_NAME_QUALIFIED
// Rule:   {LABEL}("\\"{LABEL})+

\Pants;
// Before: T_NS_SEPARATOR T_STRING
// After:  T_NAME_FULLY_QUALIFIED
// Rule:   ("\\"{LABEL})+

namespace\Pants;
// Before: T_NAMESPACE T_NS_SEPARATOR T_STRING
// After:  T_NAME_RELATIVE
// Rule:   "namespace"("\\"{LABEL})+

比较字符串和数字

日期:2019-02-26

作者:尼基塔·波波夫

票数:44 票/ 1 票

PHP 中字符串和数字比较的状态充其量是“工作中”。有许多不一致的地方,0 == "foobar"是其中之一。只有当字符串实际上是数字时,PHP 8 才会使用多种比较方法,从而使非严格比较更有用,更不容易出错。如果字符串不是数字,数字将被转换为字符串,并执行字符串比较。

PHP 支持两种不同类型的比较运算符:严格比较===!==,非严格比较==, !=, >, >=, <, <=<=>。它们之间的主要区别在于,严格比较要求两个操作数的类型相同,并且不执行隐式类型强制。

下面是 PHP 8 中非严格比较的前后对比。

Comparison    | Before | After
------------------------------
 0 == "0"     | true   | true
 0 == "0.0"   | true   | true
 0 == "foo"   | true   | false
 0 == ""      | true   | false
42 == "   42" | true   | true
42 == "42foo" | true   | false

确保魔术方法的正确签名

版本:1.0

日期:2020 年 4 月 5 日

作者:加布里埃尔·卡鲁索

实现: https://github.com/php/php-src/pull/4177

票数:45 票/ 2 票

目前,像__clone()__isset()这样的神奇方法可以将它们的签名设置为不适当的值,比如__clone(): float__isset(): closure。此修复将添加参数和返回类型检查,以促进魔法方法的正确使用。

Foo::__call(string $name, array $arguments): mixed;
Foo::__callStatic(string $name, array $arguments): mixed;
Foo::__clone(): void;
Foo::__debugInfo(): ?array;

Foo::__get(string $name): mixed;
Foo::__invoke(mixed $arguments): mixed;
Foo::__isset(string $name): bool;
Foo::__serialize(): array;
Foo::__set(string $name, mixed $value): void;
Foo::__set_state(array $properties): object;
Foo::__sleep(): array;
Foo::__unserialize(array $data): void;
Foo::__unset(string $name): void;
Foo::__wakeup(): void;

在闭包使用列表中允许尾随逗号

版本:0.2

日期:2020 年 7 月 1 日

作者:泰森·安德烈

实现: https://github.com/php/php-src/pull/5793

票数:49 票/ 0 票

PHP 8 已经在参数和形参列表中被接受,它正在用代码的其余部分赶上使用列表。在这个例子中,我们将看到这是如何实现的。

$longPants_longPantsVars = function (
    $longPant,
    $longerPant,
    $reallyLongPant,  // Trailing commas were allowed in parameter lists in PHP 8.0
) use (
    $longPant1,
    $longerPant2,
    $muchLongerPant3
) {

   // body
};
$longPants_LongPantVars(
    $longPantValue,
    $obj->longPantsMethod(),
    $obj->longPantsValue ?? $longDefault,
);

移除私有方法上不适当的继承签名检查

版本:0.3

日期:2020 年 4 月 16 日

作者:佩德罗·麦哲伦

票数:24 票/ 11 票

根据 PHP 面向对象编程继承文档,只有公共和受保护的方法被继承。不过,这并不是一个一成不变的规则。当您拥有一个与其父 final private、static private 或 concrete private 方法同名的子方法时,这些规则似乎就失效了。

<?php
class A
{
    final private function finalPrivate() {
        echo __METHOD__ . PHP_EOL;
    }
}

class B extends A
{
    private function finalPrivate() {
        echo __METHOD__ . PHP_EOL;
    }
}

该代码会产生以下结果:

Fatal error: Cannot override final method A::finalPrivate()

PHP 8 将允许并处理这样的代码:

<?php
class A
{
    function callYourPrivate() {
        $this->myPrivate();
    }

    function notOverriden_callYourPrivate() {
        $this->myPrivate();
    }
    final private function myPrivate() {
        echo __METHOD__ . PHP_EOL;
    }
}

class B extends A
{
    function callYourPrivate() {
        $this->myPrivate();
    }
    private function myPrivate() {
        echo __METHOD__ . PHP_EOL;
    }
}

$a = new A();
$a->callYourPrivate();
$a->notOverriden_callYourPrivate();

$b = new B();
$b->callYourPrivate();
$b->notOverriden_callYourPrivate();

结果如下:

Warning: Private methods cannot be final as they are never overridden by other classes in ...
A::myPrivate
A::myPrivate
B::myPrivate
A::myPrivate

Saner 数字字符串

版本:1.4

日期:2020 年 6 月 28 日

原作者:安德里亚·福尔兹

原始 RFC: PHP RFC:允许数字字符串中的尾随空格

作者:乔治·彼得·班亚德

实现: https://github.com/php/php-src/pull/5762

PHP 包含了数字字符串的概念,或者可以解释为数字的字符串。字符串的数值可以用四种方式描述:数值型前导数值型、非数值型、整数型。

  • 一个数字字符串是一个包含数字的字符串,开头有可选的空格:“311”或“311”

  • 一个前导数字串是一个包含非数字字符(包括空格)结尾的数字串:“311abc”或“311”

  • 非数字字符串既不是数字字符串,也不是非数字字符串。

  • 一个整数字符串是一个用作数组索引的数字字符串,但是它有前面的非数字或空白字符的附加约束:“311”

算术或按位运算符将所有操作数转换为等效的数字或整数,同时在字符串格式不正确或无效时记录通知或警告。当操作数都是字符串并且使用了~运算符时,例外情况是&|^位运算符,在这种情况下,它将对组成字符串的 ASCII 值执行运算,结果也将是一个字符串。

问题

数字字符串目前表现如下:

  • 带有前导空格的数字字符串被认为比带有尾随空格的数字字符串“更数字”。

  • ("fe3d2fee-575d-4122-b75b-11dc068027fd")这样的 UUIDs 字符串可能会被错误地解释为数字,从而导致不可预知的结果。

  • \ is_numeric ()和弱模式参数检查将返回不一致的结果。

  • 前导数字字符串会导致不直观和不一致的行为。

建议

提出的解决方案是,仅当前导和尾随空格都允许时,将各种数字字符串模式统一为数字字符的单一概念,将任何其他类型的字符串声明为非数字,并在试图用于数字上下文时抛出 TypeErrors。这会将E_NOTICE "A non well formed numeric value encountered"更改为E_WARNING "A non-numeric value encountered",但前提是前导数字字符串只包含尾随空格。

类型声明

function pants(int $i) { var_dump($i); }
pants("123   "); // int(123)
pants("123abc"); // TypeError

\is_numeric返回带有任何尾随空格的数字字符串的true

var_dump(is_numeric("311   ")); // bool(true)

字符串偏移量

$str = 'The Pants;
var_dump($str['5str']);   // string(1) "a" with E_WARNING "Illegal string offset '5str'"
var_dump($str['5.4']);    // string(1) "a" with E_WARNING "String offset cast occurred"
var_dump($str['promises']); // TypeError

算术运算

var_dump(311 + "311   "); // int(622)
var_dump(311 + "311abc"); // int(622) with E_WARNING "A non-numeric value encountered"
var_dump(311 + "string"); // TypeError

使用++--操作符会将带有尾随空格的数字字符串转换为整数或浮点数,而不是应用字母数字增量规则:

$c = "4 ";
var_dump(++$c); // int(5)

字符串对字符串的比较

var_dump("311" == "311   "); // bool(true)

按位运算

var_dump(311 & "311  ");  // int(311)
var_dump(311 & "311abc"); // int(311) with E_WARNING "A non-numeric value encountered"

var_dump(311 & "abc");    // TypeError

使排序稳定

日期:2020 年 5 月 12 日

作者:尼基塔·波波夫

票数:45 票/ 0 票

实现: https://github.com/php/php-src/pull/5236

PHP 排序的稳定性已经成为一个问题有一段时间了。PHP 5 最初提供了不稳定的排序,PHP 7 为 16 个或更少元素的数组解决了这个问题。PHP 8 现在一劳永逸地提供了稳定的排序。眼前的问题是,如果输入数组中的多个元素比较起来相等,排序会相邻发生。如果发现排序不稳定,元素的相对顺序就得不到保证,因此看起来是随机的。稳定排序将保证相等的元素保持最初在原始数组中分配的顺序。这包括sortrsortusortasortarsortuasortksortkrsortuksortarray_multisort以及ArrayObject.上对应的方法

我们来看看不稳定排序是什么样子的。

$array = [
    'cat' => 1,
    'dog' => 1,
    'apple' => 0,
    'banana' => 0,
];
asort($array);

这是不稳定排序的结果:

['banana' => 0, 'apple' => 0, 'cat' => 1, 'dog' => 1]
['apple' => 0, 'banana' => 0, 'dog' => 1, 'cat' => 1]
['banana' => 0, 'apple' => 0, 'dog' => 1, 'cat' => 1]

稳定排序将始终返回以下内容:

['apple' => 0, 'banana' => 0, 'cat' => 1, 'dog' => 1]

现在,稳定排序是通过显式存储元素的原始顺序来实现的,但是这种方法效率很低,而且很粗糙。为了实现新的稳定排序,将使用当前的zend_sort来额外存储数组元素的原始顺序,如果需要用例,这些元素将用作后备。

稳定排序也带来了比较函数行为方式的改变。根据文档,比较函数必须返回小于、等于或大于零的整数。这很好;然而,由于 PHP 的性质,无论一个值是否更大,都有可能返回一个布尔值。这个问题在 PHP 8 中有两种解决方法。首先,每排序一次,将抛出一个警告,指出不赞成使用。

usort(): Returning bool from comparison function is deprecated, return an integer less than, equal to, or greater than zero

第二部分是,如果返回一个布尔值并且是false,比较将再次发生,这次交换参数,允许 PHP 确定false实际上是“等于”还是“小于”。这种行为计划在 PHP 的未来版本中删除。

十三、杂项

类构造函数属性提升

在以前的 PHP 版本中,定义一个简单的值对象所需的声明是冗长而重复的。

class Pants {
    public float $x;
    public float $y;
    public float $z;

    public function __construct(
        float $x = 0.0,
        float $y = 0.0,
        float $z = 0.0,
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

在这个新特性中,PHP 为实现一个新类提供了一个更优化的方法。

class Pants {
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0,
    ) {}
}

限制

不允许使用非抽象构造函数,这将导致错误。

// Error: Not a constructor.
function pants(private $x) {}

abstract class Pants {
    // Error: Abstract constructor.
    abstract public function __construct(private $x);
}

interface Pants {
    // Error: Abstract constructor.
    public function __construct(private $x);
}

如果使用 traits,类构造函数属性提升将被允许。提升的属性还必须使用可见性关键字之一(public、private、protected)。不支持使用var

class Pants {
    // Error: "var" keyword is not supported.
    public function __construct(var $foo) {}
}

与普通属性声明相同的限制也适用于通过提升参数声明的属性。这意味着不可能两次声明同一个属性。

class Test {
    public $prop;

    // Error: Redeclaration of property.
    public function __construct(public $prop) {}
}

可调用类型不可用,因为它们不支持作为属性类型。

class Test {
    // Error: Callable type not supported for properties.
    public function __construct(public callable $callback) {}
}

新的 fdiv()函数

fdiv()函数* 将执行浮点除法,同时将除以零视为完全合法的操作,在这种情况下不会发出任何类型的诊断。相反,它将返回 IEEE-754 要求的 INF/-INF/NAN 结果。它镜像了现有的 fmod() 功能。*

—Nikita Popov

*目前,被零除会导致不可预测的行为。根据 PHP 8 中新的引擎警告,波波夫想在fmod()intdiv()家族中加入fdiv()

让我们看一些这种行为的例子。

$output = intdiv(1, 0);
Fatal error: Uncaught DivisionByZeroError:
$output = 1 % 0;
Fatal error: Uncaught DivisionByZeroError
----
actually dividing by zero does not behave the same.
$output = 1 / 0;
Warning: Division by zero.

fdiv将遵循 IEEE-754 语义并返回INF/-INF/NaN而不抛出关键错误。

总是为不兼容的方法签名生成致命错误

日期:2019-04-08

作者:尼基塔·波波夫

表决结果:39 票赞成、3 票反对

这是又一次错误升级。不兼容的方法签名总是会引发致命错误。以前的版本会抛出警告或致命错误,但是 PHP 8 把所有的错误都归为致命错误。

从负索引开始的数组

版本:0.4

日期:2017-04-20

作者:佩德罗·麦哲伦

表决结果:17 票赞成、2 票反对

任何第一个数字键有数字n的给定数组都将被隐式地分配下一个键为n+1(如果 n > = 0)或0(如果 n < 0)。PHP 8 对此不会有不同的处理,总是将下一个键指定为n+1,而不管第一个键的值。

在 ext/dom 中实现新的 DOM 生活标准 API

版本:0.3

日期:2019-09-15

作者:Benjamin eberhee(beber lei @ PHP . net),thomas weinert

表决结果:37 票赞成

最初的文档对象模型(DOM)是作为 HTML 和 XML 的接口而创建的,由 W3 在 2004 年建立,但已被 Web 超文本应用技术工作组(WHATWG)接管,并转化为生活标准。之所以采用这个标准,是因为新的 API 改进了数据的遍历和操作。这种采用也确保了随着标准的发展,PHP 支持也会发展。与标准本身有一些偏差,主要是因为它是为浏览器或 JavaScript 编写的,PHP 必须适应这些情况。

实施

<?php

interface DOMParentNode
{
    /** access to the first child of this node that is a DOMElement */
    public readonly ?DOMElement $firstElementChild;

    /** access to the last child of this node that is a DOMElement */
    public readonly ?DOMElement $lastElementChild;

    /** counts all child nodes that are DOMElements */
    public readonly int $childElementCount;

    /** appends one or many nodes to the list of children behind the last child node */
    public function append(...DOMNode|string|null $nodes) : void;

    /** prepends one or many nodes to the list of children before the first child node */
    public function prepend(...DOMNode|string|null $nodes) : void;
}

class DOMDocument implements DOMParentNode {}
class DOMElement implements DOMParentNode {}
class DOMDocumentFragment implements DOMParentNode {}

interface DOMChildNode
{
    /** Returns the previous node in the same hierarchy that is a DOMElement or NULL if there is none */
    public readonly ?DOMElement $previousElementSibling;

    /** Returns the next node in the same hierachy that is a DOMElement or NULL if there is none */
    public readonly ?DOMElement $nextElementSibling;

    /** acts as a simpler version of $element->parentNode->removeChild($element); */
    public function remove() : void;

    /** add passed node(s) before the current node */
    public function before(...DOMNode|string|null $nodes) : void;

    /** add passed node(s) after the current node */
    public function after(...DOMNode|string|null $nodes) : void;

    /** replace current node with new node(s), a combination of remove() + append() */
    public function replaceWith(...DOMNode|string|null $nodes) : void;
}
class DOMElement implements DOMChildNode {}
class DOMCharacterData implements DOMChildNode {}

生活标准包含一个中间特征(接口)DOMNonDocumentTypeChildNode,它定义了previousElementSiblingnextElementSibling属性。PHP 不允许接口声明属性;因此,该接口不可用,但是属性在实现DOMChildNode的每个类上都可用。另外两种方法,querySelectorquerySelectorAll,也被排除在这个植入之外。这些是面向层叠样式表(CSS)选择的,已经有特定的库(PhpCss 或 Symfony CSS 选择器)可以更好地处理这一功能。

静态返回类型

日期:2020 年 1 月 8 日

作者:尼基塔·波波夫

实现: https://github.com/php/php-src/pull/5062

投票结果:54 票赞成

目前,使用静态特殊类名指的是调用方法的实际类。无论该方法是否被继承,这都适用。这被称为后期静态绑定 (LSB)。LSB 通过存储最后一个非转发调用中命名的类来工作。对于静态方法调用,这是显式命名的类(通常在 :: 运算符的左边);对于非静态方法调用,它是对象的类。转移呼叫是由self::parent::static::引入的静态呼叫,如果在等级结构中向上,还有forward_static_call()

class A {
    public function test(): self {}
}
class B extends A {
    public function test(): static {}
}
class C extends B {}
class A {
    public function test(): A {}
}
class B extends A {}
class C extends B {
    public function test(): static {}
}

变量语法调整

这是为了更新 PHP 7 中的统一变量语法 RFC。

日期:2020 年 1 月 7 日

作者:尼基塔·波波夫

实现: https://github.com/php/php-src/pull/5061

表决结果:47 票赞成

PHP 中有四种主要类型的“取消引用”操作:

  • 数组:$foo[$bar], $foo{$bar}

  • 对象:$foo->bar, $foo->bar()

  • 静态:Foo::$bar, Foo::bar(), Foo::BAR

  • 通话:foo()

插值和非插值字符串

目前,像"pants"这样的非插入字符串被认为是完全不可引用的;也就是说,像"pants"[0]"pants"->shorts()这样的结构在语法上是合法的。插入的字符串如"pants$shorts"是不可区分的。

魔法、类和常规常量

__PANTS__这样的魔法常量现在将被视为普通常量,并允许数组去引用。

PANTS[0] and __PANTS__[0]

类似地,使用PANTS{0}PANTS->length().可以取消对类常量的引用

类常数可取消引用性

目前Foo::$bar::$baz是合法的,而Foo::BAR::$baz不是。PHP 8 将改变这一点,使Foo::BAR::$bazFoo::BAR::BAZ成为合法的。

对 new 和 instanceof 的任意表达式支持

PHP 8 还将引入语法new (expr)$x instanceof (expr).

添加字符串接口

版本:0.9

日期:2020 年 1 月 15 日

作者:尼古拉斯·格雷卡斯

表决结果:29 / 9

stringable 接口将为所有实现__toString()方法的类添加一个 stringable 接口。这个接口有两个用途。第一个是允许使用string|Stringable来表达string|object-with-__toString().,第二个是提供从 PHP 7 到 PHP 8 的正向升级路径。

获取调试类型

日期:2020 年 2 月 15 日

作者:马克·兰道尔

表决结果:42 票赞成、3 票反对

PHP 8 的这一新增功能不同于显而易见的兄弟get_type(),它返回本机类型名int而不是integer,同时还解析类名。这个函数的主要目的是在处理 PHP 在运行时无法处理的类型时,通过现有的基于参数类型的检查来代替更复杂的过程。特别是在处理数组中的参数类型时,这是非常有益的。

$pants = $arr['key'];
if (!($pants instanceof Hive)) {
    throw new TypeError('Expected ' . Hive::class . ' got ' . (is_object($pants) ? get_class($pants) : gettype($pants)));
}

表 13-1 列出了gettypeget_debug_type的不同返回值。

表 13-1

gettypeget_debug_type的返回值

|

价值

|

get_debug_type()

|

gettype()

|
| --- | --- | --- |
| Zero | (同 Internationalorganizations)国际组织 | 整数 |
| Zero point one | 漂浮物 | 两倍 |
| 真实的 | 弯曲件 | 布尔 |
| 错误的 | 弯曲件 | 布尔 |
| "你好" | 线 |   |
| [ ] | 排列 |   |
| 空 | 空 | 空 |
| 名为“Pants\Hive”的类 | 裤子\蜂巢 | 目标 |
| 匿名班级 | 匿名类 | 目标 |
| 一种资源 | 资源(xxx) | 资源 |
| 封闭资源 | 资源(已关闭) |   |

// PHP 8 would allow for
if (!($pants instanceof Hive)) {
  throw new TypeError('Expected ' . Hive::class . ' got ' . get_debug_type($pants));
}
$pants>someHiveMethod();

这在 PHP 中很受欢迎,几乎没有争议。波波夫在投票时给出了更多的细节。

考虑到命名的讨论,我认为这里有一个重要的部分需要强调:对于匿名类,它只返回 "class@anonymous" ,而不是 "class@anonymous\0SOME_RANDOM_UNIQUE_STRING_HERE" 。因而这个功能肯定不能在一个 ::type 下构造(这个应该返回全称),甚至 get_canonical_type() 似乎也不合适

该函数特别适用于在错误信息或类似信息中包含类型名称,因此 get_debug_type()

—Nikita Popov

New preg_last_error_msg()

作者:尼科·奥尔加特

PHP 有一个标准的函数类,称为 Perl 兼容正则表达式(PCRE)。PCRE 函数允许 PHP 中正则表达式的标准使用。在以前的版本中,PHP 用户只能得到返回错误代码的preg_last_error()。使用preg_last_error_msg(),我们现在可以改变这一点:

<?php
preg_match('/(?:\D+|<\d+>)*[!?]/', 'foobar foobar foobar');
var_dump(preg_last_error()); // 2
var_dump(preg_last_error_msg()); // Backtrack limit was exhausted

添加 CMS 支持

日期:2020 年 5 月 13 日

作者:艾略特·李尔

不,这与 WordPress、Laravel、Drupal 或类似的软件无关!加密消息语法(CMS)是 PKCS#7 的较新版本。由 RSA Security,LLC 开发的 PKCS#7(公钥加密标准)是一种标准,其中数字签名和证书的生成和验证由公钥基础设施(PKI)管理。这是目前在电子邮件和物联网设备中使用的加密标准。

PKCS#7 和 OpenSSL 函数之间的关系如表 13-2 所示。

表 13-2

PKCS#7 和 OpenSSL 函数

|

PKCS#7 函数

|

新 CMS 功能

|
| --- | --- |
| openssl_pkcs7_encrypt() | openssl_cms_encrypt() |
| openssl_pkcs7_decrypt() | openssl_cms_decrypt() |
| openssl_pkcs7_sign() | openssl_cms_sign() |
| openssl_pkcs7_verify() | openssl_cms_verify() |
| openssl_pkcs7_read() | openssl_cms_read() |

function openssl_cms_sign(string $infile, string $outfile, $signcert, $signkey, ?array $headers, int $flags = 0, int $encoding = OPENSSL_ENCODING_SMIME, ?string $extracertsfilename = null): bool {}

此函数使用 X.509 证书和密钥对文件进行签名。论据如下。

  • $infile:待签名文件的名称

  • $outfile:存放结果的文件的名称

  • $signcert:包含签名证书的文件的名称

  • $signkey:包含与$signcert关联的密钥的文件名

  • $headers:要包含在 S/MIME 输出中的标题数组

  • $flags:要传递给cms_sign()的标志

  • $encoding:输出文件的编码

  • $extracertsfilename:签名中包含的中间证书

function openssl_cms_verify(string $filename, int $flags = 0, string $signerscerts = UNKNOWN, array $cainfo = UNKNOWN, string $extracerts = UNKNOWN, string $content = UNKNOWN, string $pk7 = UNKNOWN, string $sigfile = UNKNOWN, $encoding = OPENSSL_ENCODING_SMIME ): bool {}

此函数使用指定的编码验证 CMS 签名,无论是附加的还是分离的。

以下是论点:

  • $filename:输入文件

  • $flags:将被传递给cms_verify的标志

  • $signercerts:签名者证书和可选的中间证书的文件

  • $cainfo:包含自签名认证机构证书的数组

  • $extracerts:包含附加中间证书的文件

  • $content:分离签名时指向内容的文件

  • $pk7:保存签名的文件

  • $encoding:支持的三种编码之一(PEM/DER/SMIME)。

如果成功,则返回TRUE,如果失败,则返回FALSE

function openssl_cms_encrypt(string $infile, string $outfile, $recipcerts, ?array $headers, int $flags = 0, int $encoding = OPENSSL_ENCODING_SMIME,  int $cipher = OPENSSL_CIPHER_RC2_40): bool {}

该函数根据传递给它的证书对一个或多个接收者的内容进行加密。

以下是论点:

  • $infile:要加密的文件

  • $outfile:输出文件

  • $recipcerts:要加密的收件人

  • $headers:使用 S/MIME 时要包含的标题

  • $flags:要传递给CMS_sign的标志

  • $encoding:要输出的编码

  • 要使用的密码

如果成功,则返回值TRUE,如果失败,则返回值FALSE

function openssl_cms_decrypt(string $infilename, string $outfilename, $recipcert, $recipkey = UNKNOWN, int $encoding = OPENSSL_ENCODING_SMIME): bool {}

这个函数解密一个 CMS 消息。以下是论点:

  • $infilename:包含加密内容的文件的名称

  • $outfilename:存放解密内容的文件名

  • $recipcert:包含接收方证书的文件的名称

  • $recipkey:包含 PKCS#8 密钥的文件的名称

  • $encoding:输入文件的编码。

该函数成功时返回TRUE,失败时返回FALSE

function openssl_cms_read(string $infilename, &$certs): bool {}

该功能完全模拟openssl_pkcs7_read()

还包括新的常数。

OPENSSL_ENCODING_CMS /* encoding is a CMS-encoded message */
OPENSSL_ENCODING_DER /* encoding is DER (Distinguished Encoding Rules) */
OPENSSL_ENCODING_PEM /* encoding is PEM (Privacy-Enhanced Mail) */
OPENSSL_CMS_DETACHED
OPENSSL_CMS_TEXT
OPENSSL_CMS_NOINTERN
OPENSSL_CMS_NOVERIFY
OPENSSL_CMS_NOCERTS
OPENSSL_CMS_NOATTR
OPENSSL_CMS_BINARY
OPENSSL_CMS_NOSIGS

对象上的 Allow ::class

日期:2020 年 1 月 9 日

作者:尼基塔·波波夫

投票结果:60 / 0

在 PHP 的早期版本中,常量::class允许访问完全限定的类名。此常数考虑了使用和当前可用的命名空间。

namespace 311\Albums;
use Grass\Roots;
use Roots\Music as Album;

class Pants {}
// `use` statement
echo Roots::class; // 'Grass\Roots"

// `use` X `as` Y
echo Album::class; // "Roots\Music"

// Current namespace
echo Albums::class; // "311\Albums\Pants"

以前版本的 PHP 在对象上使用::class时会抛出致命错误,但在 PHP 8 中,这种情况现在可以按预期执行。

$object = new Grass\Roots;
echo $object::class;
// "Grass\Roots"

如果$object是一个object,那么$object::class返回get_class($object)。否则它抛出一个TypeError异常。

$object = new stdClass;
var_dump($object::class); // "stdClass"

$object = null;
var_dump($object::class); // TypeError

基于对象的 token_get_all()替代

日期:2020 年 2 月 13 日

作者:nikic@php.net

实现: https://github.com/php/php-src/pull/5176

表决结果:47 票赞成

Popov 提出的PhpToken::getAll()方法是对token_get_all()的替代,它将返回一个PhpToken对象的数组,而不是字符串和数组的混合。这主要有两个原因。首先,返回一个对象数组将使返回结构标准化。现在,开发人员只需要期待对象的数组,而不是单个字符串或数组的可能性。第二个也是最有益的是使用数组时内存使用的减少。

Default:
    Memory Usage: 14.0MiB
    Time: 0.43s (for 100 tokenizations)
TOKEN_AS_OBJECT:
    Memory Usage: 8.0MiB
    Time: 0.32s (for 100 tokenizations)

此外,还包含了方法getTokenName(),它主要用于调试目的。对于 ID 小于 256 的单字符令牌,它返回与 ID 对应的扩展 ASCII 字符。对于已知的令牌,它返回与token_name()相同的结果。对于未知令牌,它返回 null。

下面是新的类实现:

class PhpToken {
    /** A T_* constant, or an integer < 256 representing a single-char token. */
    public int $id;
    /** The context of the token. */
    public string $text;
    /** starting line number (1-based) of the token. */
    public int $line;
    /** starting position (0-based) in the tokenized string. */
    public int $pos;
/**
     * Same as token_get_all(), but returning array of PhpToken.
     * @return static[]
     */
    public static function getAll(string $code, int $flags = 0): array;

    final public function __construct(int $id, string $text, int $line = -1, int $pos = -1);
    /** Get the name of the token. */
    public function getTokenName(): ?string;

因为PhpToken::getAll()方法返回static[],所以扩展这个类很容易。

class thePhpToken extends PhpToken {
    public function getLowerText() {
        return strtolower($this->text);
    }
}

$tokens = thePhpToken::getAll($code);
var_dump($tokens[0] instanceof thePhpToken); // true
$tokens[0]->getLowerText(); // works

抽象特征方法的验证

日期:2020 年 2 月 7 日

作者:尼基塔·波波夫

实现: https://github.com/php/php-src/pull/5068

投票结果:52 票对 0 票

PHP 中的特征类似于类,但是可以在多个实例中重用。例如:

<?php
trait CustomReturn {
    function getFirstReturnType() { /*1*/ }
    function getFirstReturnDesc() { /*2*/ }
}

class thisFakeMethod extends FakeMethod {
    use CustomReturn;
    /* ... */
}

class thisFakeFunction extends FakeFunction {
    use CustomReturn;
    /* ... */
}
?>

PHP 8 将总是根据独立于其来源的实现方法来验证抽象特征方法的签名。如果实现方法与抽象特征方法不兼容,就会产生致命错误。不相容的定义如下:

  • 签名必须兼容,包括奇偶校验兼容、逆变参数类型兼容和协变返回类型兼容。

  • 方法的静态性必须匹配。

此外,现在可以只在 traits 中声明抽象私有方法了。抽象私有方法在术语上是矛盾的,因为声明实现的方法从发布需求的类中是不可见的。然而,trait 提供了对抽象私有方法的明确访问,因为 trait 方法可以访问 using 类的私有方法。

抛出表达式

日期:2020 年 3 月 21 日

作者:Ilija Tovilo

实现: https://github.com/php/php-src/pull/5279

表决结果:46 票赞成,3 票反对

Throwcatch一起用于处理编程逻辑中的异常。被一个try块包围着,你可以抛出一个异常来捕获和处理,而不是破坏代码。但是Throw只在这类场景中使用过,不能作为表达式使用。这将允许使用带有箭头函数的throw、联合操作符和三元/elvis 操作符。

$pants = fn() => throw new Exception();
$pants = $nullyMcNull ?? throw new NullException();
$pants = $isFalse ?: throw new FalseException();
$pants = !empty($array)
    ? reset($array)
    : throw new EmptyException();
$isWinning && throw new Exception();
$isWinning || throw new Exception();
$isWinning and throw new Exception();
$isWinning or throw new Exception();

优先级也将变得重要。throw语句之后的所有内容将被认为具有更高的重要性。也就是说,throw将采用最低的操作符优先级。这是有意义的,因为一般来说,throw应该在逻辑语句的末尾。

throw (static::wrongPasswordException());
throw ($cheeseIsFree ? new payForCheeseException() : new noCheeseException());
throw ($foreverAlone ?? new Exception());
throw ($lightning = new Exception());
throw ($promises ??= new words());
throw ($fu && $gazi ? new selling() : new buying());

需要断言特殊性的一个地方是短路操作符。如果选择这样做,必须使用括号来区分优先级。

$transistor || (throw new Exception('$transistor must be resistor') && $unplugged || (throw new Exception('$unplugged must be resistor')));

独立于区域设置的浮点到字符串转换

版本:1.0

日期:2020 年 3 月 11 日

作者:乔治·彼得·班亚,梅特·科 csis

实现: https://github.com/php/php-src/pull/5224

表决结果:42 票赞成,1 票反对

在 PHP 中将浮点数转换成字符串已经成为一个问题有一段时间了。出现此问题是因为不同的国家表示十进制字符(。或者,)不同地。例如,3.14 在美国是相当标准的,可以确定为圆周率值的开始。然而,如果你在波兰发展,你会使用“3,14”。这种差异的主要问题是,当使用floatstring的转换时,由于地区不同,结果会不一致。这一增加将用“.”使这些问题标准化作为主要的小数占位符。

setlocale(LC_ALL, "pl_PL");
$f = 3.11;
(string) $f;            // 3,11 would become 3.11
strval($f);             // 3,11 would become 3.11
print_r($f);            // 3,11 would become 3.11
var_dump($f);           // float(3,11) would become float(3.11)
debug_zval_dump($f);    // float(3,11) would become float(3.11)
settype($f, "string");  // 3,11 would become 3.11
implode([$f]);          // 3,11 would become 3.11
xmlrpc_encode($f);      // 3,11 would become 3.11

这种行为在 userland 并不是前所未有的。例如,PDO 扩展利用这一点来标准化浮点的字符串表示。在var_exportserializejson_encode中也使用了语言环境独立性。然而,printf已经有了一个用%f指定非比例感知转换的选项,保持不变。

非捕获捕获

版本:0.9

日期:2020 年 4 月 5 日

作者:马克斯·塞姆尼克

实现: https://github.com/php/php-src/pull/5345

表决结果:48 票赞成,1 票反对

简而言之,我希望能够做到以下:

请分享你的想法!😃

这名男子的名字在纽约一名 2013-16 岁的男子家中被杀。

我看起来不错。

—斯塔斯·马利舍夫

try {
foo();
catch (SomeExceptionClass) {
bar();
}}

编程中的try / catch范例允许定义一个代码块并测试其错误(try),同时定义另一个代码块来处理所述错误(catch)。在 PHP 中,这是通过使用catch块中的一个变量来实现的,这个变量将自己分配给错误。

try {
    foo();
} catch (weekendException $ex) {
    die($ex->showMessage());
}

会变成

try {
    foo();
} catch (weekendException) {
    echo "This does not work on the weekend";
}

这种增加不再需要变量的说明。catch的意图很清楚,因此不需要任何其他细节。

始终可用的 JSON 扩展

版本:0.3

日期:2020 年 4 月 29 日

作者:泰森·安德烈

实现: https://github.com/php/php-src/pull/5495

票数:56 票/ 0 票

通过这次添加,PHP 团队迈出了将 JSON 从 PECL 扩展转变为 PHP 核心特性的第一步。目前,在 PHP 版本中禁用 JSON 的唯一方法是通过./configure --disable-json,这使得代码有可能要求 JSON 出现故障。在以前的 PHP 版本中禁用 JSON 的唯一原因是由于许可问题,这个问题已经解决了。

然而,这种改变会有向后兼容性的问题。脚本或命令行界面(CLI)指令中使用的--enable-json or --disable-json将需要更新,因为这些选项将不再存在。这同样适用于任何使用extension_loaded('json'),的代码,因为这将始终是true

Zend . exception _ String _ param _ max _ len:getTraceAsString()中可配置的字符串长度

自 2003 年以来,两个非常有用的功能被限制为 15 字节的信息。Throwable->getTraceAsString()Throwable->__toString()被期望将堆栈跟踪返回给急切地等待挤压他们代码中的 bug 的开发人员。直到 PHP 8,这些都返回了类似于"/path/to/the/vesion/file.php 1349 function(whe ...",的半有用的字符串,考虑到路径、URL 和 UUIDs 的使用,这些信息实际上是不够的。

这个问题的解决方案是一个.ini设置zend.exception_string_param_max_len,它允许将字符串的字节限制更改为从 0 到 1000000 的任何值。如果没有设置值,默认值将保持为 15。然而,与此相关的一个问题是,随着暴露数据的增加,无意中获得敏感数据的风险也会增加。

function badHTMLRenderingExample(string $secretCode, string $secretPassword) {
   echo "<h1>Welcome AOL</h1>\n";
   try {
       process($secretCode);
   } catch (Exception $e) {
       // The output will include both $secretCode and $secretPassword.
       // in PHP 7x, only 15 bytes would be displayed.
       echo "ID: 10 T error, please feed the dev team: $e\n";
   }
}

15 字节的默认设置将保持实际执行此操作的遗留代码的安全性,除非.ini设置被更改。

解包 ext/xmlrpc

版本:1.0

日期:2020 年 5 月 12 日

作者:克里斯托弗·m·贝克尔

通过 https://github.com/php/php-src/pull/5640 进行拆分

票数:50 / 0

似乎并没有强烈的需求去暗示人们应该尽快停止使用它。扩展部分(更重要的是,底层库)已经很多年没有维护了,搬到 PECL 不会在这方面有实质性的改变。

——Nikita Popov

目前,是 PHP 中一个不可避免的弊端。对于那些不了解的人来说,XML-RPC 是允许在系统之间使用 XML 的规范,因此是必要的。尽管 XML 本质上并不邪恶,但出于多种原因,目前这种移植的使用是邪恶的。第一,ext/xmlrpc依赖于被抛弃的libxmlrpc-epi。这种放弃可以通过滚动 xmlrpc-epi-dev 邮件列表中当前的垃圾邮件数量来验证。这是第一击。打击二是 PHP 目前实现的是 0.51 版本,但最新版本是 0.54。一个解决方案是控制库并继续更新和维护。这不是 PHP 团队的目标或宗旨。该提案已提交给 unbundle ext/xmlrpc,这意味着它将被视为通过 PECL 的第三方扩展,因此选择使用或不使用它成为最终用户的决定和责任。这里需要注意的是,团队并不认为ext/xmlrpc是无用的或者需要被废弃。API 和功能按预期工作。这仅仅是为这个库或 PHP 关于 XML 的下一步做准备的一步。

不要在 getMetadata()之外自动取消 Phar 元数据的序列化

版本:0.4

日期:2020 年 7 月 7 日

作者:泰森·安德烈

实现: https://github.com/php/php-src/pull/5855

票数:25 票/ 0 票

除了它吸引人的名字,PHP 8 的这一新增功能带来了一个急需的安全更新。快速搜索“php phar 流包装器漏洞”将返回几个影响 Drupal、WordPress、Prestashop 等的结果。基本上今天互联网上的大多数网站在 PHP 8 之前都是易受攻击的。正如 RFC 指出的,“由于对象实例化和自动加载,非序列化会导致代码被加载和执行,恶意用户可能会利用这一点。”

提案

当 PHP 打开 phar 文件时,不要自动解序列化元数据。仅当直接调用Phar->getMetadata()PharFile->getMetadata()时,才使 PHP 不序列化元数据。

另外,添加一个数组$unserialize_options = [] parameter to both getMetadata()实现,默认为当前默认的unserialize()行为,比如允许任何类。(作为一个实现细节,如果$unserialize_options被设置为默认值以外的任何值,那么产生的元数据将不会被缓存,并且不会从缓存中返回值。例如,setMetadata(new stdClass())之后的getMetaData(['allowed_classes' => []])将可能触发内部的unserialize(['allowed_classes' => []])呼叫。

向后不兼容的更改

加载 phar 时,在元数据非序列化期间或之后触发的来自__wakeup()__destruct()等的任何副作用都将停止发生,只有在直接调用getMetadata()时才会发生。*

posted @ 2024-08-03 11:23  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报