Главная/Статьи/Валидация email в PHP: три подхода

Валидация email в PHP: три подхода

Сравнение методов проверки email-адресов: filter_var(), регулярные выражения и PHPMailer. Примеры кода и рекомендации по выбору.

ДМ
Дмитрий Мещеряков
📅 23 апреля 2023 г.📖 7 мин чтения

Валидация email — задача, где стремление к строгости приносит больше вреда, чем пользы. Каждая дополнительная проверка отсекает какое-то количество мусора и какое-то количество настоящих пользователей, и второе обычно дороже.

Разберём способы проверки и, главное, где остановиться.

Зачем валидировать email

Проверка email-адреса — обязательный этап при работе с формами. Некорректный адрес приводит к:

  • Потерянным письмам и уведомлениям
  • Засорению базы данных невалидными записями
  • Проблемам с отправкой рассылок (падение репутации домена)

Способ 1: filter_var() — рекомендуемый

Встроенная функция PHP с фильтром FILTER_VALIDATE_EMAIL:

php
<?php

function isValidEmail(string $email): bool
{
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

// Примеры
$tests = [
    'user@example.com',        // ✅ true
    'user.name@example.co.uk', // ✅ true
    'user+tag@example.com',    // ✅ true
    'user@localhost',          // ✅ true (технически валидный)
    'user@',                   // ❌ false
    '@example.com',            // ❌ false
    'user example.com',        // ❌ false
    '',                        // ❌ false
];

foreach ($tests as $email) {
    $result = isValidEmail($email) ? '' : '';
    echo "{$result} {$email}\n";
}

Преимущества

  • Встроен в PHP, не требует зависимостей
  • Быстрый и оптимизированный
  • Соответствует RFC 822/5322

Расширенная проверка

php
<?php

class EmailValidator
{
    /**
     * Базовая валидация формата
     */
    public static function isValid(string $email): bool
    {
        return filter_var(trim($email), FILTER_VALIDATE_EMAIL) !== false;
    }

    /**
     * Проверка с очисткой
     */
    public static function sanitize(string $email): ?string
    {
        $clean = filter_var(trim($email), FILTER_SANITIZE_EMAIL);
        
        if (self::isValid($clean)) {
            return strtolower($clean);
        }
        
        return null;
    }

    /**
     * Проверка существования домена (MX-запись)
     */
    public static function hasMxRecord(string $email): bool
    {
        if (!self::isValid($email)) {
            return false;
        }
        
        $domain = substr(strrchr($email, '@'), 1);
        
        return checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A');
    }

    /**
     * Полная валидация: формат + DNS
     */
    public static function isDeliverable(string $email): bool
    {
        return self::isValid($email) && self::hasMxRecord($email);
    }
}

// Использование
$email = 'user@gmail.com';

if (EmailValidator::isDeliverable($email)) {
    echo "Email валиден и домен существует";
}

// Очистка и нормализация
$dirty = '  User@EXAMPLE.COM  ';
$clean = EmailValidator::sanitize($dirty);
echo $clean; // user@example.com
💡 Совет

Совет: Проверка MX-записи добавляет сетевой запрос. Используйте её только при регистрации, не при каждом логине.

⚠️ Важно

Не пишите свою регулярку для email. Формат адреса описан в RFC 5322, и он гораздо сложнее, чем что-то@что-то.что-то: допустимы плюсы, кавычки, точки в неожиданных местах, доменные имена в национальных алфавитах, новые доменные зоны длиннее четырёх символов.

Любая самописная регулярка отсекает часть валидных адресов. Классические жертвы: имя+тег@gmail.com (частый приём для фильтрации почты), адреса в зонах вида .museum и .travel, адреса на кириллических доменах.

filter_var($email, FILTER_VALIDATE_EMAIL) реализует проверку по стандарту и работает предсказуемо. Раздел ниже полезен для понимания, как устроен формат, но в рабочем коде брать оттуда регулярку не нужно.

Способ 2: Регулярные выражения

Альтернативный подход с preg_match():

php
<?php

function isValidEmailRegex(string $email): bool
{
    // RFC 5322 упрощённый паттерн
    $pattern = '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/';
    
    return (bool) preg_match($pattern, $email);
}

// Более строгий паттерн (только стандартные домены)
function isValidEmailStrict(string $email): bool
{
    $pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
    
    return (bool) preg_match($pattern, $email);
}

Когда использовать regex

  • Нужна кастомная логика (например, запрет определённых доменов)
  • Требуется извлечение частей адреса
  • Работа в среде без filter_var()
php
<?php

class EmailPatternValidator
{
    /**
     * Проверка с блоком временных почт
     */
    public static function isNotDisposable(string $email): bool
    {
        $disposable = [
            'tempmail.com',
            '10minutemail.com',
            'guerrillamail.com',
            'throwaway.email',
            'mailinator.com',
        ];
        
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return false;
        }
        
        $domain = strtolower(substr(strrchr($email, '@'), 1));
        
        return !in_array($domain, $disposable);
    }

    /**
     * Извлечение частей адреса
     */
    public static function parse(string $email): ?array
    {
        $pattern = '/^([^@]+)@(.+)$/';
        
        if (preg_match($pattern, $email, $matches)) {
            return [
                'local' => $matches[1],
                'domain' => $matches[2],
            ];
        }
        
        return null;
    }

    /**
     * Только корпоративные адреса
     */
    public static function isCorporate(string $email): bool
    {
        $freeProviders = [
            'gmail.com',
            'yahoo.com',
            'hotmail.com',
            'outlook.com',
            'mail.ru',
            'yandex.ru',
        ];
        
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return false;
        }
        
        $domain = strtolower(substr(strrchr($email, '@'), 1));
        
        return !in_array($domain, $freeProviders);
    }
}

// Примеры
$email = 'user@company.com';

var_dump(EmailPatternValidator::isNotDisposable($email)); // true
var_dump(EmailPatternValidator::isCorporate($email));      // true

print_r(EmailPatternValidator::parse($email));
// ['local' => 'user', 'domain' => 'company.com']

Способ 3: PHPMailer

Библиотека PHPMailer включает собственный валидатор:

bash
composer require phpmailer/phpmailer
php
<?php

use PHPMailer\PHPMailer\PHPMailer;

require 'vendor/autoload.php';

$mail = new PHPMailer();

$tests = [
    'user@example.com',
    'invalid-email',
    'user@localhost',
];

foreach ($tests as $email) {
    $valid = $mail->validateAddress($email);
    $result = $valid ? '' : '';
    echo "{$result} {$email}\n";
}

Когда использовать PHPMailer

  • Уже используете библиотеку для отправки писем
  • Нужна проверка с учётом IDN (интернационализированные домены)

Сравнение методов

МетодСкоростьЗависимостиRFC-совместимостьРекомендация
filter_var()⚡ БыстрыйНетПолная✅ Основной
Regex⚡ БыстрыйНетЗависит от паттернаДля кастомных правил
PHPMailer🐢 СреднийComposerПолная + IDNЕсли уже используется

Полный класс валидатора

php
<?php

namespace App\Validation;

class EmailValidator
{
    private array $disposableDomains = [];
    private array $allowedDomains = [];

    public function __construct(array $options = [])
    {
        $this->disposableDomains = $options['disposable'] ?? [];
        $this->allowedDomains = $options['allowed'] ?? [];
    }

    /**
     * Комплексная валидация
     */
    public function validate(string $email, array $rules = []): ValidationResult
    {
        $email = trim(strtolower($email));
        $errors = [];

        // Базовая проверка формата
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return new ValidationResult(false, ['Некорректный формат email']);
        }

        $domain = substr(strrchr($email, '@'), 1);

        // Проверка временных почт
        if (in_array('no_disposable', $rules)) {
            if (in_array($domain, $this->disposableDomains)) {
                $errors[] = 'Временные почтовые ящики не разрешены';
            }
        }

        // Проверка белого списка доменов
        if (in_array('allowed_only', $rules) && !empty($this->allowedDomains)) {
            if (!in_array($domain, $this->allowedDomains)) {
                $errors[] = 'Домен не в списке разрешённых';
            }
        }

        // Проверка MX-записи
        if (in_array('check_mx', $rules)) {
            if (!checkdnsrr($domain, 'MX') && !checkdnsrr($domain, 'A')) {
                $errors[] = 'Домен не существует или не принимает почту';
            }
        }

        return new ValidationResult(empty($errors), $errors, $email);
    }
}

class ValidationResult
{
    public function __construct(
        public readonly bool $isValid,
        public readonly array $errors = [],
        public readonly ?string $normalizedEmail = null
    ) {}
}

// Использование
$validator = new EmailValidator([
    'disposable' => ['tempmail.com', 'mailinator.com'],
    'allowed' => [], // Пусто = все разрешены
]);

$result = $validator->validate('user@gmail.com', ['no_disposable', 'check_mx']);

if ($result->isValid) {
    echo "Email валиден: {$result->normalizedEmail}";
} else {
    echo "Ошибки: " . implode(', ', $result->errors);
}

Где остановиться

Проверки можно наращивать бесконечно, поэтому полезно понимать, что каждая из них даёт и чего стоит.

Проверка формата (filter_var) — обязательна, стоит ничего, ложных срабатываний почти не даёт. Берите.

Проверка MX-записи домена — отсекает опечатки в домене (gmali.com), но добавляет сетевой запрос в форму регистрации. Это задержка при отправке и зависимость от DNS: при проблемах с резолвингом вы перестанете регистрировать пользователей вообще. Если делаете — с коротким таймаутом и с обработкой ошибки в сторону «пропустить», а не «отклонить».

Списки одноразовых доменов — работают на известных сервисах и бесполезны против новых, которые появляются постоянно. Плюс регулярно отсекают легитимных пользователей: часть корпоративных доменов попадает в такие списки по ошибке. Применять стоит осознанно и только там, где одноразовые адреса действительно вредят бизнесу.

Белые списки доменов — оправданы в узких случаях (корпоративный сервис для сотрудников одной компании) и вредны во всех остальных.

⚠️ Важно

Ни одна из этих проверок не отвечает на главный вопрос: существует ли адрес и читает ли его человек. Ответ на него даёт только письмо со ссылкой подтверждения — и именно оно является настоящей валидацией.

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

И обратное: если подтверждения нет, никакая валидация формата его не заменит.

Итоги

Минимум, который нужен почти всем: filter_var для формата, нормализация (обрезка пробелов, приведение к нижнему регистру), подтверждение по ссылке.

Нормализация важнее, чем кажется. Пробел, скопированный вместе с адресом, и заглавные буквы — самые частые причины «письмо не приходит». Обрезать и привести к нижнему регистру нужно и при регистрации, и при входе, иначе пользователь не сможет войти под тем же адресом, под которым зарегистрировался.

Сообщение об ошибке должно быть понятным. «Проверьте адрес: возможно, опечатка в домене» полезнее, чем «Некорректный email», особенно если вы уже знаете, что именно не так.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.