Хотя каждый из них устарел, замена Foreach занимает намного больше времени

С PHP 7.2 each устарел. В документации говорится:

Предупреждение Эта функция была отключена с PHP 7.2.0. Полагаться на эту функцию крайне не рекомендуется.

Я работаю над адаптацией приложения электронной коммерции и конвертированием всех в while-each цикл в (предположительно) эквивалент foreach.

Как вы можете видеть ниже, я уже заменил все циклы reset & while эквивалентным foreach.

Это в основном работало нормально. Тем не менее, у нас был клиент с очень длинным списком предметов в своей тележке, которые пытались проверить и жаловались, что она получает ошибку 502 с сервера. Я попытался воспроизвести это и обнаружил, что только ее тележка терпит неудачу, для загрузки страницы проверки занимает более 2 минут, а затем раз с 502. Затем я начал отлаживать большое количество файлов, которые я недавно изменил, проб и ошибок, до тех пор, пока я выяснили, что проблема связана с этим конкретным файлом и конкретной функцией. Всякий раз, когда я включаю первый foreach цикл обратно в while цикл, клиент может загрузить страницу оформления заказа менее чем за секунду. Переключение обратно в foreach - и снова это займет минуты, но php истекает до завершения выполнения.

Я сделал выполнять тесты, конечно, на выходе этого foreach против в while петля (var_dump $products_id и $this->contents, например), и все они кажутся одинаковыми. Я уже переписал код, чтобы он работал плавно и оставался совместимым с PHP 7.2, но я до сих пор не могу понять, почему это происходит.

Это полная функция:

function get_content_type() {
  $this->content_type = false;

  if ( (DOWNLOAD_ENABLED == 'true') && ($this->count_contents() > 0) ) {

    // reset($this->contents);
    // while (list($products_id, ) = each($this->contents)) {
    foreach(array_keys($this->contents) as $products_id) {

      if (isset($this->contents[$products_id]['attributes'])) {
        // reset($this->contents[$products_id]['attributes']);
        // while (list(, $value) = each($this->contents[$products_id]['attributes'])) {
        foreach ($this->contents[$products_id]['attributes'] as $value) {
          $virtual_check_query = tep_db_query("select count(*) as total from " . TABLE_PRODUCTS_ATTRIBUTES . " pa, " . TABLE_PRODUCTS_ATTRIBUTES_DOWNLOAD . " pad where pa.products_id = '" . (int)$products_id . "' and pa.options_values_id = '" . (int)$value . "' and pa.products_attributes_id = pad.products_attributes_id");
          $virtual_check = tep_db_fetch_array($virtual_check_query);

          if ($virtual_check['total'] > 0) {
            switch ($this->content_type) {
              case 'physical':
                $this->content_type = 'mixed';

                return $this->content_type;
                break;
              default:
                $this->content_type = 'virtual';
                break;
            }
          } else {
            switch ($this->content_type) {
              case 'virtual':
                $this->content_type = 'mixed';

                return $this->content_type;
                break;
              default:
                $this->content_type = 'physical';
                break;
            }
          }
        }

      } elseif ($this->show_weight() == 0) {
      // reset($this->contents);  
      //  while (list($products_id, ) = each($this->contents)) { 
        foreach (array_keys($this->contents) as $products_id) {
          $virtual_check_query = tep_db_query("select products_weight from " . TABLE_PRODUCTS . " where products_id = '" . $products_id . "'");
          $virtual_check = tep_db_fetch_array($virtual_check_query);
          if ($virtual_check['products_weight'] == 0) {
            switch ($this->content_type) {
              case 'physical':
                $this->content_type = 'mixed';

                return $this->content_type;
                break;
              default:
                $this->content_type = 'virtual';
                break;
            }
          } else {
            switch ($this->content_type) {
              case 'virtual':
                $this->content_type = 'mixed';

                return $this->content_type;
                break;
              default:
                $this->content_type = 'physical';
                break;
            }
          }
        }

      } else {
        switch ($this->content_type) {
          case 'virtual':
            $this->content_type = 'mixed';

            return $this->content_type;
            break;
          default:
            $this->content_type = 'physical';
            break;
        }
      }
    }
  } else {
    $this->content_type = 'physical';
  }

  return $this->content_type;
}

Спасибо

EDIT: вот массив: https://pastebin.com/VawX3XpW

Проблема была протестирована и воспроизведена во всех конфигурациях, которые я пробовал:

1) Высококачественные окна 10 шт + WAMP (Apache 2.4 + MariaDB 10.2 + PHP 5. 6+/7 +/7. 1+/7. 2+)

2) Высокопроизводительный сервер CentOS/cPanel + Litespeed + MariaDB 10.1 + PHP 5. 6+

Просто чтобы подчеркнуть, я не хочу переписывать код или подражать each а затем переписывать код, поскольку мы мало что узнаем из этого. Я просто пытаюсь найти логическое объяснение или способ решить/отладить эту тайну. Возможно, кто-то где-то столкнулся с такой проблемой и может пролить свет на это.

ОБНОВЛЕНИЕ 01/авг /2018

Я пытался отлаживать это в течение нескольких дней, в конце концов заметил что-то интересное. Я добавил "эхо-точки" и exit в первый цикл foreach while цикле:

function get_content_type() {
  $this->content_type = false;

  if ( (DOWNLOAD_ENABLED == 'true') && ($this->count_contents() > 0) ) {

    // reset($this->contents);
    // while (list($products_id, ) = each($this->contents)) { echo '1 ';
    foreach(array_keys($this->contents) as $products_id) { echo '1 ';

      if (isset($this->contents[$products_id]['attributes'])) { echo '2 ';
        // reset($this->contents[$products_id]['attributes']);
        // while (list(, $value) = each($this->contents[$products_id]['attributes'])) {
        foreach ($this->contents[$products_id]['attributes'] as $value) { echo '3 ';
          $virtual_check_query = tep_db_query("select count(*) as total from " . TABLE_PRODUCTS_ATTRIBUTES . " pa, " . TABLE_PRODUCTS_ATTRIBUTES_DOWNLOAD . " pad where pa.products_id = '" . (int)$products_id . "' and pa.options_values_id = '" . (int)$value . "' and pa.products_attributes_id = pad.products_attributes_id");
          $virtual_check = tep_db_fetch_array($virtual_check_query);

          if ($virtual_check['total'] > 0) {
            switch ($this->content_type) {
              case 'physical':
                $this->content_type = 'mixed'; echo '4 ';

                return $this->content_type;
                break;
              default:
                $this->content_type = 'virtual'; echo '5 ';
                break;
            }
          } else {
            switch ($this->content_type) {
              case 'virtual':
                $this->content_type = 'mixed'; echo '6 ';

                return $this->content_type;
                break;
              default:
                $this->content_type = 'physical'; echo '7 ';
                break;
            }
          }
        }

      } elseif ($this->show_weight() == 0) {
      // reset($this->contents);  
      //  while (list($products_id, ) = each($this->contents)) { 
        foreach (array_keys($this->contents) as $products_id) {
          $virtual_check_query = tep_db_query("select products_weight from " . TABLE_PRODUCTS . " where products_id = '" . $products_id . "'");
          $virtual_check = tep_db_fetch_array($virtual_check_query);
          if ($virtual_check['products_weight'] == 0) {
            switch ($this->content_type) {
              case 'physical':
                $this->content_type = 'mixed'; echo '8 ';

                return $this->content_type;
                break;
              default:
                $this->content_type = 'virtual'; echo '9 ';
                break;
            }
          } else {
            switch ($this->content_type) {
              case 'virtual':
                $this->content_type = 'mixed'; echo '10 ';

                return $this->content_type;
                break;
              default:
                $this->content_type = 'physical'; echo '11 ';
                break;
            }
          }
        }

      } else {
        switch ($this->content_type) {
          case 'virtual':
            $this->content_type = 'mixed'; echo '12 ';

            return $this->content_type;
            break;
          default:
            $this->content_type = 'physical'; echo '13 ';
            break;
        }
      }
    } exit; //Exiting from the loop to check output
  } else {
    $this->content_type = 'physical';
  }

  return $this->content_type;
}

Когда я работал с циклом while, вывод, который я получил, был всего лишь "1 13" один раз, что означает, что цикл работает только один раз и останавливается. Однако, когда я изменил его на foreach, у меня появился длинный список "1 13 1 13 1 13...", то есть он многократно зацикливался. Я пошел дальше исследовать, если есть какая-либо разница между breaks циклов while и foreach, и я до сих пор не нашел никакой вспомогательной информации. Затем я переписал последний break; break 2; и протестировали foreach снова и на этот раз он, кажется, был запущен только один раз, так же, как когда он был в while петля с break; (не break 2;) РЕДАКТИРОВАТЬ: просто уточнить - нет разницы между break и break. Они работают одинаково.

UPDATE # 2: я пересмотрел } elseif ($this->show_weight() == 0) { к } elseif (2 == 0) { и в while цикл в настоящее время работает как много раз, как foreach цикла. var_dump($this->show_weight()); результаты float 4466.54. Вопрос до сих пор не имеет для меня никакого смысла.

еще раз спасибо

Ответы

Ответ 1

Это на самом деле очень простая алгоритмическая проблема, и это связано с тем, что когда show_weight() равен 0 вы выполняете цикл одного и того же массива (редактирование: и на основе ваших комментариев сам show_weight() также выполняет цикл одного и того же массива),

TL; DR С в while все эти циклы разделяли один и тот же внутренний указатель и воздействуя друг друга. С foreach каждый цикл является независимым, и поэтому он запускает больше итераций, следовательно, является проблемой производительности.

Так как пример стоит тысячи слов, мы надеемся, что следующий код упростит ситуацию:

<?php

$array = ['foo','bar','baz'];

foreach ( array_keys($array) as $key ) {
    echo $array[$key],"\n";
    foreach ( array_keys($array) as $key ) {
        echo "\t",$array[$key],"\n";
    }
}

echo "---------------\n";

while ( list($key,) = each($array) ) {
    echo $array[$key],"\n";
    reset($array);
    while ( list($key,) = each($array) ) {
        echo "\t",$array[$key],"\n";
    }
}

Это приведет к выводу:

foo
        foo
        bar
        baz
bar
        foo
        bar
        baz
baz
        foo
        bar
        baz
---------------
foo
        foo
        bar
        baz

Как вы можете видеть, для массива размером 3, foreach принимает итерации 3 ², while занимает всего 3. Это ваша проблема с производительностью.

Почему в while быстрее?

Потому что в конце второго (внутреннего) while внутренний указатель $array указывал бы на конец массива, поэтому первый (внешний) while просто остановился.

С помощью foreach, поскольку вы используете результат из двух разных вызовов array_keys, вы используете 2 разных массива, которые не используют один и тот же внутренний указатель, поэтому нет причин для остановки цикла. Простое return после второго (внутреннего) foreach должно решить проблему.

Ответ 2

Как решение для резервного копирования, а как насчет падения в замене, выполняющего одно и то же действие?

function eachLegacy( &$array )
{
  if( current( $array ) !== false )
  {
    $return = array( 1 => current( $array ), 'value' => current( $array ), 0 => key( $array ), 'key' => key( $array ) ); // Get the current values
    next( $array ); // Advance the cursor
    return $return;
  }
  return false;
}

Ответ 3

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

foreach($this->contents as $products_id => $products_value) {
    echo '1 ';
    if (isset($products_value['attributes'])) {
        echo '2 ';
        foreach ($products_value['attributes'] as $value) {
            echo '3 ';
            ...

использование return in switch приведет к разрыву всей функции, выйдите также. если вы хотите разбить цикл из оператора switch, используйте:

break NUMBER_OF_PARENT_STATEMENTS;

перерыв 2; сломает переключатель и родительский foreach

перерыв 3; сломает переключатель, а первый и второй родительский foreach

Ответ 4

В исходном коде, если на первой итерации возвращается true:

$this->show_weight() == 0

Код циклически проходит через $this-> содержимое, используя each(), устанавливая указатель массива в $this-> содержимое до конца. Поэтому, когда мы возвращаемся к первому оператору while(), он предполагает, что это уже сделано, потому что следующий вызов каждого ($this-> содержимого) возвращает false.