Speed Meets Precision
Today we’re releasing Gateway, a free WordPress plugin that brings Laravel’s renowned Eloquent ORM to WordPress development. If you’ve ever felt constrained by WordPress’s data architecture or envied the elegance of modern frameworks, this is for you.
The Problem: Custom Tables Are Second-Class Citizens
WordPress was built on a brilliant premise: everything is a post. Custom Post Types (CPTs) made this paradigm extensible, and for many use cases, they work beautifully. But there’s always been an uncomfortable truth that experienced WordPress developers know well:
The moment your data doesn’t fit the post/postmeta paradigm, you’re on your own.
Need to build a membership system? An inventory manager? A booking platform? A CRM? You’ll quickly discover that shoehorning complex relational data into wp_posts and wp_postmeta leads to performance nightmares and architectural compromises.
The alternative—custom database tables—has always been the “right” solution technically. But it comes with a steep cost:
- Writing raw SQL queries (or using
$wpdbwith string-based queries) - No built-in relationship handling
- Manual joins for related data
- No elegant way to handle eager loading
- Inconsistent query patterns across your codebase
- Higher barrier to entry for team members
Custom tables became the “expert only” path, while CPTs remained the default even when they weren’t the best choice.
ARC Forge changes this equation entirely.
Custom Tables, Meet Developer Ergonomics
ARC Forge integrates Laravel’s Eloquent ORM into WordPress, bringing one of the most beloved database abstraction layers in PHP to the WordPress ecosystem. What does this mean in practice?
Custom database tables are now as easy to work with as Custom Post Types—arguably easier.
Before Gateway: The WordPress Way
Let’s say you’re building a task management plugin. Here’s how you might query for high-priority tasks assigned to a specific user:
global $wpdb;
$user_id = 42;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT t.*, a.user_id, a.assigned_at, u.display_name
FROM {$wpdb->prefix}tasks t
INNER JOIN {$wpdb->prefix}task_assignments a ON t.id = a.task_id
INNER JOIN {$wpdb->users} u ON a.user_id = u.ID
WHERE a.user_id = %d
AND t.priority = %s
AND t.status != %s
ORDER BY t.due_date ASC",
$user_id,
'high',
'completed'
)
);
foreach ($results as $task) {
// Manually handle the relationship data
$task->assignee_name = $task->display_name;
// Get comments separately
$comments = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}task_comments
WHERE task_id = %d
ORDER BY created_at DESC",
$task->id
)
);
$task->comments = $comments;
}
This works, but it’s verbose, error-prone, and requires you to manually handle relationships. Want to get the assignee’s profile picture? Another query. Need to filter by team? More SQL string concatenation.
After Gateway: The Eloquent Way
use \Collections\Task;
$tasks = Task::with('assignee', 'comments')
->where('priority', 'high')
->where('status', '!=', 'completed')
->whereHas('assignments', fn($q) => $q->where('user_id', 42))
->orderBy('due_date')
->get();
foreach ($tasks as $task) {
echo $task->assignee->display_name;
echo $task->comments->count() . ' comments';
}
Same functionality. One-fifth the code. Infinitely more readable.
Why This Matters: Relationships Made Simple
The real power of Eloquent isn’t just prettier syntax—it’s the relationship system. Once you define how your models relate to each other, querying becomes intuitive:
// Define the relationship once in your Task model
class Task extends \Gateway\Collection {
public function assignee() {
return $this->belongsTo(User::class, 'user_id');
}
public function comments() {
return $this->hasMany(TaskComment::class);
}
public function tags() {
return $this->belongsToMany(Tag::class, 'task_tags');
}
}
Now every Task instance automatically knows how to access its related data:
$task = Task::find(1);
// These relationships "just work"
$assignee = $task->assignee;
$comments = $task->comments;
$tags = $task->tags;
// Eager load to avoid N+1 queries
$tasks = Task::with('assignee', 'comments', 'tags')->get();
// Query through relationships
$overdueTasks = Task::whereHas('assignee', function($query) {
$query->where('department', 'engineering');
})->where('due_date', '<', now())->get();
Try doing this with wp_posts and wp_postmeta. You can’t. The post/postmeta architecture doesn’t support true relational data modeling.
Custom Tables Are Now First-Class Citizens
Here’s what ARC Forge enables that was previously painful or impossible:
1. True Relational Data Modeling
Stop fighting with the wp_posts table structure. Model your data as it actually exists in your domain:
// A membership platform with proper relationships
$member = Member::with('subscription.plan', 'invoices', 'activities')->find(1);
echo $member->subscription->plan->name;
echo $member->invoices->sum('amount');
echo $member->activities->where('type', 'login')->count();
2. Performant Queries Out of the Box
Eloquent handles eager loading, preventing N+1 query problems that plague WordPress plugins:
// Bad: 101 queries (1 + 100 for each post's meta)
$posts = get_posts(['posts_per_page' => 100]);
foreach ($posts as $post) {
$author_name = get_post_meta($post->ID, 'author_name', true);
}
// Good: 2 queries total (with ARC Forge)
$articles = Article::with('author')->take(100)->get();
foreach ($articles as $article) {
echo $article->author->name;
}
3. Consistent Query Patterns
Whether you’re querying WordPress users, your custom tables, or a mix of both, the syntax is consistent:
// Query custom table
$tasks = Task::where('status', 'open')->get();
// Query WordPress users (yes, Eloquent works with WP core tables too)
$editors = User::where('role', 'editor')->get();
// Join them together
$tasks = Task::with('assignee')
->whereHas('assignee', fn($q) => $q->where('role', 'editor'))
->get();
4. IDE Autocomplete and Type Safety
Because models are PHP classes with defined properties and relationships, your IDE can autocomplete everything:
$task->| // IDE shows: assignee, comments, tags, due_date, priority, etc.
Compare this to Custom Post Types where field names are strings in arrays with no IDE support:
get_post_meta($post_id, 'field_name_you_hope_you_spelled_right', true);Real-World Use Cases
Gateway shines when building:
SaaS Platforms & Membership Sites
- Complex user relationships (teams, organizations, roles)
- Subscription management with proper relational integrity
- Activity logging and audit trails
E-commerce Beyond WooCommerce
- Custom inventory systems with suppliers, warehouses, variants
- Multi-vendor marketplaces with complex commission structures
- Booking systems with availability and scheduling logic
Data-Heavy Applications
- Analytics dashboards pulling from custom tables
- Reporting systems with complex aggregations
- API integrations storing external data