awobaz/compoships
Compoships enables Laravel Eloquent relationships on composite keys—match two or more columns for hasOne/hasMany/belongsTo, including eager loading. Ideal for legacy or third‑party schemas where single-column foreign keys aren’t possible.
Installation:
composer require awobaz/compoships
Add the package to your composer.json and run composer update.
Model Integration: Choose one of these approaches for your models:
use Awobaz\Compoships\Database\Eloquent\Model;
class User extends Model { ... }
use Awobaz\Compoships\Compoships;
class User { use Compoships; ... }
Define a Composite Relationship:
public function tasks()
{
return $this->hasMany(Task::class, ['team_id', 'category_id'], ['team_id', 'category_id']);
}
Test with Eager Loading:
$users = User::with('tasks')->get();
Verify the relationship works as expected.
Scenario: A User has many Tasks, but tasks are scoped to both a team_id and category_id (e.g., a user manages tasks for a specific team/category combo).
Solution:
// User model
public function tasks()
{
return $this->hasMany(Task::class, ['team_id', 'category_id'], ['team_id', 'category_id']);
}
// Task model (inverse relationship)
public function user()
{
return $this->belongsTo(User::class, ['team_id', 'category_id'], ['team_id', 'category_id']);
}
Key Check: Ensure the inverse relationship is defined in the related model (Task in this case).
// Parent model (e.g., Team)
public function users()
{
return $this->hasMany(User::class, ['team_id', 'department_id'], ['team_id', 'department_id']);
}
// Child model (e.g., User)
public function team()
{
return $this
->belongsTo(Team::class, ['team_id', 'department_id'], ['id', 'department_id']);
}
['foreign1', 'foreign2'] must match ['local1', 'local2']).Use where clauses after defining the relationship to avoid eager-loading issues:
$teams = Team::with(['users' => function ($query) {
$query->where('is_active', 1);
}])->get();
Extend your factory with the ComposhipsFactory trait to support composite relationships:
use Awobaz\Compoships\Database\Eloquent\Factories\ComposhipsFactory;
class UserFactory extends Factory
{
use ComposhipsFactory;
// ...
}
Then use has() with composite keys:
User::factory()->has(
Task::factory()->count(3),
'tasks',
['team_id' => 1, 'category_id' => 2]
);
For relationships where keys are dynamic (e.g., based on user input), use closures:
public function dynamicTasks($teamId, $categoryId)
{
return $this->hasMany(Task::class)
->where('team_id', $teamId)
->where('category_id', $categoryId);
}
Warning: This bypasses Compoships' eager-loading optimizations. Use only when necessary.
Simulate polymorphic relationships with composite keys:
// Model: Comment
public function commentable()
{
return $this->morphTo(['id', 'commentable_type'], ['commentable_id', 'commentable_type']);
}
Limitation: Only hasOne, hasMany, and belongsTo are supported. For morphTo, use a custom solution or extend the package.
Ensure composite keys are indexed in the database:
ALTER TABLE tasks ADD INDEX idx_team_category (team_id, category_id);
Performance Impact: Without indexes, queries will be slow even with Compoships.
Use Laravel's API resources to shape composite relationships:
public function toArray($request)
{
return [
'id' => $this->id,
'tasks' => TaskResource::collection($this->whenLoaded('tasks')),
];
}
Create reusable scopes for composite relationships:
public function scopeActive($query)
{
return $query->where(function ($q) {
$q->where('status', 'active')
->orWhere(function ($q) {
$q->where('team_id', auth()->user()->team_id)
->where('category_id', auth()->user()->category_id);
});
});
}
Cache composite relationships to reduce database load:
$cacheKey = "user_{$user->id}_tasks_{$user->team_id}_{$user->category_id}";
return Cache::remember($cacheKey, now()->addHours(1), function () use ($user) {
return $user->tasks()->get();
});
Document the exit strategy for Compoships:
user_id + task_id → user_task_id).Schema::table('tasks', function (Blueprint $table) {
$table->unsignedBigInteger('user_task_id')->unique();
});
where clauses fail during eager loading because $this->localKey is null.// WRONG (fails with eager loading)
return $this->hasMany(Task::class)->where('team_id', $this->team_id);
// CORRECT (use Compoships for the base relationship)
return $this->hasMany(Task::class, ['team_id', 'category_id'], ['team_id', 'category_id']);
null composite keys fail.null cases explicitly:
public function tasks()
{
return $this->hasMany(Task::class, ['team_id', 'category_id'], ['team_id', 'category_id'])
->where(function ($query) {
$query->whereNotNull('team_id')
->orWhereNotNull('category_id');
});
}
prefix_) isn’t applied correctly.return $this->hasMany(Task::class, ['prefix_team_id', 'prefix_category_id'], ['team_id', 'category_id']);
hasMany in Model A but forgetting the belongsTo in Model B.// Model A (hasMany)
public function bs()
{
return $this->hasMany(B::class, ['fk1', 'fk2'], ['local1', 'local2']);
}
// Model B (belongsTo)
public function a()
{
return $this->belongsTo(A::class, ['fk1', 'fk2'], ['local1', 'local2']);
}
2.5.5) and monitor for updates:
"awobaz/compoships": "2.5.5"
How can I help you explore Laravel packages today?