Как активировать код до / после foreach l oop, только если он будет введен, эффективным способом в PHP 7.4+? - PullRequest
5 голосов
/ 02 марта 2020

Это несколько похоже на вопрос: Как определить первую и последнюю итерацию в foreach l oop? , однако, этот вопрос +10 лет сильно ориентирован на array и ни один из ответ совместим с тем фактом, что многие различные типы могут быть зациклены на.

При условии oop для чего-то, что может повторяться в PHP> = 7.4 (массивы, итераторы , генераторы, PDOStatement, DatePeriod, свойства объекта, ...), как мы можем эффективно запустить код, который должен произойти до / после l oop, , но только в В случае ввода l oop ?

Типичным вариантом использования может быть генерация списка HTML:

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

<ul> и </ul> должен быть напечатан только , если есть некоторые элементы.

Это ограничение, которое я обнаружил до сих пор:

  1. empty(): Не может использоваться на генераторах / итераторах .
  2. each(): устарело.
  3. * 104 0 *iterator_to_array() побеждает преимущество генераторов.
  4. A логический флаг, протестированный внутри и после того, как l oop не считается эффективным , поскольку он приведет к тому, что этот тест будет выполняться на каждой отдельной итерации, а не один раз в начале и один раз в конце l oop.
  5. в то время как буферизация вывода или конкатенации строк для генерации вывод может использоваться в приведенном выше примере, он не подходит для случая, когда al oop не будет производить никакого вывода. (спасибо @ barmar за дополнительную идею)

Следующий фрагмент кода суммирует множество различных типов, для которых мы можем перебрать 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>

Ответы [ 3 ]

0 голосов
/ 02 марта 2020

Не думаю, что нам нужно добавлять что-то новое в язык. В 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;
}

Конечно, это не гарантирует, что l oop будет повторяться хотя бы один раз. В вашем тестовом примере вы можете просто использовать буферизацию вывода, но это не будет применяться к ситуациям, когда вам нужно выполнить действие, только если l oop будет повторяться хотя бы один раз. Это невозможно. Было бы технически невозможно реализовать такую ​​согласованность. Позвольте мне привести простой пример:

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, не выполняйте итерации по нему.

0 голосов
/ 03 марта 2020

Решение, которое работает для всех случаев (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;
    }
}

Полная демонстрация на 3v4l.org .

0 голосов
/ 02 марта 2020

Ну, это был хотя бы интересный мысленный эксперимент ...

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>

Если итератор вы pass не дает значений, вы не получаете вывод.

См. https://3v4l.org/0Xa1a

Я никоим образом не одобряю это как хорошую идею, это полностью ваше дело , Я уверен, что есть некоторые более изящные способы сделать это с некоторыми из более неясных классов SPL. Также может быть проще сделать это через расширение вместо композиции.

...