Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Compoships Laravel Package

awobaz/compoships

Adds composite-key relationship support to Laravel Eloquent. Define hasOne/hasMany/belongsTo relations matching two or more columns so eager loading works with legacy or third‑party schemas, using a custom base Model or Compoships trait.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation:

    composer require awobaz/compoships
    
  2. Model Integration: Choose either extending the base model:

    use Awobaz\Compoships\Database\Eloquent\Model;
    class User extends Model { ... }
    

    or using the trait:

    use Awobaz\Compoships\Compoships;
    class User { use Compoships; ... }
    
  3. Define Composite Relationship:

    public function tasks()
    {
        return $this->hasMany(Task::class, ['team_id', 'category_id'], ['team_id', 'category_id']);
    }
    
  4. Test Eager Loading:

    $users = User::with('tasks')->get();
    

First Use Case

Scenario: A User belongs to a Team and is responsible for a Category of Tasks. The relationship between User and Task is defined by both team_id and category_id.

// 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']);
}

Usage:

$user = User::find(1);
$tasks = $user->tasks; // Works with eager loading
$task = Task::find(1);
$user = $task->user;   // Inverse works too

Implementation Patterns

Core Workflows

1. One-to-Many/Many-to-One Relationships

// User has many Tasks (composite keys)
public function tasks()
{
    return $this->hasMany(Task::class, ['user_id', 'team_id'], ['id', 'team_id']);
}

// Task belongs to User (inverse)
public function user()
{
    return $this->belongsTo(User::class, ['user_id', 'team_id'], ['id', 'team_id']);
}

2. Many-to-Many Relationships

// User belongs to many Projects (composite pivot keys)
public function projects()
{
    return $this->belongsToMany(
        Project::class,
        'user_project',
        ['user_team_id', 'user_department_id'], // Foreign keys for User
        ['project_team_id', 'project_department_id'], // Foreign keys for Project
        ['team_id', 'department_id'], // Local keys for User
        ['team_id', 'department_id']  // Local keys for Project
    );
}

3. Attaching/Detaching in Many-to-Many

// Attach projects to a user (composite tuples)
$user->projects()->attach([
    ['EU', 2], // [team_id, department_id]
    ['US', 1],
]);

// Sync with attributes
$user->projects()->sync([
    json_encode(['EU', 2]) => ['role' => 'reviewer'],
    json_encode(['US', 1]) => ['role' => 'lead'],
], ['note' => 'bulk applied']);

4. Composite Primary Keys

class TenantUser extends Model
{
    use Compoships;

    protected $primaryKey = 'id';
    public $incrementing = false;
    protected $compositeKey = ['id', 'tenant_id']; // Opt into composite handling
}

Integration Tips

  1. Factories: Use ComposhipsFactory trait for testing:

    use Awobaz\Compoships\Database\Eloquent\Factories\ComposhipsFactory;
    class UserFactory extends Factory { use ComposhipsFactory; }
    
  2. Queueable Collections: Wrap collections for queue jobs:

    use Awobaz\Compoships\Queue\QueueableCompositeCollection;
    $queueable = QueueableCompositeCollection::for($users);
    
  3. Custom Pivot Models: Extend Awobaz\Compoships\Database\Eloquent\Relations\Pivot for composite-key pivots:

    class UserProjectPivot extends Pivot { ... }
    
  4. Eager Loading: Works out-of-the-box:

    User::with('tasks')->get();
    
  5. Query Scoping: Use whereHas with composite keys:

    User::whereHas('tasks', function ($query) {
        $query->where('category_id', 'web');
    })->get();
    

Gotchas and Tips

Pitfalls

  1. Null Composite Keys: Relationships with all null composite keys are unsupported. Ensure at least one column has a non-null value.

  2. Trait Consistency: Both models in a relationship must use Compoships (either via trait or base class). Mixing will break eager loading.

  3. Composite Primary Key Validation: If $compositeKey doesn’t include $primaryKey, the first save()/delete() throws InvalidUsageException.

  4. Queueable Collections: Raw Collection with composite keys won’t restore from queues. Always wrap with QueueableCompositeCollection.

  5. Nullable Columns in WHERE: Nullable composite-key columns use IS NULL instead of = NULL to avoid SQL false positives.

  6. Route Model Binding: Still uses $primaryKey (scalar). Composite keys are not used for binding (e.g., /users/{id}).

  7. Custom setKeysForSaveQuery Overrides: Call parent::setKeysForSaveQuery($query) first to retain composite key handling.

Debugging Tips

  1. Eager Loading Issues: Verify both models use Compoships. Check for typos in column names.

  2. Attachment Errors: Ensure attach()/sync() tuples match pivot table columns. Use json_encode() for map keys:

    $user->projects()->attach([json_encode(['EU', 2]) => ['role' => 'admin']]);
    
  3. Composite Key Queries: Log the generated SQL to debug WHERE clauses:

    \DB::enableQueryLog();
    $user->save();
    dd(\DB::getQueryLog());
    
  4. Queue Restoration Failures: Confirm QueueableCompositeCollection is used. Check for mixed-class collections.

  5. Soft Deletes: Composite keys work with SoftDeletes, but ensure $compositeKey includes the soft-deletable columns.

Extension Points

  1. Custom Pivot Logic: Extend Awobaz\Compoships\Database\Eloquent\Relations\Pivot for custom pivot behavior.

  2. Composite Key Macros: Add macros to models for reusable composite logic:

    class User extends Model {
        public function scopeInTeam($query, $teamId) {
            return $query->where('team_id', $teamId);
        }
    }
    
  3. Query Builder Extensions: Extend Awobaz\Compoships\Database\Query\Builder for custom composite queries.

  4. Event Listeners: Listen for compoships.* events (e.g., compoships.attaching) for custom logic during attachments.

  5. Composite Key Validation: Override validateCompositeKey() in your model to enforce custom rules:

    protected function validateCompositeKey()
    {
        if (empty($this->team_id) && empty($this->department_id)) {
            throw new \Exception('Composite key cannot be empty');
        }
    }
    

Performance Notes

  1. Indexing: Ensure composite foreign keys are indexed for performance:

    ALTER TABLE tasks ADD INDEX idx_user_team_category (user_id, team_id, category_id);
    
  2. Batch Operations: Use chunk() for large attach()/sync() operations to avoid memory issues:

    $user->projects()->sync($projects->chunk(100));
    
  3. Avoid N+1: Always eager load composite relationships:

    User::with('tasks')->get(); // Not User::find(1)->tasks
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
calmfox/watch-sylius
damienfern/grpc-symfony-bundle
atoolo/index-bundle
atoolo/genai-bundle
coprotoai/laravel-ticket
davidjln/llm-carbon-bundle
cryonighter/valid-request-bundle
coolms/taxonomy-bundle
coolms/field-bundle
articulate-orm/symfony
aaix/laravel-tall-architect
ephoto/akeneo-connector
emmanuelballery/eb-plantumlbundle
emielburgman/symfony-visitor-beacon
emielburgman/symfony-visit-storage
emielburgman/symfony-security-headers
emielburgman/symfony-log-viewer
emarref/xdebug-bundle
emarref/pubnub-bundle
elriseio/finance-money-bundle