一、介绍新的 PHP8 OOP 特性

在本章中,我们将向您介绍针对面向对象编程OOP的新PHP:Hypertext Preprocessor 8PHP 8特性)。本章介绍了一组可用于生成验证码图像的类(验证码完全自动公共图灵测试的首字母缩写,用于区分计算机和人类,清楚地说明了新的 PHP8 功能和概念。本章对于帮助您将新的 PHP8 特性快速融入到自己的实践中至关重要。这样,您的代码将运行得更快、效率更高,bug 更少。

本章涵盖以下主题:

  • 使用构造函数属性提升
  • 使用属性
  • 将匹配表达式合并到程序代码中
  • 理解命名参数
  • 探索新的数据类型
  • 使用类型化属性改进代码

技术要求

要检查并运行本章中提供的代码示例,此处列出了推荐的最低硬件:

  • 基于 x86_64 的台式 PC 或笔记本电脑
  • 1 GB(GB可用磁盘空间
  • 4 GB 的随机存取存储器RAM
  • 500千比特每秒Kbps或更快的互联网连接

此外,您还需要安装以下软件:

  • 码头工人
  • Docker Compose

本书使用预构建的 Docker 映像,其中包含创建和运行本书中介绍的 PHP8 代码示例所需的所有软件。您不需要在计算机上安装 PHP、Apache 或 MySQL:只需使用 Docker 和提供的映像即可。

要设置测试环境以运行代码示例,请按以下步骤进行:

  1. Install Docker.

    如果您正在运行 Windows,请从这里开始:

    https://docs.docker.com/docker-for-windows/install/

    如果您使用的是 Mac 电脑,请从这里开始:

    https://docs.docker.com/docker-for-mac/install/

    如果您使用的是 Linux,请查看以下内容:

    https://docs.docker.com/engine/install/

  2. Install Docker Compose. For all operating systems, start here:

    https://docs.docker.com/compose/install/

  3. Install the source code associated with this book onto your local computer.

    如果已安装 Git,请使用以下命令:

    php git clone https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices.git ~/repo

    否则,您只需从统一资源定位器URL下载源代码即可 https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices/archive/main.zip 。然后,您可以将其解压缩到您创建的文件夹中,我们在本书中称之为/repo

  4. You can now start the Docker daemon running. For Windows or Mac, all you need to do is to activate the Docker Desktop app.

    如果您正在运行 Ubuntu 或 Debian Linux,请发出以下命令:

    sudo service docker start

    对于 Red Hat、Fedora 或 CentOS,请使用以下命令:

    sudo systemctl start docker

  5. Build a Docker container associated with this book and bring it online. To do so, proceed as follows.

    从本地计算机上,打开命令提示符(终端窗口)。将目录更改为/repo。仅第一次发出docker-compose build命令来构建环境。请注意,您可能需要root(管理员)权限才能运行 Docker 命令。如果是这种情况,请以管理员身份运行(对于 Windows)或在命令前面加上sudo。根据您的连接速度,初始构建可能需要相当长的时间才能完成!

  6. 要提起容器,请执行以下操作

  7. From your local computer, open Command Prompt (terminal window). Change the directory to /repo. Bring the Docker container online in background mode by running the following command:

    php docker-compose up -d

    请注意,实际上不需要单独构建容器。如果您发出docker-compose up命令时容器未生成,则容器将自动生成。另一方面,单独构建容器可能比较方便,在这种情况下,docker build就足够了。

    下面是一个有用的命令,用于确保所有容器都在运行:

    php docker-compose ps

  8. To access the running Docker container web server, proceed as follows.

    打开本地计算机上的浏览器。输入此 URL 以访问 PHP 8 代码:

    http://localhost:8888

    输入此 URL 以访问 PHP 7 代码:

    http://localhost:7777

  9. To open a command shell into the running Docker container, proceed as follows.

    从本地计算机上,打开命令提示符(终端窗口)。发出以下命令以访问 PHP 8 容器:

    php docker exec -it php8_tips_php8 /bin/bash

    发出以下命令以访问 PHP 7 容器:

    php docker exec -it php8_tips_php7 /bin/bash

  10. 使用完容器后,要使其脱机,请从本地计算机打开命令提示符(终端窗口),并发出以下命令:

    php docker-compose down

本章的源代码位于以下位置:

https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices

重要提示

如果您的主机使用高级 RISC 机器ARM)体系结构(例如,Raspberry Pi),则需要使用修改过的 Dockerfile。

提示

通过回顾这篇文章,快速了解 Docker 技术和术语将是一个非常好的主意:https://docs.docker.com/get-started/.

现在,我们可以通过查看构造函数属性提升来开始讨论。

使用构造函数属性提升

除了即时JIT编译器外,PHP 8 中引入的最大新特性之一是构造函数属性提升。这个新特性结合了__construct()方法签名中的属性声明和参数列表,以及指定默认值。在本节中,您将学习如何大幅减少属性声明以及__construct()方法签名和正文中所需的编码量。

物业促销语法

调用构造函数属性提升所需的语法与 PHP 7 及更早版本中使用的语法相同,但有以下区别:

  • 您需要定义一个可见性级别
  • 您不必事先明确声明属性
  • 您不需要在__construct()方法的主体中进行赋值

下面是一个使用构造函数属性提升的简单代码示例:

// /repo/ch01/php8_prop_promo.php
declare(strict_types=1);
class Test {
    public function __construct(
        public int $id,
        public int $token = 0,
        public string $name = '')
    { }
}
$test = new Test(999);
var_dump($test);

执行前面的代码块时,这是输出:

object(Test)#1 (3) {
  ["id"]=> int(999)
  ["token"]=> int(0)
  ["name"]=> string(0) ""
}

这表明已经使用默认值创建了Test类型的实例。现在,让我们来看看这个特性是如何节省大量编码的。

使用属性提升进行代码缩减

在传统的 OOP PHP 类中,需要做以下三件事:

  1. 声明属性,如下所示:

    php /repo/src/Php8/Image/SingleChar.php namespace Php7\Image; class SingleChar {     public $text     = '';     public $fontFile = '';     public $width    = 100;     public $height   = 100;     public $size     = 0;     public $angle    = 0.00;     public $textX    = 0;     public $textY    = 0;

  2. __construct()方法签名中标识属性及其数据类型,如所示:

    php const DEFAULT_TX_X = 25; const DEFAULT_TX_Y = 75; const DEFAULT_TX_SIZE  = 60; const DEFAULT_TX_ANGLE = 0; public function __construct(     string $text,     string $fontFile,     int $width  = 100,     int $height = 100,     int $size   = self::DEFAULT_TX_SIZE,     float $angle = self::DEFAULT_TX_ANGLE,     int $textX  = self::DEFAULT_TX_X,     int $textY  = self::DEFAULT_TX_Y)

  3. __construct()方法的主体中,为属性赋值,如:

    php {   $this->text     = $text;     $this->fontFile = $fontFile;     $this->width    = $width;     $this->height   = $height;     $this->size     = $size;     $this->angle    = $angle;     $this->textX    = $textX;     $this->textY    = $textY;     // other code not shown }

随着构造函数参数数量的增加,您需要做的工作也会显著增加。当应用构造函数属性提升时,执行与前面所示相同操作所需的代码量将减少到原来的三分之一。

现在让我们看一看前面所示的同一块代码,但使用这个强大的新 PHP 8 功能进行了重写:

// /repo/src/Php8/Image/SingleChar.php
// not all code shown
public function __construct(
    public string $text,
    public string $fontFile,
    public int    $width    = 100,
    public int    $height   = 100,
    public int    $size     = self::DEFAULT_TX_SIZE,
    public float   $angle    = self::DEFAULT_TX_ANGLE,
    public int    $textX    = self::DEFAULT_TX_X,
    public int    $textY    = self::DEFAULT_TX_Y)
    { // other code not shown }

令人惊讶的是,在 PHP7 和更早版本中,使用这个新的 PHP8 特性,24 行代码可以折叠成 8 行代码!

您完全可以在构造函数中包含其他代码。然而,在许多情况下,构造函数属性提升处理在__construct()方法中通常完成的所有事情,这意味着您可以将其保留为空({ }

现在,在下一节中,您将了解一个称为属性的新功能。

提示

在这里查看 PHP 7 的完整 SingleChar 类:

https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices/tree/main/src/Php7/Image

此外,这里还可以找到等效的 PHP 8 类:

https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices/tree/main/src/Php8/Image

有关这一新功能的更多信息,请查看以下内容:

https://wiki.php.net/rfc/constructor_promotion

使用属性

PHP8 的另一个重要添加是添加了一个全新的类和语言结构,称为属性。简单地说,属性是遵循规定语法的传统 PHP 注释块的替代品。编译 PHP 代码时,这些属性在内部转换为Attribute类实例。

这一新特性不会立即影响您的代码。然而,随着各种 PHP 开源供应商开始将属性合并到他们的代码中,它将开始变得越来越有影响力。

Attribute类解决了我们在本节中讨论的一个潜在的重要性能问题,即滥用传统 PHP 注释块来提供元指令。在深入探讨这个问题以及Attribute类实例如何解决这个问题之前,我们首先必须回顾 PHP 注释。

PHP 评论概述

随着普通 PHP 注释的使用(和滥用!)的增加,对这种形式的语言构造的需求也随之增加。如您所知,评论有多种形式,包括以下所有形式:

# This is a "bash" shell script style comment
// this can either be inline or on its own line
/* This is the traditional "C" language style */
/**
 * This is a PHP "DocBlock"
 */

最后一项,著名的 PHPDocBlock,现在被广泛使用,已经成为事实上的标准。使用 docblock 并不是一件坏事。相反,这通常是唯一的方式开发人员能够交流有关属性、类和方法的信息。问题只出现在 PHP 解释过程如何处理它。

PHP DocBlock 注意事项

PHP DocBlock的原意已经被一些极其重要的 PHP 开源项目所延伸。一个引人注目的例子是条令对象关系映射器ORM项目。尽管不是强制性的,但许多开发人员选择使用嵌套在 PHP DocBlocks 中的注释来定义 ORM 属性。

看看这个部分代码示例,它定义了一个与名为events的数据库表交互的类:

namespace Php7\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Table(name="events")
 * @ORM\Entity("Application\Entity\Events")
 */
class Events {
    /**
     * @ORM\Column(name="id",type="integer",nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;
    /**
     * @ORM\Column(name="event_key", type="string", 
          length=16, nullable=true, options={"fixed"=true})
     */
    private $eventKey;
    // other code not shown

如果要将此类用作 Doctrine ORM 实现的一部分,Doctrine 将打开文件并解析 DocBlocks,搜索@ORM注释。尽管对解析 docblock 所需的时间和资源有一些担心,但这是定义对象属性和数据库表列之间关系的一种非常方便的方法,并且受到使用该原则的开发人员的欢迎。

提示

Doctrine 提供了许多替代这种形式的 ORM 的方法,包括可扩展标记语言XML)和本机 PHP 数组。更多信息,请参见https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#annotations-参考文献

docblock 使用不当的隐患

还有另一种危险与滥用 DocBlock 的原始用途有关。在php.ini文件中,有一个名为opcache.save_comments的设置。如果禁用,这将导致操作码缓存引擎(操作缓存)忽略所有注释,包括 DocBlocks。如果此设置有效,则在 DocBlocks 中使用@ORM注释的基于条令的应用将出现故障。

另一个问题是如何解析注释,或者更重要的是,如何解析注释而不是。为了使用注释的内容,PHP 应用需要打开文件并逐行解析。就时间和资源利用率而言,这是一个昂贵的过程。

属性类

为了解决隐患,PHP8 中提供了一个新的Attribute类。开发人员可以以属性的形式定义等价物,而不是使用带注释的 docblock。使用属性而不是 docblock 的一个优点是,它们是语言的正式部分,因此会与代码的其余部分一起标记和编译。

重要提示

在本章中,以及在 PHP 文档中,对属性的引用是指Attribute类的实例。

将包含 docblock 的 PHP 代码的加载与包含属性的代码的加载进行比较的实际性能指标还不可用。

尽管这种方法的好处尚未显现,但随着各种开源 project 供应商开始将属性纳入其产品中,您将开始看到速度和性能的改进。

以下是Attribute类定义:

class Attribute {
    public const int TARGET_CLASS = 1;
    public const int TARGET_FUNCTION = (1 << 1);
    public const int TARGET_METHOD = (1 << 2);
    public const int TARGET_PROPERTY = (1 << 3);
    public const int TARGET_CLASS_CONSTANT = (1 << 4);
    public const int TARGET_PARAMETER = (1 << 5);
    public const int TARGET_ALL = ((1 << 6) - 1);
    public function __construct(
        int $flags = self::TARGET_ALL) {}
}

从类定义中可以看到,PHP 8 内部使用的这个类的主要贡献是一组类常量。这些常量表示可以使用位运算符组合的位标志。

属性语法

属性使用借用自Rust编程语言的特殊语法封装。方括号内的内容基本上留给了开发人员。可以在以下代码段中看到一个示例:

#[attribute("some text")] 
// class, property, method or function (or whatever!)

回到我们的SingleChar类示例,下面是使用传统 DocBlock 时它的显示方式:

// /repo/src/Php7/Image/SingleChar.php
namespace Php7\Image;
/**
 * Creates a single image, by default black on white
 */
class SingleChar {
    /**
     * Allocates a color resource
     *
     * @param array|int $r,
     * @param int $g
     * @param int $b]
     * @return int $color
     */
    public function colorAlloc() 
    { /* code not shown */ } 

现在,使用属性来看看同样的事情:

// /repo/src/Php8/Image/SingleChar.php
namespace Php8\Image;
#[description("Creates a single image")]
class SingleChar {
    #[SingleChar\colorAlloc\description("Allocates color")]
    #[SingleChar\colorAlloc\param("r","int|array")]
    #[SingleChar\colorAlloc\param("g","int")]
    #[SingleChar\colorAlloc\param("b","int")]
    #[SingleChar\colorAlloc\returns("int")]
    public function colorAlloc() { /* code not shown */ }

正如您所看到的,除了提供更健壮的编译和避免上述隐患之外,它在空间使用方面也更高效。

提示

方括号内的内容确实有一些限制;例如,虽然允许使用#[returns("int")],但这不是:#[return("int")。这是因为return是一个关键词。

另一个例子与联合类型有关(在探索新数据类型一节中解释)。您可以在属性中使用#[param("int|array test")],但不允许使用:#[int|array("test")]。另一个特点是类级属性必须放置在紧挨着关键字class的前面和任何use语句之后。

使用反射查看属性

如果您需要从 PHP8 类获取属性信息,Reflection扩展名已经更新,以包含属性支持。添加了一个新的返回ReflectionAttribute实例数组的getAttributes()方法。

在下面的代码块中,显示了Php8\Image\SingleChar::colorAlloc()方法的所有属性:

<?php
// /repo/ch01/php8_attrib_reflect.php
define('FONT_FILE', __DIR__ . '/../fonts/FreeSansBold.ttf');
require_once __DIR__ . '/../src/Server/Autoload/Loader.php';
$loader = new \Server\Autoload\Loader();
use Php8\Image\SingleChar;
$char    = new SingleChar('A', FONT_FILE);
$reflect = new ReflectionObject($char);
$attribs = $reflect->getAttributes();
echo "Class Attributes\n";
foreach ($attribs as $obj) {
    echo "\n" . $obj->getName() . "\n";
    echo implode("\t", $obj->getArguments());
}
echo "Method Attributes for colorAlloc()\n";
$reflect = new ReflectionMethod($char, 'colorAlloc');
$attribs = $reflect->getAttributes();
foreach ($attribs as $obj) {
    echo "\n" . $obj->getName() . "\n";
    echo implode("\t", $obj->getArguments());
}

以下是前面代码片段中显示的代码的输出:

<pre>Class Attributes
Php8\Image\SingleChar
Php8\Image\description
Creates a single image, by default black on whiteMethod
Attributes for colorAlloc()
Php8\Image\SingleChar\colorAlloc\description
Allocates a color resource
Php8\Image\SingleChar\colorAlloc\param
r    int|array
Php8\Image\SingleChar\colorAlloc\param
g    int
Php8\Image\SingleChar\colorAlloc\param
b    int
Php8\Image\SingleChar\colorAlloc\returns
int

前面的输出显示可以使用Reflection扩展类检测属性。最后,在本代码示例中,实际方法如所示:

namespace Php8\Image;use Attribute;
use Php8\Image\Strategy\ {PlainText,PlainFill};
#[SingleChar]
#[description("Creates black on white image")]
class SingleChar {
    // not all code is shown
    #[SingleChar\colorAlloc\description("Allocates color")]
    #[SingleChar\colorAlloc\param("r","int|array")]
    #[SingleChar\colorAlloc\param("g","int")]
    #[SingleChar\colorAlloc\param("b","int")]
    #[SingleChar\colorAlloc\returns("int")]    
    public function colorAlloc(
         int|array $r, int $g = 0, int $b = 0) {
        if (is_array($r))
            [$r, $g, $b] = $r;
        return \imagecolorallocate(
              $this->image, $r, $g, $b);
    }
}

既然您已经了解了如何使用属性,那么让我们继续讨论match表达式,然后讨论命名参数,来讨论新特性。

提示

有关此新功能的更多信息,请查看以下网页:

https://wiki.php.net/rfc/attributes_v2

另外,请参阅此更新:

https://wiki.php.net/rfc/shorter_attribute_syntax_change

关于 PHP DocBlocks 的信息可以在这里找到:

https://phpdoc.org/

有关条令 ORM 的更多信息,请在此处查看:

https://www.doctrine-project.org/projects/orm.html

有关php.ini文件设置的文档可在此处找到:

https://www.php.net/manual/en/ini.list.php

在这里阅读 PHP 反射:

https://www.php.net/manual/en/language.attributes.reflection.php

有关 Rust 编程语言的信息可以在本书中找到:https://www.packtpub.com/product/mastering-rust-second-edition/9781789346572

将匹配表达式合并到程序代码中

在 PHP8 中引入的许多非常有用的特性中,匹配表达式绝对是最突出的。Match表达式是一种更精确的速记语法,有可能取代直接来自 C 语言的陈旧switch语句。在本节中,您将学习如何通过将switch语句替换为match表达式来生成更干净、更准确的程序代码。

匹配表达式通用语法

Match表达式语法很像数组,其中键是要匹配的项,值是表达式。以下是match的一般语法:

$result = match(<EXPRESSION>) {
    <ITEM> => <EXPRESSION>,
   [<ITEM> => <EXPRESSION>,]
    default => <DEFAULT EXPRESSION>
};

表达式必须是有效的 PHP 表达式。表达式示例可包括以下任何一种:

  • 特定值(例如,"some text"
  • 一个操作(例如,$a + $b
  • 匿名函数或类

唯一的限制是表达式必须在一行代码中定义。matchswitch之间的主要差异总结如下:

Table 1.1 – Differences between match and switch

表 1.1–匹配和切换之间的差异

除了注意到的差异外,matchswitch都允许案例聚合,并支持默认案例。

切换并匹配示例

下面是一个使用switch呈现货币符号的简单示例:

// /repo/ch01/php7_switch.php
function get_symbol($iso) {
    switch ($iso) {
        case 'CNY' :
            $sym = '¥';
            break;
        case 'EUR' :
            $sym = '€';
            break;
        case 'EGP' :
        case 'GBP' :
            $sym = '£';
            break;
        case 'THB' :
            $sym = '฿';
            break;
        default :
            $sym = '$';
    }
    return $sym;
}
$test = ['CNY', 'EGP', 'EUR', 'GBP', 'THB', 'MXD'];
foreach ($test as $iso)
    echo 'The currency symbol for ' . $iso
         . ' is ' . get_symbol($iso) . "\n";

执行此代码时,您会看到$test数组中的国际标准化组织ISO)货币代码的货币符号。使用以下代码,可以在 PHP 8 中获得与前面代码片段中所示相同的结果:

// /repo/ch01/php8_switch.php
function get_symbol($iso) {
    return match ($iso) {
        'EGP','GBP' => '£',
        'CNY'       => '¥',
        'EUR'       => '€',
        'THB'       => '฿',
        default     => '$'
    };
}
$test = ['CNY', 'EGP', 'EUR', 'GBP', 'THB', 'MXD'];
foreach ($test as $iso)
    echo 'The currency symbol for ' . $iso
         . ' is ' . get_symbol($iso) . "\n";

两个示例产生相同的输出,如所示:

The currency symbol for CNY is ¥
The currency symbol for EGP is £
The currency symbol for EUR is €
The currency symbol for GBP is £
The currency symbol for THB is ฿
The currency symbol for MXD is $

如前所述,两个代码示例都为存储在$test数组中的 ISO 货币代码列表生成货币符号列表。

复杂匹配示例

回到我们的验证码项目,假设我们希望引入失真,使验证码字符更难阅读。为了实现这一目标,我们引入了许多策略类,每个类产生不同的失真,如下表所示:

Table 1.2 – CAPTCHA distortion strategy classes

表 1.2–验证码失真策略类别

在将要采用的策略列表随机化后,我们使用一个match表达式来执行结果,如下所示:

  1. 首先我们定义一个自动加载器,导入要使用的类,并列出要使用的潜在策略,如下面的代码片段所示:

    php // /repo/ch01/php8_single_strategies.php // not all code is shown require_once __DIR__ . '/../src/Server/Autoload/Loader.php'; $loader = new \Server\Autoload\Loader(); use Php8\Image\SingleChar; use Php8\Image\Strategy\ {LineFill,DotFill,Shadow,RotateText}; $strategies = ['rotate', 'line', 'line',                'dot', 'dot', 'shadow'];

  2. 接下来,我们生成验证码短语,如下所示:

    php $phrase = strtoupper(bin2hex(random_bytes(NUM_BYTES))); $length = strlen($phrase);

  3. 然后,我们循环验证码短语中的每个字符,并创建一个SingleChar实例。对writeFill()的初始调用创建白色背景画布。我们还需要调用shuffle()将失真策略列表随机化。流程如以下代码片段所示:

    php $images = []; for ($x = 0; $x < $length; $x++) {     $char = new SingleChar($phrase[$x], FONT_FILE);     $char->writeFill();     shuffle($strategies);

  4. 然后,我们在原始图像上循环使用策略和层失真。这就是match表达式发挥作用的地方。注意,一个策略需要额外的代码行。由于match只能支持一个表达式,我们只需将多行代码包装成匿名函数,如下所示:

    php foreach ($strategies as $item) {     $func = match ($item) {             'rotate' => RotateText::writeText($char),         'line' => LineFill::writeFill(             $char, rand(1, 10)),         'dot' => DotFill::writeFill($char, rand(10, 20)),         'shadow' => function ($char) {             $num = rand(1, 8);             $r   = rand(0x70, 0xEF);             $g   = rand(0x70, 0xEF);             $b   = rand(0x70, 0xEF);             return Shadow::writeText(                 $char, $num, $r, $g, $b);},         'default' => TRUE     };     if (is_callable($func)) $func($char); }

  5. 剩下要做的就是通过无参数的调用writeText()将图像与实际的验证码短语叠加。之后,我们将失真图像保存为便携式网络图形PNG文件进行显示,如下面的代码片段所示:

    php     $char->writeText();     $fn = $x . '_'          . substr(basename(__FILE__), 0, -4)          . '.png';     $char->save(IMG_DIR . '/' . $fn);     $images[] = $fn; } include __DIR__ . '/captcha_simple.phtml';

以下是从指向与本书关联的 Docker 容器的浏览器运行上述示例的结果:

Figure 1.1 – Distorted CAPTCHA using match expression

图 1.1–使用匹配表达式的失真验证码

接下来,我们来看看另一个非常棒的特性:命名参数。

提示

您可以在这里看到match表达式的原始提案:https://wiki.php.net/rfc/match_expression_v2

理解命名参数

命名参数代表了一种在使用大量参数调用函数或方法时避免混淆的方法。这不仅有助于避免以错误顺序提供参数的问题,还可以帮助您跳过具有默认值的参数。在本节中,您将学习如何应用命名参数来提高代码的准确性,减少未来维护周期中的混乱,并使方法和函数调用更加简洁。我们首先检查使用命名参数所需的通用语法。

命名参数泛型语法

为了使用命名参数,您需要知道函数或方法签名中使用的变量的名称。然后指定该名称,不带美元符号,后跟冒号和要提供的值,如下所示:

$result = function_name( arg1 : <VALUE>, arg2 : <value>);

调用function_name()函数时,将值传递给arg1arg2等对应的参数。

使用命名参数调用核心函数

使用命名参数的最常见的原因之一是当您调用具有大量参数的核心 PHP 函数时。例如,setcookie()的函数签名如下:

setcookie ( string $name [, string $value = "" 
    [, int $expires = 0 [, string $path = "" 
    [, string $domain = "" [, bool $secure = FALSE 
    [, bool $httponly = FALSE ]]]]]] ) : bool

假设您真正想要设置的是namevaluehttponly参数。在 PHP8 之前,您必须查找默认值并按顺序提供它们,直到找到希望覆盖的值为止。在以下情况下,我们希望将httponly设置为TRUE

setcookie('test',1,0,0,'','',FALSE,TRUE);

使用命名参数,PHP 8 中的等效参数如下:

setcookie('test',1,httponly: TRUE);

注意,我们不需要命名前两个参数,因为它们是按顺序提供的。

提示

在 PHP 扩展中,命名参数并不总是与 PHP 文档中函数或方法签名的变量名称相匹配。例如,函数imagettftext()在其函数签名中显示变量$font_filename。但是,如果再向下滚动一点,您将在参数部分看到,命名的参数是fontfile

如果您遇到致命错误:Unknown named parameter $NAMED_PARAM。始终使用文档参数部分中列出的名称,而不是函数或方法签名中的变量名称。

订单独立性和文件编制

命名参数的另一个用途是提供顺序独立性。此外,对于某些核心 PHP 函数,参数的数量之多是一个文档噩梦。

作为一个例子,请看一下imagefttext()的函数签名(注意,该函数是生成安全验证码图像章节项目的核心):

imagefttext ( object $image , float $size , float $angle , 
    int $x , int $y , int $color , string $fontfile , 
    string $text [, array $extrainfo ] ) : array 

可以想象,在 6 个月后回顾工作时,试图记住这些参数的名称和顺序可能会有问题。

重要提示

在 PHP8 中,图像创建函数(例如,imagecreate()现在返回一个GdImage对象实例,而不是资源。GD 扩展中的所有图像函数都已重写以适应此更改。没有必要重写你的代码!

因此,使用命名参数,在 PHP 8 中可以接受以下函数调用:

// /repo/ch01/php8_named_args.php
// not all code is shown
$rotation = range(40, -40, 10);
foreach ($rotation as $key => $offset) {
    $char->writeFill();
    [$x, $y] = RotateText::calcXYadjust($char, $offset);
    $angle = ($offset > 0) ? $offset : 360 + $offset;
    imagettftext(
        angle        : $angle,
        color        : $char->fgColor,
        font_filename : FONT_FILE,
        image        : $char->image,
        size         : 60,                
        x            : $x,
        y            : $y,
        text         : $char->text);
    $fn = IMG_DIR . '/' . $baseFn . '_' . $key . '.png';
    imagepng($char->image, $fn);
    $images[] = basename($fn);
}

刚才显示的代码示例将一组扭曲字符作为一组 PNG 图像文件写出。每个字符相对于其相邻图像顺时针旋转 10 度。注意命名参数是如何应用的,以使imagettftext()函数的参数更容易理解。

命名参数也可以应用于您自己创建的函数和方法。在下一节中,我们将介绍新的数据类型。

提示

命名的参数的详细分析可在此处找到:

https://wiki.php.net/rfc/named_params

探索新的数据类型

任何入门级 PHP 开发人员都会学到的一件事是PHP 提供了哪些数据类型以及如何使用它们。基本数据类型包括int(整数)、floatbool(布尔值)和string。复杂的数据类型包括arrayobject。此外,还有其他数据类型,如NULLresource。在本节中,我们将讨论 PHP8 中引入的一些新数据类型,包括联合类型和混合类型。

重要提示

不要将数据类型数据格式混淆,这一点非常重要。本节介绍数据类型。另一方面,数据格式是表示数据的一种方式,用作传输或存储的一部分。数据格式的示例包括 XML、JavaScript 对象表示法JSON),以及YAML 不是标记语言YAML)。

活接头类型

intstring等其他数据类型不同,需要注意的是,没有明确称为union的数据类型。相反,当您看到对联合类型的引用时,这意味着 PHP8 引入了一种新语法,允许您指定一个类型联合,而不仅仅是一个类型联合。现在让我们看看联合类型的通用语法。

联合类型语法

联合类型的通用语法如下:

function ( type|type|type $var) {}

您可以提供任何现有的数据类型(例如,floatstring,以代替type。然而,有一些限制在很大程度上是完全合理的。此表总结了更重要的限制:

Table 1.3 – Disallowed union types

表 1.3–不允许的活接头类型

从这个例外列表中可以看出,定义联合类型主要是一个常识问题。

提示

最佳实践:当使用联合类型时,类型强制(PHP 在内部转换数据类型以满足函数要求的过程)如果不强制执行严格的类型检查,可能会成为一个问题。因此,最好在使用联合类型的任何文件的顶部添加以下内容:declare(strict_types=1);

有关更多信息,请参见此处的文档参考:

https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict

联合类型示例

为了简单的说明,让我们回到本章中用作示例的SingleChar类。其中一种方法是colorAlloc()。此方法利用imagecolorallocate()函数从图像中分配颜色。它接受表示红色、绿色和蓝色的整数值作为参数。

为了便于讨论,假设第一个参数实际上可以是一个数组,表示三个值,分别为红色、绿色和蓝色。在这种情况下,第一个值的参数类型不能是int,否则,如果提供了数组,如果要启用严格的类型检查,则会抛出错误。

在 PHP 的早期版本中,唯一的解决方案是从第一个参数中删除任何类型检查,并指示在关联的 DocBlock 中接受多个类型。以下是该方法在 PHP 7 中的显示方式:

/**
 * Allocates a color resource
 *
 * @param array|int $r
 * @param int $g
 * @param int $b]
 * @return int $color
 */
public function colorAlloc($r, $g = 0, $b = 0) {
    if (is_array($r)) {
        [$r, $g, $b] = $r;
    }
    return \imagecolorallocate($this->image, $r, $g, $b);
}

第一个参数$r的数据类型的唯一指示是@param array|int $rDocBlock 注释以及没有与该参数相关联的数据类型提示这一事实。在 PHP 8 中,利用联合类型,注意这里的区别:

#[description("Allocates a color resource")]
#[param("int|array r")]
#[int("g")]
#[int("b")]
#[returns("int")]
public function colorAlloc(
    int|array $r, int $g = 0, int $b = 0) {
    if (is_array($r)) {
        [$r, $g, $b] = $r;
    }
    return \imagecolorallocate($this->image, $r, $g, $b);
}

在前面的示例中,除了存在表示第一个参数可以接受arrayint类型的attribute之外,在方法签名本身中,int|array联合类型清楚地表明了这一选择。

混合型

mixed是 PHP8 中引入的另一个新类型。与联合类型不同,mixed是实际的数据类型,表示类型的最终联合。它用于指示接受任何和所有数据类型。从某种意义上说,PHP 已经具备了这种功能:只需完全省略数据类型,它就是一种隐含的mixed类型!

提示

您将在 PHP 文档中看到对mixed类型的引用。PHP8 将这种表示形式化,使其成为实际的数据类型。

为什么要使用混合类型?

请稍等片刻,此时您可能正在思考:为什么还要麻烦使用mixed类型呢?为了让你放心,这是一个很好的问题,没有令人信服的理由使用这种类型。

但是,通过在函数或方法签名中使用mixed,您可以清楚地表示您打算使用此参数。如果您只是将数据类型保留为空,那么稍后使用或查看您的代码的其他开发人员可能会认为您忘记添加该类型。至少,他们将不确定非类型论点的性质。

混合型对遗传的影响

由于mixed类型代表加宽的最终示例,因此当一个类从另一个类扩展时,它可以用于加宽数据类型定义。下面是一个使用mixed类型的示例,说明了这一原理:

  1. 首先,我们使用更严格的数据类型object定义父类,如下所示:

    php // /repo/ch01/php8_mixed_type.php declare(strict_types=1); class High {     const LOG_FILE = __DIR__ . '/../data/test.log';       protected static function logVar(object $var) {              $item = date('Y-m-d') . ':'               . var_export($var, TRUE);         return error_log($item, 3, self::LOG_FILE);     } }

  2. Next, we define a Low class that extends High, as follows:

    php class Low extends High {     public static function logVar(mixed $var) {         $item = date('Y-m-d') . ':'             . var_export($var, TRUE);         return error_log($item, 3, self::LOG_FILE);     } }

    注意在Low类中logVar()方法的数据类型已经加宽mixed

  3. 最后,我们创建了一个实例Low,并用测试数据执行它。从以下代码片段中显示的结果可以看出,一切正常:

    php if (file_exists(High::LOG_FILE)) unlink(High::LOG_FILE) $test = [     'array' => range('A', 'F'),     'func' => function () { return __CLASS__; },     'anon' => new class () {         public function __invoke() {             return __CLASS__; } }, ]; foreach ($test as $item) Low::logVar($item); readfile(High::LOG_FILE);

以下是前一示例的输出:

2020-10-15:array (
  0 => 'A',
  1 => 'B',
  2 => 'C',
  3 => 'D',
  4 => 'E',
  5 => 'F',
)2020-10-15:Closure::__set_state(array(
))2020-10-15:class@anonymous/repo/ch01/php8_mixed_type.php:28$1::__set_state(array())

前面的代码块记录各种不同的数据类型,然后显示日志文件的内容。在这个过程中,这向我们展示了当子类重写父类方法并用mixed数据类型代替更严格的数据类型(如object时,PHP 8 中没有继承问题。

接下来,我们来看看如何使用类型化属性。

提示

最佳实践:定义函数或方法时,为所有参数指定特定的数据类型。如果可以接受几种不同的数据类型,请定义联合类型。否则,如果这些都不适用,则返回到mixed类型。

有关接头类型的信息,请参阅本文档页:

https://wiki.php.net/rfc/union_types_v2

有关mixed类型的更多信息,请查看此处:https://wiki.php.net/rfc/mixed_type_v2.

使用类型化属性改进代码

在本章的第一节中使用构造函数属性提升,我们讨论了如何使用数据类型来控制作为函数或类方法的参数提供的数据类型。但是,这种方法无法保证数据类型永远不变。在本节中,您将了解如何在属性级别分配数据类型,从而更严格地控制 PHP8 中变量的使用。

什么是类型化属性?

PHP7.4 中引入了这一极其重要的特性,PHP8 中继续介绍这一特性。简单地说,类型化属性是一个预分配了数据类型的类属性。下面是一个简单的例子:

// /repo/ch01/php8_prop_type_1.php
declare(strict_types=1)
class Test {
    public int $id = 0;
    public int $token = 0;
    public string $name = '';
}
$test = new Test();
$test->id = 'ABC';

在本例中,如果我们试图将表示除int之外的数据类型的值分配给$test->id,则抛出Fatal error。以下是输出:

Fatal error: Uncaught TypeError: Cannot assign string to property Test::$id of type int in /repo/ch01/php8_prop_type_1.php:11 Stack trace: #0 {main} thrown in /repo/ch01/php8_prop_type_1.php on line 11 

从前面的输出可以看出,当为类型化属性分配了错误的数据类型时,会抛出一个Fatal error

您已经接触过一种属性类型:构造函数属性提升。使用构造函数属性提升定义的所有属性都将自动进行属性类型化!

为什么属性类型很重要?

类型化属性是 PHP 发展趋势的一部分,最早出现在 PHP7 中。现在的趋势是对语言进行改进,限制和收紧代码的使用。这将导致更好的代码,这意味着更少的 bug。

以下示例说明了仅依赖属性类型暗示来控制属性数据类型的危险:

// /repo/ch01/php7_prop_danger.php
declare(strict_types=1);
class Test {
    protected $id = 0;
    protected $token = 0;
    protected $name = '';
    public function __construct(
        int $id, int $token, string $name) {
        $this->id = $id;
        $this->token = md5((string) $token);
        $this->name = $name;
    }
}
$test = new Test(111, 123456, 'Fred');
var_dump($test);

在前面的示例中,请注意在__construct()方法中,$token属性意外转换为字符串。以下是输出:

object(Test)#1 (3) {
  ["id":protected]=>  int(111)
  ["token":protected]=>
  string(32) "e10adc3949ba59abbe56e057f20f883e"
  ["name":protected]=>  string(4) "Fred"
}

任何期望$token为整数的后续代码可能会失败或产生意外的结果。现在,使用类型化属性查看 PHP 8 中的相同内容:

// /repo/ch01/php8_prop_danger.php
declare(strict_types=1);
class Test {
    protected int $id = 0;
    protected int $token = 0;
    protected string $name = '';
    public function __construct(
        int $id, int $token, string $name) {        
        $this->id = $id;
        $this->token = md5((string) $token);
        $this->name = $name;
    }
}
$test = new Test(111, 123456, 'Fred');
var_dump($test);

属性类型可以防止对预分配的数据类型进行任何更改,正如您可以从此处显示的输出中看到的:

Fatal error: Uncaught TypeError: Cannot assign string to property Test::$token of type int in /repo/ch01/php8_prop_danger.php:12

从前面的输出可以看出,当为类型化属性分配了错误的数据类型时,会抛出一个Fatal error。这个例子说明,在进行直接赋值时,将数据类型分配给属性不仅可以防止误用,而且还可以防止类方法内部的属性误用!

属性类型可以减少代码

在代码中引入属性类型的另一个好处是潜在地减少了所需的代码量。作为一个例子,考虑目前的做法,标记属性与知名度的 T0 或 T1,然后,To.T5A.创建一系列的 AutoT2 和 T3 的方法来控制访问(也被称为 OutT7,损坏者 ToT T8A.和 AUTY T9 设置 SET T10TY)。

以下是可能出现的情况:

  1. 首先,我们定义一个具有受保护属性的Test类,如下所示:

    php // /repo/ch01/php7_prop_reduce.php declare(strict_types=1); class Test { protected $id = 0; protected $token = 0; protected $name = '';o

  2. 接下来,我们定义了一系列的getset方法来控制对受保护属性的访问,如下所示:

    php     public function getId() { return $this->id; }     public function setId(int $id) { $this->id = $id;     public function getToken() { return $this->token; }     public function setToken(int $token) {         $this->token = $token;     }     public function getName() {         return $this->name;     }     public function setName(string $name) {         $this->name = $name;     } }

  3. 然后我们使用set方法赋值,如下所示:

    php $test = new Test(); $test->setId(111); $test->setToken(999999); $test->setName('Fred');

  4. 最后,我们将结果显示在一个表中,使用get方法检索属性值,如下所示:

    php $pattern = '<tr><th>%s</th><td>%s</td></tr>'; echo '<table width="50%" border=1>'; printf($pattern, 'ID', $test->getId()); printf($pattern, 'Token', $test->getToken()); printf($pattern, 'Name', $test->getName()); echo '</table>';

以下是可能出现的情况:

Table 1.4 – Output using Get methods

表 1.4–使用 Get 方法的输出

将属性标记为protected(或private)并定义gettersetter的主要目的是控制访问。通常,这会转化为防止属性数据类型更改的愿望。如果是这种情况,可以通过指定属性类型替换整个基础结构。

简单地将可见性更改为public可以减少对getset方法的需要;但是,它不会阻止属性数据被更改!使用 PHP8 属性类型可以实现两个目标:它消除了对getset方法的需要,还可以防止数据类型被意外更改。

请注意,在 PHP 8 中使用属性类型实现相同的结果所需的代码要少得多:

// /repo/ch01/php8_prop_reduce.php
declare(strict_types=1);
class Test {
    public int $id = 0;
    public int $token = 0;
    public string  $name = '';
}
// assign values
$test = new Test();
$test->id = 111;
$test->token = 999999;
$test->name = 'Fred';
// display results
$pattern = '<tr><th>%s</th><td>%s</td></tr>';
echo '<table width="50%" border=1>';
printf($pattern, 'ID', $test->id);
printf($pattern, 'Token', $test->token);
printf($pattern, 'Name', $test->name);
echo '</table>';

上面显示的代码示例生成与前面示例完全相同的输出,还实现了对属性数据类型的更好控制。在本例中,通过使用类型化属性,我们在生成相同结果所需的代码量上实现了50%的减少

提示

最佳实践:尽可能使用类型化属性,除非您明确希望允许数据类型更改。

总结

在本章中,您学习了如何使用新的 PHP8 数据类型(混合类型和联合类型)编写更好的代码。您还了解了使用命名参数不仅可以提高代码的可读性,还可以帮助防止意外误用类方法和 PHP 函数,以及提供跳过默认参数的好方法。

本章还向您介绍了如何使用新的Attribute类作为 PHP DocBlocks 的最终替代品,以提高代码的整体性能,同时提供一种记录类、方法和函数的可靠方法。

此外,我们还研究了 PHP8 如何通过利用构造函数参数提升和类型化属性,大大减少早期 PHP 版本所需的代码量。

在下一章中,您将了解 PHP8 在功能和过程级别的新特性。