mr-punyapal/laravel-extended-relationships
Adds efficient, custom Eloquent relationships for Laravel via a HasExtendedRelationships trait, reducing query count and duplicate code. Includes helpers like belongsToManyKeys and more, aimed at improving performance in real-world apps.
composer require mrpunyapal/laravel-extended-relationships
use MrPunyapal\LaravelExtendedRelationships\HasExtendedRelationships;
class Post extends Model {
use HasExtendedRelationships;
}
Define a single belongsToManyKeys relationship to replace multiple belongsTo calls for audit fields:
public function auditors() {
return $this->belongsToManyKeys(
related: User::class,
foreignKey: 'id',
relations: [
'created_by' => 'creator',
'updated_by' => 'updater',
'deleted_by' => 'deleter',
]
);
}
Usage:
$post = Post::with('auditors')->find(1);
$post->auditors->creator; // Single query for all audit fields
Pattern: Replace 3+ belongsTo relationships with one belongsToManyKeys call.
Workflow:
Post).$model->relationship->alias (e.g., $post->auditors->creator).Example:
// Before (3 queries)
$post->createdBy->name;
$post->updatedBy->name;
$post->deletedBy->name;
// After (1 query)
$post->auditors->creator->name;
$post->auditors->updater->name;
Pattern: Handle JSON/array columns (e.g., users.companies = [1, 2, 3]).
Workflow:
hasManyArrayColumn in the parent model (e.g., User).belongsToArrayColumn in the related model (e.g., Company).Example:
// User model
public function companies() {
return $this->hasManyArrayColumn(
related: Company::class,
foreignKey: 'id',
localKey: 'company_ids'
);
}
// Company model (inverse)
public function users() {
return $this->belongsToArrayColumn(
related: User::class,
foreignKey: 'id',
localKey: 'company_ids',
isString: true // If IDs are stored as strings
);
}
Pattern: Define bidirectional relationships with minimal code. Workflow:
hasManyKeys in the parent model (e.g., User for audited posts).created_by → created).Example:
// User model
public function audited() {
return $this->hasManyKeys(
related: Post::class,
relations: [
'created_by' => 'created',
'updated_by' => 'updated',
]
);
}
// Usage
$user->audited->created; // Posts created by the user
Pattern: Access relationships without eager loading. Workflow:
$post->auditors->creator).Example:
$post = Post::find(1); // No eager loading
$post->auditors->creator; // Single query on access
Pattern: Use AI-assisted development for relationship queries. Workflow:
composer require laravel/boost --dev).php artisan boost:update --discover to enable the skill.Example:
# Ask Boost for help with a relationship query
php artisan boost:ask "How do I fetch all posts created by a user?"
Foreign Key Mismatches:
foreignKey doesn’t match the related model’s primary key, queries fail silently.foreignKey (e.g., foreignKey: 'user_id').Array Column Data Types:
hasManyArrayColumn assumes localKey values match the related model’s primary key type.isString: true if IDs are stored as strings (e.g., ["7", "8"]).Lazy Loading Overhead:
with()) for critical paths.Boost Skill Not Triggering:
php artisan boost:update --discover
Query Logging: Enable Laravel’s query log to verify single-query behavior:
DB::enableQueryLog();
$post = Post::with('auditors')->find(1);
dd(DB::getQueryLog()); // Check for 1 query instead of 3+
Relationship Sorting: The package sorts relationships alphabetically for consistent lazy-loading behavior.
with()) for predictable ordering.Type Safety:
php.ini (strict_types=1) for full benefits.Custom Relationship Logic: Extend the trait to add domain-specific relationships:
// app/Models/Concerns/ExtendedRelationships.php
public function customRelationship() {
return $this->belongsToManyKeys(
related: CustomModel::class,
relations: ['field1' => 'alias1', 'field2' => 'alias2']
);
}
Global Configuration: Override default behavior via a config file (published by the package):
php artisan vendor:publish --provider="MrPunyapal\LaravelExtendedRelationships\ServiceProvider"
Testing: Use Pest or PHPUnit to test relationships:
public function test_audit_relationships() {
$user = User::factory()->create();
$post = Post::factory()->create(['created_by' => $user->id]);
$this->assertInstanceOf(User::class, $post->auditors->creator);
}
Single Query vs. Multiple:
belongsToManyKeys and hasManyKeys always use a single query, even with lazy loading.hasManyArrayColumn) may require additional joins if the array is large.Memory Usage:
users.companies = [1, 2, ..., 1000]) can increase memory usage.take() or paginate:
return $this->hasManyArrayColumn(..., take: 100);
Indexing:
belongsToArrayColumn, ensure the localKey column is indexed in the related table:
ALTER TABLE users ADD INDEX idx_company_ids ON company_ids USING GIN;
How can I help you explore Laravel packages today?