Перейти к содержимому
Поиск
Php Разработка

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

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

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 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 появляется только в следующем запросе, а закрытая вкладка не обязана немедленно остановить серверный код.

Карта веб-возможности
  1. Вход: HTTP-запрос, конфигурация SAPI и внешняя служба
  2. Действие: PHP читает вход, проверяет контракт и формирует ответ
  3. Результат: статус, заголовки, тело либо диагностированный отказ
ЗадачаТочное продолжение
Понять запрос и ответЖизненный цикл 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 из базы.

Настоящий HTTP endpoint с challengeПример для указанного окружения
<?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 видит клиент; храните там идентификатор или настройку, а не пароль и не право доступа.

Собрать политику 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 современные браузеры отклоняют.
HttpOnlyJavaScript не читает cookie; это снижает ущерб XSS, но не исправляет сам XSS.
SameSiteLax/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().

POST-upload
  1. Вход: браузер → multipart + Content-Length
  2. Действие: веб-сервер и PHP применяют лимиты, создают временный файл и $_FILES
  3. Результат: ваш код принимает или отклоняет, затем сохраняет вне public
Сначала объяснить код ошибки, потом читать tmp_nameСамостоятельный пример
<?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-имена дают более глубокую форму; не обходите её без ограничений глубины и количества.

Собрать параллельные поля одного upload в записьСамостоятельный пример
<?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; до этого парсер обычно принадлежал приложению или фреймворку. Нельзя одновременно читать поток несколько раз и ожидать прежние байты.

PUT: метод, Content-Type, размер, JSON и поляСамостоятельный пример
<?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 и не должен включаться ради получения данных.

Удалённый JSON через HTTP wrapper с явным контекстомПример для указанного окружения
<?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. Он не утверждает, что браузерный разрыв можно надёжно сымитировать без конкретного веб-сервера и прокси.

Безопасная долгая операция
  1. Принять запрос и проверить право
  2. Зафиксировать задание в базе/очереди
  3. Быстро вернуть 202 + job_id
  4. Worker завершает работу независимо от вкладки
  5. Клиент читает статус отдельным запросом

Persistent DB connection: меньше handshakes, больше состояния и лимитов

Persistent connection — соединение, которое драйвер кеширует внутри процесса PHP и предлагает следующему запросу с теми же параметрами. Это не одна глобальная связь на весь сайт и не пользовательская сессия. В FPM каждый worker способен держать собственные соединения; восемь worker по три разных DSN уже требуют до 24 соединений базы.

Посчитать верхнюю границу до включения persistent modeСамостоятельный пример
<?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, количеством приложений и лимитом СУБД.

Persistent PDO: опция конструктора и явное состояние запросаПример для указанного окружения
<?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 и документацию драйвера.
Источники и обсуждения

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

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

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

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