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' => 'test@example.com',
]);
$response->assertStatus(200);
$response->assertJson(['status' => 'valid']);
}
public function test_requires_authentication()
{
$response = $this->post('/email-validator/validate', [
'email' => 'test@example.com',
]);
$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