Models & Database
This guide covers creating Eloquent models, database migrations, and seeders for your addon.
Eloquent Models
Models are stored in the Entities/ directory.
Basic Model
// Entities/ValidationResult.php
<?php
namespace Addons\EmailValidator\Entities;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ValidationResult extends Model
{
protected $table = 'email_validation_results';
protected $fillable = [
'user_id',
'email',
'status',
'score',
'mx_found',
'is_disposable',
'is_role_based',
'reason',
'raw_response',
];
protected $casts = [
'mx_found' => 'boolean',
'is_disposable' => 'boolean',
'is_role_based' => 'boolean',
'score' => 'integer',
'raw_response' => 'array',
];
// Relationships
public function user(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class);
}
}
Model with Scopes
// Entities/ValidationResult.php
<?php
namespace Addons\EmailValidator\Entities;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class ValidationResult extends Model
{
protected $table = 'email_validation_results';
protected $fillable = [
'user_id', 'email', 'status', 'score'
];
// Query Scopes
public function scopeForUser(Builder $query, int $userId): Builder
{
return $query->where('user_id', $userId);
}
public function scopeValid(Builder $query): Builder
{
return $query->where('status', 'valid');
}
public function scopeInvalid(Builder $query): Builder
{
return $query->where('status', 'invalid');
}
public function scopeRecent(Builder $query, int $days = 7): Builder
{
return $query->where('created_at', '>=', now()->subDays($days));
}
public function scopeHighScore(Builder $query, int $minScore = 80): Builder
{
return $query->where('score', '>=', $minScore);
}
}
Usage:
// Get valid results for current user from last 7 days
$results = ValidationResult::forUser(auth()->id())
->valid()
->recent()
->get();
// Get high-scoring invalid results
$suspicious = ValidationResult::invalid()
->highScore(70)
->get();
Model with Accessors and Mutators
// Entities/ValidationRule.php
<?php
namespace Addons\EmailValidator\Entities;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class ValidationRule extends Model
{
protected $table = 'email_validation_rules';
protected $fillable = [
'user_id', 'name', 'pattern', 'action', 'priority', 'is_active'
];
protected $casts = [
'is_active' => 'boolean',
'priority' => 'integer',
];
// Modern accessor (Laravel 9+)
protected function name(): Attribute
{
return Attribute::make(
get: fn (string $value) => ucfirst($value),
set: fn (string $value) => strtolower($value),
);
}
// Check if pattern is valid regex
public function getIsValidPatternAttribute(): bool
{
return @preg_match('/' . $this->pattern . '/', '') !== false;
}
// Get human-readable action
public function getActionLabelAttribute(): string
{
return match($this->action) {
'accept' => 'Accept Email',
'reject' => 'Reject Email',
'flag' => 'Flag for Review',
default => 'Unknown',
};
}
}
Model Events
// Entities/ValidationResult.php
<?php
namespace Addons\EmailValidator\Entities;
use Illuminate\Database\Eloquent\Model;
class ValidationResult extends Model
{
protected $table = 'email_validation_results';
protected static function boot()
{
parent::boot();
// Before creating
static::creating(function ($model) {
$model->uuid = \Illuminate\Support\Str::uuid();
});
// After creating
static::created(function ($model) {
// Update user's validation count
$model->user->increment('total_validations');
// Fire custom hook
run_hook_in_background('EmailValidated', [
'result' => $model,
'user' => $model->user
]);
});
// Before deleting
static::deleting(function ($model) {
// Cleanup related data
$model->user->decrement('total_validations');
});
}
}
Relationships
// Entities/ValidationBatch.php
<?php
namespace Addons\EmailValidator\Entities;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ValidationBatch extends Model
{
protected $table = 'email_validation_batches';
protected $fillable = [
'user_id', 'name', 'status', 'total_count', 'processed_count'
];
public function user(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class);
}
public function results(): HasMany
{
return $this->hasMany(ValidationResult::class, 'batch_id');
}
public function validResults(): HasMany
{
return $this->hasMany(ValidationResult::class, 'batch_id')
->where('status', 'valid');
}
public function invalidResults(): HasMany
{
return $this->hasMany(ValidationResult::class, 'batch_id')
->where('status', 'invalid');
}
// Calculated attribute
public function getProgressAttribute(): float
{
if ($this->total_count === 0) return 0;
return round(($this->processed_count / $this->total_count) * 100, 2);
}
public function getIsCompleteAttribute(): bool
{
return $this->processed_count >= $this->total_count;
}
}
Database Migrations
Migrations are stored in Database/Migrations/.
Creating Migrations
php artisan make:migration create_validation_results_table --path=Addons/EmailValidator/Database/Migrations
Basic Migration
// Database/Migrations/2024_01_01_000001_create_validation_results_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('email_validation_results', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('batch_id')->nullable()->constrained('email_validation_batches')->onDelete('set null');
$table->string('email', 320);
$table->enum('status', ['valid', 'invalid', 'unknown', 'pending'])->default('pending');
$table->unsignedTinyInteger('score')->default(0);
$table->boolean('mx_found')->default(false);
$table->boolean('is_disposable')->default(false);
$table->boolean('is_role_based')->default(false);
$table->string('reason')->nullable();
$table->json('raw_response')->nullable();
$table->timestamps();
// Indexes
$table->index('email');
$table->index('status');
$table->index(['user_id', 'status']);
$table->index(['user_id', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('email_validation_results');
}
};
Safe Migration Pattern
Check before creating to handle reinstallation:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Check if table exists before creating
if (!Schema::hasTable('email_validation_results')) {
Schema::create('email_validation_results', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('email');
$table->string('status');
$table->timestamps();
});
}
}
public function down(): void
{
Schema::dropIfExists('email_validation_results');
}
};
Adding Columns
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Safe column addition
if (Schema::hasTable('email_validation_results')) {
if (!Schema::hasColumn('email_validation_results', 'smtp_check')) {
Schema::table('email_validation_results', function (Blueprint $table) {
$table->boolean('smtp_check')->default(false)->after('mx_found');
});
}
if (!Schema::hasColumn('email_validation_results', 'catch_all')) {
Schema::table('email_validation_results', function (Blueprint $table) {
$table->boolean('catch_all')->default(false)->after('smtp_check');
});
}
}
}
public function down(): void
{
Schema::table('email_validation_results', function (Blueprint $table) {
$table->dropColumn(['smtp_check', 'catch_all']);
});
}
};
Modifying Users Table
Add columns to the core users table:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasColumn('users', 'email_validation_credits')) {
Schema::table('users', function (Blueprint $table) {
$table->unsignedInteger('email_validation_credits')->default(0)->after('email');
$table->unsignedInteger('total_validations')->default(0)->after('email_validation_credits');
});
}
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['email_validation_credits', 'total_validations']);
});
}
};
Running Migrations
Migrations run automatically during addon installation. To run manually:
# Run addon migrations
php artisan module:migrate EmailValidator
# Run with seed
php artisan module:migrate-refresh EmailValidator --seed
# Rollback
php artisan module:migrate-rollback EmailValidator
# Reset (rollback all)
php artisan module:migrate-reset EmailValidator
Database Seeders
Creating Seeders
// Database/Seeders/EmailValidatorDatabaseSeeder.php
<?php
namespace Addons\EmailValidator\Database\Seeders;
use Illuminate\Database\Seeder;
class EmailValidatorDatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
DefaultRulesSeeder::class,
DisposableDomainsSeeder::class,
]);
}
}
// Database/Seeders/DefaultRulesSeeder.php
<?php
namespace Addons\EmailValidator\Database\Seeders;
use Illuminate\Database\Seeder;
use Addons\EmailValidator\Entities\ValidationRule;
class DefaultRulesSeeder extends Seeder
{
public function run(): void
{
$rules = [
[
'name' => 'Block Disposable',
'pattern' => '@(mailinator|guerrillamail|10minutemail)\.',
'action' => 'reject',
'priority' => 1,
'is_active' => true,
'is_system' => true,
],
[
'name' => 'Flag Role Accounts',
'pattern' => '^(admin|info|support|sales|contact)@',
'action' => 'flag',
'priority' => 2,
'is_active' => true,
'is_system' => true,
],
];
foreach ($rules as $rule) {
ValidationRule::firstOrCreate(
['name' => $rule['name'], 'is_system' => true],
$rule
);
}
}
}
Running Seeders
php artisan module:seed EmailValidator
SQL Installation Files
For complex setup, use SQL files in Settings/install/:
-- Settings/install/v1.0.sql
-- Initial installation SQL
CREATE TABLE IF NOT EXISTS `email_validation_disposable_domains` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`domain` VARCHAR(255) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `domain_unique` (`domain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert common disposable domains
INSERT IGNORE INTO `email_validation_disposable_domains` (`domain`) VALUES
('mailinator.com'),
('guerrillamail.com'),
('10minutemail.com'),
('tempmail.com'),
('throwaway.email');
-- Settings/install/v1.1.sql
-- Version 1.1 updates
ALTER TABLE `email_validation_results`
ADD COLUMN IF NOT EXISTS `provider` VARCHAR(50) NULL AFTER `reason`;
UPDATE `email_validation_results`
SET `provider` = 'internal'
WHERE `provider` IS NULL;
SQL files are executed in version order during installation/update.
Query Builder
Complex Queries
use Addons\EmailValidator\Entities\ValidationResult;
use Illuminate\Support\Facades\DB;
// Aggregate statistics
$stats = ValidationResult::forUser(auth()->id())
->selectRaw('
COUNT(*) as total,
SUM(CASE WHEN status = "valid" THEN 1 ELSE 0 END) as valid_count,
SUM(CASE WHEN status = "invalid" THEN 1 ELSE 0 END) as invalid_count,
AVG(score) as avg_score
')
->first();
// Daily counts
$dailyCounts = ValidationResult::forUser(auth()->id())
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
->where('created_at', '>=', now()->subDays(30))
->groupBy('date')
->orderBy('date')
->get();
// Top domains
$topDomains = ValidationResult::forUser(auth()->id())
->selectRaw('SUBSTRING_INDEX(email, "@", -1) as domain, COUNT(*) as count')
->groupBy('domain')
->orderByDesc('count')
->limit(10)
->get();
Chunking Large Datasets
// Process large datasets efficiently
ValidationResult::forUser(auth()->id())
->where('status', 'pending')
->chunk(100, function ($results) {
foreach ($results as $result) {
// Process each result
$this->processValidation($result);
}
});
// Lazy loading for memory efficiency
ValidationResult::forUser(auth()->id())
->where('created_at', '>=', now()->subMonth())
->lazy()
->each(function ($result) {
// Process without loading all into memory
});
Transactions
use Illuminate\Support\Facades\DB;
public function processBatch(array $emails): ValidationBatch
{
return DB::transaction(function () use ($emails) {
// Create batch
$batch = ValidationBatch::create([
'user_id' => auth()->id(),
'name' => 'Batch ' . now()->format('Y-m-d H:i'),
'status' => 'pending',
'total_count' => count($emails),
'processed_count' => 0,
]);
// Create pending results
foreach ($emails as $email) {
ValidationResult::create([
'user_id' => auth()->id(),
'batch_id' => $batch->id,
'email' => $email,
'status' => 'pending',
]);
}
// Deduct credits
auth()->user()->decrement('email_validation_credits', count($emails));
return $batch;
});
}
Best Practices
Model Organization
- One model per database table
- Keep models in
Entities/directory - Use meaningful table names with addon prefix
- Define all relationships
Migration Tips
- Always check if table/column exists before creating
- Use meaningful migration names with dates
- Keep migrations small and focused
- Test rollback functionality
Performance
- Add indexes for frequently queried columns
- Use eager loading to prevent N+1 queries
- Chunk large datasets
- Use database transactions for atomic operations