PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 34 · Практика
Интеграции: HTTP-клиент, JSON и контролируемые ошибки
Транспорт, статус, формат и бизнес-ответ проверяются отдельно; сетевые вызовы получают лимиты.
Два файла: увидеть ответ другой программы
Нужно показать остаток товара, который хранится у другой программы. Сначала обе программы будут на вашем компьютере: одна принимает HTTP-запрос, другая его посылает. Получится строка Молоток — 8 шт. Здесь вы увидите реальную передачу, а не заранее записанный ответ внутри клиента.
- Терминал 2: example.php просит http://127.0.0.1:8791/
- Терминал 1: server.php формирует заголовок и тело
- HTTP 200; Content-Type: text/plain; charset=UTF-8
- Тело возвращается клиенту; echo показывает строку товара
Нужен PHP с расширением cURL. Команда php -m должна содержать curl; настройки CLI и PHP сайта могут различаться. Создайте отдельную папку и сохраните в ней два файла ниже в UTF-8. Начальные <?php включают PHP; круглые скобки содержат аргументы вызова, запятая отделяет их, точка с запятой завершает действие. Строка в кавычках — текст. В двойных кавычках
означает перевод строки.
Файл server.php. header(...) сообщает получателю формат ответа; echo добавляет само содержимое. По умолчанию успешный ответ имеет статус 200. Сервер пока возвращает один заранее заданный товар.
<?php
header('Content-Type: text/plain; charset=UTF-8');
echo "Молоток — 8 шт.\n";В первом терминале из этой папки выполните php -S 127.0.0.1:8791 server.php. Терминал остаётся занят сервером. Откройте второй терминал в той же папке: в нём затем выполните php example.php. Встроенный сервер подходит для этого локального опыта. Остановка — Ctrl+C в первом терминале. Адрес 127.0.0.1 означает эту машину, число после двоеточия — её порт.
| Запись в клиенте | Что она делает |
|---|---|
$base = ... | Переменная $base хранит адрес. Знак = кладёт значение справа в переменную слева. getenv читает необязательную настройку окружения; оператор ?: выбирает адрес справа, когда настройка пуста. |
$client = curl_init($base) | Создаётся настроечный объект клиента. Само создание ещё не посылает запрос. |
curl_setopt($client, CURLOPT_RETURNTRANSFER, true) | Именованная константа выбирает настройку «вернуть тело». true включает её; иначе cURL обычно выводит полученные байты непосредственно. |
echo curl_exec($client) | exec выполняет запрос; его возвращённая строка сразу передаётся echo. Здесь временно рассматривается только известный успешный ответ. |
<?php
declare(strict_types=1);
$base = getenv('PHP_MANUAL_LOCAL_URL') ?: 'http://127.0.0.1:8791';
$client = curl_init($base);
curl_setopt($client, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($client);
Ожидаемый результат
Молоток — 8 шт.
Это минимальный успешный обмен. В архиве server.php и example.php; base.php для этого первого клиента не требуется. Если остановить сервер, этот короткий вариант ещё не объяснит отказ: следующая ступень добавляет проверки.
Если порт занят, запустите сервер на 8792, а во втором PowerShell задайте $env:PHP_MANUAL_LOCAL_URL = 'http://127.0.0.1:8792'. Оба процесса должны использовать один порт. Клиент запускается в CLI: обращение обработчика обратно к тому же занятому однопоточному серверу может заблокировать опыт.
От короткого вызова к проверке отказов
Теперь нужен пригодный для работы клиент: если сервер выключен, человек должен получить понятное сообщение; если сервер сообщил HTTP 404, это отдельный исход. if выполняет блок в фигурных скобках при истинном условии. === false проверяет именно логический отказ: строка "0" — полноценное тело, а пустая строка может быть нормальным ответом 204. throw прерывает работу исключением; на границе приложения его перехватывает catch.
- curl_exec: удалось ли получить ответ
- curl_getinfo: какой HTTP-статус сообщил сервер
- Содержимое: соответствует ли оно договору нашей программы
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init($base . '/product?id=1');
if ($client === false) { throw new RuntimeException('cURL не создал клиент'); }
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
echo $body;
Ожидаемый результат
Молоток — 8 шт.
base.php выбирает и проверяет только адрес нашего локального сервера. В архиве server.php содержит /product?id=1. TIMEOUT=2 — предел всей передачи в секундах; CONNECTTIMEOUT=1 ограничивает установление соединения. Они не меняют бизнес-смысл статуса 404.
В следующих архивах require __DIR__ . '/base.php' подключает общий файл настроек рядом с примером. __DIR__ — каталог самого файла, точка соединяет строки. Полный файл ниже: ?? подставляет значение при отсутствующем ключе, || означает «или», isset проверяет наличие, !== — строгое несовпадение. Проверка допускает только собственный HTTP-стенд, поэтому адрес не берётся из посетительской формы.
<?php
$base = getenv('PHP_MANUAL_LOCAL_URL') ?: 'http://127.0.0.1:8791';
$parts = parse_url($base);
if ($parts === false || ($parts['scheme'] ?? '') !== 'http'
|| ($parts['host'] ?? '') !== '127.0.0.1'
|| isset($parts['user']) || isset($parts['pass']) || isset($parts['query'])
|| isset($parts['fragment']) || !empty($parts['path'])) {
throw new RuntimeException('Учебный сервер должен находиться на http://127.0.0.1:порт');
}| Сбой | Что проверить и сказать человеку |
|---|---|
| curl_exec вернул false | Порт, работа сервера, настройки сети; диагностический текст curl_error оставьте оператору. Пользователю: «Сервис сейчас недоступен, повторите позже». |
| HTTP 404 | Соединение работает, однако товар не найден. Проверьте id; повтор той же операции сам по себе не создаст товар. |
| HTTP 409 | Запрос понятен, но противоречит текущему состоянию, например запрошено больше остатка. |
| HTTP 200, неверный JSON | Не подставляйте нули вместо сломанного ответа. Сообщите о недоступности данных и сохраните ограниченную диагностику. |
Какие заголовки и байты получает сервер
Одинаковые PHP-массивы можно передать разными форматами. Сервер разбирает байты тела и Content-Type. Передача массива в POSTFIELDS создаёт multipart; передача готовой строки формы даёт urlencoded; JSON требует json_encode и явного заголовка. Следующие опыты вызывают один собственный /inspect: он возвращает то, что действительно получил.
- Клиент выбирает формат и создаёт байты
- HTTP переносит Content-Type и тело
- PHP заполняет $_POST только для поддерживаемой POST-формы
- Для JSON обработчик читает php://input и вызывает json_decode
http_build_query кодирует пары полей для формы. Ключ quantity становится именем поля; число 2 превращается в текст 2. json_encode сохраняет JSON-число; у JSON нет автоматического заполнения $_POST. json_decode(..., true) превращает полученный объект JSON в ассоциативный массив PHP, где значение читают как $reply['raw'].
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init($base . '/inspect');
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
curl_setopt_array($client, [CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>http_build_query(['sku'=>'A42', 'quantity'=>2]), CURLOPT_HTTPHEADER=>['Content-Type: application/x-www-form-urlencoded']]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
$reply = json_decode($body, true, 8, JSON_THROW_ON_ERROR);
echo $reply['method'], "\n", explode(';', $reply['content_type'])[0], "\n";
echo 'Тело: ', $reply['raw'], "\n";
echo 'Поля формы: ', json_encode($reply['fields'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
POST
application/x-www-form-urlencoded
Тело: sku=A42&quantity=2
Поля формы: {"sku":"A42","quantity":"2"}
Вывод построен из ответа /inspect. У multipart случайная boundary задаётся cURL; вручную заголовок без boundary не устанавливаем. В результате показан основной тип и реально разобранные поля. У двух строковых форматов показано точное тело; [] означает пустой $_POST, а не пустой HTTP-запрос.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init($base . '/inspect');
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
curl_setopt_array($client, [CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>json_encode(['sku'=>'A42', 'quantity'=>2], JSON_THROW_ON_ERROR), CURLOPT_HTTPHEADER=>['Content-Type: application/json']]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
$reply = json_decode($body, true, 8, JSON_THROW_ON_ERROR);
echo $reply['method'], "\n", explode(';', $reply['content_type'])[0], "\n";
echo 'Тело: ', $reply['raw'], "\n";
echo 'Поля формы: ', json_encode($reply['fields'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
POST
application/json
Тело: {"sku":"A42","quantity":2}
Поля формы: []
Вывод построен из ответа /inspect. У multipart случайная boundary задаётся cURL; вручную заголовок без boundary не устанавливаем. В результате показан основной тип и реально разобранные поля. У двух строковых форматов показано точное тело; [] означает пустой $_POST, а не пустой HTTP-запрос.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init($base . '/inspect');
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
curl_setopt_array($client, [CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>['sku'=>'A42', 'quantity'=>'2'], CURLOPT_HTTPHEADER=>[]]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
$reply = json_decode($body, true, 8, JSON_THROW_ON_ERROR);
echo $reply['method'], "\n", explode(';', $reply['content_type'])[0], "\n";
echo 'Поля формы: ', json_encode($reply['fields'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
POST
multipart/form-data
Поля формы: {"sku":"A42","quantity":"2"}
Вывод построен из ответа /inspect. У multipart случайная boundary задаётся cURL; вручную заголовок без boundary не устанавливаем. В результате показан основной тип и реально разобранные поля. У двух строковых форматов показано точное тело; [] означает пустой $_POST, а не пустой HTTP-запрос.
При стандартной обработке multipart PHP расходует тело на поля и файлы; отсутствие его копии в php://input не означает, что данные не пришли. Файлы попадают в $_FILES. Передача настоящего inventory.txt показывает отдельный полный опыт. Чтобы увидеть реализацию /inspect, откройте следующий исходник стенда.
Живой расчёт цены: запрос, проверка и ответ
Следующая задача полезна человеку: запросить цену двух молотков. Клиент посылает только id=1 и quantity=2. Цена 450 и остаток 8 принадлежат серверному каталогу; посетитель не должен назначать их своей строкой запроса. Результат: Молоток: 2 шт.; 900 руб. Это предложение цены, без оплаты и списания склада.
- POST /quote; Content-Type: application/json
- Тело {"id":1,"quantity":2}
- Сервер: целое количество ≤8; цена 450 ×2
- HTTP 200; JSON product/quantity/unit_price/total → строка предложения
| Новая конструкция | Зачем она нужна здесь |
|---|---|
['id'=>1, 'quantity'=>2] | Массив именованных значений. Стрелка => связывает ключ и значение; json_encode создаёт из него JSON-текст. |
$_SERVER и php://input | Первое содержит параметры входящего HTTP-запроса; второе позволяет прочитать его тело. Это данные приёмника, а не клиента. |
is_array / is_int / is_string | Проверяют фактический тип: строка "2" и число 2 различаются. [] вместо quantity не должно превратиться в допустимое количество. |
JSON_THROW_ON_ERROR и try / catch | Ошибка формата становится исключением; сервер перехватывает её и возвращает осмысленный HTTP 400. |
exit | Завершает этот обработчик после ответа об ошибке, чтобы он не продолжил расчёт. |
* и цена сервера | Умножение 450 на проверенное количество. Целые рубли выбраны для простоты; дробные деньги требуют определённой единицы и правила округления. |
server.php этого опыта. Это самостоятельный небольшой обработчик. Скопируйте его из архива и запустите той же командой сервера, предварительно остановив предыдущий. Проверка media type намеренно строгая для точно показанного клиента; общий обработчик может отдельно разбирать допустимые параметры Content-Type.
<?php
header('Content-Type: application/json; charset=UTF-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Allow: POST');
echo '{"error":"Нужен POST"}';
exit;
}
if (($_SERVER['CONTENT_TYPE'] ?? '') !== 'application/json') {
http_response_code(415);
echo '{"error":"Нужен application/json"}';
exit;
}
$raw = file_get_contents('php://input', false, null, 0, 4097);
try {
if ($raw === false || strlen($raw) > 4096) {
throw new RuntimeException('Слишком большое тело');
}
$input = json_decode($raw, true, 8, JSON_THROW_ON_ERROR);
if (!is_array($input) || ($input['id'] ?? null) !== 1
|| !is_int($input['quantity'] ?? null) || $input['quantity'] < 1) {
throw new RuntimeException('Нужны id=1 и целое quantity от 1');
}
} catch (JsonException | RuntimeException $error) {
http_response_code(400);
echo '{"error":"Проверьте id и целое количество"}';
exit;
}
if ($input['quantity'] > 8) {
http_response_code(409);
echo '{"error":"На складе только 8 штук"}';
exit;
}
$reply = ['product'=>'Молоток', 'quantity'=>$input['quantity'],
'unit_price'=>450, 'total'=>450 * $input['quantity']];
echo json_encode($reply, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);example.php. Настройки заголовков — массив полных строк; Accept описывает желаемый формат ответа, Content-Type — формат отправленного тела. Сначала проверяется транспорт, затем HTTP-статус, формат и поля. Текст ошибки оператора не следует выводить посетителю вместо понятного сообщения.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$request = json_encode(['id'=>1, 'quantity'=>2], JSON_THROW_ON_ERROR);
$client = curl_init($base . '/quote');
curl_setopt_array($client, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>2,
CURLOPT_CONNECTTIMEOUT=>1, CURLOPT_POST=>true,
CURLOPT_HTTPHEADER=>['Content-Type: application/json', 'Accept: application/json'],
CURLOPT_POSTFIELDS=>$request,
]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Сервис не ответил: '.curl_error($client)); }
$status = curl_getinfo($client, CURLINFO_RESPONSE_CODE);
$type = curl_getinfo($client, CURLINFO_CONTENT_TYPE);
if ($type === null || !str_starts_with(strtolower($type), 'application/json')) {
throw new RuntimeException('Сервис вернул другой формат');
}
$reply = json_decode($body, true, 8, JSON_THROW_ON_ERROR);
if ($status !== 200) {
echo 'Предложение не создано: ', $reply['error'] ?? 'неизвестная причина', "\n";
} else {
if (!is_array($reply) || !is_string($reply['product'] ?? null)
|| !is_int($reply['quantity'] ?? null) || !is_int($reply['total'] ?? null)) {
throw new RuntimeException('В ответе нет нужных полей');
}
echo $reply['product'], ': ', $reply['quantity'], ' шт.; ', $reply['total'], " руб.\n";
}
Ожидаемый результат
Молоток: 2 шт.; 900 руб.
Клиент действительно послал JSON, обработчик проверил данные и вычислил 900. Все три файла server.php/base.php/example.php приложены. Ответ выводится как текст; при вставке названия в HTML используйте htmlspecialchars.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$request = json_encode(['id'=>1, 'quantity'=>9], JSON_THROW_ON_ERROR);
$client = curl_init($base . '/quote');
curl_setopt_array($client, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>2,
CURLOPT_CONNECTTIMEOUT=>1, CURLOPT_POST=>true,
CURLOPT_HTTPHEADER=>['Content-Type: application/json', 'Accept: application/json'],
CURLOPT_POSTFIELDS=>$request,
]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Сервис не ответил: '.curl_error($client)); }
$status = curl_getinfo($client, CURLINFO_RESPONSE_CODE);
$type = curl_getinfo($client, CURLINFO_CONTENT_TYPE);
if ($type === null || !str_starts_with(strtolower($type), 'application/json')) {
throw new RuntimeException('Сервис вернул другой формат');
}
$reply = json_decode($body, true, 8, JSON_THROW_ON_ERROR);
if ($status !== 200) {
echo 'Предложение не создано: ', $reply['error'] ?? 'неизвестная причина', "\n";
} else {
if (!is_array($reply) || !is_string($reply['product'] ?? null)
|| !is_int($reply['quantity'] ?? null) || !is_int($reply['total'] ?? null)) {
throw new RuntimeException('В ответе нет нужных полей');
}
echo $reply['product'], ': ', $reply['quantity'], ' шт.; ', $reply['total'], " руб.\n";
}
Ожидаемый результат
Предложение не создано: На складе только 8 штук
Единственное изменение входа — quantity=9. HTTP 409 не является транспортной ошибкой, тело содержит error. Повтор без изменения количества не решит проблему.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$request = json_encode(['id'=>1, 'quantity'=>'2'], JSON_THROW_ON_ERROR);
$client = curl_init($base . '/quote');
curl_setopt_array($client, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>2,
CURLOPT_CONNECTTIMEOUT=>1, CURLOPT_POST=>true,
CURLOPT_HTTPHEADER=>['Content-Type: application/json', 'Accept: application/json'],
CURLOPT_POSTFIELDS=>$request,
]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Сервис не ответил: '.curl_error($client)); }
$status = curl_getinfo($client, CURLINFO_RESPONSE_CODE);
$type = curl_getinfo($client, CURLINFO_CONTENT_TYPE);
if ($type === null || !str_starts_with(strtolower($type), 'application/json')) {
throw new RuntimeException('Сервис вернул другой формат');
}
$reply = json_decode($body, true, 8, JSON_THROW_ON_ERROR);
if ($status !== 200) {
echo 'Предложение не создано: ', $reply['error'] ?? 'неизвестная причина', "\n";
} else {
if (!is_array($reply) || !is_string($reply['product'] ?? null)
|| !is_int($reply['quantity'] ?? null) || !is_int($reply['total'] ?? null)) {
throw new RuntimeException('В ответе нет нужных полей');
}
echo $reply['product'], ': ', $reply['quantity'], ' шт.; ', $reply['total'], " руб.\n";
}
Ожидаемый результат
Предложение не создано: Проверьте id и целое количество
В JSON отправлена строка "2", хотя договор требует число. Сервер отвечает 400, клиент показывает причину. Это строгий API-договор; у обычной HTML-формы строки сначала валидируют и преобразуют отдельно.
Упражнение: запросите три молотка. Предложенный ответ — Молоток: 3 шт.; 1350 руб.. Затем замените quantity на массив: ожидается 400 и сообщение о проверке количества. Не меняйте цену в клиенте: в этом договоре её там вообще нет. Для повторов изменяющих операций сначала нужен договор идемпотентности; повтор расчёта и повтор оплаты — разные действия.
Ноль, пустой ответ, HTTP-ошибка и сломанный JSON
Сеть может успешно вернуть ноль, пустое тело, HTTP-ошибку или испорченный JSON. Для каждого случая нужна своя политика. В следующем опыте стенд намеренно даёт все эти ответы, а клиент сообщает результат по смыслу.
| Адрес стенда | Ответ | Решение клиента |
|---|---|---|
| /zero | 200, строка 0 | Успешный текст, не false |
| /empty | 204, пустое тело | Не запускать JSON-декодер |
| /json/product?id=99 | 404, JSON ошибки | Товар отсутствует |
| /broken-json | 200, {broken | Ошибка формата, данных товара нет |
foreach повторяет тело блока для каждого пути; elseif выбирает следующий случай только если предыдущий не подошёл. Внутри цикла новый клиент отделяет настройки одного опыта от другого. Это четыре заданных ответа своего сервера, поэтому их смысл известен заранее.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
foreach (['/zero', '/empty', '/json/product?id=99', '/broken-json'] as $path) {
$client = curl_init($base . $path);
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>2]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException(curl_error($client)); }
$status = curl_getinfo($client, CURLINFO_RESPONSE_CODE);
if ($status === 204) { echo "204: ответ без тела\n"; }
elseif ($status === 404) { echo "404: товар не найден\n"; }
elseif ($path === '/zero') { echo '200: текст ', $body, "\n"; }
else {
try { json_decode($body, true, 8, JSON_THROW_ON_ERROR); }
catch (JsonException $error) { echo "200: неверный JSON, товар не показываем\n"; }
}
}
Ожидаемый результат
200: текст 0
204: ответ без тела
404: товар не найден
200: неверный JSON, товар не показываем
Ни один из ответов не превращён в абстрактный bool. У 200 с повреждённым JSON транспорт и HTTP работают, но данные непригодны для показа товара.
Полный локальный сервер для справочных карточек
Все предыдущие карточки, кроме двух явно уменьшенных серверов, используют этот стенд. Его исходник находится в каждом архиве. Здесь функции появляются после простого обработчика: reply собирает единый JSON-ответ и завершает запрос. Условия по пути выбирают маршрут, таблица $catalog задаёт весь набор данных. Для POST /quote проверены метод, Content-Type, размер, JSON и типы. /upload только читает временный учебный файл, не публикует его.
| Маршрут | Заданные данные и результат |
|---|---|
| GET /product?id=1 и /json/product?id=1 | Молоток, остаток 8, цена 450; текст или JSON |
| id=2; id=3 | Перчатки: 15, цена 120; Кисть: 0, цена 80 |
| POST /inspect | Возвращает метод, Content-Type, X-Request-Id, тело, $_POST и $_GET |
| POST /quote | Проверяет количество и вычисляет сумму, без изменения каталога |
| /cookie/set → /cookie/read | Первый ответ устанавливает cookie, следующий запрос приносит её обратно |
| PUT /parse-body | PHP 8.4+: разбор формы PUT посредством request_parse_body |
<?php
declare(strict_types=1);
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
$products = [
1 => ['id'=>1, 'name'=>'Молоток', 'stock'=>8, 'price'=>450],
2 => ['id'=>2, 'name'=>'Перчатки', 'stock'=>15, 'price'=>120],
3 => ['id'=>3, 'name'=>'Кисть', 'stock'=>0, 'price'=>80],
];
function reply(int $status, array $data): never {
http_response_code($status);
header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: no-store');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;
}
if ($path === '/health') { header('Content-Type: text/plain'); exit('ready'); }
if ($path === '/empty') { http_response_code(204); exit; }
if ($path === '/zero') { header('Content-Type: text/plain'); exit('0'); }
if ($path === '/broken-json') { header('Content-Type: application/json'); exit('{broken'); }
if ($path === '/redirect') { header('Location: /product?id=1', true, 302); exit; }
if ($path === '/product' || $path === '/json/product') {
$rawId = $_GET['id'] ?? null;
if (!is_string($rawId) || !ctype_digit($rawId)) { reply(400, ['error'=>'Нужен целый номер товара']); }
$product = $products[(int)$rawId] ?? null;
if ($product === null) { reply(404, ['error'=>'Товар не найден']); }
header('X-Catalog: workshop');
if ($path === '/json/product') { reply(200, $product); }
header('Content-Type: text/plain; charset=UTF-8');
echo $product['name'], ' — ', $product['stock'], " шт.\n";
exit;
}
if ($path === '/quote') {
if ($method !== 'POST') { header('Allow: POST'); reply(405, ['error'=>'Нужен POST']); }
$type = strtolower(trim(explode(';', $_SERVER['CONTENT_TYPE'] ?? '')[0]));
if ($type !== 'application/json') { reply(415, ['error'=>'Нужен Content-Type application/json']); }
$raw = file_get_contents('php://input', false, null, 0, 4097);
if ($raw === false || strlen($raw) > 4096) { reply(413, ['error'=>'Тело запроса слишком велико']); }
try { $input = json_decode($raw, true, 8, JSON_THROW_ON_ERROR); }
catch (JsonException) { reply(400, ['error'=>'Повреждён JSON']); }
if (!is_array($input) || !is_int($input['id'] ?? null) || !is_int($input['quantity'] ?? null)
|| $input['quantity'] < 1 || $input['quantity'] > 100) { reply(422, ['error'=>'Нужны целые id и quantity от1 до100']); }
$product = $products[$input['id']] ?? null;
if ($product === null) { reply(404, ['error'=>'Товар не найден']); }
if ($input['quantity'] > $product['stock']) { reply(409, ['error'=>'Недостаточно товара']); }
reply(200, ['product'=>$product['name'], 'quantity'=>$input['quantity'],
'unit_price'=>$product['price'], 'total'=>$product['price']*$input['quantity']]);
}
if ($path === '/inspect') {
$type = $_SERVER['CONTENT_TYPE'] ?? '';
$raw = file_get_contents('php://input');
reply(200, ['method'=>$method, 'content_type'=>$type,
'request_id'=>$_SERVER['HTTP_X_REQUEST_ID'] ?? null, 'raw'=>$raw, 'fields'=>$_POST, 'query'=>$_GET]);
}
if ($path === '/upload') {
if ($method !== 'POST') { header('Allow: POST'); reply(405, ['error'=>'Нужен POST']); }
$file = $_FILES['inventory'] ?? null;
if (!is_array($file) || ($file['error'] ?? null) !== UPLOAD_ERR_OK
|| !is_string($file['tmp_name'] ?? null) || !is_uploaded_file($file['tmp_name'])) {
reply(422, ['error'=>'Ожидается файл inventory']);
}
if ($file['size'] > 4096) { reply(413, ['error'=>'Файл слишком велик']); }
$data = file_get_contents($file['tmp_name']);
if ($data === false) { reply(500, ['error'=>'Временный файл не читается']); }
reply(200, ['name'=>$file['name'], 'declared_type'=>$file['type'], 'data'=>$data]);
}
if ($path === '/cookie/set' || $path === '/cookie/raw') {
$before = $_COOKIE['theme'] ?? 'не выбрана';
$options = ['path'=>'/', 'httponly'=>true, 'samesite'=>'Lax'];
if ($path === '/cookie/raw') { setrawcookie('theme', 'blue%20sky', $options); }
else { setcookie('theme', 'blue sky', $options); }
header('Content-Type: text/plain; charset=UTF-8');
echo 'Тема входящего запроса: ', $before, "\n";
exit;
}
if ($path === '/cookie/read') {
header('Content-Type: text/plain; charset=UTF-8');
echo 'Тема следующего запроса: ', $_COOKIE['theme'] ?? 'не выбрана', "\n";
exit;
}
if ($path === '/headers/list') {
header('Content-Type: text/plain; charset=UTF-8');
header('X-Catalog: workshop');
$selected = array_values(array_filter(headers_list(), fn($line) => str_starts_with($line, 'X-Catalog:')));
echo implode("\n", $selected), "\nМолоток — 8 шт.\n";
exit;
}
if ($path === '/headers/remove') {
header('X-Debug-Token: temporary');
header_remove('X-Debug-Token');
header('X-Catalog: workshop');
header('Content-Type: text/plain; charset=UTF-8');
exit("Молоток — 8 шт.\n");
}
if ($path === '/headers/callback') {
header_register_callback(function (): void { header('X-Report-State: prepared'); });
header('Content-Type: text/plain; charset=UTF-8');
exit("Молоток — 8 шт.\n");
}
if ($path === '/headers/sent') {
ob_start();
echo "Молоток — 8 шт.\n";
if (headers_sent($file, $line)) { throw new RuntimeException('Ответ уже отправлен'); }
header('Content-Type: text/plain; charset=UTF-8');
header('X-Report-State: prepared-before-output');
ob_end_flush();
exit;
}
if ($path === '/parse-body') {
if ($method !== 'PUT') { header('Allow: PUT'); reply(405, ['error'=>'Нужен PUT']); }
[$fields, $files] = request_parse_body();
reply(200, ['fields'=>$fields, 'files'=>array_keys($files)]);
}
reply(404, ['error'=>'Маршрут не найден']);Переход к HTTPS на настоящем сервисе требует доверенной цепочки сертификатов и проверки имени. Локальный HTTP-опыт не проверяет TLS. Нельзя переносить учебную привязку loopback, отсутствие авторизации и development server как готовую публичную инфраструктуру. Установка CA и проверка HTTPS.
Что значит вызвать API другого сервиса
Сайт доставки умеет рассчитывать стоимость. Ваш PHP отправляет ему HTTP-запрос с адресом и весом, получает ответ и использует результат. API — договор об адресах, параметрах, форматах и ошибках, а не специальный тип переменной.
Что уже пригодится. переменная и присваивание, массив, объявление и вызов функции, JSON и вложенные данные
- Массив PHP с параметрами
- Кодирование по договору API
- HTTP-запрос → сервис
- HTTP-статус и тело ответа
- Проверка → json_decode → массив
Вызов HTTP и разбор JSON — разные действия. Можно получить ответ 500, HTML вместо JSON или корректный JSON неподходящей формы. В самом маленьком опыте сначала изучим только последнюю часть без сети.
Этот вариант. Это сохранённый учебный ответ, а не обращение к реальному перевозчику. true просит представить JSON-объект как PHP-массив; флаг JSON_THROW_ON_ERROR превращает ошибку разбора в исключение. Эти параметры разобраны в главе JSON. Копейки не переводим в float для вычислений.
<?php
declare(strict_types=1);
$body = '{"delivery_kopecks":29900}';
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($data) || !isset($data['delivery_kopecks']) || !is_int($data['delivery_kopecks'])) {
throw new UnexpectedValueException('Ответ не соответствует договору');
}
echo $data['delivery_kopecks'], "\n";
Ожидаемый результат
29900
Следующая ступень. Теперь связываем тот же разбор с HTTP-клиентом, таймаутом, авторизацией и проверкой статуса. Ниже приведены вызовы cURL и причины выбора библиотеки.
Успешное соединение ещё не означает успешную операцию
Ответ API становится данными только после проверок
Сервис прислал ответ на запрос каталога. Соединение, HTTP, JSON и смысл данных проверяются отдельно.
- 1Запрос с ограничениями↓
Клиент задаёт доверенный адрес, авторизацию, таймауты и предусмотренный лимит ответа.
- 2Результат транспорта↓
Удалось получить ответ? Таймаут или ошибка соединения не превращаются в пустой каталог.
- 3HTTP-статус↓
200, 401 и 429 требуют разных действий. Наличие тела ответа само по себе не означает успех.
- 4Разобрать JSON↓
json_decode с обработкой ошибок отличает данные от повреждённого текста.
- 5Проверить договор данных
Нужные поля, типы и единицы совпали — передаём каталог приложению. Иначе сообщаем об отказе.
Даже корректный JSON может сообщать об ошибке API или иметь неожиданную структуру. Разбор текста не заменяет проверку схемы.
Сервис доставки присылает тарифы в JSON. Для получения результата приложение должно пройти несколько границ: разрешить адрес, установить соединение, проверить TLS, дождаться HTTP-ответа, оценить статус, разобрать JSON и проверить ожидаемую структуру. На каждом этапе возможна самостоятельная ошибка. curl_exec может успешно вернуть HTML страницы 404: транспорт сработал, но бизнес-результата нет.
Не сводите эти ошибки к одному пустому массиву. Таймаут может требовать отложенного повтора, 401 — исправления учётных данных, 422 — изменения запроса, 429 — ожидания по политике сервиса. Ответ 200 с некорректным JSON — нарушение протокола. Валидный JSON null тоже не является списком тарифов. Проверяйте обязательные ключи, типы и диапазоны после json_decode.
URL интеграции лучше задавать конфигурацией, а пользователь передаёт только ограниченный идентификатор. Если сервер скачивает произвольный URL, возникает SSRF: можно обратиться к локальным сервисам или облачным метаданным. Проверка строки через FILTER_VALIDATE_URL не решает эту задачу. Для произвольных адресов нужны политика DNS/IP, запрет внутренних диапазонов, контроль перенаправлений и сетевые ограничения. Фиксированный доверенный endpoint сильно упрощает задачу.
Ограничиваем время и размер ответа
Этот вариант. Нужны cURL, доверенный endpoint и актуальные CA. Это определение функции, без сетевого вызова; проверен синтаксис. example.com обозначает место вашего контрактного API. Каждую запись rates затем проверяют отдельно. Не выключайте проверку сертификата для устранения ошибки окружения.
<?php
declare(strict_types=1);
function fetchRates(string $postcode): array {
if (!preg_match('/\A[0-9]{6}\z/D',$postcode)) throw new InvalidArgumentException('postcode');
$url = 'https://api.example.com/rates?'.http_build_query(['postcode'=>$postcode]);
$curl = curl_init($url);
if ($curl === false) throw new RuntimeException('Cannot create HTTP client');
$body = '';
$tooLarge = false;
curl_setopt_array($curl,[
CURLOPT_CONNECTTIMEOUT_MS=>1500, CURLOPT_TIMEOUT_MS=>5000,
CURLOPT_FOLLOWLOCATION=>false,
CURLOPT_PROTOCOLS=>CURLPROTO_HTTPS,
CURLOPT_SSL_VERIFYPEER=>true, CURLOPT_SSL_VERIFYHOST=>2,
CURLOPT_HTTPHEADER=>['Accept: application/json'],
CURLOPT_WRITEFUNCTION=>static function ($handle,string $chunk) use (&$body,&$tooLarge): int {
if (strlen($body)+strlen($chunk)>1_000_000) { $tooLarge=true; return 0; }
$body .= $chunk;
return strlen($chunk);
},
]);
$ok = curl_exec($curl);
$status = curl_getinfo($curl,CURLINFO_RESPONSE_CODE);
if ($tooLarge) throw new RuntimeException('Response too large');
if ($ok === false) throw new RuntimeException('Transport failure: '.curl_errno($curl));
if ($status !== 200) throw new RuntimeException('Unexpected HTTP status: '.$status);
$data = json_decode($body,true,64,JSON_THROW_ON_ERROR);
if (!is_array($data) || !isset($data['rates']) || !is_array($data['rates'])) {
throw new UnexpectedValueException('Invalid API schema');
}
return $data['rates'];
}
Лимит времени соединения и общий таймаут отвечают на разные вопросы. Без общего таймаута сервер может очень медленно отправлять тело. Ограничение Content-Length недостаточно: заголовок может отсутствовать или быть недостоверным. Callback ограничивает реально накопленные байты. Если включена распаковка, учитывайте размер уже распакованного результата. Не выводите curl_error с внутренним адресом пользователю.
Редиректы запрещены в примере намеренно: новый адрес требует отдельного решения о доверии, особенно если отправляется Authorization. Для изменяющих запросов не включайте автоматические повторы без знания семантики сервиса. При HTTPS-проблеме настройте CA и проверьте часы сервера, а не отключайте CURLOPT_SSL_VERIFYPEER.
Разбор JSON и проверка прикладного контракта
Этот вариант. Пример отличает повреждённый JSON от нарушения схемы. Денежное значение ожидается целым числом минимальных единиц, поэтому строка отвергается. Если API документирует decimal-строку, нужен другой валидатор, а не случайное приведение float.
<?php
declare(strict_types=1);
function readRate(string $body): int {
$value = json_decode($body,true,16,JSON_THROW_ON_ERROR);
if (!is_array($value) || !is_int($value['priceMinor'] ?? null)
|| $value['priceMinor'] < 0 || ($value['currency'] ?? null) !== 'RUB') {
throw new UnexpectedValueException('rate-schema');
}
return $value['priceMinor'];
}
foreach (['{"priceMinor":39000,"currency":"RUB"}',
'{"priceMinor":"39000","currency":"RUB"}', '{oops'] as $body) {
try { echo readRate($body)."
"; }
catch (JsonException) { echo "invalid-json
"; }
catch (UnexpectedValueException) { echo "invalid-schema
"; }
}
Ожидаемый результат
39000
invalid-schema
invalid-json
Секреты, наблюдаемость и тестирование
Храните credentials отдельно от URL, чтобы они не попали в журналы прокси и историю. Заголовки Authorization и подписи редактируют перед логированием. Для диагностики сохраняйте код статуса, длительность, идентификатор запроса провайдера, номер попытки и безопасный код ошибки. Полное тело ответа может содержать персональные данные и не должно попадать в общий лог автоматически.
Клиентскую библиотеку удобно скрыть за интерфейсом. В тестах подставляйте ответы 200, 401, 429, 503, повреждённый JSON и обрыв соединения. Отдельный контрактный тест работает с sandbox сервиса. Успех локального JSON-теста не означает, что боевые сертификаты, прокси и права настроены. Схема может изменяться: неизвестные необязательные поля обычно можно игнорировать, но исчезновение обязательного поля следует обнаружить сразу.
Для каталога допустим короткий кеш и последняя успешная цена с пометкой времени; для списания денег такое поведение неприемлемо. Политику деградации выбирают по задаче. Когда внешний сервис долго недоступен, ограничение параллельности и временное прекращение попыток защищают ваше приложение от исчерпания воркеров. Эти решения полезнее бесконечного увеличения таймаута.
Массивы из API и устойчивость контракта
Внешний JSON часто содержит массив заказов, внутри каждого — массив позиций, а внутри позиции — вложенный объект цены. После json_decode(..., true) это становится многомерным PHP-массивом. Однако ключ items ещё не доказывает список: проверьте is_array и при необходимости array_is_list, затем каждую запись. Отдельно ограничьте число заказов и позиций, чтобы формально корректное тело не запустило чрезмерную работу.
Не все числовые строки являются числами для вычислений. Артикул 00042, телефон и внешний ID могут требовать сохранения ведущих нулей. Очень большой JSON integer способен не помещаться в PHP int; JSON_BIGINT_AS_STRING помогает сохранить цифры, если контракт допускает такой идентификатор. Денежные decimal-значения требуют точной стратегии, а не безусловного float. Ошибка схемы должна указывать безопасный путь вроде orders[3].items[2].quantity, чтобы интеграцию можно было исправить.
Клиент может исчезнуть во время ожидания, но это не отменяет уже выполненную операцию партнёра. Сохраняйте внешний correlation/idempotency ID до вызова, если он определяет восстановление. Общий бюджет запроса распределяют между попытками, DNS, соединением и чтением. Три попытки по пять секунд плюс паузы не укладываются в HTTP-дедлайн десять секунд; политика должна учитывать весь путь.
Практика: проверьте решение, а не только успешный путь
API вернул 200 и {"rates":null}; curl_exec не вернул false. Можно ли считать тарифы пустыми?
Разбор решения
Нет: транспорт и HTTP успешны, но контракт нарушен. Верните контролируемую ошибку интеграции и запишите безопасную диагностику. Пустой список [] допустим только если так определено протоколом. Тест должен отличать отсутствие тарифов от повреждённого ответа.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
curl_initСоздать настраиваемый HTTP-клиент перед запросом к поставщику; сам вызов ещё ничего не отправляет.
PHP 8.2+ · curl
curl_init(?string $url = null): CurlHandle|falseПрочитать остаток Молотка у своего HTTP-сервера. Создаёт состояние клиента для заданного URL. В адресе /product — маршрут, id=1 — параметр номера. Создание объекта ещё не передаёт запрос.
- PHP-клиент → GET /product?id=1 → 127.0.0.1:8791
- server.php выбирает Молоток со stock8
- Сервер → HTTP200, Content-Type text/plain, тело «Молоток — 8 шт.»
- Клиент проверяет транспорт и статус, затем выводит тело
В архиве есть server.php с известным каталогом, base.php и этот клиент example.php. В первом терминале запустите php -S 127.0.0.1:8791 server.php, во втором — php example.php. Полная подготовка и первые два файла.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init($base . '/product?id=1');
if ($client === false) { throw new RuntimeException('cURL не создал клиент'); }
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
echo $body;
Ожидаемый результат
Молоток — 8 шт.
Серверный каталог полностью задан в приложенном server.php. $client — объект настройки; $body — полученный текст. Это разные значения. Тело ответа можно вывести, сохранить или разобрать по согласованному формату.
Ошибки и следующий шаг. Для неизвестного id99 сервер возвращает 404 и JSON с причиной. Для пустого успешного ответа используйте === false, не if (!$body). Таймаут сети и HTTP-ошибка требуют разных сообщений. Опции устанавливаются до exec; их ошибки тоже нужно учитывать в библиотечном коде.
Примените ту же проверку передачи в самостоятельном расчёте цены с успешным ответом, HTTP 409 и поддельным типом. Он показывает, как полученные байты превращаются в полезное сообщение, и содержит собственный сервер.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Создать настраиваемый HTTP-клиент перед запросом к поставщику; сам вызов ещё ничего не отправляет.
cURL выполняет запрос от сервера PHP к другому адресу. $handle хранит состояние клиента, а не ответ. curl_init создаёт это состояние, curl_setopt задаёт настройку, curl_exec запускает передачу. Скобки содержат аргументы; = сохраняет результат. === false отличает отказ от пустой строки или строки0.
В массивах ключ => значение связывает настройку с её значением. if выбирает действие по условию; try/finally гарантирует обычную уборку при исключении. Как устроен вызов функции, запрос и HTTP-ответ и JSON и массивы объясняют эти понятия отдельно.
| Часть вызова | Договор и границы |
|---|---|
| url | Необязательный адрес; null — настроить позднее через CURLOPT_URL. Это настройка адреса, не выполненный запрос и не проверка доступности. С PHP8 nullable. |
| Возврат | CurlHandle с PHP8; раньше resource. false при невозможности создать handle. Объект непрозрачный: к нему применяют функции curl_*. |
| Протоколы | Поддержка зависит от сборки libcurl. file-протокол отключается при open_basedir; не следует использовать это как единственную границу для пользовательского URL. |
- curl_init создаёт $handle
- curl_setopt задаёт способ работы
- curl_exec выполняет запрос
- Проверяем транспорт, HTTP и данные
Этот вариант. Ноль здесь означает отсутствие записанной ошибки, а не успешный ответ сервера: запроса ещё не было.
<?php
declare(strict_types=1);
$handle = curl_init();
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
echo get_class($handle), "\n";
var_dump(curl_errno($handle));
Ожидаемый результат
CurlHandle
int(0)
Этот вариант. example.invalid — демонстрационный текст. curl_exec не вызывается, поэтому нет DNS/сети. Рабочий URL храните в доверенной конфигурации, а не берите напрямую из формы.
<?php
declare(strict_types=1);
$first = curl_init('https://example.invalid/catalog');
$second = curl_init();
if ($first === false || $second === false) { throw new RuntimeException('cURL unavailable'); }
curl_setopt($second, CURLOPT_URL, 'https://example.invalid/catalog');
echo curl_getinfo($first, CURLINFO_EFFECTIVE_URL), "\n";
var_dump(curl_getinfo($first, CURLINFO_EFFECTIVE_URL) === curl_getinfo($second, CURLINFO_EFFECTIVE_URL));
Ожидаемый результат
https://example.invalid/catalog
bool(true)
Преимущество handle проявляется при серии запросов: у него есть настройки и кэши. Для короткой разовой операции можно создать новый; для повторных обращений к одному сервису reuse способен сократить повторные подключения, но требует контроля cookies, авторизации и прежних опций. Как обернуть обмен в понятный метод клиента.
Связанные операции: curl_setopt; curl_exec; curl_reset; curl_copy_handle.
Продолжение: сохранённая прежняя практика.
Создаёт CurlHandle либо false. В PHP 8 это объект, не старый resource. Создание handle ещё не отправляет запрос.
Этот вариант. Сетевой запрос не выполняется, создаётся только объект.
<?php
declare(strict_types=1);
$handle=curl_init('https://api.example.com/catalog');
echo $handle instanceof CurlHandle ? "client prepared\n" : "failed\n";
Ожидаемый результат
client prepared
Этот вариант. Разделение создания и конфигурации полезно в фабрике клиента. Передача не запускается.
<?php
declare(strict_types=1);
$handle=curl_init();
if ($handle===false) throw new RuntimeException('curl');
curl_setopt($handle,CURLOPT_URL,'https://api.example.com/status');
echo "endpoint configured\n";
Ожидаемый результат
endpoint configured
Рядом по задаче. curl_setopt_array задаёт набор параметров, curl_exec выполняет передачу.
curl_setopt_arrayЗадать согласованный набор настроек и остановиться, если хотя бы одна отклонена.
PHP 8.2+ · curl
curl_setopt_array(CurlHandle $handle, array $options): boolПрочитать остаток Молотка у своего HTTP-сервера. Массив связывает константы настроек с их значениями. RETURNTRANSFER сохраняет тело в переменную, TIMEOUT2 ограничивает полную передачу, CONNECTTIMEOUT1 — стадию соединения.
- PHP-клиент → GET /product?id=1 → 127.0.0.1:8791
- server.php выбирает Молоток со stock8
- Сервер → HTTP200, Content-Type text/plain, тело «Молоток — 8 шт.»
- Клиент проверяет транспорт и статус, затем выводит тело
В архиве есть server.php с известным каталогом, base.php и этот клиент example.php. В первом терминале запустите php -S 127.0.0.1:8791 server.php, во втором — php example.php. Полная подготовка и первые два файла.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init($base . '/product?id=1');
if ($client === false) { throw new RuntimeException('cURL не создал клиент'); }
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
echo $body;
Ожидаемый результат
Молоток — 8 шт.
Серверный каталог полностью задан в приложенном server.php. $client — объект настройки; $body — полученный текст. Это разные значения. Тело ответа можно вывести, сохранить или разобрать по согласованному формату.
Ошибки и следующий шаг. Для неизвестного id99 сервер возвращает 404 и JSON с причиной. Для пустого успешного ответа используйте === false, не if (!$body). Таймаут сети и HTTP-ошибка требуют разных сообщений. Опции устанавливаются до exec; их ошибки тоже нужно учитывать в библиотечном коде.
Примените ту же проверку передачи в самостоятельном расчёте цены с успешным ответом, HTTP 409 и поддельным типом. Он показывает, как полученные байты превращаются в полезное сообщение, и содержит собственный сервер.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Задать согласованный набор настроек и остановиться, если хотя бы одна отклонена.
cURL выполняет запрос от сервера PHP к другому адресу. $handle хранит состояние клиента, а не ответ. curl_init создаёт это состояние, curl_setopt задаёт настройку, curl_exec запускает передачу. Скобки содержат аргументы; = сохраняет результат. === false отличает отказ от пустой строки или строки0.
В массивах ключ => значение связывает настройку с её значением. if выбирает действие по условию; try/finally гарантирует обычную уборку при исключении. Как устроен вызов функции, запрос и HTTP-ответ и JSON и массивы объясняют эти понятия отдельно.
| Часть вызова | Договор и границы |
|---|---|
| handle | Объект CurlHandle (PHP 8+), созданный curl_init(). |
| options | Массив: целочисленная константа опции => документированное значение. Ключи-строки с именами констант не подходят. |
| Результат | true — все опции приняты; false — обход остановился на отказе. Предыдущие изменения не откатываются. |
| Ошибки | PHP 8.0+: ValueError для неизвестной опции или строкового ключа; тип/значение проверяется как curl_setopt. |
- Адрес → CURLOPT_URL
- true → RETURNTRANSFER
- 1500 → TIMEOUT_MS
- Обход идёт последовательно
- Только полный успех разрешает переход к curl_exec
Когда запросу нужны адрес, лимиты и возврат тела строкой, массив делает настройки обозримыми в одном месте. Его ключи — значения констант, а не текстовые имена. Запись CURLOPT_URL => $url связывает опцию с её значением; [] создаёт массив.
Этот вариант. RETURNTRANSFER просит будущий curl_exec вернуть тело строкой, а не печатать его. Массив не является самим запросом; curl_exec намеренно не вызывается.
<?php
declare(strict_types=1);
$handle = curl_init();
if ($handle === false) throw new RuntimeException('Нет клиента');
$options = [
CURLOPT_URL => 'http://127.0.0.1:8781/order/42',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT_MS => 1500,
];
if (!curl_setopt_array($handle, $options)) throw new RuntimeException('Настройка не завершена');
echo "Готово к запросу; сеть ещё не использована\n";
Ожидаемый результат
Готово к запросу; сеть ещё не использована
Этот вариант. Есть сразу две учебные границы. Одинаковый ключ CURLOPT_URL в самом PHP-массиве уже заменён последним значением до вызова функции. Затем этот URL устанавливается, а строковый ключ вызывает ValueError; отката URL нет. После отказа не выполняйте запрос на частично настроенном клиенте.
<?php
declare(strict_types=1);
$handle = curl_init();
if ($handle === false) throw new RuntimeException('Нет клиента');
try {
curl_setopt_array($handle, [
CURLOPT_URL => 'http://127.0.0.1:8781/first',
'CURLOPT_TIMEOUT_MS' => 1000,
CURLOPT_URL => 'http://127.0.0.1:8781/final',
]);
} catch (ValueError $error) {
echo "Текстовый ключ запрещён\n";
}
echo curl_getinfo($handle, CURLINFO_EFFECTIVE_URL), "\n";
Ожидаемый результат
Текстовый ключ запрещён
http://127.0.0.1:8781/final
Этот вариант. Первый URL остался, следующая опция после отказа не выполнялась. PRIVATE — строка приложения, не отправляемая серверу. После исключения здесь только диагностика, никаких запросов.
<?php
declare(strict_types=1);
$handle = curl_init();
if ($handle === false) throw new RuntimeException('Нет клиента');
try {
curl_setopt_array($handle, [
CURLOPT_URL => 'http://127.0.0.1:8781/first',
0 => true,
CURLOPT_PRIVATE => 'must-not-be-applied',
]);
} catch (ValueError $error) {
echo "Настройка остановлена\n";
}
echo curl_getinfo($handle, CURLINFO_EFFECTIVE_URL), "\n";
echo curl_getinfo($handle, CURLINFO_PRIVATE) === false ? "Последняя опция не применена\n" : "Иное состояние\n";
Ожидаемый результат
Настройка остановлена
http://127.0.0.1:8781/first
Последняя опция не применена
Это последовательность изменений, не транзакция: при false функция сразу прекращает работу, оставшиеся опции игнорируются, уже применённые сохраняются. Исключение также прекращает обход. Ошибочную конфигурацию лучше отбросить вместе с клиентом или явно reset, затем настроить заново. Порядок важен для связанных параметров (например метода и POSTFIELDS).
Массив для CURLOPT_POSTFIELDS означает multipart/form-data; URL-encoded строка — application/x-www-form-urlencoded, а готовую JSON-строку сопровождают соответствующим Content-Type. В Notes официальной страницы curl_setopt_array написано CURLOPT_POST; это неточность: переключатель POST не является полем тела, нужная опция — POSTFIELDS. Договор curl_setopt и реализация PHP это различают. Файлы передают через CURLFile/CURLStringFile; PHP-массив полей не преобразуется в JSON автоматически.
Не используйте строковые ключи "CURLOPT_URL": ключом должно быть значение константы. Для объединения массивов числовых опций не подходит array_merge, потому что он перенумеровывает числовые ключи; выбирайте явное присваивание/array_replace с определённой политикой замены. Оператор + сохраняет значение левого массива при конфликте — это другая политика.
Связанные операции: curl_setopt; curl_exec; curl_reset; curl_getinfo.
Продолжение: сохранённая прежняя практика.
Применяет карту CURLOPT_* и возвращает bool. При неуспехе часть предыдущих параметров могла уже примениться; неверные аргументы могут вызвать исключение.
Этот вариант. Не выполняет сеть. Для тела большого размера дополнительно нужен лимит байтов.
<?php
declare(strict_types=1);
$h=curl_init('https://api.example.com/catalog');
$ok=curl_setopt_array($h,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_CONNECTTIMEOUT_MS=>1000,CURLOPT_TIMEOUT_MS=>3000,CURLOPT_FOLLOWLOCATION=>false]);
echo $ok ? "bounded client\n" : "configuration failed\n";
Ожидаемый результат
bounded client
Этот вариант. Сеть не выполняется. Массив в POSTFIELDS означал бы другой формат, поэтому JSON передаётся строкой.
<?php
declare(strict_types=1);
$h=curl_init('https://api.example.com/quotes');
$body=json_encode(['sku'=>'book','quantity'=>2],JSON_THROW_ON_ERROR);
$ok=curl_setopt_array($h,[CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>$body,CURLOPT_HTTPHEADER=>['Content-Type: application/json'],CURLOPT_TIMEOUT_MS=>3000,CURLOPT_RETURNTRANSFER=>true]);
echo $ok ? "json request prepared\n" : "failed\n";
Ожидаемый результат
json request prepared
Рядом по задаче. curl_setopt меняет один параметр; полный ограниченный клиент показан в интеграциях.
curl_execВыполнить подготовленную передачу и осознанно выбрать: вывести тело или получить его в переменную.
PHP 8.2+ · curl
curl_exec(CurlHandle $handle): string|boolСтоимость зависит от внешней работы. Измеряйте время ожидания, объём ответа и число вызовов. Короткая строка PHP может скрывать медленный SQL или сеть.
Прочитать остаток Молотка у своего HTTP-сервера. Выполняет подготовленный обмен. При RETURNTRANSFER=true возвращает тело строкой; false означает транспортный отказ. Код HTTP404 сам по себе не делает возврат false.
- PHP-клиент → GET /product?id=1 → 127.0.0.1:8791
- server.php выбирает Молоток со stock8
- Сервер → HTTP200, Content-Type text/plain, тело «Молоток — 8 шт.»
- Клиент проверяет транспорт и статус, затем выводит тело
В архиве есть server.php с известным каталогом, base.php и этот клиент example.php. В первом терминале запустите php -S 127.0.0.1:8791 server.php, во втором — php example.php. Полная подготовка и первые два файла.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init($base . '/product?id=1');
if ($client === false) { throw new RuntimeException('cURL не создал клиент'); }
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
echo $body;
Ожидаемый результат
Молоток — 8 шт.
Серверный каталог полностью задан в приложенном server.php. $client — объект настройки; $body — полученный текст. Это разные значения. Тело ответа можно вывести, сохранить или разобрать по согласованному формату.
Ошибки и следующий шаг. Для неизвестного id99 сервер возвращает 404 и JSON с причиной. Для пустого успешного ответа используйте === false, не if (!$body). Таймаут сети и HTTP-ошибка требуют разных сообщений. Опции устанавливаются до exec; их ошибки тоже нужно учитывать в библиотечном коде.
Примените ту же проверку передачи в самостоятельном расчёте цены с успешным ответом, HTTP 409 и поддельным типом. Он показывает, как полученные байты превращаются в полезное сообщение, и содержит собственный сервер.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Выполнить подготовленную передачу и осознанно выбрать: вывести тело или получить его в переменную.
cURL выполняет запрос от сервера PHP к другому адресу. $handle хранит состояние клиента, а не ответ. curl_init создаёт это состояние, curl_setopt задаёт настройку, curl_exec запускает передачу. Скобки содержат аргументы; = сохраняет результат. === false отличает отказ от пустой строки или строки0.
В массивах ключ => значение связывает настройку с её значением. if выбирает действие по условию; try/finally гарантирует обычную уборку при исключении. Как устроен вызов функции, запрос и HTTP-ответ и JSON и массивы объясняют эти понятия отдельно.
| Часть вызова | Договор и границы |
|---|---|
| handle | Настроенный CurlHandle. Создание handle и установка URL не выполняли передачу; здесь начинается I/O. |
| Без RETURNTRANSFER | Тело по умолчанию уходит в stdout, результат true при успехе и false при ошибке. Не перепутайте побочный вывод с возвращённым значением. |
| С RETURNTRANSFER=true | Тело возвращается строкой, false при ошибке. Пустая строка и строка0 — возможный успешный ответ; используйте === false. |
| HTTP и протокол | HTTP404/500 по умолчанию не превращаются в false. Проверяйте код ответа и бизнес-структуру отдельно; FAILONERROR меняет обработкуHTTP>=400. |
| Ожидание и память | Easy-вызов блокирует текущий PHP-поток до завершения/таймаута. Полный RETURNTRANSFER держит тело в памяти; callback/файл удобнее для больших данных. |
- Параметры → curl_exec
- Транспорт: строка или false
- HTTP:200/404/500 и прочие статусы
- Данные: JSON/схема/состояние заказа
HTTP-примеры используют собственный учебный сервер: переменная окружения PHP_MANUAL_HTTP_BASE содержит его адрес, /ok возвращает READY, /missing — статус404 и missing, /zero — строку0, /empty — пустой ответ. Это явно заданный стенд, а не произвольный внешний сайт. Код действительно выполнен с таким сервером отдельно от общей проверки синтаксиса.
Этот вариант. RETURNTRANSFER возвращает строку в body. Мы сами решаем, вывести её, разобрать JSON или сохранить. Таймаут3сек — предел учебного запроса, не обязательное число для всех API. В архиве приложен http-fixture.py: запустите python http-fixture.py в отдельном терминале, а клиенту задайте переменную окружения PHP_MANUAL_HTTP_BASE=http://127.0.0.1:8772 способом своей оболочки. Сервер слушает только loopback; после опыта остановите его Ctrl+C.
<?php
declare(strict_types=1);
$base = getenv('PHP_MANUAL_HTTP_BASE');
if (!is_string($base) || $base === '') { throw new RuntimeException('Start the local HTTP fixture'); }
$handle = curl_init($base . '/ok');
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
curl_setopt_array($handle, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3]);
$body = curl_exec($handle);
if ($body === false) { throw new RuntimeException('Transport failed: ' . curl_errno($handle)); }
echo $body;
Ожидаемый результат
READY
Этот вариант. Самостоятельный файловый опыт: report.txt приложен в папке примера. READY ушёл в stdout, в result находится true. Нужен разрешённый file-протокол, без open_basedir. В общем пути с пробелами/спецсимволами компоненты URL дополнительно кодируют.
<?php
declare(strict_types=1);
$path = str_replace('\\', '/', __DIR__ . '/report.txt');
$url = 'file:///' . ltrim($path, '/');
$handle = curl_init($url);
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
$result = curl_exec($handle);
var_dump($result);
Ожидаемый результат
READY
bool(true)
Этот вариант. if (!$body) неправильно объединило бы оба успешных ответа с транспортным отказом. Пустое тело при204 нормально по протоколу, а для ожидаемого JSON может быть отдельной ошибкой контракта. В архиве приложен http-fixture.py: запустите python http-fixture.py в отдельном терминале, а клиенту задайте переменную окружения PHP_MANUAL_HTTP_BASE=http://127.0.0.1:8772 способом своей оболочки. Сервер слушает только loopback; после опыта остановите его Ctrl+C.
<?php
declare(strict_types=1);
$base = getenv('PHP_MANUAL_HTTP_BASE');
if (!is_string($base) || $base === '') { throw new RuntimeException('Start the local HTTP fixture'); }
foreach (['zero', 'empty'] as $route) {
$handle = curl_init($base . '/' . $route);
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
curl_setopt_array($handle, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3]);
$body = curl_exec($handle);
var_dump($body === false, $body);
}
Ожидаемый результат
bool(false)
string(1) "0"
bool(false)
string(0) ""
Этот вариант. Сервер успешно сообщил, что ресурса нет. Приложение должно принять решение по404, хотя TCP/HTTP-обмен не сорвался. В архиве приложен http-fixture.py: запустите python http-fixture.py в отдельном терминале, а клиенту задайте переменную окружения PHP_MANUAL_HTTP_BASE=http://127.0.0.1:8772 способом своей оболочки. Сервер слушает только loopback; после опыта остановите его Ctrl+C.
<?php
declare(strict_types=1);
$base = getenv('PHP_MANUAL_HTTP_BASE');
if (!is_string($base) || $base === '') { throw new RuntimeException('Start the local HTTP fixture'); }
$handle = curl_init($base . '/missing');
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
curl_setopt_array($handle, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3]);
$body = curl_exec($handle);
var_dump($body === false, curl_errno($handle));
echo curl_getinfo($handle, CURLINFO_RESPONSE_CODE), "\n", $body;
Ожидаемый результат
bool(false)
int(0)
404
missing
Этот вариант. Callback возвращает фактически принятое число байтов. Намеренный0 останавливает передачу: собственный флаг объясняет причину CURLE_WRITE_ERROR. Это ограничение принятого приложением тела, не жёсткий предел всей внутренней памяти libcurl.
<?php
declare(strict_types=1);
$path = str_replace('\\', '/', __DIR__ . '/report.txt');
$url = 'file:///' . ltrim($path, '/');
$handle = curl_init($url);
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
$body = '';
$tooLarge = false;
curl_setopt($handle, CURLOPT_WRITEFUNCTION, function (CurlHandle $handle, string $chunk) use (&$body, &$tooLarge): int {
if (strlen($body) + strlen($chunk) > 3) { $tooLarge = true; return 0; }
$body .= $chunk;
return strlen($chunk);
});
$result = curl_exec($handle);
var_dump($result === false, $tooLarge, curl_errno($handle) === CURLE_WRITE_ERROR);
Ожидаемый результат
bool(true)
bool(true)
bool(true)
Повторный вызов может повторно отправить POST, поэтому retry требует понимания идемпотентности API. Сетевой timeout не доказывает, что партнёр ничего не сохранил. Готовый клиент отделяет подготовку запроса, передачу, разбор ответа и решение о повторе. Общий метод интеграции и паузы,429 и повторные попытки.
Связанные операции: curl_init; curl_setopt_array; curl_errno; curl_getinfo; curl_multi_exec.
Продолжение: сохранённая прежняя практика.
С RETURNTRANSFER возвращает тело, без него выводит данные и возвращает true; транспортный сбой даёт false. Callback записи изменяет способ приёма. HTTP 404 сам по себе не обязан давать false.
Этот вариант. Определение без вызова. Handle обязан быть заранее настроен с RETURNTRANSFER, лимитами и доверенным endpoint.
<?php
declare(strict_types=1);
function executeBuffered(CurlHandle $h): string {
$body=curl_exec($h);
if ($body===false) throw new RuntimeException('Transport code '.curl_errno($h));
if (!is_string($body)) throw new LogicException('RETURNTRANSFER must be configured');
return $body;
}
Этот вариант. Определение не выполняет сеть. Следующий слой проверяет Content-Type, JSON и схему; здесь только транспорт+статус.
<?php
declare(strict_types=1);
function fetchSuccessfulBody(CurlHandle $h): string {
$body=curl_exec($h);
if ($body===false) throw new RuntimeException('transport');
$status=curl_getinfo($h,CURLINFO_RESPONSE_CODE);
if ($status!==200) throw new RuntimeException('HTTP '.$status);
if (!is_string($body)) throw new LogicException('Buffered client required');
return $body;
}
Рядом по задаче. curl_getinfo читает статус; curl_errno/curl_error объясняют транспортный сбой.
curl_getinfoПолучить HTTP-статус, адрес, размеры и тайминги последней передачи, не пытаясь извлечь их из текста тела.
PHP 8.2+ · curl
curl_getinfo(CurlHandle $handle, ?int $option = null): mixedПрочитать остаток Молотка у своего HTTP-сервера. Читает сведения уже выполненной передачи. HTTP-статус проверяют отдельно от текста тела: сообщение об ошибке тоже может быть получено успешно.
- PHP-клиент → GET /product?id=1 → 127.0.0.1:8791
- server.php выбирает Молоток со stock8
- Сервер → HTTP200, Content-Type text/plain, тело «Молоток — 8 шт.»
- Клиент проверяет транспорт и статус, затем выводит тело
В архиве есть server.php с известным каталогом, base.php и этот клиент example.php. В первом терминале запустите php -S 127.0.0.1:8791 server.php, во втором — php example.php. Полная подготовка и первые два файла.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init($base . '/product?id=1');
if ($client === false) { throw new RuntimeException('cURL не создал клиент'); }
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
$status = curl_getinfo($client, CURLINFO_RESPONSE_CODE);
echo 'HTTP ', $status, "\n";
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
echo $body;
Ожидаемый результат
HTTP 200
Молоток — 8 шт.
Серверный каталог полностью задан в приложенном server.php. $client — объект настройки; $body — полученный текст. Это разные значения. Тело ответа можно вывести, сохранить или разобрать по согласованному формату.
Ошибки и следующий шаг. Для неизвестного id99 сервер возвращает 404 и JSON с причиной. Для пустого успешного ответа используйте === false, не if (!$body). Таймаут сети и HTTP-ошибка требуют разных сообщений. Опции устанавливаются до exec; их ошибки тоже нужно учитывать в библиотечном коде.
Примените ту же проверку передачи в самостоятельном расчёте цены с успешным ответом, HTTP 409 и поддельным типом. Он показывает, как полученные байты превращаются в полезное сообщение, и содержит собственный сервер.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Получить HTTP-статус, адрес, размеры и тайминги последней передачи, не пытаясь извлечь их из текста тела.
cURL выполняет запрос от сервера PHP к другому адресу. $handle хранит состояние клиента, а не ответ. curl_init создаёт это состояние, curl_setopt задаёт настройку, curl_exec запускает передачу. Скобки содержат аргументы; = сохраняет результат. === false отличает отказ от пустой строки или строки0.
В массивах ключ => значение связывает настройку с её значением. if выбирает действие по условию; try/finally гарантирует обычную уборку при исключении. Как устроен вызов функции, запрос и HTTP-ответ и JSON и массивы объясняют эти понятия отдельно.
| Часть вызова | Договор и границы |
|---|---|
| handle | Тот же CurlHandle, который выполнял передачу. |
| option | Константа CURLINFO_* выбирает один показатель с его собственным типом. null возвращает набор полей. С PHP8 nullable, прежде default0. |
| Возврат | Значение выбранного показателя, либо ассоциативный массив общих сведений; false при отказе. Тип зависит от option, не всегда int. |
| PRIVATE | CURLOPT_PRIVATE хранит прикладной идентификатор. В общем массиве его нет; запрашивайте CURLINFO_PRIVATE отдельно. До установки в PHP8.5.11 возвращается false. |
| Повторное использование | Часть информации сохраняется между операциями, пока её не заменит новый результат. Статистика не доказательство выполнения свежего запроса. |
- curl_exec получает тело
- curl_getinfo RESPONSE_CODE → статус
- Другие CURLINFO → адрес/время/размер
- Приложение проверяет договор ответа
HTTP-примеры используют собственный учебный сервер: переменная окружения PHP_MANUAL_HTTP_BASE содержит его адрес, /ok возвращает READY, /missing — статус404 и missing, /zero — строку0, /empty — пустой ответ. Это явно заданный стенд, а не произвольный внешний сайт. Код действительно выполнен с таким сервером отдельно от общей проверки синтаксиса.
Этот вариант. PRIVATE — локальная пометка handle. Удобна при multi-запросах, но не устанавливает HTTP-заголовок и не отправляет значение партнёру.
<?php
declare(strict_types=1);
$handle = curl_init();
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
var_dump(curl_getinfo($handle, CURLINFO_PRIVATE));
curl_setopt($handle, CURLOPT_PRIVATE, 'order-25');
echo curl_getinfo($handle, CURLINFO_PRIVATE), "\n";
$info = curl_getinfo($handle);
var_dump(array_key_exists('private', $info));
Ожидаемый результат
bool(false)
order-25
bool(false)
Этот вариант. Content-Type сообщает сервер; это не проверка, что фактическое тело ему соответствует. Тайминг намеренно не сравниваем с фиксированными миллисекундами. В архиве приложен http-fixture.py: запустите python http-fixture.py в отдельном терминале, а клиенту задайте переменную окружения PHP_MANUAL_HTTP_BASE=http://127.0.0.1:8772 способом своей оболочки. Сервер слушает только loopback; после опыта остановите его Ctrl+C.
<?php
declare(strict_types=1);
$base = getenv('PHP_MANUAL_HTTP_BASE');
if (!is_string($base) || $base === '') { throw new RuntimeException('Start the local HTTP fixture'); }
$handle = curl_init($base . '/ok');
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
curl_setopt_array($handle, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3]);
if (curl_exec($handle) === false) { throw new RuntimeException('Transport failed'); }
echo curl_getinfo($handle, CURLINFO_RESPONSE_CODE), "\n";
echo curl_getinfo($handle, CURLINFO_CONTENT_TYPE), "\n";
$info = curl_getinfo($handle);
var_dump($info['size_download'] === 6.0, $info['total_time'] >= 0);
Ожидаемый результат
200
text/plain; charset=UTF-8
bool(true)
bool(true)
Этот вариант. В собственном стенде оба адреса доверенные. Одного ограничения http-протокола недостаточно для произвольного пользовательского URL: каждый redirect может вести к другому хосту/внутреннему адресу. В архиве приложен http-fixture.py: запустите python http-fixture.py в отдельном терминале, а клиенту задайте переменную окружения PHP_MANUAL_HTTP_BASE=http://127.0.0.1:8772 способом своей оболочки. Сервер слушает только loopback; после опыта остановите его Ctrl+C.
<?php
declare(strict_types=1);
$base = getenv('PHP_MANUAL_HTTP_BASE');
if (!is_string($base) || $base === '') { throw new RuntimeException('Start the local HTTP fixture'); }
$handle = curl_init($base . '/redirect');
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
curl_setopt_array($handle, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 2, CURLOPT_PROTOCOLS_STR => 'http', CURLOPT_REDIR_PROTOCOLS_STR => 'http', CURLOPT_TIMEOUT => 3]);
$body = curl_exec($handle);
if ($body === false) { throw new RuntimeException('Transport failed'); }
echo curl_getinfo($handle, CURLINFO_REDIRECT_COUNT), "\n";
var_dump(curl_getinfo($handle, CURLINFO_EFFECTIVE_URL) === $base . '/ok');
echo $body;
Ожидаемый результат
1
bool(true)
READY
| Поля общего массива | Смысл и ограничение |
|---|---|
| url, redirect_url, redirect_count | Адрес передачи, потенциальный адрес перехода и число выполненных редиректов. Не путать с исходной доверенной конфигурацией. |
| content_type, http_code | Объявленный MIME и код ответа; HTTPcode0 обычно означает отсутствие полученного HTTP-ответа, не особый успешный статус. |
| header_size, request_size | Учёт заголовков/запроса libcurl; при redirect/proxy/промежуточных ответах это не всегда размер одного последнего блока. |
| filetime | Удалённое время модификации при соответствующем запросе; −1 если неизвестно. |
| ssl_verify_result, certinfo | Результат TLS-проверки и сведения сертификатов при включённом сборе; наличие массива не заменяет verify_peer/verify_host. |
| total_time, namelookup_time, connect_time, pretransfer_time, starttransfer_time, redirect_time | Времена в секундах, многие отсчитываются от начала операции. Не складывайте их как независимые длительности этапов. |
| size_upload, size_download, speed_upload, speed_download | Объём и средняя скорость передачи; единицы и типы конкретной CURLINFO-константы проверяют отдельно. |
| download_content_length, upload_content_length | Заявленная длина; может быть неизвестна/отрицательна, не обязательный фактический размер принятого тела. |
| primary_ip, primary_port, local_ip, local_port | Удалённая и локальная сторона подключения, если доступны. При proxy смысл зависит от соединения. |
| request_header | Исходящие заголовки, только если заранее включён CURLINFO_HEADER_OUT через curl_setopt. Могут содержать секреты. |
| posttransfer_time_us | С PHP8.4/libcurl8.10: время до отправки последнего байта запроса в микросекундах. |
В полном списке CURLINFO также есть целочисленные варианты *_T, подробные TLS/proxy/HTTP показатели и данные Retry-After. PHP7.3 добавил ряд *_T, HTTP_VERSION, PROTOCOL, SCHEME; PHP8.2 — PROXY_ERROR/REFERER/RETRY_AFTER; PHP8.3 — CAINFO/CAPATH; PHP8.4 — POSTTRANSFER_TIME_T. Наличие константы зависит и от сборки libcurl. Подробности каждой находятся в общем справочнике констант этой главы.
Связанные операции: curl_exec; curl_errno; curl_version; curl_setopt.
Продолжение: сохранённая прежняя практика.
С конкретным CURLINFO_* возвращает соответствующее значение, без option — набор сведений. До передачи данные могут быть нулевыми/неполными.
Этот вариант. Вызывать после передачи; функция не выполняет сеть. Длительность помогает обнаружить медленный API.
<?php
declare(strict_types=1);
function requestMetrics(CurlHandle $h): array {
return ['status'=>curl_getinfo($h,CURLINFO_RESPONSE_CODE),'seconds'=>curl_getinfo($h,CURLINFO_TOTAL_TIME)];
}
Этот вариант. Строгий контракт конкретного API. Если он использует application/problem+json, добавьте это явно. Заголовок не заменяет json_decode.
<?php
declare(strict_types=1);
function requireJsonType(CurlHandle $h): void {
$type=curl_getinfo($h,CURLINFO_CONTENT_TYPE);
if (!is_string($type) || strtolower(trim(explode(';',$type,2)[0]))!=='application/json') {
throw new UnexpectedValueException('Expected JSON content type');
}
}
Рядом по задаче. curl_errno отражает ошибки транспорта, а не HTTP-бизнес-ошибки.
curl_errnoПолучить числовой код последней ошибки передачи этого handle.
PHP 8.2+ · curl
curl_errno(CurlHandle $handle): intОбъяснить ошибочный адрес до запроса и затем показать каталог. Числовой CURLE-код описывает причину последней транспортной ошибки. Ноль означает отсутствие такой ошибки, но не гарантирует HTTP200.
- Порт 99999 недопустим → запрос не отправлен
- exec=false, errno=CURLE_URL_MALFORMAT
- Фактическая диагностика записана в transport-error.json
- Исправленный локальный URL → строка Молотка
В архиве есть server.php с известным каталогом, base.php и этот клиент example.php. В первом терминале запустите php -S 127.0.0.1:8791 server.php, во втором — php example.php. Полная подготовка и первые два файла.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init('http://127.0.0.1:99999/product?id=1');
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
$body = curl_exec($client);
$code = curl_errno($client);
if ($body !== false || $code !== CURLE_URL_MALFORMAT) { throw new RuntimeException('Ожидался отказ формата URL'); }
$details = curl_errno($client);
if (file_put_contents(__DIR__ . '/transport-error.json', json_encode($details, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)) === false) { throw new RuntimeException('Не удалось записать диагностику'); }
echo "Неверный адрес: запрос не отправлен\n";
curl_setopt($client, CURLOPT_URL, $base . '/product?id=1');
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
echo $body;
Ожидаемый результат
Неверный адрес: запрос не отправлен
Молоток — 8 шт.
Сервер не получал первый запрос: libcurl отклонила недопустимый порт. Второй запрос использует правильный собственный адрес. transport-error.json рядом с example.php содержит реальный код или текст выбранной диагностической функции.
Ошибки и следующий шаг. Считывайте ошибку сразу после отказа, до следующего exec. Повторять автоматически безопасно не любую операцию: после сетевого обрыва POST сервер мог уже выполнить действие. Не публикуйте диагностические URL с секретами.
Примените ту же проверку передачи в самостоятельном расчёте цены с успешным ответом, HTTP 409 и поддельным типом. Он показывает, как полученные байты превращаются в полезное сообщение, и содержит собственный сервер.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Получить числовой код последней ошибки передачи этого handle.
cURL выполняет запрос от сервера PHP к другому адресу. $handle хранит состояние клиента, а не ответ. curl_init создаёт это состояние, curl_setopt задаёт настройку, curl_exec запускает передачу. Скобки содержат аргументы; = сохраняет результат. === false отличает отказ от пустой строки или строки0.
В массивах ключ => значение связывает настройку с её значением. if выбирает действие по условию; try/finally гарантирует обычную уборку при исключении. Как устроен вызов функции, запрос и HTTP-ответ и JSON и массивы объясняют эти понятия отдельно.
| Часть вызова | Договор и границы |
|---|---|
| handle | CurlHandle именно той операции, которую проверяете. |
| Возврат | Код ошибки;0 при отсутствии зарегистрированного отказа. |
| HTTP | 404/500 сами по себе обычно не являются ошибкой транспорта; проверяйте CURLINFO_RESPONSE_CODE. CURLOPT_FAILONERROR меняет этот договор, но не проверяет бизнес-данные. |
| Multi | До чтения сообщения curl_multi_info_read PHP может показывать0/пустую строку. Надёжный код отдельной завершённой передачи находится в message[result]; его поясняет curl_strerror. |
- curl_exec → отказ транспорта или тело
- errno/error поясняют транспорт
- HTTP-статус описывает ответ сервера
- JSON и бизнес-поля проверяются после этого
Этот вариант. Это состояние до выполнения запроса, не свидетельство готовности сервиса.
<?php
declare(strict_types=1);
$handle = curl_init();
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
var_dump(curl_errno($handle));
Ожидаемый результат
int(0)
Этот вариант. Несуществующий протокол отклоняется до DNS/сети. Сохраняйте диагностику сразу после проблемной операции: следующий вызов может изменить состояние.
<?php
declare(strict_types=1);
$handle = curl_init('unsupported-lesson://example.invalid/');
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($handle);
var_dump($result === false);
var_dump(curl_errno($handle) === CURLE_UNSUPPORTED_PROTOCOL);
Ожидаемый результат
bool(true)
bool(true)
В технический журнал записывают код, этап и безопасный идентификатор запроса. Полный URL, заголовки авторизации и тело ошибки могут содержать секреты; удалённый текст не выводят посетителю без экранирования. Не повторяйте автоматически любую ошибку POST: действие могло выполниться, а ответ потеряться.
Связанные операции: curl_exec; curl_getinfo; curl_strerror; curl_multi_info_read.
Продолжение: сохранённая прежняя практика.
Возвращает CURLE_* код, 0 означает отсутствие зафиксированной cURL-ошибки. HTTP 404/500 рассматривают отдельно.
Этот вариант. Отсутствие ошибки здесь не означает доступность сервера: запрос ещё не выполнялся.
<?php
declare(strict_types=1);
$h=curl_init('https://api.example.com/');
echo curl_errno($h)."\n";
Ожидаемый результат
0
Этот вариант. Используется после curl_exec===false. Даже таймаут изменяющего запроса может означать неизвестный результат, а не безопасный повтор.
<?php
declare(strict_types=1);
function isTimeout(CurlHandle $h): bool {
return curl_errno($h)===CURLE_OPERATION_TIMEDOUT;
}
Рядом по задаче. curl_error даёт текст, curl_getinfo — HTTP-статус.
curl_errorПолучить диагностический текст последней ошибки передачи этого handle.
PHP 8.2+ · curl
curl_error(CurlHandle $handle): stringОбъяснить ошибочный адрес до запроса и затем показать каталог. Текст последней ошибки нужен для диагностики. Он может зависеть от libcurl/ОС, поэтому приложение не распознаёт ошибку только по фразе.
- Порт 99999 недопустим → запрос не отправлен
- exec=false, errno=CURLE_URL_MALFORMAT
- Фактическая диагностика записана в transport-error.json
- Исправленный локальный URL → строка Молотка
В архиве есть server.php с известным каталогом, base.php и этот клиент example.php. В первом терминале запустите php -S 127.0.0.1:8791 server.php, во втором — php example.php. Полная подготовка и первые два файла.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init('http://127.0.0.1:99999/product?id=1');
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
$body = curl_exec($client);
$code = curl_errno($client);
if ($body !== false || $code !== CURLE_URL_MALFORMAT) { throw new RuntimeException('Ожидался отказ формата URL'); }
$details = curl_error($client);
if (file_put_contents(__DIR__ . '/transport-error.json', json_encode($details, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)) === false) { throw new RuntimeException('Не удалось записать диагностику'); }
echo "Неверный адрес: запрос не отправлен\n";
curl_setopt($client, CURLOPT_URL, $base . '/product?id=1');
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
echo $body;
Ожидаемый результат
Неверный адрес: запрос не отправлен
Молоток — 8 шт.
Сервер не получал первый запрос: libcurl отклонила недопустимый порт. Второй запрос использует правильный собственный адрес. transport-error.json рядом с example.php содержит реальный код или текст выбранной диагностической функции.
Ошибки и следующий шаг. Считывайте ошибку сразу после отказа, до следующего exec. Повторять автоматически безопасно не любую операцию: после сетевого обрыва POST сервер мог уже выполнить действие. Не публикуйте диагностические URL с секретами.
Примените ту же проверку передачи в самостоятельном расчёте цены с успешным ответом, HTTP 409 и поддельным типом. Он показывает, как полученные байты превращаются в полезное сообщение, и содержит собственный сервер.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Получить диагностический текст последней ошибки передачи этого handle.
cURL выполняет запрос от сервера PHP к другому адресу. $handle хранит состояние клиента, а не ответ. curl_init создаёт это состояние, curl_setopt задаёт настройку, curl_exec запускает передачу. Скобки содержат аргументы; = сохраняет результат. === false отличает отказ от пустой строки или строки0.
В массивах ключ => значение связывает настройку с её значением. if выбирает действие по условию; try/finally гарантирует обычную уборку при исключении. Как устроен вызов функции, запрос и HTTP-ответ и JSON и массивы объясняют эти понятия отдельно.
| Часть вызова | Договор и границы |
|---|---|
| handle | CurlHandle именно той операции, которую проверяете. |
| Возврат | Текст последней ошибки либо пустая строка. Формулировка зависит от версии/окружения; не стройте ветвление на точном тексте. |
| HTTP | 404/500 сами по себе обычно не являются ошибкой транспорта; проверяйте CURLINFO_RESPONSE_CODE. CURLOPT_FAILONERROR меняет этот договор, но не проверяет бизнес-данные. |
| Multi | До чтения сообщения curl_multi_info_read PHP может показывать0/пустую строку. Надёжный код отдельной завершённой передачи находится в message[result]; его поясняет curl_strerror. |
- curl_exec → отказ транспорта или тело
- errno/error поясняют транспорт
- HTTP-статус описывает ответ сервера
- JSON и бизнес-поля проверяются после этого
Этот вариант. Это состояние до выполнения запроса, не свидетельство готовности сервиса.
<?php
declare(strict_types=1);
$handle = curl_init();
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
var_dump(curl_error($handle));
Ожидаемый результат
string(0) ""
Этот вариант. Несуществующий протокол отклоняется до DNS/сети. Сохраняйте диагностику сразу после проблемной операции: следующий вызов может изменить состояние.
<?php
declare(strict_types=1);
$handle = curl_init('unsupported-lesson://example.invalid/');
if ($handle === false) { throw new RuntimeException('cURL unavailable'); }
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($handle);
var_dump($result === false);
$message = curl_error($handle);
var_dump(is_string($message), $message !== '');
Ожидаемый результат
bool(true)
bool(true)
bool(true)
В технический журнал записывают код, этап и безопасный идентификатор запроса. Полный URL, заголовки авторизации и тело ошибки могут содержать секреты; удалённый текст не выводят посетителю без экранирования. Не повторяйте автоматически любую ошибку POST: действие могло выполниться, а ответ потеряться.
Связанные операции: curl_exec; curl_getinfo; curl_strerror; curl_multi_info_read.
Продолжение: сохранённая прежняя практика.
Возвращает строку, пустую при отсутствии ошибки. Текст не является стабильным машинным кодом и может содержать детали окружения.
Этот вариант. Создание объекта не проверяет сеть.
<?php
declare(strict_types=1);
$h=curl_init();
echo curl_error($h)==='' ? "no error recorded\n" : "error\n";
Ожидаемый результат
no error recorded
Этот вариант. Определение для закрытого диагностического слоя. Перед записью detail примените политику редактирования чувствительных данных.
<?php
declare(strict_types=1);
function transportDiagnostic(CurlHandle $h): array {
return ['code'=>curl_errno($h),'detail'=>curl_error($h)];
}
Рядом по задаче. curl_errno для классификации, безопасный request_id для пользовательского сообщения.
Источники и обсуждения
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


