Laravel Email Validation
Complete guide to email validation in Laravel
What You'll Learn
- Integrate VerifyForge API in Laravel using HTTP Client
- Create custom validation rules
- Validate emails in Form Requests
- Build a reusable validation service
- Handle validation errors
- Best practices for production
Prerequisites
- Laravel 10+ installed
- PHP 8+ installed
- Basic knowledge of Laravel
- VerifyForge API key (get free key)
Installation
No package installation needed - we'll use Laravel's built-in HTTP client.
Quick Start: Validate Single Email
<?php
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'X-API-Key' => config('services.verifyforge.api_key')
])->get('https://verifyforge.com/api/validate', [
'email' => 'user@example.com'
]);
$result = $response->json();
if ($result['data']['isValid']) {
echo "✓ Email is valid!";
echo "Reachability: " . $result['data']['reachability'];
} else {
echo "✗ Email is invalid";
}
Tutorial 1: Create Validation Service
Step 1: Create VerifyForge Service
<?php
// app/Services/VerifyForgeService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Exception;
class VerifyForgeService
{
protected string $apiKey;
protected string $baseUrl;
public function __construct()
{
$this->apiKey = config('services.verifyforge.api_key');
$this->baseUrl = config('services.verifyforge.base_url', 'https://verifyforge.com');
}
/**
* Validate a single email address
*/
public function validate(string $email): array
{
try {
$response = Http::withHeaders([
'X-API-Key' => $this->apiKey
])->get("{$this->baseUrl}/api/validate", [
'email' => $email
]);
if ($response->failed()) {
throw new Exception('Email validation failed');
}
return $response->json();
} catch (Exception $e) {
throw new Exception('Email validation service unavailable: ' . $e->getMessage());
}
}
/**
* Validate with caching
*/
public function validateCached(string $email): array
{
$cacheKey = "email_validation:{$email}";
return Cache::remember($cacheKey, 3600, function () use ($email) {
return $this->validate($email);
});
}
/**
* Bulk validate emails
*/
public function validateBulk(array $emails): array
{
try {
$response = Http::withHeaders([
'X-API-Key' => $this->apiKey,
'Content-Type' => 'application/json'
])->post("{$this->baseUrl}/api/validate/bulk", [
'emails' => $emails
]);
if ($response->failed()) {
throw new Exception('Bulk validation failed');
}
return $response->json();
} catch (Exception $e) {
throw new Exception('Bulk validation service unavailable: ' . $e->getMessage());
}
}
/**
* Check if email is valid
*/
public function isValid(string $email): bool
{
$result = $this->validate($email);
return $result['data']['isValid'] ?? false;
}
}
Step 2: Add Configuration
<?php
// config/services.php
return [
// ... other services
'verifyforge' => [
'api_key' => env('VERIFYFORGE_API_KEY'),
'base_url' => env('VERIFYFORGE_BASE_URL', 'https://verifyforge.com'),
],
];
# .env
VERIFYFORGE_API_KEY=your_api_key_here
Step 3: Register Service Provider (Optional)
<?php
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\VerifyForgeService;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(VerifyForgeService::class, function ($app) {
return new VerifyForgeService();
});
}
}
Tutorial 2: Custom Validation Rule
Step 1: Create Custom Rule
<?php
// app/Rules/ValidVerifyForgeEmail.php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use App\Services\VerifyForgeService;
class ValidVerifyForgeEmail implements ValidationRule
{
protected bool $allowDisposable;
protected bool $allowRisky;
public function __construct(
bool $allowDisposable = false,
bool $allowRisky = false
) {
$this->allowDisposable = $allowDisposable;
$this->allowRisky = $allowRisky;
}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$service = app(VerifyForgeService::class);
try {
$result = $service->validateCached($value);
$data = $result['data'];
if (!$data['isValid']) {
$fail('The :attribute is not a valid email address.');
return;
}
if (!$this->allowDisposable && $data['disposable']) {
$fail('Temporary email addresses are not allowed.');
return;
}
if (!$this->allowRisky && $data['reachability'] === 'risky') {
$fail('This email address appears risky. Please use another email.');
return;
}
if ($data['reachability'] === 'invalid') {
$fail('This email address cannot receive mail.');
return;
}
} catch (\Exception $e) {
// Log error but don't block validation
logger()->error('VerifyForge validation error: ' . $e->getMessage());
}
}
}
Step 2: Use in Form Request
<?php
// app/Http/Requests/RegisterRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Rules\ValidVerifyForgeEmail;
class RegisterRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'email',
'unique:users',
new ValidVerifyForgeEmail(
allowDisposable: false,
allowRisky: false
)
],
'password' => ['required', 'string', 'min:8', 'confirmed'],
];
}
public function messages(): array
{
return [
'email.required' => 'Please enter your email address',
'email.email' => 'Please enter a valid email format',
'email.unique' => 'This email is already registered',
];
}
}
Step 3: Use in Controller
<?php
// app/Http/Controllers/AuthController.php
namespace App\Http\Controllers;
use App\Http\Requests\RegisterRequest;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function register(RegisterRequest $request)
{
// Email is already validated by ValidVerifyForgeEmail rule
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
auth()->login($user);
return redirect()->route('dashboard')
->with('success', 'Registration successful!');
}
}
Tutorial 3: API Controller for Email Validation
<?php
// app/Http/Controllers/Api/EmailValidationController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\VerifyForgeService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class EmailValidationController extends Controller
{
public function __construct(
protected VerifyForgeService $verifyForge
) {}
/**
* Validate single email
*/
public function validate(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'email' => 'required|email'
]);
if ($validator->fails()) {
return response()->json([
'error' => 'Invalid email format'
], 400);
}
try {
$result = $this->verifyForge->validate($request->email);
return response()->json([
'success' => true,
'data' => [
'email' => $result['data']['email'],
'isValid' => $result['data']['isValid'],
'reachability' => $result['data']['reachability'],
'disposable' => $result['data']['disposable'],
'roleAccount' => $result['data']['roleAccount'],
'freeProvider' => $result['data']['freeProvider'],
'suggestion' => $result['data']['suggestion'] ?? null,
]
]);
} catch (\Exception $e) {
return response()->json([
'error' => 'Validation service unavailable'
], 503);
}
}
/**
* Bulk validate emails
*/
public function validateBulk(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'emails' => 'required|array|min:1|max:100',
'emails.*' => 'required|email'
]);
if ($validator->fails()) {
return response()->json([
'error' => 'Invalid request',
'details' => $validator->errors()
], 400);
}
try {
$result = $this->verifyForge->validateBulk($request->emails);
return response()->json([
'success' => true,
'data' => $result['data'],
'summary' => $result['data']['summary'] ?? null,
]);
} catch (\Exception $e) {
return response()->json([
'error' => 'Bulk validation service unavailable'
], 503);
}
}
}
Add Routes
<?php
// routes/api.php
use App\Http\Controllers\Api\EmailValidationController;
Route::post('/validate-email', [EmailValidationController::class, 'validate']);
Route::post('/validate-bulk', [EmailValidationController::class, 'validateBulk']);
Tutorial 4: Middleware for Email Validation
<?php
// app/Http/Middleware/ValidateEmailMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Services\VerifyForgeService;
use Symfony\Component\HttpFoundation\Response;
class ValidateEmailMiddleware
{
public function __construct(
protected VerifyForgeService $verifyForge
) {}
public function handle(Request $request, Closure $next): Response
{
$email = $request->input('email');
if (!$email) {
return response()->json([
'error' => 'Email is required'
], 400);
}
try {
$result = $this->verifyForge->validateCached($email);
if (!$result['data']['isValid']) {
return response()->json([
'error' => 'Invalid email address'
], 422);
}
// Add validation result to request
$request->merge([
'email_validation' => $result['data']
]);
} catch (\Exception $e) {
// Log but allow request to continue
logger()->error('Email validation middleware error: ' . $e->getMessage());
}
return $next($request);
}
}
Register in app/Http/Kernel.php:
protected $middlewareAliases = [
// ... other middleware
'validate.email' => \App\Http\Middleware\ValidateEmailMiddleware::class,
];
Use in routes:
Route::post('/subscribe', [NewsletterController::class, 'subscribe'])
->middleware('validate.email');
Tutorial 5: Artisan Command for Bulk Validation
<?php
// app/Console/Commands/ValidateSubscribers.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Subscriber;
use App\Services\VerifyForgeService;
class ValidateSubscribers extends Command
{
protected $signature = 'subscribers:validate {--batch-size=100}';
protected $description = 'Validate all subscriber emails';
public function handle(VerifyForgeService $verifyForge): int
{
$batchSize = (int) $this->option('batch-size');
$subscribers = Subscriber::whereNull('validated_at')->get();
$total = $subscribers->count();
if ($total === 0) {
$this->info('No subscribers to validate');
return 0;
}
$this->info("Validating {$total} subscribers...");
$bar = $this->output->createProgressBar($total);
$bar->start();
// Process in batches
$subscribers->chunk($batchSize)->each(function ($chunk) use ($verifyForge, $bar) {
$emails = $chunk->pluck('email')->toArray();
try {
$result = $verifyForge->validateBulk($emails);
// Update each subscriber
foreach ($result['data']['results'] as $validation) {
$subscriber = Subscriber::where('email', $validation['email'])->first();
if ($subscriber) {
$subscriber->update([
'is_valid' => $validation['isValid'],
'reachability' => $validation['reachable'],
'validated_at' => now(),
]);
}
$bar->advance();
}
} catch (\Exception $e) {
$this->error("Batch failed: {$e->getMessage()}");
}
});
$bar->finish();
$this->newLine();
$this->info('Validation complete!');
return 0;
}
}
Run with:
php artisan subscribers:validate --batch-size=100
Best Practices for Production
1. Queue Validation for Background Processing
<?php
// app/Jobs/ValidateEmailJob.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Services\VerifyForgeService;
use App\Models\Subscriber;
class ValidateEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public string $email,
public ?int $subscriberId = null
) {}
public function handle(VerifyForgeService $verifyForge): void
{
try {
$result = $verifyForge->validate($this->email);
if ($this->subscriberId) {
$subscriber = Subscriber::find($this->subscriberId);
$subscriber?->update([
'is_valid' => $result['data']['isValid'],
'reachability' => $result['data']['reachability'],
'validated_at' => now(),
]);
}
} catch (\Exception $e) {
logger()->error("Email validation job failed: {$e->getMessage()}");
}
}
}
Dispatch the job:
ValidateEmailJob::dispatch($email, $subscriber->id);
2. Rate Limiting
<?php
// app/Http/Controllers/NewsletterController.php
use Illuminate\Support\Facades\RateLimiter;
public function subscribe(Request $request)
{
$key = 'subscribe:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'error' => "Too many requests. Try again in {$seconds} seconds."
], 429);
}
RateLimiter::hit($key, 60); // 60 seconds
// Proceed with subscription...
}
3. Graceful Error Handling
<?php
// app/Exceptions/Handler.php
use App\Services\VerifyForgeService;
public function register(): void
{
$this->reportable(function (\Exception $e) {
if (str_contains($e->getMessage(), 'VerifyForge')) {
logger()->error('VerifyForge service error', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
});
}
Complete Example: Newsletter Subscription
<?php
// app/Http/Controllers/NewsletterController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Subscriber;
use App\Services\VerifyForgeService;
use Illuminate\Support\Facades\RateLimiter;
class NewsletterController extends Controller
{
public function __construct(
protected VerifyForgeService $verifyForge
) {}
public function subscribe(Request $request)
{
// Rate limiting
$key = 'newsletter:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 3)) {
return back()->withErrors([
'email' => 'Too many subscription attempts. Please try again later.'
]);
}
RateLimiter::hit($key, 60);
// Validate input
$request->validate([
'email' => 'required|email'
]);
try {
// Validate with VerifyForge
$result = $this->verifyForge->validate($request->email);
if (!$result['data']['isValid']) {
return back()->withErrors([
'email' => 'Please enter a valid email address'
])->withInput();
}
if ($result['data']['disposable']) {
return back()->withErrors([
'email' => 'Temporary email addresses are not allowed'
])->withInput();
}
// Create or update subscriber
$subscriber = Subscriber::updateOrCreate(
['email' => $request->email],
[
'is_valid' => $result['data']['isValid'],
'reachability' => $result['data']['reachability'],
'subscribed_at' => now(),
]
);
return redirect()->route('newsletter.success')
->with('success', 'Successfully subscribed to our newsletter!');
} catch (\Exception $e) {
logger()->error('Newsletter subscription error: ' . $e->getMessage());
return back()->withErrors([
'email' => 'Subscription failed. Please try again.'
])->withInput();
}
}
}
<?php
// app/Models/Subscriber.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Subscriber extends Model
{
protected $fillable = [
'email',
'is_valid',
'reachability',
'subscribed_at',
'validated_at',
];
protected $casts = [
'is_valid' => 'boolean',
'subscribed_at' => 'datetime',
'validated_at' => 'datetime',
];
}
Troubleshooting
Problem: API key not found
Solution: Ensure VERIFYFORGE_API_KEY is set in .env file
Problem: HTTP client timeout
Solution: Increase timeout in service:
Http::timeout(30)->withHeaders([...])->get(...)
Problem: Too many API calls
Solution: Implement caching as shown in validateCached() method
