Skip to main content

Best Practices

This guide covers recommended patterns, conventions, and practices for building maintainable, performant, and user-friendly addons.

Code Organization

Follow Laravel Conventions

Stick to Laravel's established patterns:

✓ Controllers handle HTTP requests
✓ Models represent database entities
✓ Services contain business logic
✓ Jobs handle async processing
✓ Events/Listeners for decoupled communication

Keep Controllers Thin

Move business logic to dedicated services:

// ❌ Bad - Logic in controller
class ValidatorController extends Controller
{
public function validate(Request $request)
{
$email = $request->email;

// 50 lines of validation logic...

return view('results', compact('result'));
}
}

// ✓ Good - Logic in service
class ValidatorController extends Controller
{
public function __construct(
private ValidatorService $validator
) {}

public function validate(Request $request)
{
$result = $this->validator->validate($request->email);

return view('emailvalidator::results', compact('result'));
}
}

Use Action Classes for Complex Operations

// Actions/ValidateBatch.php
class ValidateBatch
{
public function __construct(
private ValidatorService $validator,
private NotificationService $notifications
) {}

public function execute(array $emails, User $user): ValidationBatch
{
$batch = ValidationBatch::create([
'user_id' => $user->id,
'total_count' => count($emails),
]);

foreach ($emails as $email) {
dispatch(new ValidateEmailJob($email, $batch));
}

$this->notifications->send($user, 'Batch validation started');

return $batch;
}
}

// In controller
public function validateBatch(Request $request, ValidateBatch $action)
{
$batch = $action->execute($request->emails, auth()->user());

return redirect()->route('emailvalidator.batch', $batch);
}

Database Best Practices

Use Migrations Correctly

// ✓ Always check before creating
public function up(): void
{
if (!Schema::hasTable('my_table')) {
Schema::create('my_table', function (Blueprint $table) {
// ...
});
}
}

// ✓ Check columns before adding
if (!Schema::hasColumn('my_table', 'new_column')) {
Schema::table('my_table', function (Blueprint $table) {
$table->string('new_column')->nullable();
});
}

Use Appropriate Indexes

Schema::create('email_validator_results', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('email', 320);
$table->string('status', 20);
$table->timestamps();

// ✓ Index frequently queried columns
$table->index('email');
$table->index('status');

// ✓ Composite index for common query patterns
$table->index(['user_id', 'status']);
$table->index(['user_id', 'created_at']);
});

Use Database Transactions

public function processBatch(array $emails): ValidationBatch
{
return DB::transaction(function () use ($emails) {
$batch = ValidationBatch::create([...]);

foreach ($emails as $email) {
ValidationResult::create([
'batch_id' => $batch->id,
'email' => $email,
]);
}

auth()->user()->decrement('credits', count($emails));

return $batch;
});
}

Performance

Use Eager Loading

// ❌ Bad - N+1 queries
$batches = ValidationBatch::all();
foreach ($batches as $batch) {
echo $batch->user->name; // Query for each batch
}

// ✓ Good - Eager loading
$batches = ValidationBatch::with('user')->get();
foreach ($batches as $batch) {
echo $batch->user->name; // No additional queries
}

Chunk Large Datasets

// ❌ Bad - Loads all into memory
$results = ValidationResult::all();
foreach ($results as $result) {
$this->process($result);
}

// ✓ Good - Processes in chunks
ValidationResult::chunk(100, function ($results) {
foreach ($results as $result) {
$this->process($result);
}
});

// ✓ Even better - Lazy loading
ValidationResult::lazy()->each(function ($result) {
$this->process($result);
});

Cache Expensive Operations

public function getDisposableDomains(): array
{
return Cache::remember('emailvalidator.disposable_domains', 3600, function () {
return DisposableDomain::pluck('domain')->toArray();
});
}

public function getStats(int $userId): array
{
return Cache::tags(['emailvalidator', "user.{$userId}"])
->remember("stats.{$userId}", 300, function () use ($userId) {
return $this->calculateStats($userId);
});
}

// Invalidate when needed
Cache::tags(['emailvalidator', "user.{$userId}"])->flush();

Use Queue Jobs for Heavy Operations

// ❌ Bad - Blocking request
public function validateBatch(Request $request)
{
foreach ($request->emails as $email) {
$this->validator->validate($email); // Takes 2-3 seconds each
}

return redirect()->back();
}

// ✓ Good - Queued processing
public function validateBatch(Request $request)
{
$batch = ValidationBatch::create([...]);

foreach ($request->emails as $email) {
dispatch(new ValidateEmailJob($email, $batch));
}

return redirect()->route('emailvalidator.batch', $batch);
}

Security

Validate All Input

// Use form requests
class ValidateEmailRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => 'required|email|max:320',
'options' => 'array',
'options.check_mx' => 'boolean',
];
}
}

// Sanitize output
public function show(ValidationResult $result)
{
// Blade automatically escapes
return view('emailvalidator::show', compact('result'));

// For raw HTML, escape manually
$safeEmail = htmlspecialchars($result->email, ENT_QUOTES, 'UTF-8');
}

Use Authorization

// Create policy
class ValidationResultPolicy
{
public function view(User $user, ValidationResult $result): bool
{
return $user->id === $result->user_id || $user->isAdmin();
}

public function delete(User $user, ValidationResult $result): bool
{
return $user->id === $result->user_id;
}
}

// In controller
public function show(ValidationResult $result)
{
$this->authorize('view', $result);

return view('emailvalidator::show', compact('result'));
}

Protect Sensitive Data

// ❌ Bad - Exposing API key
return response()->json([
'api_key' => config('emailvalidator.api.key'),
]);

// ✓ Good - Only expose necessary data
return response()->json([
'credits_remaining' => $user->email_validation_credits,
]);

// Use encryption for sensitive settings
public function saveApiKey(string $key): void
{
Setting::set('api_key', encrypt($key));
}

public function getApiKey(): string
{
return decrypt(Setting::get('api_key'));
}

Error Handling

Graceful Degradation

public function validate(string $email): array
{
try {
return $this->apiClient->validate($email);
} catch (ConnectionException $e) {
Log::warning('Validation API unavailable', ['email' => $email]);

// Return partial validation instead of failing
return [
'email' => $email,
'status' => 'unknown',
'message' => 'API temporarily unavailable',
];
}
}

Meaningful Error Messages

// ❌ Bad - Generic error
throw new \Exception('Error');

// ✓ Good - Specific error
throw new ValidationException('Invalid email format: missing @ symbol');
throw new QuotaExceededException('Daily validation limit reached. Upgrade your plan for more.');
throw new ApiException('Validation API returned error: ' . $response['error']);

Log Appropriately

// Use appropriate log levels
Log::debug('Processing email', ['email' => $email]); // Development
Log::info('Batch completed', ['count' => $count]); // Normal operation
Log::warning('API rate limited', ['retry_after' => 60]); // Attention needed
Log::error('Validation failed', ['error' => $e->getMessage()]); // Errors
Log::critical('Database connection lost'); // Critical failures

// Include context
Log::error('Email validation failed', [
'email' => $email,
'user_id' => auth()->id(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

User Experience

Provide Feedback

// Use flash messages
return redirect()
->route('emailvalidator.index')
->with('success', 'Validation completed! 95 valid, 5 invalid emails.');

// For AJAX, return structured responses
return response()->json([
'success' => true,
'message' => 'Email validated successfully',
'data' => $result,
]);

Show Progress

{{-- For long operations, show progress --}}
<div id="progress-container">
<div class="progress">
<div class="progress-bar" id="validation-progress" style="width: 0%"></div>
</div>
<p id="progress-text">Validating... <span id="current">0</span>/<span id="total">0</span></p>
</div>

<script>
// Poll for progress
setInterval(function() {
fetch('/email-validator/batch/{{ $batch->id }}/progress')
.then(r => r.json())
.then(data => {
document.getElementById('validation-progress').style.width = data.percent + '%';
document.getElementById('current').textContent = data.processed;
});
}, 2000);
</script>

Handle Edge Cases

// Empty states
@if($results->isEmpty())
<div class="empty-state">
<i class="flaticon-email fa-3x"></i>
<h4>No validation results yet</h4>
<p>Start by validating some email addresses.</p>
<a href="{{ route('emailvalidator.validate.form') }}" class="btn btn-primary">
Validate Emails
</a>
</div>
@else
{{-- Show results table --}}
@endif

Hooks Best Practices

Keep Hooks Lightweight

// ❌ Bad - Heavy processing in hook
add_hook('AddContact', 5, function ($vars) {
$result = makeExternalApiCall($vars['contact']->email); // Slow
updateDatabase($result); // More work
sendNotification($vars['user']); // Even more
});

// ✓ Good - Queue heavy work
add_hook('AddContact', 5, function ($vars) {
dispatch(new ValidateNewContactJob($vars['contact']));
});

Use Appropriate Priorities

// Priority 1-3: Critical, must run first
add_hook('AddContact', 1, function ($vars) {
// Validate email format
});

// Priority 5-10: Standard processing
add_hook('AddContact', 5, function ($vars) {
// Check against blocklist
});

// Priority 50+: Logging, notifications
add_hook('AddContact', 100, function ($vars) {
Log::info('Contact added', ['id' => $vars['contact']->id]);
});

Always Return Empty String for Output Hooks

add_hook('AlertBar', 5, function ($vars) {
$user = auth()->user();

if (!$user || $user->credits > 100) {
return ''; // ✓ Always return string
}

return '<div class="alert alert-warning">Low credits!</div>';
});

Testing

Write Tests

// Tests/Feature/ValidationTest.php
class ValidationTest extends TestCase
{
public function test_can_validate_email()
{
$user = User::factory()->create();

$response = $this->actingAs($user)
->post('/email-validator/validate', [
'email' => '[email protected]',
]);

$response->assertStatus(200);
$response->assertJson(['status' => 'valid']);
}

public function test_requires_authentication()
{
$response = $this->post('/email-validator/validate', [
'email' => '[email protected]',
]);

$response->assertRedirect('/login');
}

public function test_validates_email_format()
{
$user = User::factory()->create();

$response = $this->actingAs($user)
->post('/email-validator/validate', [
'email' => 'not-an-email',
]);

$response->assertSessionHasErrors('email');
}
}

Documentation

Document Your Addon

Create a README.md in your addon root:

# Email Validator Addon

Validate email addresses in real-time before sending campaigns.

## Features

- Syntax validation
- MX record checking
- Disposable email detection
- Role-based email detection
- Batch validation

## Installation

1. Upload to `/Addons/EmailValidator`
2. Navigate to Setup > Addons
3. Click Install

## Configuration

Set your API key in Settings > Email Validator.

## Usage

Navigate to Email Validator in the sidebar to validate emails.

## Changelog

### v1.1.0
- Added batch validation
- Improved disposable email detection

### v1.0.0
- Initial release

Comment Your Code

/**
* Validate an email address.
*
* Performs syntax check, MX lookup, and disposable domain check.
*
* @param string $email The email address to validate
* @param array $options Validation options:
* - check_mx: bool - Check MX records (default: true)
* - check_disposable: bool - Check disposable domains (default: true)
*
* @return array Validation result with keys: valid, score, reason
*
* @throws ValidationException If email format is invalid
* @throws ApiException If external API call fails
*/
public function validate(string $email, array $options = []): array
{
// ...
}

Summary Checklist

Before releasing your addon:

  • Code follows Laravel conventions
  • All input is validated
  • Authorization checks in place
  • Database queries optimized
  • Heavy operations queued
  • Errors handled gracefully
  • Meaningful error messages
  • Tests written
  • README documentation
  • Migrations check table/column existence
  • Hooks are lightweight
  • Cache used where appropriate
  • No hardcoded credentials
  • User-friendly UI with feedback