Глава 25 · С нуля → интеграции
Потоки, обёртки и контексты: читать и писать данные по шагам
От строки и файла к позиции чтения, временным отчётам, php://input/output и проверяемому HTTP-ответу.
Задача: прочитать выгрузку, а не держать весь файл в памяти
Две порции из одного открытого файла
В orders.txt лежит TEA\nCOFFEE\n: 11 байт. Символы \n на схеме обозначают переводы строки.
- fopen: позиция 0
Открываем orders.txt для чтения. В переменной ресурс канала, а не весь текст файла.
- fread(…, 4): TEA\n
Читаем 4 байта. Позиция следующего чтения становится 4.
- Читаем остаток: COFFEE\n
stream_get_contents продолжает с позиции 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 перемотка есть, у других каналов её может не быть.
- fwrite записал TEA;2 и перевод строки: позиция 6
- Чтение с позиции 6 → пустая строка
- rewind → позиция 0
- Чтение с позиции 0 → TEA;2 и перевод строки
<?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.
<?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-статус; подробный разбор регулярных выражений — в соответствующей главе.
<?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.
<?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
// Функция вызывается 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
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Прочитать до указанного числа байтов от текущей позиции.
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+ |
- stock.txt: TEA12
- fread 3 → TEA
- Позиция 3
- fread 2 → 12
<?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Записать байты в открытый поток и узнать, сколько удалось записать.
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 | Строка байтов; перевод строк добавляете сами. |
| length | null — вся data; иначе максимум байтов, не больше длины data. В PHP8.5.11 length <= 0 возвращает 0 без записи, а не ValueError. |
| Возврат | Количество записанных байтов, включая 0; false при отказе. |
| Отказ | При отказе записи E_WARNING; несовместимый тип аргумента в PHP8 даёт TypeError. |
| Версия | PHP 4+; nullable length с PHP 8.0 |
- pickup.txt создаётся один раз
- Первая запись: «Выдача заказа 104»
- Вторая запись: «Дрель: 2»
- Обе строки остаются в файле
<?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 с тем же контрактом.
<?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Создать отдельный набор настроек для одного маршрута чтения или записи.
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. |
- GET получает собственный timeout=2
- POST получает отдельный timeout=5
- Каждое открытие получает свой context
- Один вызов не меняет политику другого
Сервису нужен короткий запрос чтения, а отправке большого отчёта — другие настройки. Контекст позволяет передать настройки конкретному открытию потока вместо изменения всего процесса. Сначала создадим пакет настроек и прочитаем его: создания соединения пока нет.
<?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.