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

Дерево руководства
Глава 36 · С нуля → системные границы
Возможности PHP в вебе: HTTP-аутентификация, cookie, uploads и жизненный цикл соединения
Полная веб-часть официальной ветки Features: сначала видимый запрос и результат, затем Basic auth, cookie, canonical sessions/uploads, remote wrappers, client disconnect и persistent database connections.
Возможность — это совместная работа PHP и окружения
Раздел Features отвечает не на вопрос «какая функция существует», а на вопрос «какое поведение PHP получается вместе с HTTP, веб-сервером, браузером, файловой загрузкой, базой и способом запуска». Здесь одна и та же строка PHP может вести себя по-разному в CLI и FPM, cookie появляется только в следующем запросе, а закрытая вкладка не обязана немедленно остановить серверный код.
- Вход: HTTP-запрос, конфигурация SAPI и внешняя служба
- Действие: PHP читает вход, проверяет контракт и формирует ответ
- Результат: статус, заголовки, тело либо диагностированный отказ
| Задача | Точное продолжение |
|---|---|
| Понять запрос и ответ | Жизненный цикл HTTP |
| Сохранить вход пользователя | Cookie и сессия |
| Принять файл формы | Полная безопасная загрузка |
| Открыть URL как поток | Протоколы, wrappers и context |
| Работать с базой | Подключение, запрос и ошибка |
| Запустить PHP без браузера | CLI: файл, аргументы и потоки |
HTTP Basic: закрыть служебную страницу, не строя систему входа
HTTP Basic нужен, когда небольшой закрытый инструмент должен принять логин и пароль на уровне HTTP. Сервер отвечает 401 Unauthorized и WWW-Authenticate: Basic realm="…"; браузер повторяет запрос, а PHP получает PHP_AUTH_USER, PHP_AUTH_PW и AUTH_TYPE внутри $_SERVER. Встроенная поддержка PHP относится только к Basic. Логин и пароль передаются в каждом запросе, поэтому без HTTPS их нельзя считать защищёнными.
<?php
declare(strict_types=1);
function basicAuthDecision(array $server, array $accounts): array
{
$user = $server['PHP_AUTH_USER'] ?? null;
$password = $server['PHP_AUTH_PW'] ?? null;
if (!is_string($user) || !is_string($password)) {
return ['status' => 401, 'user' => null];
}
$storedHash = $accounts[$user] ?? null;
if (!is_string($storedHash) || !password_verify($password, $storedHash)) {
return ['status' => 401, 'user' => null];
}
return ['status' => 200, 'user' => $user];
}
$accounts = ['anna' => password_hash('river-42', PASSWORD_DEFAULT)];
foreach ([
[],
['PHP_AUTH_USER' => 'anna', 'PHP_AUTH_PW' => 'wrong'],
['PHP_AUTH_USER' => 'anna', 'PHP_AUTH_PW' => 'river-42'],
] as $server) {
$answer = basicAuthDecision($server, $accounts);
echo $answer['status'], ' ', $answer['user'] ?? '-', "\n";
}
Ожидаемый результат
401 -
401 -
200 anna
Первый вход не содержит заголовка, второй содержит неверный пароль, третий проходит password_verify. Hash создаётся только ради изолированного опыта; production читает сохранённый hash из базы.
<?php
declare(strict_types=1);
$accounts = ['anna' => '$2y$12$replace_with_a_hash_from_password_hash'];
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
$hash = is_string($user) ? ($accounts[$user] ?? null) : null;
if (!is_string($password) || !is_string($hash) || !password_verify($password, $hash)) {
header('WWW-Authenticate: Basic realm="Warehouse"');
http_response_code(401);
echo 'Требуется вход';
exit;
}
header('Content-Type: text/plain; charset=UTF-8');
echo 'Остатки доступны для ', $user;
Ожидаемый результат
Первый запрос: HTTP 401 и WWW-Authenticate: Basic realm="Warehouse". После верного ввода: HTTP 200 и «Остатки доступны для anna».
Это lint-пример: итог зависит от HTTPS, SAPI, веб-сервера и браузера. Realm пишется в двойных кавычках, перед кодом 401 в status line нужен ровно один пробел. В Apache активная внешняя AuthType может менять источник данных; за CGI/FastCGI прокси должен передать Authorization по доверенной конфигурации.
Cookie: значение приходит только со следующим запросом
Cookie — маленькая пара «имя → значение», которую сервер добавляет в заголовок Set-Cookie, а браузер приносит в последующих подходящих запросах. После успешного setcookie() новое значение не обязано сразу появиться в текущем $_COOKIE: этот массив описывает уже пришедший запрос. Данные cookie видит клиент; храните там идентификатор или настройку, а не пароль и не право доступа.
<?php
declare(strict_types=1);
function preferenceCookie(string $theme, int $now): array
{
if (!in_array($theme, ['light', 'dark'], true)) {
throw new InvalidArgumentException('Неизвестная тема');
}
return [
'name' => 'theme',
'value' => $theme,
'options' => [
'expires' => $now + 86400 * 30,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
],
];
}
$cookie = preferenceCookie('dark', 1_800_000_000);
echo $cookie['name'], '=', $cookie['value'], "\n";
echo $cookie['options']['expires'], "\n";
echo $cookie['options']['samesite'], ' secure=', $cookie['options']['secure'] ? 'yes' : 'no', "\n";
Ожидаемый результат
theme=dark
1802592000
Lax secure=yes
Функция только строит проверенную запись и потому выполняется в CLI. В HTTP-контроллере вызов будет setcookie($cookie['name'], $cookie['value'], $cookie['options']).
| Атрибут | Что ограничивает и где ошибаются |
|---|---|
| expires / Max-Age | Срок хранения. Удаление — тот же name/path/domain с прошедшим сроком. |
| path и domain | Каким адресам браузер отправляет cookie. Лишний domain расширяет область; старый path мешает удалить запись. |
| Secure | Передача только по HTTPS. SameSite=None без Secure современные браузеры отклоняют. |
| HttpOnly | JavaScript не читает cookie; это снижает ущерб XSS, но не исправляет сам XSS. |
| SameSite | Lax/Strict/None управляют межсайтовой отправкой. Это дополнительный слой, а не замена CSRF-проверке. |
setcookie() URL-кодирует значение; setrawcookie() оставляет допустимые байты как есть. Оба формируют заголовок и должны вызываться до HTML, пробела или другого вывода. Проверьте headers_sent() при предупреждении «headers already sent».
Сессии: официальный вход ведёт к полному каноническому уроку
Сессия связывает запросы через идентификатор, обычно сохранённый в cookie, а данные держит на сервере. Официальная ветка Features даёт вход в механизм; полный учебный маршрут уже находится в главе о cookie, сессии, блокировке и входе. Там показаны session_start(), ротация ID после входа, сроки, блокировка параллельных запросов и POST-выход.
Загрузка файла начинается до выполнения вашего обработчика
Форма method="post" enctype="multipart/form-data" отправляет поля и байты файлов. PHP сначала принимает тело запроса во временный каталог, затем создаёт $_FILES. Обработчик обязан проверить наличие поля, тип каждого элемента, код error, факт HTTP-upload через is_uploaded_file(), собственный лимит, определённый сервером MIME и только затем переместить файл с move_uploaded_file().
- Вход: браузер → multipart + Content-Length
- Действие: веб-сервер и PHP применяют лимиты, создают временный файл и $_FILES
- Результат: ваш код принимает или отклоняет, затем сохраняет вне public
<?php
declare(strict_types=1);
function uploadMessage(int $error): string
{
return match ($error) {
UPLOAD_ERR_OK => 'Файл принят во временное хранилище',
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Файл больше разрешённого',
UPLOAD_ERR_PARTIAL => 'Передача оборвалась',
UPLOAD_ERR_NO_FILE => 'Файл не выбран',
UPLOAD_ERR_NO_TMP_DIR => 'На сервере нет временного каталога',
UPLOAD_ERR_CANT_WRITE => 'Сервер не смог записать временный файл',
UPLOAD_ERR_EXTENSION => 'Расширение PHP остановило загрузку',
default => 'Неизвестный код загрузки',
};
}
foreach ([UPLOAD_ERR_OK, UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE, 99] as $error) {
echo $error, ': ', uploadMessage($error), "\n";
}
Ожидаемый результат
0: Файл принят во временное хранилище
1: Файл больше разрешённого
3: Передача оборвалась
4: Файл не выбран
99: Неизвестный код загрузки
match перечисляет все официальные коды. Пока код не равен UPLOAD_ERR_OK, tmp_name, type и size нельзя считать полезными данными файла.
Скрытое поле MAX_FILE_SIZE должно стоять перед file input и может дать UPLOAD_ERR_FORM_SIZE, но клиент способен его изменить; серверные upload_max_filesize, post_max_size, max_file_uploads, max_input_time, свободное место и права временного каталога остаются обязательными. Если весь multipart превышает post_max_size, $_POST и $_FILES могут быть пустыми; сравните CONTENT_LENGTH с серверным пределом и отвечайте 413, а не «файл не выбран».
POST: имя HTML-поля становится ключом $_FILES
Минимальная HTML-форма и полная безопасная обработка уже разобраны в канонической главе Uploads. Имя photo в <input type="file" name="photo"> становится ключом $_FILES['photo']; это соответствие показано до проверок MIME и перемещения. После сохранения файл получает новое серверное имя, а исходное имя остаётся только подписью.
Для одного файла PHP даёт поля name, full_path, type, tmp_name, error, size. type присылает браузер и не подтверждает формат; full_path относителен и не должен использоваться как путь назначения. Пути клиента не дают серверу доступа к исходному диску.
Ошибки 0–8: транспорт прошёл либо остановился раньше вашей проверки
Коды 0–8 — результат транспортного этапа, а не финальная оценка безопасности файла. Даже UPLOAD_ERR_OK означает только, что PHP создал временный upload. Дальше следуют MIME, размер, декодирование содержимого, квоты и безопасное имя. Первый разбор ошибки показывает этот порядок без скачка к сложной функции.
| Код | Решение обработчика |
|---|---|
| 0 / OK | Переходить к проверке типа, размера и содержимого. |
| 1 / INI_SIZE | Файл больше upload_max_filesize; увеличить лимит только после оценки ресурсов. |
| 2 / FORM_SIZE | Превышено MAX_FILE_SIZE; не считать клиентский лимит защитой. |
| 3 / PARTIAL | Передача неполная; не сохранять обрывок, предложить повтор. |
| 4 / NO_FILE | Нет файла; различить обязательное и необязательное поле. |
| 6 / NO_TMP_DIR | Ошибка конфигурации сервера; пользователю нейтральный 5xx, оператору диагностика. |
| 7 / CANT_WRITE | Проверить место, права и файловую систему временного каталога. |
| 8 / EXTENSION | Расширение PHP остановило загрузку; искать имя расширения в конфигурации и журнале. |
Распространённые ловушки: успешная передача ещё не делает файл безопасным
Типичная ошибка — проверить только расширение имени и затем положить файл в web-root. Практический маршрут другой: отказ транспорта → серверный лимит → is_uploaded_file → MIME через Fileinfo → декодирование изображения/документа → новое случайное имя → каталог без выполнения PHP → выдача через контроллер. Угрозы и безопасное хранилище дают готовый маршрут.
- Не используйте клиентский
typeкак доказательство формата. - Не соединяйте исходное имя с каталогом назначения.
- Не считайте
acceptв HTML защитой: это подсказка выбора. - Не вызывайте
move_uploaded_fileдо проверки кода ошибки. - Не обещайте прогресс загрузки средствами обычного PHP POST: тело принимается до запуска обработчика.
Несколько файлов: одинаковый индекс связывает name, error и tmp_name
Поле documents[] создаёт не «массив файлов», а несколько параллельных массивов: все name, все tmp_name, все error. Нормализация собирает значения одного индекса в одну запись, после чего каждый файл проходит тот же полный pipeline. Вложенные HTML-имена дают более глубокую форму; не обходите её без ограничений глубины и количества.
<?php
declare(strict_types=1);
function normalizeUploads(array $files): array
{
$result = [];
foreach ($files['name'] ?? [] as $index => $name) {
$result[] = [
'name' => (string) $name,
'tmp_name' => (string) ($files['tmp_name'][$index] ?? ''),
'error' => (int) ($files['error'][$index] ?? UPLOAD_ERR_NO_FILE),
'size' => (int) ($files['size'][$index] ?? 0),
];
}
return $result;
}
$files = [
'name' => ['front.png', 'back.png'],
'tmp_name' => ['/tmp/a', '/tmp/b'],
'error' => [UPLOAD_ERR_OK, UPLOAD_ERR_PARTIAL],
'size' => [1250, 400],
];
foreach (normalizeUploads($files) as $file) {
echo $file['name'], ': error=', $file['error'], ', bytes=', $file['size'], "\n";
}
Ожидаемый результат
front.png: error=0, bytes=1250
back.png: error=3, bytes=400
Второй элемент не готов к сохранению: error=3. В реальном запросе tmp_name проверяется is_uploaded_file; здесь пути намеренно фиктивны и не используются.
PUT: сырые байты не попадают в $_FILES автоматически
Обычный raw PUT не создаёт $_FILES: тело читают из php://input, а метод — из $_SERVER['REQUEST_METHOD']. Если клиент отправляет multipart/form-data, PHP 8.4+ предоставляет request_parse_body() для неблокирующего контракта PUT/PATCH; до этого парсер обычно принадлежал приложению или фреймворку. Нельзя одновременно читать поток несколько раз и ожидать прежние байты.
<?php
declare(strict_types=1);
function acceptPut(string $method, string $contentType, string $body, int $limit = 1024): string
{
if ($method !== 'PUT') { return '405 Method Not Allowed'; }
if ($contentType !== 'application/json') { return '415 Unsupported Media Type'; }
if (strlen($body) > $limit) { return '413 Content Too Large'; }
try {
$data = json_decode($body, true, 8, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return '400 Invalid JSON';
}
if (!is_array($data) || !is_int($data['stock'] ?? null) || $data['stock'] < 0) {
return '422 Invalid stock';
}
return '200 stock=' . $data['stock'];
}
foreach ([
['POST', 'application/json', '{"stock":8}'],
['PUT', 'text/plain', '{"stock":8}'],
['PUT', 'application/json', '{bad'],
['PUT', 'application/json', '{"stock":8}'],
] as [$method, $type, $body]) {
echo acceptPut($method, $type, $body), "\n";
}
Ожидаемый результат
405 Method Not Allowed
415 Unsupported Media Type
400 Invalid JSON
200 stock=8
Функция получает уже прочитанное тело, поэтому проверяется детерминированно. HTTP endpoint передаст file_get_contents('php://input') и ответит соответствующим статусом.
Куда идти после загрузки
После официальной ветки загрузок продолжайте по задаче: проверить и сохранить файл, определить MIME по содержимому, защитить файловую границу, показать ошибки около поля. Эти ссылки дополняют официальный See Also и ведут на уже авторские объяснения, а не на голые имена функций.
Удалённый файл — сетевой запрос с политикой отказа
Если allow_url_fopen=On и wrapper поддерживает операцию, функции fopen, file_get_contents и похожие принимают URL. Это удобная единая модель потока, но она не превращает сеть в локальный файл: нужны timeout, HTTP-статус, лимит байтов, проверка формата и политика редиректов. allow_url_include управляет сетевыми URL для include/require и не должен включаться ради получения данных.
<?php
declare(strict_types=1);
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 3.0,
'ignore_errors' => true,
'header' => [
'Accept: application/json',
'User-Agent: WarehouseImporter/1.0',
],
],
]);
$json = file_get_contents('https://inventory.example.test/products/42', false, $context);
if ($json === false) {
throw new RuntimeException('Транспорт не вернул тело');
}
$statusLine = $http_response_header[0] ?? '';
if (!preg_match('~^HTTP/\S+ 200(?: |$)~', $statusLine)) {
throw new RuntimeException('Неуспешный HTTP-статус: ' . $statusLine);
}
$product = json_decode($json, true, 16, JSON_THROW_ON_ERROR);
if (!is_string($product['name'] ?? null) || !is_int($product['stock'] ?? null)) {
throw new RuntimeException('Контракт ответа изменился');
}
Ожидаемый результат
При HTTP 200 и контракте {"name":"Молоток","stock":8} переменная $product содержит проверенные name и stock; при transport/status/JSON/schema отказе выбрасывается отдельная RuntimeException.
Синтаксис проверяется локально. Домен .test намеренно не существует: пример нельзя выдавать за выполненный сетевой запрос. Для production API cURL удобнее при TLS, redirects, detailed errors и нескольких запросах.
- Не подставляйте произвольный пользовательский URL: это SSRF к внутренним адресам и метаданным облака.
- Не помещайте пароль в URL: он попадает в журнал и диагностику.
- Проверяйте активный ini именно нужного SAPI: CLI и FPM могут иметь разные allow_url_fopen.
- FTP wrapper имеет другой набор операций и ошибок; HTTP обычно read-only на уровне file functions.
Закрытая вкладка — ненадёжный сигнал для бизнес-операции
PHP различает нормальное завершение, разрыв клиента и timeout. connection_status() возвращает битовую маску, connection_aborted() — признак разрыва, ignore_user_abort() определяет, должен ли разрыв завершать выполнение, а set_time_limit() управляет временем скрипта с платформенными нюансами. Однако веб-сервер часто сообщает разрыв только во время записи, а буферы и прокси задерживают сигнал.
<?php
declare(strict_types=1);
function connectionLabel(int $status): string
{
$parts = [];
if ($status === CONNECTION_NORMAL) { return 'normal'; }
if (($status & CONNECTION_ABORTED) !== 0) { $parts[] = 'aborted'; }
if (($status & CONNECTION_TIMEOUT) !== 0) { $parts[] = 'timeout'; }
return implode('+', $parts) ?: 'unknown';
}
foreach ([CONNECTION_NORMAL, CONNECTION_ABORTED, CONNECTION_TIMEOUT,
CONNECTION_ABORTED | CONNECTION_TIMEOUT] as $status) {
echo $status, ': ', connectionLabel($status), "\n";
}
Ожидаемый результат
0: normal
1: aborted
2: timeout
3: aborted+timeout
Пример объясняет константы в CLI. Он не утверждает, что браузерный разрыв можно надёжно сымитировать без конкретного веб-сервера и прокси.
- Принять запрос и проверить право
- Зафиксировать задание в базе/очереди
- Быстро вернуть 202 + job_id
- Worker завершает работу независимо от вкладки
- Клиент читает статус отдельным запросом
Persistent DB connection: меньше handshakes, больше состояния и лимитов
Persistent connection — соединение, которое драйвер кеширует внутри процесса PHP и предлагает следующему запросу с теми же параметрами. Это не одна глобальная связь на весь сайт и не пользовательская сессия. В FPM каждый worker способен держать собственные соединения; восемь worker по три разных DSN уже требуют до 24 соединений базы.
<?php
declare(strict_types=1);
function connectionBudget(int $workers, int $connectionsPerWorker, int $reserve): array
{
if ($workers < 1 || $connectionsPerWorker < 1 || $reserve < 0) {
throw new InvalidArgumentException('Параметры бюджета должны быть положительными');
}
$application = $workers * $connectionsPerWorker;
return ['application' => $application, 'minimum_server_limit' => $application + $reserve];
}
$budget = connectionBudget(8, 3, 6);
echo 'Приложение: ', $budget['application'], "\n";
echo 'С резервом: ', $budget['minimum_server_limit'], "\n";
Ожидаемый результат
Приложение: 24
С резервом: 30
Резерв оставлен для миграций, администратора и фоновых задач. Реальный максимум сверяют с размером pool, количеством приложений и лимитом СУБД.
<?php
declare(strict_types=1);
$pdo = new PDO(
'mysql:host=db.internal;dbname=shop;charset=utf8mb4',
getenv('DB_USER') ?: '',
getenv('DB_PASSWORD') ?: '',
[
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
try {
$pdo->exec("SET SESSION time_zone = '+00:00'");
$pdo->beginTransaction();
// ... один законченный unit of work ...
$pdo->commit();
} catch (Throwable $error) {
if ($pdo->inTransaction()) { $pdo->rollBack(); }
throw $error;
}
Ожидаемый результат
При доступной MySQL и верных credentials соединение создаётся/переиспользуется, timezone задаётся заново, транзакция завершается commit либо rollback.
Это lint-пример: локальная проверка не поднимает MySQL и не доказывает повторное использование между FPM-запросами. У PDO ATTR_PERSISTENT передают в constructor options; поздний setAttribute не включает persistent connection задним числом.
| Риск | Практическая защита |
|---|---|
| Слишком много соединений | Считать workers × варианты credentials/DSN, ограничить pool и наблюдать сервер. |
| Оставшаяся транзакция/lock | Всегда commit/rollback в try/catch/finally; не полагаться на окончание запроса. |
| Session variables, timezone, charset | Устанавливать требуемое состояние при получении connection либо использовать driver reset. |
| Малая выгода | Сначала измерить latency создания connection; локальная БД может не оправдать сложность. |
| ODBC pooling | Не накладывать PDO persistent без проверки на собственный ODBC pool и документацию драйвера. |
Источники и обсуждения
- PHP Manual: Features
- PHP Manual: HTTP authentication with PHP
- PHP Manual: Cookies
- PHP Manual: Sessions
- PHP Manual: Handling file uploads
- PHP Manual: POST method uploads
- PHP Manual: Error Messages Explained
- PHP Manual: Common Pitfalls
- PHP Manual: Uploading multiple files
- PHP Manual: PUT method support
- PHP Manual: File upload: See Also
- PHP Manual: Using remote files
- PHP Manual: Connection handling
- PHP Manual: Persistent Database Connections
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


