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

URL: https://dmeshcheryakov.ru/blog/php-email-validation/
Раздел: PHP
Теги: PHP, Валидация, Email
Опубликовано: 2023-04-23
Обновлено: 2026-09-07
Автор: Дмитрий Мещеряков (https://dmeshcheryakov.ru)

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

Валидация 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», особенно если вы уже знаете, что именно не так.