Skip to main content

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