Custom Validation Examples
Advanced validation patterns for installer steps.
#Database connection validation
Test database credentials before saving.
namespace App\Installer\Steps; use Olakunlevpn\Installer\Steps\BaseStep;use Olakunlevpn\Installer\Concerns\HasFormData;use Illuminate\Support\Facades\DB;use Illuminate\Support\Facades\Config; class DatabaseStep extends BaseStep{ use HasFormData; protected function validateStep(): void { // First run basic validation $this->validate(); // Then test the connection if (!$this->testDatabaseConnection()) { $this->errorMessage = __('database::database.connection_failed'); return; } $this->successMessage = __('database::database.connection_success'); } protected function testDatabaseConnection(): bool { try { // Temporarily set database config Config::set('database.connections.test', [ 'driver' => $this->formData['driver'], 'host' => $this->formData['host'], 'port' => $this->formData['port'], 'database' => $this->formData['database'], 'username' => $this->formData['username'], 'password' => $this->formData['password'], 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', ]); // Test connection DB::connection('test')->getPdo(); // Try to select from database DB::connection('test')->select('SELECT 1'); return true; } catch (\Exception $e) { Log::error('Database connection test failed', [ 'error' => $e->getMessage(), 'host' => $this->formData['host'], ]); return false; } }}
#API key validation
Verify API key with remote server.
protected function validateStep(): void{ $this->validate(); // Validate API key format if (!$this->isValidKeyFormat($this->formData['api_key'])) { $this->addError('formData.api_key', __('api::api.invalid_format')); return; } // Verify with remote server if (!$this->verifyApiKey($this->formData['api_key'])) { $this->addError('formData.api_key', __('api::api.verification_failed')); return; } $this->successMessage = __('api::api.verified');} protected function isValidKeyFormat(string $key): bool{ // Example: key must be 40 characters, alphanumeric return preg_match('/^[a-zA-Z0-9]{40}$/', $key);} protected function verifyApiKey(string $key): bool{ try { $response = Http::timeout(10) ->withHeaders(['X-API-Key' => $key]) ->get('https://api.example.com/validate'); return $response->successful() && $response->json('valid'); } catch (\Exception $e) { return false; }}
#Email validation with DNS check
Verify email exists and domain has MX records.
protected function validateStep(): void{ $this->validate(); $email = $this->formData['email']; // Check DNS for MX records if (!$this->hasValidMxRecords($email)) { $this->addError('formData.email', __('account::account.invalid_email_domain')); return; } // Optional: Send verification code if (config('installer.verify_email', false)) { $this->sendVerificationCode($email); $this->successMessage = __('account::account.verification_sent'); }} protected function hasValidMxRecords(string $email): bool{ $domain = substr(strrchr($email, '@'), 1); return checkdnsrr($domain, 'MX');} protected function sendVerificationCode(string $email): void{ $code = rand(100000, 999999); $this->storeInSession('verification_code', $code); $this->storeInSession('verification_email', $email); Mail::to($email)->send(new VerificationCode($code));}
#URL reachability check
Verify URL is accessible.
protected function validateStep(): void{ $this->validate(); $url = $this->formData['webhook_url']; // Validate URL format if (!filter_var($url, FILTER_VALIDATE_URL)) { $this->addError('formData.webhook_url', __('webhook::webhook.invalid_url')); return; } // Check if URL is reachable if (!$this->isUrlReachable($url)) { $this->addError('formData.webhook_url', __('webhook::webhook.unreachable')); return; } $this->successMessage = __('webhook::webhook.verified');} protected function isUrlReachable(string $url): bool{ try { $response = Http::timeout(5)->head($url); return $response->successful() || $response->status() === 405; } catch (\Exception $e) { return false; }}
#File upload validation
Validate uploaded files.
namespace App\Installer\Steps; use Olakunlevpn\Installer\Steps\BaseStep;use Illuminate\Support\Facades\Storage; class LogoUploadStep extends BaseStep{ public $logo; protected function rules(): array { return [ 'logo' => 'required|image|mimes:png,jpg,svg|max:2048', ]; } protected function validateStep(): void { $this->validate(); // Additional validation if (!$this->isValidImageDimensions()) { $this->addError('logo', __('logo::logo.invalid_dimensions')); return; } } protected function isValidImageDimensions(): bool { $path = $this->logo->getRealPath(); $imageSize = getimagesize($path); if (!$imageSize) { return false; } [$width, $height] = $imageSize; // Logo must be at least 200x200 return $width >= 200 && $height >= 200; } protected function execute(): void { $path = $this->logo->store('logos', 'public'); $this->storeInSession('logo_path', $path); Setting::updateOrCreate( ['key' => 'app_logo'], ['value' => $path] ); }}
#Multi-field validation
Validate fields together.
protected function validateStep(): void{ $this->validate(); // Password confirmation if ($this->formData['password'] !== $this->formData['password_confirmation']) { $this->addError('formData.password_confirmation', __('account::account.passwords_must_match')); return; } // Port range based on protocol $protocol = $this->formData['mail_protocol']; $port = $this->formData['mail_port']; if ($protocol === 'smtp' && !in_array($port, [25, 465, 587])) { $this->addError('formData.mail_port', __('mail::mail.invalid_smtp_port')); return; } // Validate SMTP credentials if enabled if ($this->formData['mail_enabled'] && !$this->testSmtpConnection()) { $this->errorMessage = __('mail::mail.connection_failed'); return; } $this->successMessage = __('mail::mail.validated');}
#Async validation with progress
Show validation progress for slow checks.
namespace App\Installer\Steps; use Olakunlevpn\Installer\Steps\BaseStep;use Livewire\Attributes\On; class SystemCheckStep extends BaseStep{ public array $checks = []; public bool $validating = false; public int $progress = 0; protected function mount(): void { $this->checks = [ 'php_version' => ['status' => 'pending', 'message' => ''], 'extensions' => ['status' => 'pending', 'message' => ''], 'permissions' => ['status' => 'pending', 'message' => ''], 'database' => ['status' => 'pending', 'message' => ''], ]; } public function runChecks(): void { $this->validating = true; $this->progress = 0; // Check PHP version $this->checkPhpVersion(); $this->progress = 25; // Check extensions $this->checkExtensions(); $this->progress = 50; // Check permissions $this->checkPermissions(); $this->progress = 75; // Check database $this->checkDatabase(); $this->progress = 100; $this->validating = false; } protected function checkPhpVersion(): void { $required = '8.2.0'; if (version_compare(PHP_VERSION, $required, '>=')) { $this->checks['php_version'] = [ 'status' => 'passed', 'message' => "PHP " . PHP_VERSION, ]; } else { $this->checks['php_version'] = [ 'status' => 'failed', 'message' => "PHP {$required}+ required", ]; } } protected function checkExtensions(): void { $required = ['pdo', 'mbstring', 'openssl', 'tokenizer', 'xml']; $missing = array_filter($required, fn($ext) => !extension_loaded($ext)); if (empty($missing)) { $this->checks['extensions'] = [ 'status' => 'passed', 'message' => 'All extensions loaded', ]; } else { $this->checks['extensions'] = [ 'status' => 'failed', 'message' => 'Missing: ' . implode(', ', $missing), ]; } } protected function validateStep(): void { $failed = array_filter($this->checks, fn($check) => $check['status'] === 'failed'); if (!empty($failed)) { $this->errorMessage = __('system::system.checks_failed'); return; } }}
View with progress:
<div> <x-installer::card title="System Requirements"> @if($validating) <div class="mb-4"> <div class="w-full bg-gray-700 rounded-full h-2"> <div class="bg-primary h-2 rounded-full transition-all" style="width: {{ $progress }}%"></div> </div> <p class="text-sm text-gray-400 mt-2">Checking system... {{ $progress }}%</p> </div> @endif <div class="space-y-3"> @foreach($checks as $name => $check) <div class="flex items-center gap-3"> @if($check['status'] === 'passed') <x-installer::icon name="check" class="text-green-500" /> @elseif($check['status'] === 'failed') <x-installer::icon name="x" class="text-red-500" /> @else <x-installer::icon name="loading" class="text-gray-500 animate-spin" /> @endif <div> <p class="font-medium">{{ ucfirst(str_replace('_', ' ', $name)) }}</p> <p class="text-sm text-gray-400">{{ $check['message'] }}</p> </div> </div> @endforeach </div> <div class="mt-6"> <x-installer::button type="button" wire:click="runChecks" :disabled="$validating" > Run Checks </x-installer::button> </div> </x-installer::card></div>
#Conditional validation
Validate fields based on other fields.
protected function rules(): array{ $rules = [ 'formData.mail_driver' => 'required|in:smtp,sendmail,mailgun', ]; // SMTP-specific fields if ($this->formData['mail_driver'] === 'smtp') { $rules['formData.smtp_host'] = 'required|string'; $rules['formData.smtp_port'] = 'required|integer|min:1|max:65535'; $rules['formData.smtp_username'] = 'required|string'; $rules['formData.smtp_password'] = 'required|string'; } // Mailgun-specific fields if ($this->formData['mail_driver'] === 'mailgun') { $rules['formData.mailgun_domain'] = 'required|string'; $rules['formData.mailgun_secret'] = 'required|string'; } return $rules;}
#Summary
Key validation patterns:
- Test external connections
- Verify API responses
- Check file formats and dimensions
- Validate related fields together
- Show progress for slow checks
- Use conditional validation
- Provide clear error messages
Always validate thoroughly to prevent installation issues.