PHP - json_encode объект-генератор (с использованием yield)

У меня есть очень большой массив в PHP (5.6), сгенерированный динамически, который я хочу преобразовать в JSON. Проблема в том, что массив слишком большой, чтобы он не помещался в памяти - я получаю фатальную ошибку, когда пытаюсь его обработать (исчерпана память). Итак, я понял, что при использовании генераторов проблема с памятью исчезнет.

Это код, который я пробовал до сих пор (этот сокращенный пример не приводит к ошибке памяти):

<?php 
function arrayGenerator()// new way using generators
{
    for ($i = 0; $i < 100; $i++) {
        yield $i;
    }
}

function getArray()// old way, generating and returning the full array
{
    $array = [];
    for ($i = 0; $i < 100; $i++) {
        $array[] = $i;
    }
    return $array;
}

$object = [
    'id' => 'foo',
    'type' => 'blah',
    'data' => getArray(),
    'gen'  => arrayGenerator(),
];

echo json_encode($object);

Но, похоже, PHP не JSON-кодирует значения из генератора. Это вывод, который я получаю из предыдущего скрипта:

{
    "id": "foo",
    "type": "blah",
    "data": [// old way - OK
        0,
        1,
        2,
        3,
        //...
    ],
    "gen": {}// using generator - empty object!
}

Можно ли JSON-кодировать массив, созданный генератором, не генерируя полную последовательность, прежде чем я вызову json_encode?

Ответ 1

К сожалению, json_encode не может генерировать результат из функции генератора. Использование iterator_to_array по-прежнему будет пытаться создать весь массив, что все равно вызовет проблемы с памятью.

Вам нужно будет создать свою функцию, которая будет генерировать json-строку из функции-генератора. Вот пример того, как это могло бы выглядеть:

function json_encode_generator(callable $generator) {
    $result = '[';

    foreach ($generator as $value) {
        $result .= json_encode($value) . ',';
    }

    return trim($result, ',') . ']';
}

Вместо того, чтобы сразу кодировать весь массив, он кодирует только один объект за раз и объединяет результаты в одну строку.

Приведенный выше пример заботится только о кодировании массива, но его можно легко расширить до рекурсивного кодирования целых объектов.

Если созданная строка по-прежнему слишком велика для размещения в памяти, то единственной оставшейся опцией является прямое использование выходного потока. Вот как это могло выглядеть:

function json_encode_generator(callable $generator, $outputStream) {
    fwrite($outputStream, '[');

    foreach ($generator as $key => $value) {
        if ($key != 0) {
            fwrite($outputStream, ','); 
        }

        fwrite($outputStream, json_encode($value));
    }

    fwrite($outputStream, ']');
}

Как вы можете видеть, единственное отличие состоит в том, что мы теперь используем fwrite для записи в переданный поток, а не для конкатенации строк, и нам также нужно позаботиться о концевой запятой по-другому.

Ответ 2

Что такое функция генератора?

Функция генератора является фактически более компактным и эффективным способом записи Iterator. Он позволяет вам определить функцию, которая будет вычислять и возвращать значения, пока вы зацикливаете на ней:

Также в соответствии с документом из http://php.net/manual/en/language.generators.overview.php

Генераторы предоставляют простой способ реализовать простые итераторы без накладных расходов или сложности реализации класса, реализующего интерфейс Iterator.

Генератор позволяет вам писать код, который использует foreach для итерации по набору данных без необходимости создания массива в памяти, что может привести к превышению лимита памяти или потребует значительного количества времени обработки для генерации. Вместо этого вы можете написать генераторную функцию, которая является такой же, как и для нормальной функции, за исключением того, что вместо того, чтобы возвращаться один раз, генератор может выводить столько раз, сколько нужно, чтобы предоставить значения, которые нужно повторить.

Что такое yield?

Ключевое слово yield возвращает данные из функции генератора:

Сердцем функции генератора является ключевое слово yield. В своей простейшей форме оператор yield выглядит так же, как оператор return, за исключением того, что вместо того, чтобы прекратить выполнение функции и возврата, yield вместо этого дает значение циклу цикла над генератором и приостанавливает выполнение функции генератора.

Итак, в вашем случае для генерации ожидаемого результата вам нужно выполнить итерацию вывода функции arrayGenerator() с помощью цикла foreach или iterator до того, как обработать его до json (как было предложено @apokryfos)