如何在 foreach 循环之前/之后触发代码,只有在 PHP 7.4+ 中以一种有效的方式输入这个循环?

Posted

技术标签:

【中文标题】如何在 foreach 循环之前/之后触发代码,只有在 PHP 7.4+ 中以一种有效的方式输入这个循环?【英文标题】:How to trigger code before/after a foreach loop, only if this one would be entered, in an efficient way in PHP 7.4+? 【发布时间】:2020-06-15 02:19:01 【问题描述】:

这有点类似于问题:How to determine the first and last iteration in a foreach loop?,但是,+10 年的问题很大程度上是面向array 的,并且没有一个答案与可以循环许多不同类型的事实兼容。

如果在 php >= 7.4(数组、迭代器、生成器、PDOStatementDatePeriod、对象属性...)中可以迭代,我们可以以一种有效的方式触发需要在循环之前/之后发生的代码,但仅限于进入循环的情况

一个典型的用例可能是生成一个 html 列表:

<ul>
  <li>...</li>
  <li>...</li>
  ...
</ul>

&lt;ul&gt;&lt;/ul&gt; 必须在存在某些元素时打印。

这些是我目前发现的约束:

    empty():不能用于generators/iterators。 each(): 已弃用。 iterator_to_array() 破坏了生成器的优势。 在循环内部和之后测试的 boolean 标志不被认为是高效,因为它会导致该测试在每次迭代时执行,而不是在开始时执行一次并在循环结束时执行一次。 虽然在上述示例中可以使用 输出缓冲 或字符串连接来生成输出,但它不适合循环不会产生任何输出的情况。 (感谢@barmar 提供额外的想法)

下面的代码sn-p总结了很多不同的类型,我们可以用foreach迭代,它可以作为一个开始提供答案:

<?php

// Function that iterates on $iterable
function iterateOverIt($iterable) 
    // How to generate the "<ul>"?
    foreach ($iterable as $item) 
        echo "<li>", $item instanceof DateTime ? $item->format("c") : (
            isset($item["col"]) ? $item["col"] : $item
        ), "</li>\n";
    
    // How to generate the "</ul>"?


// Empty array
iterateOverIt([]);
iterateOverIt([1, 2, 3]);

// Empty generator
iterateOverIt((function () : Generator 
    return;
    yield;
)());
iterateOverIt((function () : Generator 
    yield 4;
    yield 5;
    yield 6;
)());

// Class with no public properties
iterateOverIt(new stdClass());
iterateOverIt(new class  public $a = 7, $b = 8, $c = 9;);

$db = mysqli_connect("localhost", "test", "test", "test");
// Empty resultset
iterateOverIt($db->query("SELECT 0 FROM DUAL WHERE false"));
iterateOverIt($db->query("SELECT 10 AS col UNION SELECT 11 UNION SELECT 12"));

// DatePeriod generating no dates
iterateOverIt(new DatePeriod(new DateTime("2020-01-01 00:00:00"), new DateInterval("P1D"), new DateTime("2020-01-01 00:00:00"), DatePeriod::EXCLUDE_START_DATE));
iterateOverIt(new DatePeriod(new DateTime("2020-01-01 00:00:00"), new DateInterval("P1D"), 3, DatePeriod::EXCLUDE_START_DATE));

这样的脚本应该会产生以下输出:

<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
<ul>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<ul>
<li>10</li>
<li>11</li>
<li>12</li>
</ul>
<ul>
<li>2020-01-02T00:00:00+00:00</li>
<li>2020-01-03T00:00:00+00:00</li>
<li>2020-01-04T00:00:00+00:00</li>
</ul>

【问题讨论】:

您可以使用输出缓冲功能。缓冲循环的输出。然后检查缓冲区是否为空。 或者只是将循环的结果放在一个字符串中而不是回显它,并检查该字符串是否为空。 对我来说有点矫枉过正,因为它会导致中间字符串增长,无论它是 PHP 变量还是 PHP 的内部输出缓冲变量。然后对布尔值重复测试似乎要轻得多。除此之外,这里的打印只是一个示例,我正在寻找一种通用方法来在 foreach 循环中进行预处理和后处理。循环可能不会打印任何内容。 我什至无法想到如何将其添加到语言中。生成器通常无法知道它何时处于最后一次迭代中。 我相信这个任务是不可能的,因为在你迭代之前无法判断 Generator 是否真的会生成任何值。 【参考方案1】:

我认为我们不需要为该语言添加任何新内容。在 PHP 7.1 中,我们收到了一个新类型 iterable。使用类型提示,您可以强制您的函数/方法仅接受可以迭代并且在语义上应该被迭代的参数(与不实现Traversable 的对象相反)。

// Function that iterates on $iterable
function iterateOverIt(iterable $iterable) 
    echo '<ul>'.PHP_EOL;
    foreach ($iterable as $item) 
        echo "<li>", $item instanceof DateTime ? $item->format("c") : (
            isset($item["col"]) ? $item["col"] : $item
        ), "</li>\n";
    
    echo '</ul>'.PHP_EOL;

当然,这并不能确保循环至少会迭代一次。在您的测试用例中,您可以简单地使用输出缓冲,但这不适用于仅当循环将迭代至少一次时才需要执行操作的情况。这不可能。实现这种连贯性在技术上是不可能的。举个简单的例子:

class SideEffects implements Iterator 
    private $position = 0;

    private $IHoldValues = [1, 1, 2, 3, 5, 8];

    public function __construct() 
        $this->position = 0;
    

    public function doMeBefore() 
        $this->IHoldValues = [];
        echo "HAHA!".PHP_EOL;
    

    public function rewind() 
        $this->position = 0;
    

    public function current() 
        return $this->IHoldValues[$this->position];
    

    public function key() 
        return $this->position;
    

    public function next() 
        ++$this->position;
    

    public function valid() 
        return isset($this->IHoldValues[$this->position]);
    


$it = new SideEffects();
if (1/* Some logic to check if the iterator is valid */) 
    $it->doMeBefore(); // causes side effect, which invalidates previous statement
    foreach ($it as $row) 
        echo $row.PHP_EOL;
    

正如您在这个不起眼的示例中看到的那样,您的代码不可能实现如此完美的连贯性。

我们在 PHP 中有不同的迭代方式的原因是没有“一刀切”的解决方案。你试图做的恰恰相反。您正在尝试创建一个解决方案,该解决方案适用于可以迭代的所有内容,即使它可能不应该被迭代。相反,您应该编写可以适当处理所有方法的代码。

function iterateOverIt(iterable $iterable) 
    if (is_array($iterable)) 
        echo 'The parameter is an array'.PHP_EOL;
        if ($iterable) 
            // foreach
        
     elseif ($iterable instanceof Iterator) 
        echo 'The parameter is a traversable object'.PHP_EOL;
        /**
         * @var \Iterator
         */
        $iterable = $iterable;
        if ($iterable->valid()) 
            // foreach
        
     elseif ($iterable instanceof Traversable) 
        echo 'The parameter is a traversable object'.PHP_EOL;
        // I donno, some hack?
        foreach ($iterable as $i) 
            // do PRE
            foreach ($iterable as $el) 
                // foreach calls reset first on traversable objects. Each loop should start anew
            
            // do POST
            break;
        
     else 
        // throw new Exception?
    

如果你真的想要,你甚至可以使用is_object() 在其中包含普通对象,但正如我所说,除非它实现了Traversable,否则不要对其进行迭代。

【讨论】:

实际上,我的代码有一个iterable 类型提示。我只是想让这个关于foreach 的问题非常笼统。是的,存在一个通用的解决方案(它也适用于任何可以迭代的东西,包括对象),即使那不是我的目标。【参考方案2】:

嗯,这至少是一个有趣的思想实验......

class BeforeAfterIterator implements Iterator

    private $iterator;

    public function __construct(iterable $iterator, $before, $after)
    
        if (!$iterator instanceof Iterator) 
            $iterator = (function ($iterable) 
                yield from $iterable;
            )($iterator);
        

        if ($iterator->valid()) 
            $this->iterator = new AppendIterator();
            $this->iterator->append(new ArrayIterator([$before]));
            $this->iterator->append($iterator);
            $this->iterator->append(new ArrayIterator([$after]));
         else 
            $this->iterator = new ArrayIterator([]);
        
    

    public function current()
    
        return $this->iterator->current();
    

    public function next()
    
        $this->iterator->next();
    

    public function key()
    
        return $this->iterator->key();
    

    public function valid()
    
        return $this->iterator->valid();
    

    public function rewind()
    
        $this->iterator->rewind();
    

示例用法:

function generator() 
    foreach (range(1, 5) as $x) 
        yield "<li>$x";
    


$beforeAfter = new \BeforeAfterIterator(generator(), '<ul>', '</ul>');

foreach ($beforeAfter as $value) 
    echo $value, PHP_EOL;

输出

<ul>
<li>1
<li>2
<li>3
<li>4
<li>5
</ul>

如果您传递的迭代器没有产生任何值,则不会得到任何输出。

见https://3v4l.org/0Xa1a

我绝不认为这是一个好主意,这完全取决于你。我确信有一些更优雅的方法可以使用一些更晦涩的 SPL 类。通过扩展而不是组合来实现它也可能更简单。

【讨论】:

有趣的想法,但是这真的只通过实现Iterator的东西。这个问题实际上是关于foreach 以及foreach 可以迭代的所有可能类型。 @PatrickAllaert 这是一个非常公平的观点。我已经更新它以支持iterable 的所有实例(包括数组),方法是将它们转换为生成器。另一个例子见3v4l.org/HiPWo 您需要在编辑后删除类型提示。它不再接受 Iterator 类型的对象 mysqli_result 的 DatePeriod 仍然不起作用。 为努力而投票。无论如何,这个问题似乎太理论化了。这个问题要求一个过度设计、过于笼统的答案。【参考方案3】:

适用于所有情况 (PHP >= 7.2) 的解决方案是使用双 foreach,其中第一个的行为类似于 if,它不会真正执行循环,但启动它:

function iterateOverIt($iterable) 
    // First foreach acts like a guard condition
    foreach ($iterable as $_) 

        // Pre-processing:
        echo "<ul>\n";

        // Real looping, this won't start a *NEW* iteration process but will continue the one started above:
        foreach ($iterable as $item) 
            echo "<li>", $item instanceof DateTime ? $item->format("c") : (
                isset($item["col"]) ? $item["col"] : $item
            ), "</li>\n";
        

        // Post-processing:
        echo "</ul>\n";
        break;
    

Full demo on 3v4l.org.

【讨论】:

我想这是一个通用的解决方案。这是我在回答中建议的,对于您的简单场景,它应该可以工作。我可能应该修改我的答案。

以上是关于如何在 foreach 循环之前/之后触发代码,只有在 PHP 7.4+ 中以一种有效的方式输入这个循环?的主要内容,如果未能解决你的问题,请参考以下文章

在 foreach 循环 Blazor 中触发网络请求

forEach 循环中的 promise.all —— 一切立即触发

在承诺完成之前退出 forEach

如何控制 foreach 循环次数 只循环15次

如何检查属性的foreach循环。 Laravel

PDO - 对多个 foreach 循环使用相同的查询