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 tasks in specific categories. The relationship requires matching both team_id and category_id:

// User model
public function responsibleTasks()
{
    return $this->hasMany(Task::class, ['team_id', 'category_id'], ['team_id', 'category_id']);
}

// Task model (inverse)
public function responsibleUser()
{
    return $this->belongsTo(User::class, ['team_id', 'category_id'], ['team_id', 'category_id']);
}

Key Insight: Compoships resolves the "null foreign key" issue during eager loading by deferring the relationship query until the parent model is fully hydrated.


Implementation Patterns

Core Workflows

  1. Composite Key Relationships:

    • One-to-Many:
      $this->hasMany(RelatedModel::class, ['fk1', 'fk2'], ['local1', 'local2']);
      
    • Many-to-One:
      $this->belongsTo(RelatedModel::class, ['fk1', 'fk2'], ['local1', 'local2']);
      
    • Many-to-Many (with pivot):
      $this->belongsToMany(
          RelatedModel::class,
          'pivot_table',
          ['fk1_a', 'fk2_a'], // Foreign keys for Model A
          ['fk1_b', 'fk2_b'], // Foreign keys for Model B
          ['local1_a', 'local2_a'], // Local keys for Model A
          ['local1_b', 'local2_b']  // Local keys for Model B
      );
      
  2. Eager Loading with Constraints: Use whereHas() with composite keys:

    User::whereHas('tasks', function ($query) {
        $query->where('status', 'pending')
              ->where('priority', 'high');
    })->get();
    
  3. Dynamic Composite Relationships: For polymorphic or dynamic relationships, use method closures:

    public function getRelationship($type)
    {
        $keys = $this->getKeysForType($type);
        return $this->hasMany(RelatedModel::class, $keys, $keys);
    }
    
  4. Pivot Table Operations:

    • Attach/Detach:
      $user->projects()->attach([['team_id', 'dept_id']]);
      $user->projects()->detach([['team_id', 'dept_id']]);
      
    • Sync with Attributes:
      $user->projects()->sync([
          json_encode(['EU', 2]) => ['role' => 'reviewer'],
      ]);
      

Integration Tips

  • Factories: Extend ComposhipsFactory for composite relationships in testing:

    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), 'responsibleTasks');
    
  • API Resources: Handle composite relationships in JSON responses:

    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'tasks' => $this->tasks->map(fn ($task) => [
                'id' => $task->id,
                'team_id' => $task->team_id,
                'category_id' => $task->category_id,
            ]),
        ];
    }
    
  • Query Scopes: Create reusable composite scopes:

    public function scopeAssignedToTeam($query, $teamId, $categoryId)
    {
        return $query->whereHas('responsibleUser', function ($q) use ($teamId, $categoryId) {
            $q->where('team_id', $teamId)
              ->where('category_id', $categoryId);
        });
    }
    
  • Custom Pivot Models: For belongsToMany, extend Awobaz\Compoships\Database\Eloquent\Relations\Pivot:

    class UserProjectPivot extends Pivot { ... }
    

Gotchas and Tips

Pitfalls

  1. Null Composite Keys:

    • Issue: Relationships with all-null composite keys fail silently or return empty results.
    • Fix: Ensure at least one column in the composite key is non-nullable or handle nulls explicitly:
      public function tasks()
      {
          return $this->hasMany(Task::class, ['team_id', 'category_id'])
                      ->where(function ($query) {
                          $query->whereNotNull('team_id')
                                ->orWhereNotNull('category_id');
                      });
      }
      
  2. Eager Loading Quirks:

    • Issue: N+1 queries may still occur if constraints are added after eager loading.
    • Fix: Use with() constraints or load constraints separately:
      // Correct: Load constraints after eager loading
      $users = User::with('tasks')->get();
      $users->loadMissing(['tasks' => fn ($query) => $query->where('status', 'pending')]);
      
  3. Many-to-Many Pivot Issues:

    • Issue: Custom pivot models must extend Awobaz\Compoships\Database\Eloquent\Relations\Pivot, not Laravel’s base Pivot.
    • Fix: Update pivot model inheritance:
      use Awobaz\Compoships\Database\Eloquent\Relations\Pivot;
      class UserProjectPivot extends Pivot { ... }
      
  4. Table Prefix Mismatches:

    • Issue: Queries may fail if table prefixes (e.g., snake_) aren’t applied to composite keys.
    • Fix: Ensure your database schema and queries align with Laravel’s table prefixing (e.g., snake_tasks instead of tasks).
  5. JSON-Encoded Keys in attach():

    • Issue: Invalid JSON keys in attach() throw InvalidUsageException.
    • Fix: Validate keys before passing:
      $key = json_encode(['EU', 2]);
      if (json_last_error() !== JSON_ERROR_NONE) {
          throw new \InvalidArgumentException("Invalid composite key format");
      }
      

Debugging Tips

  • Query Logging: Enable Laravel’s query logging to inspect composite key queries:

    \DB::enableQueryLog();
    $users = User::with('tasks')->get();
    dd(\DB::getQueryLog());
    
  • Composite Key Validation: Use dump() to verify composite keys during development:

    public function tasks()
    {
        $keys = ['team_id', 'category_id'];
        dump($this->{$keys[0]}, $this->{$keys[1]}); // Debug local keys
        return $this->hasMany(Task::class, $keys, $keys);
    }
    
  • Factory Debugging: For factories, ensure composite relationships are mocked correctly:

    User::factory()
        ->has(Task::factory()->state([
            'team_id' => 1,
            'category_id' => 2,
        ])->count(2), 'responsibleTasks')
        ->create();
    

Extension Points

  1. Custom Query Grammar: Override Awobaz\Compoships\Database\Eloquent\Builder to modify composite key behavior:

    class CustomBuilder extends \Awobaz\Compoships\Database\Eloquent\Builder
    {
        protected function whereKey($query, $operator, $values, $boolean = 'and')
        {
            // Custom logic for composite keys
        }
    }
    
  2. Composite Key Serialization: Extend JSON serialization for composite keys in API responses:

    public function toJson($options = 0)
    {
        return json_encode([
            'id' => $this->id,
            'composite_key' => [$this->team_id, $this->category_id],
        ]);
    }
    
  3. Dynamic Relationship Keys: Dynamically resolve composite keys based on runtime conditions:

    public function dynamicRelationship()
    {
        $keys = $this->getDynamicKeys();
        return $this->hasMany(RelatedModel::
    
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.
jayeshmepani/jpl-moshier-ephemeris-php
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui
babelqueue/php-sdk
facebook/capi-param-builder-php
babelqueue/symfony
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle