三、使用 PHP 函数式编程

在本章中,我们将介绍以下主题:

  • 发展功能
  • 暗示数据类型
  • 使用返回值数据类型
  • 使用迭代器
  • 使用生成器编写自己的迭代器

导言

在本章中,我们将考虑利用 PHP 的 To.T0.函数编程的方法。功能性编程,或过程性编程,是在 PHP 版本 4 中引入面向对象编程OOP的第一个实现之前,PHP 代码编写的传统方式。函数式编程是将程序逻辑封装到一系列离散函数中,这些函数通常存储在单独的 PHP 文件中。然后,该文件可以包含在将来的任何脚本中,从而允许随意调用定义的函数。

开发功能

最困难的方面是决定如何将编程逻辑分解为函数。另一方面,在 PHP 中开发函数的机制非常简单。只需使用function关键字,给它一个名称,然后在后面加括号。

怎么做。。。

  1. 代码本身放在大括号内,如下所示:

    php function someName ($parameter) { $result = 'INIT'; // one or more statements which do something // to affect $result $result .= ' and also ' . $parameter; return $result; }

  2. You can define one or more parameters. To make one of them optional, simply assign a default value. If you are not sure what default value to assign, use NULL:

    php function someOtherName ($requiredParam, $optionalParam = NULL) { $result = 0; $result += $requiredParam; $result += $optionalParam ?? 0; return $result; }

    不能重新定义函数。唯一的例外是在单独的名称空间中定义重复的函数。此定义将生成一个错误:

    php function someTest() { return 'TEST'; } function someTest($a) { return 'TEST:' . $a; }

  3. 如果您不知道将向函数提供多少个参数,或者如果您希望允许无限多个参数,请使用...后跟变量名。提供的所有参数将在变量

    php function someInfinite(...$params) { // any params passed go into an array $params return var_export($params, TRUE); }

    中显示为一个数组 4. A function can call itself. This is referred to as recursion. The following function performs a recursive directory scan:

    php function someDirScan($dir) { // uses "static" to retain value of $list static $list = array(); // get a list of files and directories for this path $list = glob($dir . DIRECTORY_SEPARATOR . '*'); // loop through foreach ($list as $item) { if (is_dir($item)) { $list = array_merge($list, someDirScan($item)); } } return $list; }

    在函数中使用static关键字已经有 12 年多的历史了。static所做的是初始化变量一次(即在声明static时),然后在同一请求中保留函数调用之间的值。

    如果需要在 HTTP 请求之间保留变量的值,请确保 PHP 会话已启动,并将该值存储在$_SESSION中。

  4. 在 PHP命名空间中定义时,函数受到约束。此特性可用于在函数库之间提供额外的逻辑分隔。为了锚定名称空间,您需要添加use关键字。以下示例放置在单独的名称空间中。请注意,即使函数名相同,也没有冲突,因为它们彼此不可见。

  5. 我们在名称空间Alpha中定义someFunction()。我们将其保存到一个单独的 PHP 文件中,chap_03_developing_functions_namespace_alpha.php

    ```php <?php namespace Alpha;

    function someFunction() { echo NAMESPACE . ':' . FUNCTION . PHP_EOL; } ```

  6. 然后我们在名称空间Beta中定义someFunction()。我们将其保存到一个单独的 PHP 文件中,chap_03_developing_functions_namespace_beta.php

    ```php <?php namespace Beta;

    function someFunction() { echo NAMESPACE . ':' . FUNCTION . PHP_EOL; } ```

  7. We can then call someFunction() by prefixing the function name with the namespace name:

    php include (__DIR__ . DIRECTORY_SEPARATOR . 'chap_03_developing_functions_namespace_alpha.php'); include (__DIR__ . DIRECTORY_SEPARATOR . 'chap_03_developing_functions_namespace_beta.php'); echo Alpha\someFunction(); echo Beta\someFunction();

    提示

    最佳实践

    将函数库(以及类!)放在单独的文件中被认为是最佳实践:每个名称空间一个文件,每个文件一个类或函数库。

    可以在单个命名空间中定义多个类或函数库。将开发成单独名称空间的唯一原因是,如果希望促进功能的逻辑分离。

它是如何工作的。。。

将所有逻辑相关的函数放在一个单独的 PHP 文件中被认为是最佳实践。创建一个名为chap_03_developing_functions_library.php的文件,并将这些函数(如前所述)放入:

  • someName()
  • someOtherName()
  • someInfinite()
  • someDirScan()
  • someTypeHint()

然后,该文件将包含在使用这些函数的代码中。

include (__DIR__ . DIRECTORY_SEPARATOR . 'chap_03_developing_functions_library.php');

要调用函数someName(),请使用名称并提供参数。

echo someName('TEST');   // returns "INIT and also TEST"

您可以使用一个或两个参数调用someOtherName()函数,如下所示:

echo someOtherName(1);    // returns  1
echo someOtherName(1, 1);   //  returns 2

someInfinite()函数接受无限(或可变)数量的参数。下面是调用此函数的两个示例:

echo someInfinite(1, 2, 3);
echo PHP_EOL;
echo someInfinite(22.22, 'A', ['a' => 1, 'b' => 2]);

输出如下所示:

How it works...

我们可以调用someDirScan()如下:

echo someInfinite(1, 2, 3);
echo PHP_EOL;
echo someInfinite(22.22, 'A', ['a' => 1, 'b' => 2]);

输出如下所示:

How it works...

暗示数据类型

在许多情况下,在开发函数时,您可能会在其他项目中重用相同的函数库。此外,如果您与团队合作,您的代码可能会被其他开发人员使用。为了控制代码的使用,可以使用类型的提示。这涉及到指定函数期望该特定参数的数据类型。

怎么做。。。

  1. 函数中的参数可以用类型提示作为前缀。以下类型提示在 PHP 5 和 PHP 7 中都可用:
    • 大堆
    • 可调用
  2. If a call to the function is made, and the wrong parameter type is passed, a TypeError is thrown. The following example requires an array, an instance of DateTime, and an anonymous function:

    php function someTypeHint(Array $a, DateTime $t, Callable $c) { $message = ''; $message .= 'Array Count: ' . count($a) . PHP_EOL; $message .= 'Date: ' . $t->format('Y-m-d') . PHP_EOL; $message .= 'Callable Return: ' . $c() . PHP_EOL; return $message; }

    提示

    您不必为每个参数提供类型提示。仅当提供不同的数据类型会对函数的处理产生负面影响时,才使用此技术。例如,如果您的函数使用foreach()循环,如果您不提供数组或实现Traversable的东西,则会生成错误。

  3. 在 PHP7 中,假设生成了相应的declare()指令,则允许标量(即整数、浮点、布尔和字符串)类型的提示。另一个函数演示了如何实现这一点。在包含要使用标量类型暗示的函数的代码库文件的顶部,在打开的 PHP 标记

    php declare(strict_types=1);

    之后添加此declare()指令 4. 现在您可以定义一个包含标量类型提示的函数:

    php function someScalarHint(bool $b, int $i, float $f, string $s) { return sprintf("\n%20s : %5s\n%20s : %5d\n%20s " . ": %5.2f\n%20s : %20s\n\n", 'Boolean', ($b ? 'TRUE' : 'FALSE'), 'Integer', $i, 'Float', $f, 'String', $s); }

  4. 在 PHP7 中,假设声明了严格类型暗示,布尔类型暗示的工作原理与其他三种标量类型(即整数、浮点和字符串)略有不同。您可以提供任何标量作为参数,并且不会抛出TypeError!但是,传入值在传入函数后将自动转换为布尔数据类型。如果传递标量以外的任何数据类型(即数组或对象),将抛出一个TypeError。下面是定义boolean数据类型的函数示例。请注意,返回值将自动转换为boolean

    php function someBoolHint(bool $b) { return $b; }

它是如何工作的。。。

首先,您可以将someTypeHint()someScalarHint()someBoolHint()这三个函数放在一个单独的文件中,以包括在内。在本例中,我们将文件命名为chap_03_developing_functions_type_hints_library.php。别忘了在顶部加上declare(strict_types=1)

在我们的调用代码中,您将包含以下文件:

include (__DIR__ . DIRECTORY_SEPARATOR . 'chap_03_developing_functions_type_hints_library.php');

要测试someTypeHint(),请调用函数两次,一次使用正确的数据类型,第二次使用错误的类型。但是,这将抛出一个TypeError,因此您需要将函数调用包装在一个try { ... } catch () { ...}块中:

try {
    $callable = function () { return 'Callback Return'; };
    echo someTypeHint([1,2,3], new DateTime(), $callable);
    echo someTypeHint('A', 'B', 'C');
} catch (TypeError $e) {
    echo $e->getMessage();
    echo PHP_EOL;
}

从本小节末尾显示的输出可以看出,当传递正确的数据类型时,没有问题。当传递不正确的类型时,抛出一个TypeError

在 PHP7 中,某些错误已转换为一个Error类,该类的处理方式与Exception类类似。这意味着你可以抓住一个ErrorTypeErrorError的一个特定后代,当向函数传递不正确的数据类型时会抛出该后代。

所有 PHP7Error类与Exception类一样实现Throwable接口。如果您不确定是否需要捕捉ErrorException,可以添加捕捉Throwable的块。

接下来您可以测试someScalarHint(),使用正确和错误的值调用它两次,将调用包装在try { ... } catch () { ...}块中:

try {
    echo someScalarHint(TRUE, 11, 22.22, 'This is a string');
    echo someScalarHint('A', 'B', 'C', 'D');
} catch (TypeError $e) {
    echo $e->getMessage();
}

正如预期的那样,对函数的第一次调用起作用,第二次调用抛出一个TypeError

当对布尔值进行类型暗示时,传递的任何标量值都不会导致抛出一个TypeError!相反,该值将被解释为其布尔等效值。如果随后返回此值,则数据类型将更改为布尔值。

要测试这一点,请调用前面定义的someBoolHint()函数,并将任何标量值作为参数传入。var_dump()方法显示数据类型始终为布尔型:

try {
    // positive results
    $b = someBooleanHint(TRUE);
    $i = someBooleanHint(11);
    $f = someBooleanHint(22.22);
    $s = someBooleanHint('X');
    var_dump($b, $i, $f, $s);
    // negative results
    $b = someBooleanHint(FALSE);
    $i = someBooleanHint(0);
    $f = someBooleanHint(0.0);
    $s = someBooleanHint('');
    var_dump($b, $i, $f, $s);
} catch (TypeError $e) {
    echo $e->getMessage();
}

如果您现在尝试相同的函数调用,但传入了非标量数据类型,则会抛出一个TypeError

try {
    $a = someBoolHint([1,2,3]);
    var_dump($a);
} catch (TypeError $e) {
    echo $e->getMessage();
}
try {
    $o = someBoolHint(new stdClass());
    var_dump($o);
} catch (TypeError $e) {
    echo $e->getMessage();
}

以下是总输出:

How it works...

另见

PHP7.1 引入了一个新的类型提示iterable,它允许数组、IteratorsGenerators 作为参数。有关更多信息,请参见此:

有关标量类型暗示实现背后的基本原理的背景讨论,请参阅本文:

使用返回值数据类型

PHP7 允许为函数的返回值指定数据类型。然而,与标量类型暗示不同,您不需要添加任何特殊声明。

怎么做。。。

  1. This example shows you how to assign a data type to a function return value. To assign a return data type, first define the function as you would normally. After the closing parenthesis, add a space, followed by the data type and a colon:

    php function returnsString(DateTime $date, $format) : string { return $date->format($format); }

    PHP7.1 引入了一种返回数据类型的变体,称为可空类型。您只需将string更改为?string。这允许函数返回stringNULL

  2. 函数返回的任何内容,无论其在函数中的数据类型如何,都将转换为声明的数据类型作为返回值。注意,在本例中,$a$b$c的值加在一起产生一个单一的和,并返回。通常,您希望返回值是数字数据类型。然而,在本例中,返回数据类型被声明为string,这将覆盖 PHP 的类型转换过程:

    ```php function convertsToString($a, $b, $c) : string

    return $a + $b + $c; } ```

  3. You can also assign classes as a return data type. In this example, we assign a return type of DateTime, part of the PHP DateTime extension:

    php function makesDateTime($year, $month, $day) : DateTime { $date = new DateTime(); $date->setDate($year, $month, $day); return $date; }

    makesDateTime()函数可能是标量类型暗示的候选函数。如果$year$month$day不是整数,则调用setDate()时生成Warning。如果使用标量类型暗示,并且传递了错误的数据类型,则会抛出一个TypeError。尽管生成警告或抛出TypeError并不重要,但至少TypeError会让误用代码的错误开发人员坐起来注意!

  4. If a function has a return data type, and you return the wrong data type in your function code, a TypeError will be thrown at runtime. This function assigns a return type of DateTime, but returns a string instead. A TypeError will be thrown, but not until runtime, when the PHP engine detects the discrepancy:

    php function wrongDateTime($year, $month, $day) : DateTime { return date($year . '-' . $month . '-' . $day); }

    如果返回数据类型类不是内置 PHP 类(即 SPL 的一部分),则需要确保该类已自动加载或包含。

它是如何工作的。。。

首先,将前面提到的函数放入名为chap_03_developing_functions_return_types_library.php的库文件中。此文件需要包含在调用以下函数的chap_03_developing_functions_return_types.php脚本中:

include (__DIR__ . '/chap_03_developing_functions_return_types_library.php');

现在可以调用returnsString(),提供DateTime实例和格式字符串:

$date   = new DateTime();
$format = 'l, d M Y';
$now    = returnsString($date, $format);
echo $now . PHP_EOL;
var_dump($now);

正如所料,输出是一个字符串:

How it works...

现在您可以调用convertsToString()并提供三个整数作为参数。请注意,返回类型为 string:

echo "\nconvertsToString()\n";
var_dump(convertsToString(2, 3, 4));

How it works...

为了证明这一点,您可以指定一个类作为返回值,使用三个整数参数调用makesDateTime()

echo "\nmakesDateTime()\n";
$d = makesDateTime(2015, 11, 21);
var_dump($d);

How it works...

最后,用三个整数参数调用wrongDateTime()

try {
    $e = wrongDateTime(2015, 11, 21);
    var_dump($e);
} catch (TypeError $e) {
    echo $e->getMessage();
}

请注意,在运行时抛出了一个和TypeError

How it works...

还有更多。。。

PHP7.1 增加了一个新的返回值类型void。当您不希望从函数返回任何值时,使用此选项。更多信息请参见参考https://wiki.php.net/rfc/void_return_type

另见

有关返回类型声明的更多信息,请参阅以下文章:

有关可空类型的信息,请参阅本文:

使用迭代器

迭代器是一种特殊类型的类,允许您遍历容器或列表。这里的关键字是遍历。这意味着迭代器提供了遍历列表的方法,但它本身并不执行遍历。

SPL 为不同的上下文提供了丰富的泛型和专用迭代器。例如,ArrayIterator被设计为允许数组的面向对象遍历。DirectoryIterator是为文件系统扫描而设计的。

某些 SPL 迭代器设计用于与其他迭代器一起工作,并增加价值。示例包括FilterIteratorLimitIterator。前者使您能够从父迭代器中删除不需要的值。后者提供分页功能,通过该功能,您可以指定要遍历的项目数量以及确定从何处开始的偏移量。

最后,还有一系列递归迭代器,允许您重复调用父迭代器。一个例子是RecursiveDirectoryIterator,它扫描一个目录树,从起点一直扫描到最后一个可能的子目录。

怎么做。。。

  1. We first examine the ArrayIterator class. It's extremely easy to use. All you need to do is to supply an array as an argument to the constructor. After that you can use any of the methods that are standard to all SPL-based iterators, such as current(), next(), and so on.

    php $iterator = new ArrayIterator($array);

    使用ArrayIterator将标准 PHP 数组转换为迭代器。从某种意义上说,这在过程编程和面向对象编程之间架起了一座桥梁。

  2. 作为迭代器的实际使用示例,请看一下这个示例。它需要一个迭代器并生成一系列 HTML<ul><li>标记:

    php function htmlList($iterator) { $output = '<ul>'; while ($value = $iterator->current()) { $output .= '<li>' . $value . '</li>'; $iterator->next(); } $output .= '</ul>'; return $output; }

  3. 或者,您可以简单地将ArrayIterator实例包装成一个简单的foreach()循环:

    php function htmlList($iterator) { $output = '<ul>'; foreach($iterator as $value) { $output .= '<li>' . $value . '</li>'; } $output .= '</ul>'; return $output; }

  4. CallbackFilterIterator是为您可能正在使用的任何现有迭代器增加价值的一种好方法。它允许您包装任何现有迭代器并筛选输出。在本例中,我们将定义fetchCountryName(),它通过一个数据库查询进行迭代,该查询生成一个国家名称列表。首先,我们从一个查询中定义了一个 Ont2 的实例,该实例使用了在 Ont5 中定义的 AUTT3R 类,第 1 章 AUTT6T,AUTT7。

  5. 接下来,我们定义了一个过滤方法nameFilterIterator(),它接受一个部分国家名称作为参数,同时还有ArrayIterator实例:

    php function nameFilterIterator($innerIterator, $name) { if (!$name) return $innerIterator; $name = trim($name); $iterator = new CallbackFilterIterator($innerIterator, function($current, $key, $iterator) use ($name) { $pattern = '/' . $name . '/i'; return (bool) preg_match($pattern, $current); } ); return $iterator; }

  6. LimitIterator adds a basic pagination aspect to your applications. To use this iterator, you only need to supply the parent iterator, an offset, and a limit. LimitIterator will then only produce a subset of the entire data set starting at the offset. Taking the same example mentioned in step 2, we'll paginate the results coming from our database query. We can do this quite simply by wrapping the iterator produced by the fetchCountryName() method inside a LimitIterator instance:

    php $pagination = new LimitIterator(fetchCountryName( $sql, $connection), $offset, $limit);

    使用LimitIterator时要小心。它需要将整个数据集存储在内存中才能实现限制。因此,在遍历大型数据集时,这不是一个好的工具。

  7. 迭代器可以堆叠。在这个简单示例中,ArrayIteratorFilterIterator处理,而FilterIterator又受LimitIterator的限制。首先我们设置了一个ArrayIterator

    php $i = new ArrayIterator($a);

    的实例 8. 接下来,我们将ArrayIterator插入FilterIterator实例。注意,我们正在使用新的 PHP7 匿名类特性。在这种情况下,匿名类扩展了FilterIterator并重写了accept()方法,只允许带有偶数 ASCII 码的字母:

    php $f = new class ($i) extends FilterIterator { public function accept() { $current = $this->current(); return !(ord($current) & 1); } };

  8. 最后,我们提供FilterIterator实例作为LimitIterator的参数,并提供偏移量(2在本例中)和限制(6在本例中):

    php $l = new LimitIterator($f, 2, 6);

  9. 然后,我们可以定义一个简单的函数来显示输出,并依次调用每个迭代器以查看由range('A', 'Z')

    ```php function showElements($iterator) { foreach($iterator as $item) echo $item . ' '; echo PHP_EOL; }

    $a = range('A', 'Z'); $i = new ArrayIterator($a); showElements($i); ```

    生成的简单数组上的结果 11. 这里有一个变体,它通过将一个FilterIterator堆叠在一个ArrayIterator

    php $f = new class ($i) extends FilterIterator { public function accept() { $current = $this->current(); return !(ord($current) & 1); } }; showElements($f);

    的顶部来生成每一个字母 12. And here's yet another variation that only produces F H J L N P, which demonstrates a LimitIterator that consumes a FilterIterator, which in turn consumes an ArrayIterator. The output of these three examples is as follows:

    php $l = new LimitIterator($f, 2, 6); showElements($l);

    How to do it...

  10. 回到生成国家名称列表的示例,假设我们希望遍历由国家名称和 ISO 代码组成的多维数组,而不仅仅是国家名称。到目前为止,提到的简单迭代器还不够。相反,我们将使用所谓的递归迭代器。

  11. 首先,我们需要定义一个方法,该方法使用前面提到的数据库连接类从数据库中提取所有列。与前面一样,我们返回一个填充了查询数据的ArrayIterator实例:

    php function fetchAllAssoc($sql, $connection) { $iterator = new ArrayIterator(); $stmt = $connection->pdo->query($sql); while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $iterator->append($row); } return $iterator; }

  12. 乍一看,人们很容易将一个标准的ArrayIterator实例包装在RecursiveArrayIterator中。不幸的是,这种方法只执行一个迭代,并没有给我们想要的:通过数据库查询返回的多维数组的所有元素进行迭代:

    php $iterator = fetchAllAssoc($sql, $connection); $shallow = new RecursiveArrayIterator($iterator);

  13. 虽然这将返回一个迭代,其中每个项表示数据库查询中的一行,但在本例中,我们希望提供一个迭代,该迭代将遍历查询返回的所有行的所有列。为了实现这一点,我们需要通过RecursiveIteratorIterator推出大黄铜。

  14. Monty Python 的粉丝们将为这个类名的丰富讽刺而欢欣鼓舞,因为它带回了部门的美好回忆。合适的是,这个类会让我们的老朋友RecursiveArrayIterator类超时工作,并在数组的各个级别执行深度迭代:

    php $deep = new RecursiveIteratorIterator($shallow);

它是如何工作的。。。

作为一个实际示例,您可以开发一个测试脚本,该脚本使用迭代器实现过滤和分页。对于这个示例,您可以调用chap_03_developing_functions_filtered_and_paginated.php测试代码文件。

首先,遵循最佳实践,将上述函数放入名为chap_03_developing_functions_iterators_library.php的 include 文件中。在测试脚本中,确保包含此文件。

数据源是一个名为iso_country_codes的表,其中包含 ISO2、ISO3 和国家名称。数据库连接可能位于config/db.config.php文件中。您还可以包括上一章中讨论的Application\Database\Connection类:

define('DB_CONFIG_FILE', '/../config/db.config.php');
define('ITEMS_PER_PAGE', [5, 10, 15, 20]);
include (__DIR__ . '/chap_03_developing_functions_iterators_library.php');
include (__DIR__ . '/../Application/Database/Connection.php');

在 PHP7 中,可以将常量定义为数组。在本例中,ITEMS_PER_PAGE被定义为数组,用于生成 HTMLSELECT元素。

接下来,您可以处理国家名称和每页项目数的输入参数。当前页码将从0开始,可以增加(下一页)或减少(上一页):

$name = strip_tags($_GET['name'] ?? '');
$limit  = (int) ($_GET['limit'] ?? 10);
$page   = (int) ($_GET['page']  ?? 0);
$offset = $page * $limit;
$prev   = ($page > 0) ? $page - 1 : 0;
$next   = $page + 1;

现在您已经准备好启动数据库连接并运行一个简单的SELECT查询。应将其放置在try {} catch {}块中。然后,您可以将要堆叠的迭代器放置在try {}块中:

try {
    $connection = new Application\Database\Connection(
      include __DIR__ . DB_CONFIG_FILE);
    $sql    = 'SELECT * FROM iso_country_codes';
    $arrayIterator    = fetchCountryName($sql, $connection);
    $filteredIterator = nameFilterIterator($arrayIterator, $name);
    $limitIterator    = pagination(
    $filteredIterator, $offset, $limit);
} catch (Throwable $e) {
    echo $e->getMessage();
}

现在,我们已经为 HTML 做好了准备。在这个简单的示例中,我们提供了一个表单,允许用户选择每页的项目数和国家名称:

<form>
  Country Name:
  <input type="text" name="name" 
         value="<?= htmlspecialchars($name) ?>">
  Items Per Page: 
  <select name="limit">
    <?php foreach (ITEMS_PER_PAGE as $item) : ?>
      <option<?= ($item == $limit) ? ' selected' : '' ?>>
      <?= $item ?></option>
    <?php endforeach; ?>
  </select>
  <input type="submit" />
</form>
  <a href="?name=<?= $name ?>&limit=<?= $limit ?>
    &page=<?= $prev ?>">
  << PREV</a> 
  <a href="?name=<?= $name ?>&limit=<?= $limit ?>
    &page=<?= $next ?>">
  NEXT >></a>
<?= htmlList($limitIterator); ?>

输出将如下所示:

How it works...

最后,为了测试 country 数据库查找的递归迭代,您需要包括迭代器的库文件以及Application\Database\Connection类:

define('DB_CONFIG_FILE', '/../config/db.config.php');
include (__DIR__ . '/chap_03_developing_functions_iterators_library.php');
include (__DIR__ . '/../Application/Database/Connection.php');

和前面一样,您应该将数据库查询包装在一个try {} catch {}块中。然后,您可以将测试递归迭代的代码放在try {}块中:

try {
    $connection = new Application\Database\Connection(
    include __DIR__ . DB_CONFIG_FILE);
    $sql    = 'SELECT * FROM iso_country_codes';
    $iterator = fetchAllAssoc($sql, $connection);
    $shallow  = new RecursiveArrayIterator($iterator);
    foreach ($shallow as $item) var_dump($item);
    $deep     = new RecursiveIteratorIterator($shallow);
    foreach ($deep as $item) var_dump($item);     
} catch (Throwable $e) {
    echo $e->getMessage();
}

以下是从RecursiveArrayIterator的输出中可以看到的内容:

How it works...

以下是使用RecursiveIteratorIterator后的输出:

How it works...

使用生成器编写自己的迭代器

在前面的一组配方中,我们演示了 PHP7SPL 中提供的迭代器的使用。但是如果这一套不能为您提供给定项目所需的呢?一种解决方案是开发一个函数,该函数不构建随后返回的数组,而是使用yield关键字通过迭代逐步返回值。这种功能被称为发生器。事实上,在后台,PHP 引擎会自动将您的函数转换为一个名为Generator的特殊内置类。

这种方法有几个优点。当您要遍历一个大容器(即,解析一个大文件)时,可以看到它的主要好处。传统的方法是构建一个数组,然后返回该数组。问题是你实际上把所需的内存量增加了一倍!此外,性能也会受到影响,因为只有在返回最终数组后才能获得结果。

怎么做。。。

  1. 在这个例子中,我们建立在基于迭代器的函数库上,添加了一个我们自己设计的生成器。在本例中,我们将在迭代器上复制上面一节中描述的功能,在迭代器中我们堆叠了一个ArrayIteratorFilterIteratorLimitIterator
  2. 因为我们需要访问源数组、所需的过滤器、页码和每页的项目数,所以我们将适当的参数包含到一个filteredResultsGenerator()函数中。然后,我们根据页码和限制(即每页的项目数)计算偏移量。接下来,我们在数组中循环,应用过滤器,如果偏移量尚未达到,则继续循环,如果达到限制,则中断循环:

    php function filteredResultsGenerator(array $array, $filter, $limit = 10, $page = 0) { $max = count($array); $offset = $page * $limit; foreach ($array as $key => $value) { if (!stripos($value, $filter) !== FALSE) continue; if (--$offset >= 0) continue; if (--$limit <= 0) break; yield $value; } }

  3. 你会注意到这个函数和其他函数的主要区别是yield关键字。这个关键字的作用是通知 PHP 引擎生成一个Generator实例并封装代码。

它是如何工作的。。。

为了演示filteredResultsGenerator()函数的使用,我们将让您实现一个 web 应用程序,该应用程序扫描网页,并生成一个过滤和分页的 URL 列表,这些 URL 是从HREF属性中筛选出来的。

首先,您需要将filteredResultsGenerator()函数的代码添加到前面配方中使用的库文件中,然后将前面描述的函数放入一个包含文件chap_03_developing_functions_iterators_library.php

接下来,定义一个测试查找 T2 脚本,包括 To.t3.函数库和定义了 Tyl T1 的文件,在第 4 章第 1 章中,描述了一个基础 T7:

include (__DIR__ . DIRECTORY_SEPARATOR . 'chap_03_developing_functions_iterators_library.php');
include (__DIR__ . '/../Application/Web/Hoover.php');

然后,您需要从用户那里收集有关要扫描的 URL、要用作筛选器的字符串、每页有多少项以及当前页码的输入。

空合并操作符(??是从 Web 获取输入的理想选择。如果未定义,则不会生成任何通知。如果未从用户输入接收参数,则可以提供默认值。

$url    = trim(strip_tags($_GET['url'] ?? ''));
$filter = trim(strip_tags($_GET['filter'] ?? ''));
$limit  = (int) ($_GET['limit'] ?? 10);
$page   = (int) ($_GET['page']  ?? 0);

提示

最佳实践

网络安全应始终是优先考虑的事项。在本例中,您可以使用strip_tags()并强制数据类型为整数(int)作为清理用户输入的措施。

然后,您可以定义分页列表中上一页和下一页链接中使用的变量。请注意,您还可以应用健全性检查,以确保下一页不会偏离结果集的末尾。为简洁起见,此示例中未应用此类健全性检查:

$next   = $page + 1;
$prev   = $page - 1;
$base   = '?url=' . htmlspecialchars($url) 
        . '&filter=' . htmlspecialchars($filter) 
        . '&limit=' . $limit 
        . '&page=';

然后我们需要创建一个Application\Web\Hoover实例并从目标 URL 获取HREF属性:

$vac    = new Application\Web\Hoover();
$list   = $vac->getAttribute($url, 'href');

最后,我们定义呈现输入表单的 HTML 输出,并通过前面描述的htmlList()函数运行生成器:

<form>
<table>
<tr>
<th>URL</th>
<td>
<input type="text" name="url" 
  value="<?= htmlspecialchars($url) ?>"/>
</td>
</tr>
<tr>
<th>Filter</th>
<td>
<input type="text" name="filter" 
  value="<?= htmlspecialchars($filter) ?>"/></td>
</tr>
<tr>
<th>Limit</th>
<td><input type="text" name="limit" value="<?= $limit ?>"/></td>
</tr>
<tr>
<th>&nbsp;</th><td><input type="submit" /></td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<a href="<?= $base . $prev ?>"><-- PREV | 
<a href="<?= $base . $next ?>">NEXT --></td>
</tr>
</table>
</form>
<hr>
<?= htmlList(filteredResultsGenerator(
$list, $filter, $limit, $page)); ?>

以下是一个输出示例:

How it works...