Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Потоки, обёртки и контексты: читать и писать данные по шагам
  2. Задача: прочитать выгрузку, а не держать весь файл в памяти
  3. r, w, a и плюс: что разрешено и что происходит с файлом
  4. php://temp: временный отчёт и причина пустого чтения
  5. Перенести приём в функцию: входные строки → готовый отчёт
  6. Большая выгрузка: цикл чтения, частичная запись и память
  7. Обёртка выбирает источник: file://, https:// и php://
  8. Контекст: настройки открытия, а не сам запрос
  9. Практика интеграции: ответ пришёл — ещё не значит успех
  10. HTTP input/output и CLI: не перепутать направление
  11. Когда достаточно потоков, а когда удобнее cURL
  12. Проверить понимание: пять маленьких изменений
  13. Функции и методы этой темы
  14. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 25 · С нуля → интеграции

Потоки, обёртки и контексты: читать и писать данные по шагам

От строки и файла к позиции чтения, временным отчётам, php://input/output и проверяемому HTTP-ответу.

Задача: прочитать выгрузку, а не держать весь файл в памяти

Наглядная модель

Две порции из одного открытого файла

В orders.txt лежит TEA\nCOFFEE\n: 11 байт. Символы \n на схеме обозначают переводы строки.

  1. fopen: позиция 0

    Открываем orders.txt для чтения. В переменной ресурс канала, а не весь текст файла.

  2. fread(…, 4): TEA\n

    Читаем 4 байта. Позиция следующего чтения становится 4.

  3. Читаем остаток: COFFEE\n

    stream_get_contents продолжает с позиции 4. Уже прочитанное повторно не возвращается.

  4. fclose: канал закрыт

    Ресурс больше не используем. Сам файл остаётся на диске.

Это модель чтения обычного файла. Не каждый поток поддерживает позицию или перемотку; fread считает байты.

Сайт получил текстовую выгрузку товаров. В маленьком опыте в ней две строки: TEA и COFFEE. Хотим прочитать сначала первую, затем вторую и понять, откуда PHP продолжает чтение. Тот же механизм пригодится для большого CSV, временного отчёта, тела HTTP-запроса и ответа внешнего сервиса.

Строка — уже полученные байты в значении переменной. Файл — данные по пути в файловой системе, которые могут пережить завершение скрипта. Поток — открытый канал чтения или записи. Переменная $stream содержит ресурс этого канала, а не весь текст файла. Это не поток выполнения программы и не параллельная задача.

Предпосылки: переменная и строка, вызов функции и return. Ниже для чтения сразу объяснены все новые операции.

ЗаписьЧто делает
$stream = fopen($path, 'rb');= сохраняет результат вызова в переменную; путь и режим — два аргумента в круглых скобках. r разрешает чтение, b сохраняет байты без текстового преобразования на Windows.
$part = fread($stream, 4);Прочитать до четырёх байт из текущей позиции; вернуть строку и продвинуть позицию на реально прочитанное.
ftell($stream)Узнать позицию в байтах у потока, который умеет сообщать её. У нашего файла после первой порции — 4.
stream_get_contents($stream)Прочитать оставшиеся байты от текущей позиции. В примере файл заранее маленький.
fclose($stream);Закрыть этот канал. Файл не удаляется. После закрытия этот ресурс больше не используют.

__DIR__ — папка текущего PHP-файла; точка соединяет её с именем fixture. === false отличает ошибку от нормальной пустой строки. В ветке ошибки exit(1) завершает учебную CLI-программу с ненулевым кодом.

Прочитать файл двумя порциями и увидеть позициюСамостоятельный пример
<?php
// orders.txt из архива: TEA, перевод строки, COFFEE, перевод строки.
$stream = fopen(__DIR__ . '/orders.txt', 'rb');
if ($stream === false) {
    echo "Не удалось открыть файл\n";
    exit(1);
}
$first = fread($stream, 4);
$position = ftell($stream);
$rest = stream_get_contents($stream);
if ($first === false || $position === false || $rest === false) {
    fclose($stream);
    echo "Не удалось прочитать файл\n";
    exit(1);
}
echo "Первая порция: ", trim($first), "\n";
echo "Позиция: ", $position, "\n";
echo "Остаток: ", trim($rest), "\n";
fclose($stream);
Ожидаемый результат
Первая порция: TEA
Позиция: 4
Остаток: COFFEE

Файл orders.txt включён в пример: ровно TEA\nCOFFEE\n, 11 байт. Первый fread забирает TEA и один байт перевода строки. Следующее чтение начинается с позиции 4. trim здесь только убирает перевод строки при показе, сам файл не меняется.

r, w, a и плюс: что разрешено и что происходит с файлом

Режим выбирают по задаче до открытия. Для импорта нужен rb, для журнала — дополнение ab, для отчёта, который должен полностью заменить старый, — wb. Буква + добавляет второе направление: чтение к записи либо запись к чтению.

w очищает существующий файл в момент fopen, ещё до первого fwrite и до возможного flock. Поэтому нельзя открыть общую очередь через w+, потом поставить блокировку и надеяться прочитать её старые задания.

РежимЕсли файл существуетЕсли файла нетКогда нужен
rbЧтение с началаОшибкаВыгрузка поставщика
r+bЧтение и запись без очисткиОшибкаИзменить существующие байты с контролем позиции
wb / w+bСразу очищает; w+b ещё читаетСоздаёт при наличии правНовый отчёт вместо старого
ab / a+bЗапись всегда добавляет в конец; a+b ещё читаетСоздаёт при наличии правУчебный журнал событий
xb / x+bОшибка, чтобы не затереть файлСоздаёт; x+b ещё читаетСоздать новый файл с исключением совпадения
cb / c+bНе очищает; запись с началаСоздаёт при наличии правСначала получить блокировку, затем менять файл по своему протоколу

Для a/a+ не вычисляйте будущую позицию записи через ftell или fseek: запись всё равно направлена в конец. Если нужен определённый старт чтения в a+, сделайте rewind и проверьте результат. Режим открытия сам по себе не реализует безопасную очередь или согласованную работу нескольких процессов.

Следующий опыт показывает ошибку открытия явно: throw new RuntimeException(...) прекращает обычный путь с ошибкой; finally закрывает уже открытый поток и при успехе, и при исключении. Разбор этих конструкций — в главе об ошибках.

Журнал дополняем, новый отчёт записываем зановоСамостоятельный пример
<?php
// Это только учебная копия из ZIP, не журнал работающего сайта.
$path = __DIR__ . '/demo-log.txt';
$stream = fopen($path, 'ab');
if ($stream === false) {
    throw new RuntimeException('Не удалось открыть учебный журнал');
}
try {
    if (fwrite($stream, "paid\n") !== 5) {
        throw new RuntimeException('Строка записана не целиком');
    }
} finally {
    fclose($stream);
}
$log = file_get_contents($path);
if ($log === false) {
    throw new RuntimeException('Не удалось прочитать журнал');
}
echo "После a: ", str_replace("\n", '|', $log), "\n";

$stream = fopen($path, 'wb');
if ($stream === false) {
    throw new RuntimeException('Не удалось открыть учебный отчёт');
}
try {
    if (fwrite($stream, "new\n") !== 4) {
        throw new RuntimeException('Строка записана не целиком');
    }
} finally {
    fclose($stream);
}
$report = file_get_contents($path);
if ($report === false) {
    throw new RuntimeException('Не удалось прочитать отчёт');
}
echo "После w: ", str_replace("\n", '|', $report), "\n";
Ожидаемый результат
После a: created|paid|
После w: new|

Режим a сохранил created и добавил paid. Режим w очистил файл уже при открытии; затем мы записали new. Вертикальная черта в результате заменяет перевод строки только для наглядности. При каждом новом запуске используйте исходную учебную копию demo-log.txt из архива. Здесь короткая запись обязана быть полной: иной результат превращаем в ошибку; далее появится цикл для частичных записей.

Для одной короткой строки в журнале часто проще file_put_contents($path, $line, FILE_APPEND | LOCK_EX). Когда нужны чтение частями, перемещение позиции или уже открытый канал — выбирайте поток. Согласованные блокировки, атомарная замена и пути подробнее в работе с файлами.

php://temp: временный отчёт и причина пустого чтения

Допустим, сервис строит небольшой текст отчёта, затем отдаёт его пользователю или передаёт библиотеке. php://temp даёт временный поток: сначала в памяти, при росте сверх порога — во временном файле. По умолчанию порог — 2 МиБ; php://temp/maxmemory:1048576 задаёт 1 МиБ. Это не лимит общей длины данных.

w+b означает запись и чтение байтов. После записи позиция уже в конце. rewind($stream) возвращает её на ноль; функция возвращает true/false. У обычного файла и php://temp перемотка есть, у других каналов её может не быть.

Одна запись — два разных чтения
  1. fwrite записал TEA;2 и перевод строки: позиция 6
  2. Чтение с позиции 6 → пустая строка
  3. rewind → позиция 0
  4. Чтение с позиции 0 → TEA;2 и перевод строки
Собрать текст отчёта в php://temp и прочитать егоСамостоятельный пример
<?php
$stream = fopen('php://temp', 'w+b');
if ($stream === false) {
    throw new RuntimeException('Не удалось создать временный поток');
}
try {
    if (fwrite($stream, "TEA;2\n") !== 6) {
        throw new RuntimeException('Запись неполная');
    }
    $atEnd = stream_get_contents($stream);
    if ($atEnd === false) {
        throw new RuntimeException('Ошибка чтения');
    }
    echo 'До rewind: ', strlen($atEnd), " байт\n";
    if (!rewind($stream)) {
        throw new RuntimeException('Не удалось перейти в начало');
    }
    $report = stream_get_contents($stream);
    if ($report === false) {
        throw new RuntimeException('Ошибка чтения');
    }
    echo 'После rewind: ', $report;
} finally {
    fclose($stream);
}
Ожидаемый результат
До rewind: 0 байт
После rewind: TEA;2

После fwrite позиция находится за шестью записанными байтами. Чтение «отсюда до конца» возвращает пустую строку, а не ошибку. rewind возвращает позицию на начало этого же потока. Закрытие освобождает временное хранилище; новое fopen("php://temp", ...) создаст другой пустой поток.

Не открывайте php://temp повторно, чтобы «найти» предыдущий отчёт: имя не адресует постоянный файл. Пока работаете с отчётом, передавайте один ресурс. php://memory похож по интерфейсу, но хранит данные только в памяти. Для долгого хранения нужен обычный файл или другое хранилище.

Перенести приём в функцию: входные строки → готовый отчёт

Теперь повторяем тот же опыт с полезным вызовом: $report = makeReport($rows);. В функцию входят строки заказа, наружу выходит текст. Открытие, запись, перемотка и закрытие остаются внутри, поэтому вызывающий код не обязан помнить правильный порядок.

fwrite возвращает число записанных байт или false. Число может быть меньше длины строки. Помощник writeAll дописывает остаток: strlen считает байты, substr берёт ещё не записанную часть, += двигает смещение. При 0/false останавливаемся с ошибкой.

Та же запись внутри функции: отчёт из строк заказаСамостоятельный пример
<?php
function writeAll($stream, string $text): int
{
    $offset = 0;
    $length = strlen($text);
    while ($offset < $length) {
        $written = fwrite($stream, substr($text, $offset));
        if ($written === false || $written === 0) {
            throw new RuntimeException('Не удалось продолжить запись');
        }
        $offset += $written;
    }
    return $offset;
}

function makeReport(array $rows): string
{
    $stream = fopen('php://temp', 'w+b');
    if ($stream === false) {
        throw new RuntimeException('Не удалось создать отчёт');
    }
    try {
        foreach ($rows as $row) {
            // Данные уже проверены: короткий SKU без ; и положительное количество.
            writeAll($stream, $row['sku'] . ';' . $row['quantity'] . "\n");
        }
        if (!rewind($stream)) {
            throw new RuntimeException('Не удалось перейти в начало');
        }
        $result = stream_get_contents($stream);
        if ($result === false) {
            throw new RuntimeException('Не удалось прочитать отчёт');
        }
        return $result;
    } finally {
        fclose($stream);
    }
}

$report = makeReport([
    ['sku' => 'TEA', 'quantity' => 2],
    ['sku' => 'COFFEE', 'quantity' => 1],
]);
echo $report;
Ожидаемый результат
TEA;2
COFFEE;1

makeReport получает массив строк, возвращает готовую строку и сама ничего не печатает. echo вызывается снаружи: строку можно показать или передать компоненту отправки письма. writeAll учитывает частичную запись и прекращает работу при false/0 вместо бесконечного цикла. Разделитель ; подходит только оговорённым учебным SKU; для произвольного CSV используйте fputcsv с явными настройками. Тип resource нельзя записать как обычный тип параметра: контракт $stream — открытый доступный для записи поток.

Здесь array, string и : int — объявленные типы входа/результата; return отдаёт значение вызывающему коду. finally выполняется и на пути return. Если функция должна обрабатывать внешние данные, сначала проверьте строки заказа, допустимые символы и пределы количества.

Для писем с HTML-шаблоном часто нужен буфер вывода, для произвольного CSV — fputcsv, для последующего импорта — проверка структуры входных данных.

Большая выгрузка: цикл чтения, частичная запись и память

Открыть большой файл ещё не значит загрузить его целиком. Но если в цикле делать $all .= $chunk, память всё равно будет расти вместе с файлом. Смысл порционного чтения — обработать или записать порцию и перейти к следующей без накопления исходника.

Условия конца и ошибки разные: fread === false означает сбой; пустая строка вместе с концом файла — нормальное завершение. Пустое чтение без EOF у нашего блокирующего локального источника считаем ошибкой, чтобы не получить бесконечный цикл. Для неблокирующих сокетов нужна другая логика ожидания.

Читать выгрузку порциями, не накапливая исходник в строкеСамостоятельный пример
<?php
function writeAll($stream, string $text): int
{
    $offset = 0;
    $length = strlen($text);
    while ($offset < $length) {
        $written = fwrite($stream, substr($text, $offset));
        if ($written === false || $written === 0) {
            throw new RuntimeException('Не удалось продолжить запись');
        }
        $offset += $written;
    }
    return $offset;
}

$source = fopen(__DIR__ . '/stock.txt', 'rb');
if ($source === false) {
    throw new RuntimeException('Нет учебной выгрузки');
}
try {
    $target = fopen('php://temp/maxmemory:8', 'w+b');
    if ($target === false) {
        throw new RuntimeException('Не удалось создать копию');
    }
    try {
        $total = 0;
        while (!feof($source)) {
            $chunk = fread($source, 4);
            if ($chunk === false) {
                throw new RuntimeException('Ошибка чтения');
            }
            if ($chunk === '') {
                if (feof($source)) {
                    break;
                }
                throw new RuntimeException('Чтение не продвинулось');
            }
            $total += writeAll($target, $chunk);
        }
        if (!rewind($target)) {
            throw new RuntimeException('Не удалось перейти в начало');
        }
        // Только для показа маленькой fixture. Большой файл так не собираем.
        $preview = stream_get_contents($target);
        if ($preview === false) {
            throw new RuntimeException('Ошибка чтения копии');
        }
        echo 'Скопировано: ', $total, " байт\n";
        echo $preview;
    } finally {
        fclose($target);
    }
} finally {
    fclose($source);
}
Ожидаемый результат
Скопировано: 11 байт
TEA
COFFEE

Порция 4 байта выбрана для видимого маленького опыта, а не как оптимальный размер для сервера. Цикл рассчитан на локальный блокирующий файловый поток. php://temp/maxmemory:8 разрешает временному хранилищу перейти на диск при росте; это порог памяти, а не предел размера отчёта. Произвольную сетевую неблокирующую связь этим циклом не обслуживают.

ПодходГде уместенЧего не обещает
file_get_contentsМаленький файл с известным пределомЭкономию памяти на большом содержимом
fread в циклеОбработка и копирование порциямиАвтоматическую разбивку CSV/JSON по объектам
fgets / fgetcsvСтроки текста / CSV с правилами форматаБезопасность строки произвольной огромной длины без своих пределов
stream_copy_to_streamГотовое копирование между двумя потокамиПроверку бизнес-данных внутри копии

Более мелкие порции увеличивают число вызовов, более крупные — рабочую память. Не объявляйте один размер самым быстрым: измеряйте на реальном диске и формате. Даже чтение порциями не делает обычный PHP-скрипт асинхронным; большая задача может занять worker надолго. Для неё полезны очередь и отдельный worker.

Обёртка выбирает источник: file://, https:// и php://

Обёртка (wrapper) объясняет PHP, как открыть источник с определённой схемой адреса. В https://api.example.test/stock схема — https; в php://temp — php. Одинаковые fread/fclose работают с разными каналами, но поддержка записи, перемотки и метаданных зависит от обёртки.

Обычный путь __DIR__ . "/stock.txt" открывает локальный файл. Явный file:// тоже относится к файловой системе; для переносимых локальных примеров обычный путь проще. Само слово «поток» не означает сетевую загрузку.

Адрес/каналЧто за нимПрактическое применение
file:// или обычный путьФайл с правами текущего процессаВыгрузка, журнал, отчёт
http:// / https://HTTP-запрос и тело ответаПолучить JSON от внешнего сервиса; для чувствительных данных нужен HTTPS
php://temp / php://memoryВременное хранилище текущего открытого ресурсаПодготовить отчёт для библиотеки, которая принимает поток
php://inputНеобработанное тело входящего HTTP-запросаJSON endpoint; не механизм автоматической валидации
php://outputМеханизм вывода PHP с его буферизациейОтдать данные через API, которое ожидает поток
php://stdin / stdout / stderrСтандартные каналы процесса; особенно понятны в CLIПолучить JSON из другого процесса, вернуть результат, отдельно писать диагностику

Набор зарегистрированных обёрток показывает stream_get_wrappers(). HTTP-доступ через эти функции зависит от allow_url_fopen; HTTPS также требует поддержки OpenSSL и доверенных сертификатов. Не все обёртки существуют в любом окружении.

Для передачи JSON между PHP и Python/Node через stdin/stdout есть отдельная практическая глава о процессах. В CLI stdout оставляйте под результат, stderr — под диагностику; наличие текста в stderr само по себе не заменяет код завершения процесса.

Контекст: настройки открытия, а не сам запрос

Поток отвечает «куда читаем/пишем». Контекст отвечает «с какими настройками открываем»: GET или POST, заголовки, тело запроса, таймаут, проверка TLS. Это массив настроек, из которого stream_context_create создаёт ресурс. Вызов сам по себе сеть не использует.

В записи ["http" => ["method" => "GET"]] квадратные скобки создают массив, => связывает ключ со значением. Ключ http выбирает группу опций HTTP, внутри method задаёт метод. Применяем ресурс четвёртым аргументом: fopen($url, "rb", false, $context); третий false отключает поиск по include_path.

Два контекста: прочитать остатки и подготовить отправку JSONСамостоятельный пример
<?php
$readContext = stream_context_create([
    'http' => [
        'method' => 'GET',
        'header' => ['Accept: application/json'],
        'timeout' => 5.0,
        'follow_location' => 0,
        'ignore_errors' => true,
    ],
    'ssl' => [
        'verify_peer' => true,
        'verify_peer_name' => true,
        'allow_self_signed' => false,
    ],
]);
$sendContext = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => ['Content-Type: application/json', 'Accept: application/json'],
        'content' => json_encode(['sku' => 'TEA', 'stock' => 8], JSON_THROW_ON_ERROR),
        'timeout' => 5.0,
        'follow_location' => 0,
    ],
    'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
]);
$readOptions = stream_context_get_options($readContext);
$sendOptions = stream_context_get_options($sendContext);
echo $readOptions['http']['method'], ': TLS=',
    $readOptions['ssl']['verify_peer'] ? 'on' : 'off', "\n";
echo $sendOptions['http']['method'], ': ',
    $sendOptions['http']['content'], "\n";
Ожидаемый результат
GET: TLS=on
POST: {"sku":"TEA","stock":8}

Здесь не было ни одного HTTP-запроса. stream_context_create только создаёт ресурс с настройками; stream_context_get_options позволяет их посмотреть. Чтобы применить контекст, его передают конкретному открытию потока. Для https настройки HTTP тоже лежат под ключом http, а проверка TLS — под ssl. Отправляющий контекст показан как данные: реальная запись во внешний сервис требует его контракта и политики повторов.

ОпцияЗачем она здесьГраница смысла
http.timeoutНе ждать бесконечно при чтении ответаНе равен общему дедлайну всех стадий запроса
http.follow_location = 0Обработать 3xx явноАвтоматический переход не уводит на другой источник
http.ignore_errors = trueПолучить тело даже при HTTP-ошибке404/500 остаются ошибочными статусами по контракту endpoint
ssl.verify_peer / verify_peer_nameПроверить сертификат и имя сервераОставляем true; проблему CA/имени/часов исправляем в конфигурации
ssl.cafile / capathУказать доверенные CA при необходимости окруженияНе случайный файл сертификата другого сайта и не повод отключить проверку

Используем локальный контекст для конкретного запроса; изменение глобального контекста по умолчанию влияет и на другие вызовы. Не включайте секреты в URL и не показывайте внутренние сообщения TLS посетителю. Ошибки сертификатов сначала проверяют по доверенным CA, цепочке сервера, имени и системному времени.

Практика интеграции: ответ пришёл — ещё не значит успех

Магазину нужен остаток товара TEA. Хороший результат — целое число 8. Между открытием HTTP-потока и этим числом есть отдельные проверки: транспорт, статус, размер, чтение, синтаксис JSON, структура и значение. ignore_errors полезен, чтобы получить тело отказа, но не отменяет эти этапы.

Начнём с небольшой функции без сети. Заголовки — массив строк. preg_match выделяет числовой HTTP-статус; подробный разбор регулярных выражений — в соответствующей главе.

Тело может быть JSON и при 404: сначала прочитать статусСамостоятельный пример
<?php
function responseStatus(array $headers): int
{
    $status = null;
    foreach ($headers as $line) {
        if (preg_match('~^HTTP/\d(?:\.\d)?\s+([1-5]\d{2})(?:\s|$)~i', $line, $match) === 1) {
            $status = (int) $match[1];
        }
    }
    if ($status === null) {
        throw new RuntimeException('Нет HTTP-статуса');
    }
    return $status;
}

$responses = [
    ['HTTP/1.1 200 OK', 'Content-Type: application/json'],
    ['HTTP/1.1 404 Not Found', 'Content-Type: application/json'],
    ['HTTP/1.1 100 Continue', 'HTTP/1.1 200 OK'],
];
foreach ($responses as $headers) {
    $status = responseStatus($headers);
    echo $status, $status === 200 ? ': принять' : ': отклонить', "\n";
}
try {
    responseStatus(['Content-Type: application/json']);
} catch (RuntimeException $error) {
    echo $error->getMessage(), "\n";
}
Ожидаемый результат
200: принять
404: отклонить
200: принять
Нет HTTP-статуса

Это массивы учебных заголовков, а не ответы живого сервера. Помощник запоминает последнюю строку статуса, например после промежуточного 100. Решение «принимаем только 200» относится к нашему чтению остатков: другой endpoint может ожидать 201, 204 или иной документированный результат.

Теперь тот же помощник стоит внутри контекстного чтения. Метаданные открытого HTTP-потока берём через stream_get_meta_data: заголовки лежат в wrapper_data. Это не глобальная переменная $http_response_header, объявленная устаревшей в PHP 8.5.

HTTP по контексту: статус, предел тела, таймаут чтения и JSONПример для указанного окружения
<?php
function responseStatus(array $headers): int
{
    $status = null;
    foreach ($headers as $line) {
        if (preg_match('~^HTTP/\d(?:\.\d)?\s+([1-5]\d{2})(?:\s|$)~i', $line, $match) === 1) {
            $status = (int) $match[1];
        }
    }
    if ($status === null) {
        throw new RuntimeException('Нет HTTP-статуса');
    }
    return $status;
}

function fetchTeaStock(): int
{
    // Замените только на фиксированный доверенный адрес своего API.
    $url = 'https://api.example.test/inventory/TEA';
    $context = stream_context_create([
        'http' => [
            'method' => 'GET',
            'header' => ['Accept: application/json', 'Connection: close'],
            'timeout' => 5.0,
            'follow_location' => 0,
            'ignore_errors' => true,
        ],
        'ssl' => [
            'verify_peer' => true,
            'verify_peer_name' => true,
            'allow_self_signed' => false,
        ],
    ]);
    $stream = null;
    // Узкая область: предупреждение ввода-вывода становится нашей ошибкой.
    // Путь/URL и текст низкоуровневой ошибки не возвращаем посетителю.
    set_error_handler(function (int $severity, string $message): never {
        throw new RuntimeException('Не удалось прочитать источник остатков');
    }, E_WARNING);
    try {
        $stream = fopen($url, 'rb', false, $context);
        if ($stream === false) {
            throw new RuntimeException('Источник недоступен');
        }
        $metadata = stream_get_meta_data($stream);
        $status = responseStatus($metadata['wrapper_data'] ?? []);
        // Читаем на один байт больше лимита, чтобы обнаружить переполнение.
        $body = stream_get_contents($stream, 65_537);
        $afterRead = stream_get_meta_data($stream);
        if ($body === false || ($afterRead['timed_out'] ?? false)) {
            throw new RuntimeException('Ответ не прочитан вовремя');
        }
        if (strlen($body) > 65_536) {
            throw new RuntimeException('Ответ больше допустимого');
        }
        if ($status !== 200) {
            throw new RuntimeException('Источник вернул HTTP ' . $status);
        }
        $data = json_decode($body, false, 32, JSON_THROW_ON_ERROR);
        if (!$data instanceof stdClass
            || ($data->sku ?? null) !== 'TEA'
            || !is_int($data->stock ?? null)
            || $data->stock < 0) {
            throw new RuntimeException('Неверная структура остатков');
        }
        return $data->stock;
    } finally {
        restore_error_handler();
        if (is_resource($stream)) {
            fclose($stream);
        }
    }
}
// Вызов после настройки реального endpoint: $stock = fetchTeaStock();

Контекстный пример проверяется синтаксически, сетевой запуск не заявляется. Нужны доверенный HTTPS endpoint с JSON {"sku":"TEA","stock":8}, разрешённый allow_url_fopen, OpenSSL и актуальное хранилище CA. .test — учебный адрес. Реальный ответ должен соответствовать контракту; ошибку перехватывает верхний обработчик приложения. timeout — таймаут чтения, не гарантированный общий дедлайн запроса. Редиректы отключены; код не является универсальным HTTP-клиентом.

СитуацияДействие этого примера
DNS/TLS/чтение не удалосьПрекратить операцию и передать контролируемую ошибку наверх
Ответ 404 или 500 с корректным JSONОтклонить статус; не принимать тело за успешные остатки
Ответ слишком большой / таймаутНе разбирать его как допустимый документ
JSON корректен, stock = "8" или -1Отклонить неверный тип или значение
200 и ожидаемый объект sku=TEA, stock=8Вернуть число 8 вызывающему коду

Предел 65 536 байт ограничивает удерживаемое тело, но не заменяет общий бюджет времени, политику повторов и ограничения инфраструктуры. Проверка структуры не доверяет Content-Type автоматически. Эта небольшая функция полезна для контролируемого источника; она не покрывает пагинацию, авторизацию и сложные схемы API.

HTTP input/output и CLI: не перепутать направление

У JSON endpoint запрос уже пришёл в PHP. php://input читает его сырое тело; это не запрос к другому сайту. JSON не появляется в $_POST автоматически: сначала читаем ограниченное тело, потом вызываем json_decode и проверяем полученную структуру.

Следующая функция относится именно к application/json. Для обычной формы работают её правила, для multipart/form-data — обработка полей и файлов; доступность php://input там зависит от настроек обработки POST. Эти случаи не смешиваем.

php://input: принять небольшое JSON-тело HTTP-запросаПример для указанного окружения
<?php
// Функция вызывается HTTP-обработчиком после проверки метода, доступа
// и Content-Type. Сервер также должен ограничивать размер тела запроса.
function readStockBody(): array
{
    $stream = fopen('php://input', 'rb');
    if ($stream === false) {
        throw new RuntimeException('Не удалось открыть тело запроса');
    }
    try {
        $body = stream_get_contents($stream, 8193);
        if ($body === false) {
            throw new RuntimeException('Ошибка чтения тела');
        }
        if (strlen($body) > 8192) {
            throw new LengthException('Тело больше 8192 байт');
        }
        $data = json_decode($body, false, 16, JSON_THROW_ON_ERROR);
        if (!$data instanceof stdClass
            || !is_string($data->sku ?? null)
            || preg_match('/\A[A-Z0-9-]{1,32}\z/', $data->sku) !== 1
            || !is_int($data->stock ?? null)
            || $data->stock < 0) {
            throw new InvalidArgumentException('Ожидались sku и целый stock >= 0');
        }
        return ['sku' => $data->sku, 'stock' => $data->stock];
    } finally {
        fclose($stream);
    }
}

Это часть JSON endpoint, не готовый публичный обработчик. Нужны POST, application/json, аутентификация/проверка прав; для cookie-сессии — соответствующая защита CSRF. Внешний код сопоставляет ошибки чтения, размера, JSON и полей с ответами 4xx/5xx без внутренних деталей. php://input отдаёт байты тела и не превращает их автоматически в массив $_POST. Multipart-формы и загрузки рассматриваются отдельно.

Чтение тела не проверяет право менять товар. Полный путь запроса, CSRF и безопасные поля — в формах, подпись сырых байтов и повторная доставка событий — в webhook.

php://output попадает в вывод, который можно перехватитьСамостоятельный пример
<?php
ob_start();
$stream = null;
try {
    $stream = fopen('php://output', 'wb');
    if ($stream === false) {
        throw new RuntimeException('Не удалось открыть вывод');
    }
    if (fwrite($stream, "report ready\n") !== 13) {
        throw new RuntimeException('Запись неполная');
    }
    $captured = ob_get_contents();
    if ($captured === false) {
        throw new RuntimeException('Нет буфера');
    }
} finally {
    if (is_resource($stream)) {
        fclose($stream);
    }
    ob_end_clean();
}
echo 'Перехвачено: ', $captured;
Ожидаемый результат
Перехвачено: report ready

Поток php://output идёт в механизм вывода PHP, как echo: наш ob_start перехватил запись в строку. Это не то же самое, что прямой CLI stdout. В HTTP заголовки отправляют до фактического тела, а поведение доставки клиенту ещё зависит от буферов сервера.

В CLI php://stdin читает то, что передал вызывающий процесс, php://stdout пишет результат, php://stderr — диагностику. Обычный HTTP JSON endpoint использует php://input и механизм HTTP-ответа: переносить название stdin вместо input без понимания окружения не нужно. Разбор ob_start и порядка отправки заголовков — в буферизации вывода.

Когда достаточно потоков, а когда удобнее cURL

ЗадачаРазумный стартКогда усложнять
Прочитать маленький локальный конфигfile_get_contents с проверкой результата и размераfread, если источник большой или нужен разбор по частям
Собрать временный отчёт для библиотекиphp://temp и один ресурс до завершенияПисать сразу в целевой поток, если итог слишком велик для строки
Один GET к известному HTTPS APIПоток с контекстом и явными проверкамиcURL/HTTP-клиент, если нужны отдельные connect/total timeouts, подробная диагностика, upload/progress callbacks
Много endpoint, авторизация, повторы и пагинацияОтдельный клиент с единым контрактом ошибокУправление общим бюджетом, retry, очередь; транспорт выбрать под требования

cURL — отдельное расширение с настройками HTTP-клиента. Оно удобно, когда важен контроль соединения и всей операции по отдельности. Потоки дают общий интерфейс для файлов и других источников. Ни один подход автоматически не делает внешние данные надёжными и не определяет бизнес-успех только фактом получения строки.

Продолжение: HTTP-клиент и ошибки интеграции, ограничения частоты и повторы, полный путь интеграции.

Проверить понимание: пять маленьких изменений

  • В первом опыте замените 4 на 2. Что получит первое чтение и с какой позиции продолжится второе?
  • Уберите rewind в отчёте. Почему результат пустой, хотя fwrite вернул 6?
  • Замените ab на wb в журнале. В какой момент исчезнет created?
  • Ответ API имеет HTTP 404 и тело {"sku":"TEA","stock":8}. Можно ли принять 8?
  • Вы копируете 200 МиБ порциями, а затем вызываете stream_get_contents без предела и возвращаете строку. Где пропала экономия памяти?
Разбор ответов и следующий шаг
  • Первая порция — TE, позиция 2; остаток начинается с A и содержит оба перевода строки. fread считает байты.
  • Позиция осталась в конце. Запись успешна, но доступных байт после текущей позиции нет.
  • При самом fopen в режиме w, ещё до fwrite. Это свойство режима, не результат закрытия.
  • Нет: наш контракт требует 200 и правильные данные. Чтение тела ошибки не делает его успешным.
  • На финальном чтении всех оставшихся данных в строку. Для большого отчёта передавайте целевой поток и обрабатывайте порции до конца.

Следующий самостоятельный шаг: напишите функцию, которая принимает уже открытый поток выгрузки и возвращает число прочитанных байт, не закрывая чужой ресурс. Зафиксируйте договор: открывший ресурс отвечает за закрытие; функция только читает. Затем сравните с makeReport, где ресурс открывает и закрывает сама функция.

Реальные вопросы, по которым проверены ловушки этой главы: Записали тело ответа, но чтение пустое; Повторное открытие php://temp не возвращает прежнее содержимое; Режим w+ очищает файл раньше, чем его успели прочитать; Откуда продолжает читать следующий fread; Как прочитать тело HTTP 404; TLS не проверяется: где искать причину. Обсуждения показывают проблемы, а контракт функций сверяется по PHP Manual. Старые советы отключать TLS-проверку в примеры не перенесены.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

freadПрочитать до указанного числа байтов от текущей позиции.

PHP 4+ · standard

fread(resource $stream, int $length): string|false

Прочитать до указанного числа байтов от текущей позиции.

$name — переменная; = сохраняет результат, кавычки задают строку, ; завершает команду. name(...) вызывает функцию с аргументами через запятую. echo выводит; "\n" переносит строку, var_dump показывает также тип. __DIR__ — папка текущего PHP-файла; точка . соединяет строки. fopen возвращает открытый поток (resource), а не содержимое файла. rb читает байты, w+b создаёт/очищает файл и разрешает чтение с записью. fclose освобождает поток. while (условие) повторяет блок, пока условие истинно; после чтения важно различать конец и ошибку. if выбирает ветку; === false отличает отказ от 0 байтов и пустой строки, а !== false допускает строку "0". try выполняет попытку, catch обрабатывает указанный отказ, finally освобождает ресурс и не подавляет ошибку. throw new RuntimeException сообщает о сбое. Переменные; Вызов и return; Поток и позиция чтения.

Параметр или результатДоговор
streamОткрытый совместимый поток (resource). Закрытый или чужой ресурс в PHP 8 приводит к TypeError; возможности зависят от обёртки.
lengthПоложительное максимальное число байтов; это не обещание вернуть ровно столько.
ВозвратСтрока прочитанных байтов, в том числе пустая; false — отказ.
ОтказВ PHP 8 length <= 0 даёт ValueError; повреждённый ресурс — TypeError.
ВерсияPHP 4+
Путь конкретного документа
  1. stock.txt: TEA12
  2. fread 3 → TEA
  3. Позиция 3
  4. fread 2 → 12
Прочитать SKU и количество отдельными порциямиСамостоятельный пример
<?php
declare(strict_types=1);

$stream = fopen(__DIR__ . '/stock.txt', 'rb');
if ($stream === false) { throw new RuntimeException('Не открыть остаток'); }
try {
    $sku = fread($stream, 3);
    $quantity = fread($stream, 2);
    echo $sku, ': ', $quantity, " шт.\n";
} finally { fclose($stream); }
Ожидаемый результат
TEA: 12 шт.

Формат fixture — три ASCII-байта SKU и две цифры количества без разделителя. Второй fread продолжает первый. Для общего CSV используйте fgetcsv, а не постоянные размеры.

Скопировать памятку частями в новый файлСамостоятельный пример
<?php
declare(strict_types=1);

$input = fopen(__DIR__ . '/pickup.txt', 'rb');
$output = fopen(__DIR__ . '/copy.txt', 'wb');
if ($input === false || $output === false) { throw new RuntimeException('Не открыть файлы'); }
try {
    while (!feof($input)) {
        $part = fread($input, 4);
        if ($part === false || ($part === '' && !feof($input))) {
            throw new RuntimeException('Чтение не продвинулось');
        }
        if ($part !== '' && fwrite($output, $part) !== strlen($part)) {
            throw new RuntimeException('Порция записана не полностью');
        }
    }
} finally { fclose($input); fclose($output); }
echo 'copy.txt: ', file_get_contents(__DIR__ . '/copy.txt');
Ожидаемый результат
copy.txt: 104: ready

В памяти обрабатывается одна порция из четырёх байтов; маленький готовый файл прочитан целиком только для показа. Короткую неполную запись считаем отказом; вариант writeAll в главе streams дописывает остаток. Это блокирующие локальные файлы.

Применение и границы. Сеть может вернуть только доступный пакет или завершить чтение по тайм-ауту. Для буферизованного не-обычного файла одно низкоуровневое чтение обычно ограничено chunk size (часто 8192); из уже накопленного буфера результат бывает больше. Для сети отдельно нужны timeout, бюджет и проверка состояния потока; пустая строка не всегда EOF. Для небольшой полной строки проще file_get_contents; для крупных данных полезна обработка порциями.

Сравните: fgets, fgetc, file_get_contents, ftell, feof. Полный выбор файловых операций.

Продолжение: сохранённая прежняя практика.

Нужен читаемый открытый поток и положительная длина в байтах. Результат может быть короче запрошенного. false — ошибка; пустая строка сама по себе не доказывает ошибку. Чтение продвигает текущую позицию на реально полученные байты.

Два случая: прочитать первые 4 байта выгрузки и остаток и копировать выгрузку циклом, обрабатывая конец и отсутствие продвижения. В первом случае видна закладка; во втором файл не собирается в одну растущую строку.

Длина измеряется байтами, не символами UTF-8 и не строками CSV. Не используйте результат filesize как универсальный размер для сети. При недопустимой длине возможен ValueError; ввод-вывод также может выдать предупреждение.

fwriteЗаписать байты в открытый поток и узнать, сколько удалось записать.

PHP 4+; nullable length с PHP 8.0 · standard

fwrite(resource $stream, string $data, ?int $length = null): int|false

Записать байты в открытый поток и узнать, сколько удалось записать.

$name — переменная; = сохраняет результат, кавычки задают строку, ; завершает команду. name(...) вызывает функцию с аргументами через запятую. echo выводит; "\n" переносит строку, var_dump показывает также тип. __DIR__ — папка текущего PHP-файла; точка . соединяет строки. fopen возвращает открытый поток (resource), а не содержимое файла. rb читает байты, w+b создаёт/очищает файл и разрешает чтение с записью. fclose освобождает поток. function name($x) { ... } объявляет повторяемое действие; return отдаёт результат вызову. while (условие) повторяет блок, пока условие истинно; после чтения важно различать конец и ошибку. if выбирает ветку; === false отличает отказ от 0 байтов и пустой строки, а !== false допускает строку "0". try выполняет попытку, catch обрабатывает указанный отказ, finally освобождает ресурс и не подавляет ошибку. throw new RuntimeException сообщает о сбое. Переменные; Вызов и return; Поток и позиция чтения.

Параметр или результатДоговор
streamОткрытый совместимый поток (resource). Закрытый или чужой ресурс в PHP 8 приводит к TypeError; возможности зависят от обёртки. Должен разрешать запись.
dataСтрока байтов; перевод строк добавляете сами.
lengthnull — вся data; иначе максимум байтов, не больше длины data. В PHP8.5.11 length <= 0 возвращает 0 без записи, а не ValueError.
ВозвратКоличество записанных байтов, включая 0; false при отказе.
ОтказПри отказе записи E_WARNING; несовместимый тип аргумента в PHP8 даёт TypeError.
ВерсияPHP 4+; nullable length с PHP 8.0
Путь конкретного документа
  1. pickup.txt создаётся один раз
  2. Первая запись: «Выдача заказа 104»
  3. Вторая запись: «Дрель: 2»
  4. Обе строки остаются в файле
Записать заголовок и строку выдачи в один файлСамостоятельный пример
<?php
declare(strict_types=1);

$path = __DIR__ . '/pickup.txt';
$stream = fopen($path, 'w+b');
if ($stream === false) { throw new RuntimeException('Не создать памятку'); }
try {
    fwrite($stream, "Выдача заказа 104\n");
    fwrite($stream, "Дрель: 2\n");
    rewind($stream);
    echo stream_get_contents($stream);
} finally { fclose($stream); }
Ожидаемый результат
Выдача заказа 104
Дрель: 2

Каждый вызов продолжает запись с текущей позиции; w очистил файл при открытии, а не перед каждой строкой. Здесь успешный короткий путь; следующий помощник обрабатывает частичные записи. fputs — псевдоним fwrite с тем же контрактом.

Полностью записать памятку через помощник writeBytesСамостоятельный пример
<?php
function writeBytes($h, $data) {
    $offset = 0;
    while ($offset < strlen($data)) {
        $written = fwrite($h, substr($data, $offset));
        if ($written === false || $written === 0) { throw new RuntimeException('write stopped'); }
        $offset += $written;
    }
    return $offset;
}
$h = fopen(__DIR__ . '/result.txt', 'w+b');
try { echo writeBytes($h, "104: ready\n"), "
"; }
finally { fclose($h); }
echo file_get_contents(__DIR__ . '/result.txt');
Ожидаемый результат
11
104: ready

writeBytes дописывает ещё не записанный хвост: substr берёт остаток, offset увеличивается на фактически записанные байты. false и 0 останавливают цикл. Возврат 11 подтверждает размер, после закрытия показано содержимое реального result.txt. Это локальный блокирующий поток.

Применение и границы. На сети возможна частичная запись. В append-режиме локальная ОС может обеспечивать неделимость одного небольшого write, но оговорки платформы, размера блока и файловой системы не дают общего обещания для записи из нескольких вызовов или чтения–изменения–записи. Для общей договорённости процессов нужен flock; для долговечности отдельно fflush/fsync. b сохраняет двоичные данные.

Сравните: fputs, file_put_contents, flock, fflush. Полный выбор файловых операций.

Продолжение: сохранённая прежняя практика.

Возвращает число реально записанных байт или false. Необязательная length ограничивает число байт из data; обычно передавайте всю строку. Нулевой результат не продвигает запись, а частичная запись оставляет хвост.

Два случая: добавить событие в журнал через ab и собрать отчёт через writeAll с дописыванием остатка. У короткой учебной строки частичный результат превращается в ошибку; помощник writeAll его обрабатывает.

Режим потока определяет поведение: w очищает при открытии, a всегда дописывает. fwrite не ставит блокировку за вас и не является обещанием долговечной записи на физический носитель. Для одной строки удобно file_put_contents; для CSV — fputcsv.

stream_context_createСоздать отдельный набор настроек для одного маршрута чтения или записи.

PHP 4.3.0+ · standard

stream_context_create(?array $options = null, ?array $params = null): resource

Создать отдельный набор настроек для одного маршрута чтения или записи.

Поток — открытый канал байтов, например файл или соединение. Переменная со знаком $ хранит ссылку на ресурс; это ещё не строка его содержимого. Скобки после имени функции запускают вызов, аргументы внутри передают настройки, = сохраняет результат. false означает отказ там, где это предусмотрено контрактом; пустая строка и 0 могут быть успешными результатами. Первый опыт чтения потока объясняет открытие, позицию и закрытие.

Часть контрактаЗначение
optionsМассив wrapper → option → value либо null/пропуск. Не общий плоский список.
paramsМассив параметров контекста, например notification; null/пропуск допустим.
ВозвратРесурс stream-context. Само создание не делает файлового или сетевого запроса.
Ошибки / версииPHP 8.0: options и params nullable. Неверная вложенность options даёт ValueError; значения проверяет использующий их wrapper.
Настройки двух операций
  1. GET получает собственный timeout=2
  2. POST получает отдельный timeout=5
  3. Каждое открытие получает свой context
  4. Один вызов не меняет политику другого

Сервису нужен короткий запрос чтения, а отправке большого отчёта — другие настройки. Контекст позволяет передать настройки конкретному открытию потока вместо изменения всего процесса. Сначала создадим пакет настроек и прочитаем его: создания соединения пока нет.

Упаковать timeout для будущего чтенияСамостоятельный пример
<?php
declare(strict_types=1);

$options = ['http' => ['timeout' => 2.0]];
$context = stream_context_create($options);
$saved = stream_context_get_options($context);
echo $saved['http']['timeout'], " секунд\n";
echo get_resource_type($context), "\n";
Ожидаемый результат
2 секунд
stream-context

Вложенный массив группирует timeout под именем wrapper http. Квадратные скобки при чтении выбирают сначала группу, затем настройку. Ресурс stream-context хранит настройки, а не скачанное содержимое. Ни один URL здесь не открывается.

Дать двум операциям разные настройкиСамостоятельный пример
<?php
declare(strict_types=1);

$read = stream_context_create(['http' => ['method' => 'GET', 'timeout' => 2]]);
$send = stream_context_create(['http' => ['method' => 'POST', 'timeout' => 5]]);
stream_context_set_option($read, 'http', 'timeout', 3);
foreach ([$read, $send] as $context) {
    $options = stream_context_get_options($context)['http'];
    echo $options['method'], '/', $options['timeout'], "\n";
}
Ожидаемый результат
GET/3
POST/5

Изменение первого созданного ресурса не затронуло второй. Сам POST не отправлен: его body, проверка ответа и безопасная политика повторов — отдельные решения. После присваивания $other = $read обе переменные обозначали бы один ресурс, а не независимую копию.

options имеют форму [wrapper => [option => value]], params — форму [parameter => value]. HTTP-параметры HTTPS-ссылки по-прежнему идут под http, TLS-настройки — под ssl. Наличие поля в сохранённых options не доказывает, что конкретный wrapper его поддерживает или уже применил. Передайте контекст четвёртым аргументом fopen либо соответствующим аргументом другой открывающей функции. Неверная структура options вызывает ValueError; неправильный PHP-тип — TypeError. С PHP 8.0 оба массива могут быть null.

Связанные операции: stream_context_get_options, stream_context_set_option, stream_context_get_default.

Маршрут от первого потока к контекстам и фильтрам.

options — группы опций по именам обёрток, например http и ssl. params — дополнительные параметры контекста, например notification callback; обычному GET они не нужны. Результат — ресурс контекста, не тело ответа и не выполненный запрос. Некорректная форма аргументов/опций может вызвать ошибку; доступность опций зависит от обёртки.

Два случая в самостоятельном примере: GET с Accept для чтения остатков и POST с JSON-телом для подготовленной отправки. Оба контекста исследуются без сети. Контекстное HTTP-чтение показывает передачу ресурса четвёртым аргументом fopen и отдельную проверку результата.

Для HTTPS HTTP-параметры по-прежнему помещают под http, TLS — под ssl. Не выключайте verify_peer/verify_peer_name для исправления сертификата. ignore_errors разрешает получить тело HTTP-отказа, но не отменяет проверку статуса. Родственные функции: stream_context_get_options, stream_get_meta_data; альтернатива для сложного HTTP — cURL.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.