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

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

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

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

Глава 140 · Практика

Mail: письмо с сайта, MIME, безопасность заголовков и доставка

Полный контракт mail и исторического ezmlm_hash: от частей сообщения до UTF-8, транспорта, очереди, проверок формы и диагностики.

Зачем сайту почта и где заканчивается работа PHP

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

Путь уведомления о заказе 17
  1. Сайт сохраняет заказ 17
  2. PHP подготавливает адрес, тему, текст и заголовки
  3. mail передаёт сообщение локальной программе/MTA либо Windows SMTP
  4. Почтовая система маршрутизирует письмо; возможны очередь и возврат
  5. Сервер получателя принимает или отклоняет сообщение; клиент показывает письмо
Сначала просто подготовить данные без отправкиСамостоятельный пример
<?php
declare(strict_types=1);

$to = 'customer@example.invalid';
$subject = 'Order 17 received';
$message = "Order: 17\r\nStatus: received\r\n";
echo $to, "\n", $subject, "\n";
echo str_replace("\r\n", "\n", $message);
Ожидаемый результат
customer@example.invalid
Order 17 received
Order: 17
Status: received

Три переменные — три части сообщения. Никакая сеть здесь не используется. внутри двойных кавычек обозначает пару управляющих символов для переноса почтовой строки.

Встроенная функция и внешний транспорт

На Unix mail обычно передаёт письмо sendmail-совместимой программе. Это может быть sendmail либо wrapper Postfix/qmail и другой почтовой системы. В требованиях сборки PHP указан поиск sendmail в PATH и стандартных системных каталогах; пользователь сборки должен иметь доступ. На рабочем сервере одной функции PHP недостаточно: нужен установленный и настроенный маршрут обработки писем.

Windows имеет встроенный простой SMTP-транспорт, который соединяется с указанным MTA. В нём нет автоматически предоставленного аккаунта, STARTTLS или SMTP AUTH. Если нужен защищённый SMTP провайдера с авторизацией, используют библиотеку/транспорт с такой поддержкой. Настройка sendmail_path переключает на внешнюю программу и в Windows.

Проверьте конфигурацию нужного PHP SAPI и наличие функции через function_exists. Успешный запуск в терминале не доказывает, что веб-процесс использует тот же ini или имеет те же права. Адрес, порт, внешняя программа и аутентификация — разные настройки.

Все настройки Mail

НастройкаСтандарт / доступНазначение
SMTPlocalhost / INI_ALLАдрес SMTP только для встроенного Windows транспорта. Это не настройка PHPMailer и не маршрут Unix sendmail.
smtp_port25 / INI_ALLПорт встроенного Windows SMTP; сам выбор 465 или 587 не добавляет TLS и аутентификацию.
sendmail_fromпусто / INI_ALLОтправитель по умолчанию для прямого Windows SMTP; участвует в обратном пути. Явный корректный From делает назначение видимым.
sendmail_pathобычно /usr/sbin/sendmail -t -i / INI_SYSTEMПрограмма/совместимый wrapper, принимающий письмо. Работает также на Windows: при заданном пути SMTP/smtp_port/sendmail_from не используются.
mail.force_extra_parametersпусто / INI_SYSTEMАдминистративно заменяет пятый аргумент mail, а не дописывает его. Возможна настройка php_value в httpd.conf, но такой способ не рекомендуется документацией.
mail.add_x_header0 / INI_SYSTEM|INI_PERDIRДобавляет X-PHP-Originating-Script с UID и именем файла. Может раскрывать внутренние сведения; не механизм подтверждения подлинности. Режим доступа уточнён по PHP8.5.11.
mail.logпусто / INI_SYSTEM|INI_PERDIRЖурнал вызовов: путь скрипта, строка, адрес To и заголовки. Ограничивайте доступ и срок хранения: там могут быть персональные данные.
mail.mixed_lf_and_crlf0 / INI_SYSTEM|INI_PERDIRС PHP8.2.4 совместимость с MTA, не принимающим корректный CRLF; возвращает старое смешанное поведение. Применяется по установленной причине, не как универсальное лечение доставки.
mail.cr_lf_modecrlf / INI_SYSTEM|INI_PERDIRЕсть в проверенном PHP8.5.11: crlf, lf, mixed либо os. Управляет разделителями при передаче внешнему sendmail; не включает TLS. lf также преобразует CRLF тела в LF; mixed оставляет тело как передано; os выбирает разделители заголовков по ОС. При crlf сохраняется совместимость с включённым старым mail.mixed_lf_and_crlf. Настройку нужно согласовать с конкретным MTA; не считать доступной во всех старых PHP.

INI_SYSTEM меняется в конфигурации запуска, INI_PERDIR — на разрешённом уровне каталога/сервера, INI_ALL допускает runtime-изменение. Имя SMTP пишется именно так; оно не добавляет TLS одним изменением порта. Не публикуйте phpinfo и почтовые журналы с внутренними путями для посетителей.

Конверт, заголовки и тело — три разных уровня

ЭлементЧто решаетТипичная ошибка
Envelope senderКуда отправлять служебные возвратыСчитать любой Reply-To адресом возврата.
FromКто указан автором письмаПодставить произвольный домен посетителя от имени сайта.
Reply-ToКуда почтовый клиент предложит ответитьРазрешить CR/LF из поля формы и внедрение других заголовков.
To / Cc / BccПолучатели и видимость адресовПоказать адреса всех подписчиков в To.
SubjectТемаПередать кириллицу без согласованного MIME-представления.
Content-Type / Transfer-EncodingФормат/кодировка телаДописать текст/html в тело вместо заголовка.

Сайт отправляет письмо от контролируемого проектом адреса. Адрес посетителя после проверки можно использовать как Reply-To, если это соответствует назначению формы. HTML-экранирование не предотвращает внедрение почтового заголовка: контекст другой. В шаблоне HTML-тела htmlspecialchars защищает вставляемый текст, но не становится валидатором email.

Русский текст, MIME и длина строки

Кодировка темы и кодировка тела независимы. Для темы используется encoded-word RFC2047; для тела указывают charset и Content-Transfer-Encoding. Base64 удобен для точной транспортной передачи UTF-8, но увеличивает размер. Для сложного HTML, вложений, multipart/alternative и международных адресов разумнее библиотека, которая формирует MIME целиком.

Увидеть, что MIME-кодирование сохраняет исходный текстСамостоятельный пример
<?php
declare(strict_types=1);

$subject = 'Заказ 17 принят';
$encodedSubject = mb_encode_mimeheader($subject, 'UTF-8', 'B', "\r\n");
$plain = "Спасибо за заказ.\r\n";
$encodedBody = chunk_split(base64_encode($plain), 76, "\r\n");
var_dump(mb_decode_mimeheader($encodedSubject) === $subject);
var_dump(base64_decode($encodedBody, true) === $plain);
Ожидаемый результат
bool(true)
bool(true)

Проверена подготовка байтов, без отправки. В письме также нужны заголовки Content-Type и Content-Transfer-Encoding, как в карточке mail.

Официальный простой пример mail предлагает переносить строки примерно на70 символов. RFC5322 рекомендует до78 и ограничивает строку998 символами без CRLF; у конкретного MIME-кодирования свои правила (Base64 — до76). Это не повод применять wordwrap вслепую к UTF-8, URL, заголовку или готовому HTML: можно разорвать данные. Генератор MIME выбирает переносы по типу содержимого.

С PHP8 обычные заголовки формируются с CRLF. Старые qmail-совместимые цепочки иногда сами преобразуют LF и получают двойной CR; для подтверждённой несовместимости есть mail.mixed_lf_and_crlf. Не исправляйте все письма глобальной заменой переносов «на удачу». В Windows PHP8.5.11 сам удваивает точки после в SMTP DATA; старый совет повторить str_replace(" .", " ..", ...) способен удвоить их лишний раз. Этот край проверяйте на выбранном транспорте, особенно при произвольном сыром теле.

Как не превратить форму обратной связи в почтовый ретранслятор

Пусть HTML-поле называется reply_email. После обычного POST-отправления формы PHP получает его в $_POST["reply_email"]. Клиент может прислать вместо строки массив, огромный текст либо строку с переводами строк. Сначала метод, CSRF/разрешённость действия и тип поля, затем ограничения и формат; получатель и отправитель задаются конфигурацией сайта.

Проверить значение Reply-To до формирования заголовкаСамостоятельный пример
<?php
declare(strict_types=1);

function checkedReplyEmail(mixed $raw): string {
    if (!is_string($raw) || strlen($raw) > 254) throw new InvalidArgumentException('Нужен короткий адрес');
    if (str_contains($raw, "\r") || str_contains($raw, "\n") || str_contains($raw, "\0")) {
        throw new InvalidArgumentException('Управляющие символы запрещены');
    }
    $email = trim($raw);
    if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) throw new InvalidArgumentException('Некорректный формат');
    return $email;
}
foreach (['buyer@example.invalid', ['unexpected'], "buyer@example.invalid\nBcc: other@example.invalid"] as $raw) {
    try { echo checkedReplyEmail($raw), "\n"; }
    catch (InvalidArgumentException $error) { echo "Отклонено\n"; }
}
Ожидаемый результат
buyer@example.invalid
Отклонено
Отклонено

Это намеренно ограниченная политика одного ASCII email. Она не подтверждает владение ящиком и не поддерживает автоматически весь диапазон международных адресов. Полный обработчик формы дополняет её проверкой метода, CSRF и ограничением частоты.

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

HTML-уведомление: содержимое из данных, не из произвольной разметки

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

$productName = '<Ключ> & комплект';
$safeName = htmlspecialchars($productName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$html = '<p>Товар: ' . $safeName . '</p>';
echo $html, "\n";
Ожидаемый результат
<p>Товар: &lt;Ключ&gt; &amp; комплект</p>

Это подготовка HTML-тела. При отправке указывают text/html; для клиента без HTML добавляют plain-text альтернативу через multipart. Кнопка в письме не должна сама выполнять опасное действие простым GET.

Вложения, встроенные изображения, boundary и переносы заголовков требуют согласованного MIME. Исторический PHP Manual ссылается на PEAR::Mail, Mail_Mime и Mail_Queue; их роль сохраняем как указатель старых решений. Современный проект может выбрать PHPMailer либо Symfony Mailer через Composer. Они не заменяют разрешение отправителя и настройку доставляемости.

Почему true ещё не означает доставку

НаблюдениеЧто исследовать
Функции нет / TypeError / ValueErrorВерсию, конфигурацию и собственные аргументы до транспорта.
false и предупреждениеПуть sendmail, разрешения, доступность локального SMTP, его ответ и журнал.
true, но письма нетОчередь и журнал MTA, ответ получателя, возврат, спам-папку и политику домена отправителя.
Письмо пришло дваждыПовторный вызов, retry после неопределённого результата, отсутствие идентификатора задания.
Кириллица испорченаИсходные байты, charset, MIME-тему и transport encoding.

При PHP8.5 sendmail-сбой и неожиданное завершение внешнего процесса диагностируются полнее. Предупреждение не надо подавлять @ и выдавать посетителю «всё отправлено». В пользовательском ответе показывают понятный статус, в защищённом журнале — технические сведения без токенов и полного личного текста. Конкретный SMTP/MTA журнал — источник следующего шага.

После принятия сообщения возможна отдельная ошибка доставки. Если нужно отслеживание, храните ID задания, принятый статус транспорта и события доставки от выбранной системы отдельно. Открытие письма тоже не доказывается одним результатом mail.

Большая рассылка, очередь и повторные попытки

mail в цикле для тысяч адресатов создаёт дорогое количество отдельных передач: Windows открывает SMTP-соединение для каждого письма, Unix обычно запускает программу. Задержка HTTP-ответа и таймауты становятся проблемой раньше, чем синтаксис цикла. Записывайте задания в очередь и отправляйте ограниченным worker с согласованным транспортом.

Повторять нужно с пониманием результата. Если соединение оборвалось после принятия, автоматический немедленный retry может создать дубль. Сохраняйте устойчивый ID события/уведомления и политику повторов; учитывайте лимиты провайдера и отказ подписчика. sleep и задержки сами по себе не дают надёжную очередь и гарантию доставки.

Все функции категории Mail и связанные инструменты

ИнструментРоль
mailНынешняя встроенная передача письма транспорту.
ezmlm_hashИсторический API EZMLM, удалённый в PHP8.
mb_send_mailОтдельная функция mbstring для кодированного письма.
iconv_mime_encodeПодготовка MIME-заголовка.
ob_start и шаблонПолучение HTML из шаблона в строку.

Mail не предоставляет своих классов, ресурсов и набора специальных констант; основные настройки перечислены выше. IMAP/Mailparse — отдельные категории с иными задачами, а не скрытые режимы mail.

Практика: принять заявку даже при временном сбое почты

Спроектируйте три состояния: заявка сохранена, уведомление ожидает отправки, транспорт принял уведомление. Не превращайте отсутствие письма в отсутствие заявки. Начните с модели состояния, затем свяжите её с базой и worker.

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

$request = ['id' => 17, 'saved' => true];
$notification = ['request_id' => 17, 'status' => 'pending', 'attempts' => 0];
$transportAccepted = false; // Модель отказа, сеть не вызывается.
$notification['attempts']++;
if ($transportAccepted) $notification['status'] = 'accepted';
echo $request['saved'] ? "Заявка сохранена\n" : "Заявка не сохранена\n";
echo $notification['status'], ':', $notification['attempts'], "\n";
Ожидаемый результат
Заявка сохранена
pending:1

Это модель перехода, а не готовая надёжная очередь: в рабочем проекте состояния сохраняются устойчиво, worker блокирует конкретное задание и ограничивает повторы. Полезный результат для пользователя не зависит от синхронного успеха SMTP.

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

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

mailПередать простое сообщение настроенному почтовому транспорту.

PHP4+ · Standard

mail(string $to, string $subject, string $message, array|string $additional_headers = [], string $additional_params = ""): bool

mail передаёт подготовленное письмо почтовому транспорту. Она не регистрирует SMTP-аккаунт, не добавляет самостоятельно MIME-вложение и не узнаёт, прочитал ли пользователь письмо. У функции полезная узкая роль: отдать простое сообщение уже настроенной почтовой системе.

АргументЧто передавать и зачем
toАдрес получателя либо список через запятую в допустимом формате. Для прямого Windows SMTP безопаснее простые адреса без отображаемого имени: формат Name
может разбираться иначе, чем Unix MTA.
subjectТема; для не-ASCII нужна подходящая MIME-кодировка RFC2047. Нельзя считать сырую строку из формы готовым безопасным заголовком.
messageТело: plain text либо должным образом оформленный MIME. Переносы CRLF. Не содержит заголовков From/Subject вместо четвёртого аргумента.
additional_headersАссоциативный массив имя => значение (с7.2) или собранная строка заголовков с CRLF. Задайте From. Для массива To и Subject запрещены: используйте первые два аргумента.
additional_paramsДополнительные параметры внешней sendmail-программы, например доверенный фиксированный -f для envelope sender. Не произвольные данные пользователя и не SMTP-пароль.
returntrue — транспорт принял сообщение для доставки; false — не принял. Последующая доставка, спам-фильтр и возврат письма происходят отдельно.
ОшибкиНеправильные типы/нулевые байты/запрещённые заголовки могут приводить к TypeError/ValueError; сбои транспорта — предупреждение и false. В8.5 улучшено обнаружение ошибки и неожиданного завершения sendmail.
Самый простой вызов для локального почтового стендаПример для указанного окружения
<?php
declare(strict_types=1);

$accepted = mail(
    'customer@example.invalid',
    'Order 17 received',
    "Order: 17\r\nStatus: received\r\n",
    ['From' => 'shop@example.invalid']
);
echo $accepted ? "Принято для доставки\n" : "Не принято\n";
Ожидаемый результат
Принято для доставки

Ожидаемый результат относится к транспорту, который согласился принять тестовое сообщение. Для собственного сервера нужны принадлежащие проекту адреса и настроенный маршрут; это не доказательство попадания в Inbox.

Теперь те же части можно подготовить по отдельности: адрес, тема, тело и массив заголовков остаются обычными PHP-значениями. MIME описывает представление текста. Base64 здесь — транспортное кодирование, а не шифрование или защита секретов.

Отправить короткое русское уведомление с явной кодировкойПример для указанного окружения
<?php
declare(strict_types=1);

$subject = mb_encode_mimeheader('Заказ 17 принят', 'UTF-8', 'B', "\r\n");
$plain = "Ваш заказ 17 принят.\r\nМы сообщим об отправке.\r\n";
$headers = [
    'From' => 'shop@example.invalid',
    'MIME-Version' => '1.0',
    'Content-Type' => 'text/plain; charset=UTF-8',
    'Content-Transfer-Encoding' => 'base64',
];
$body = chunk_split(base64_encode($plain), 76, "\r\n");
$accepted = mail('customer@example.invalid', $subject, $body, $headers);
echo $accepted ? "Принято для доставки\n" : "Не принято\n";
Ожидаемый результат
Принято для доставки

Предусловие: mbstring и настроенный транспорт. Тема закодирована отдельно от тела; строки Base64 ограничены 76 символами. Этот пример не конструирует вложения или multipart-письмо.

Понять старую строковую форму дополнительных заголовковПример для указанного окружения
<?php
declare(strict_types=1);

$headers = "From: shop@example.invalid\r\n"
    . "Reply-To: support@example.invalid\r\n"
    . "Content-Type: text/plain; charset=UTF-8";
$accepted = mail('customer@example.invalid', 'Ticket received', "Ticket: 17\r\n", $headers);
echo $accepted ? "Принято для доставки\n" : "Не принято\n";
Ожидаемый результат
Принято для доставки

Каждый заголовок отделён CRLF; пустая строка внутри заголовков преждевременно закончила бы их блок. В новом простом коде ассоциативный массив легче проверить.

Задать фиксированный обратный путь совместимому sendmailПример для указанного окружения
<?php
declare(strict_types=1);

// Только сервер с согласованным sendmail-совместимым транспортом.
$accepted = mail(
    'customer@example.invalid', 'Order receipt', "Order: 17\r\n",
    ['From' => 'shop@example.invalid'],
    '-fbounces@example.invalid'
);
echo $accepted ? "Принято для доставки\n" : "Не принято\n";
Ожидаемый результат
Принято для доставки

Пятый аргумент — строка параметров команды. В реальном проекте домен обратного пути должен принадлежать отправителю, а MTA должен разрешать этому системному пользователю выбранный envelope sender. sendmail trusted-users — серверная конфигурация, не настройка посетителя.

Встроенное escapeshellcmd для пятого аргумента не запрещает дополнительные флаги. Поэтому передать туда -f плюс строку посетителя опасно даже при экранировании shell. Некоторые синтаксически допустимые email-символы несовместимы с этой командной формой; для сложных адресов выбирают полноценный почтовый транспорт/библиотеку.

Увидеть отклонение нового заголовка, спрятанного в Reply-ToСамостоятельный пример
<?php
declare(strict_types=1);

try {
    mail('customer@example.invalid', 'Ticket', 'Text', [
        'From' => 'shop@example.invalid',
        'Reply-To' => "buyer@example.invalid\r\nBcc: other@example.invalid",
    ]);
} catch (ValueError $error) {
    echo "Недопустимые символы заголовка отклонены\n";
}
Ожидаемый результат
Недопустимые символы заголовка отклонены

PHP8.5 отвергает эту форму до обращения к транспорту. Это демонстрация встроенной проверки, а не причина отказаться от собственной проверки входа.

Не добавляйте дублирующие To/Subject в массив дополнительных заголовков: текущий PHP их отвергает. From/Reply-To/Cc/Bcc имеют разные назначения. Bcc не должен появляться в доставленном списке видимых адресатов; поведение конечной упаковки зависит от транспорта, поэтому массовую рассылку проверяют отдельным почтовым инструментом, а не публикуют все адреса в to.

Рядом: mb_send_mail — кодирование средствами mbstring, iconv_mime_encode, сборка шаблона письма, безопасная приёмка формы.

ezmlm_hashИсторическое распределение email по корзинам EZMLM; удалено из PHP8.

Удалено в PHP8.0 · Standard

ezmlm_hash(string $addr): int — только PHP4.0.2–7.4

ezmlm_hash обслуживала конкретный старый формат распределения адресов EZMLM/MySQL по корзинам. Это не отправка письма, не проверка email и не криптографический хеш. Результат находился в диапазоне 0…52; разные адреса легко попадают в одну корзину.

ЧастьИсторический контракт
addrСтрока адреса, обрабатываемая старым алгоритмом EZMLM.
returnЦелое число корзины. Не уникальный идентификатор подписчика.
ВерсииДоступна с PHP4.0.2, deprecated в7.4, удалена в8.0. Строка changelog на странице с7.2 противоречит реализации: в7.2/7.3 регистрация обычная, в7.4 — deprecated.
Найти зависимость от удалённой функции при переносе проектаСамостоятельный пример
<?php
declare(strict_types=1);

echo function_exists('ezmlm_hash') ? "Есть старый API\n" : "Нужна миграция\n";
Ожидаемый результат
Нужна миграция

Пример для PHP8.5. Отсутствие функции не исправляется случайной заменой на hash: если старое хранилище рассчитывает на EZMLM-корзины, другой алгоритм нарушит совместимость.

Хранить новых подписчиков по email без старого хешаСамостоятельный пример
<?php
declare(strict_types=1);

$db = new PDO('sqlite::memory:');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('CREATE TABLE subscribers (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE)');
$insert = $db->prepare('INSERT INTO subscribers(email) VALUES (:email)');
$insert->execute(['email' => 'reader@example.invalid']);
echo $db->query('SELECT email FROM subscribers')->fetchColumn(), "\n";
Ожидаемый результат
reader@example.invalid

Это современный вариант для НОВОГО списка, а не замена алгоритма существующего EZMLM. SQL-параметры передают отдельно от текста запроса; UNIQUE закрепляет выбранную схему уникальности. Подписка требует подтверждения согласия и адреса на уровне приложения.

Узнать историческую точку вызова в старом проектеПример для указанного окружения
<?php
declare(strict_types=1);

// Только старый PHP с ещё существующей функцией; не выполнять на PHP8.
$address = 'reader@example.invalid';
$bucket = ezmlm_hash($address);
echo $bucket;

Возвращаемое число зависит от адреса и старого алгоритма. Фрагмент оставлен для понимания legacy-кода; не заявляется запуск удалённого API в PHP8.

Если действительно мигрируете старый EZMLM, сохраните его правило корзин и сравните со старой системой на наборе известных адресов. Не считайте совпадение одной строки доказательством совместимости и не используйте этот алгоритм для паролей. Связанные главы: назначение хеш-функций, параметризованные запросы.

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

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

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

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

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