Compoships: Multi-Column Eloquent Relationships | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Compoships: Eloquent Relationships on Multiple Columns in Laravel        On this page       1. [  The Problem: Eloquent Matches Only One Foreign Key ](#the-problem-eloquent-matches-only-one-foreign-key)
2. [  Installation ](#installation)
3. [  Defining a Multi-Column Relationship ](#defining-a-multi-column-relationship)
4. [  Many-to-Many With a Pivot Table ](#many-to-many-with-a-pivot-table)
5. [  Composite Primary Keys on the Write Path ](#composite-primary-keys-on-the-write-path)
6. [  Queue Support ](#queue-support)
7. [  Key Takeaways ](#key-takeaways)

  ![Compoships: Eloquent Relationships on Multiple Columns in Laravel](https://cdn.msaied.com/618/d246f1cbcb9f9cd71afa1415b2329b51.png)

 [  Laravel ](https://msaied.com/articles?category=laravel) [  Composer Pacakge ](https://msaied.com/articles?category=composer-pacakge)  #eloquent   #laravel   #composer-package   #database   #relationships  

 Compoships: Eloquent Relationships on Multiple Columns in Laravel 
===================================================================

     1 Sep 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem: Eloquent Matches Only One Foreign Key  ](#the-problem-eloquent-matches-only-one-foreign-key)
2. [  02   Installation  ](#installation)
3. [  03   Defining a Multi-Column Relationship  ](#defining-a-multi-column-relationship)
4. [  04   Many-to-Many With a Pivot Table  ](#many-to-many-with-a-pivot-table)
5. [  05   Composite Primary Keys on the Write Path  ](#composite-primary-keys-on-the-write-path)
6. [  06   Queue Support  ](#queue-support)
7. [  07   Key Takeaways  ](#key-takeaways)

 The Problem: Eloquent Matches Only One Foreign Key
--------------------------------------------------

When your database schema links two tables through a pair of columns instead of a single foreign key, stock Eloquent falls short. The common workaround — chaining a `where()` onto `hasMany()` — silently returns wrong results under eager loading because Laravel builds the relationship from a fresh empty model instance, so the parent attribute you reference is `null`.

[Compoships](https://github.com/topclaudy/compoships), by Claudin J. Daniel, solves this by letting you pass an **array of column names** wherever Eloquent normally expects a single key string.

Installation
------------

Compoships requires PHP 8.2 and Laravel 12 or 13:

```bash
composer require awobaz/compoships

```

Defining a Multi-Column Relationship
------------------------------------

Add the `Awobaz\Compoships\Compoships` trait to both models (or extend `Awobaz\Compoships\Database\Eloquent\Model`). Then pass arrays to the relationship methods:

```php
use Awobaz\Compoships\Compoships;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use Compoships;

    public function lines()
    {
        return $this->hasMany(
            OrderLine::class,
            ['company_code', 'order_no'],
            ['company_code', 'order_no']
        );
    }
}

```

The inverse `belongsTo` uses the same array shape. `hasOne`, `hasMany`, `belongsTo`, and `belongsToMany` all accept column arrays.

Many-to-Many With a Pivot Table
-------------------------------

`belongsToMany` accepts four arrays: pivot columns for each side, plus the local key columns on each model:

```php
public function carriers()
{
    return $this->belongsToMany(
        Carrier::class,
        'carrier_warehouse',
        ['warehouse_region_code', 'warehouse_code'],
        ['carrier_region_code', 'carrier_code'],
        ['region_code', 'code'],
        ['region_code', 'code']
    );
}

```

`attach()`, `detach()`, `sync()`, `toggle()`, `withPivot()`, and `whereHas()` all work. Where Laravel normally takes a list of IDs, Compoships takes a list of tuples:

```php
$warehouse->carriers()->attach([
    ['EU', 'DHL'],
    ['EU', 'UPS'],
]);

```

To pass per-row pivot attributes, use `json_encode()` on the tuple as the array key:

```php
$warehouse->carriers()->attach([
    json_encode(['EU', 'DHL']) => ['priority' => 1],
    json_encode(['EU', 'UPS']) => ['priority' => 2],
]);

```

Composite Primary Keys on the Write Path
----------------------------------------

Declare `$compositeKey` to scope `save()`, `update()`, `delete()`, `refresh()`, and `fresh()` by every key column, preventing updates from hitting the wrong row:

```php
class Invoice extends Model
{
    use Compoships;

    protected $primaryKey = 'invoice_no';
    public $incrementing = false;
    protected $keyType = 'string';
    protected $compositeKey = ['invoice_no', 'company_code'];
}

```

When a key column is `null`, the trait writes `WHERE column IS NULL` rather than binding null into an equality check. If you change a key column before calling `save()`, the WHERE clause uses the original stored value while the SET clause writes the new one.

Queue Support
-------------

A single composite-keyed model serializes safely via `SerializesModels` — `getQueueableId()` returns the JSON-encoded key tuple and the worker decodes it back into a scoped query.

For **collections**, use `QueueableCompositeCollection` to avoid the empty-collection bug that occurs when Laravel re-keys models by their scalar key:

```php
use Awobaz\Compoships\Queue\QueueableCompositeCollection;

public function __construct(Collection $invoices)
{
    $this->invoices = QueueableCompositeCollection::for($invoices);
}

public function handle(): void
{
    $invoices = $this->invoices->restore();
}

```

The wrapper preserves original order, eager-loaded relations, and the connection. Mixed-class collections raise a `LogicException` at wrap time.

Key Takeaways
-------------

- Compoships extends `hasOne`, `hasMany`, `belongsTo`, and `belongsToMany` to accept column arrays, fixing eager-loading bugs caused by null parent attributes.
- `$compositeKey` scopes all write operations (save, update, delete) by every declared key column.
- `belongsToMany` pivot operations (`attach`, `sync`, etc.) accept tuples instead of scalar IDs.
- `QueueableCompositeCollection` is required when dispatching collections of composite-keyed models to a queue.
- A single scalar primary key remains the better default for schemas you control; Compoships targets legacy or external schemas.
- Requires PHP 8.2 and Laravel 12 or 13.

---

*Source: [Compoships: Eloquent Relationships on Multiple Columns — Laravel News](https://laravel-news.com/compoships)*

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcompoships-eloquent-relationships-on-multiple-columns-in-laravel&text=Compoships%3A+Eloquent+Relationships+on+Multiple+Columns+in+Laravel) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcompoships-eloquent-relationships-on-multiple-columns-in-laravel) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Why does chaining where() onto hasMany() break eager loading in Laravel?        During eager loading, Laravel builds the relationship query from a new empty model instance, so any parent attribute you reference in the where() clause is null. This means the constraint is never applied correctly and the wrong rows are returned. Compoships fixes this by building the multi-column constraint directly into the relationship definition. 

      Q02  How do you attach pivot records when using Compoships belongsToMany?        Instead of passing a list of scalar IDs, you pass a list of tuples — one value per pivot key column. For example: $warehouse-&gt;carriers()-&gt;attach([['EU', 'DHL'], ['EU', 'UPS']]). To include per-row pivot attributes, use json_encode() on the tuple as the array key. 

      Q03  Does Compoships work with Laravel's queue system for composite-keyed models?        Yes. A single composite-keyed model serializes safely via SerializesModels using a JSON-encoded key tuple. For collections, you must use QueueableCompositeCollection::for($collection) at dispatch time and call -&gt;restore() in the job handler, otherwise Laravel's default collection restoration returns an empty result. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Livewire v3 Performance: Computed Properties, Dehydration Budgets, and Wire:model Lazy](https://cdn.msaied.com/620/e4d958595b3e6a6b47c586df3f972938.png) livewire laravel performance 

### Livewire v3 Performance: Computed Properties, Dehydration Budgets, and Wire:model Lazy

Stop over-fetching on every request cycle. This deep-dive covers Livewire v3 computed property memoisation, co...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 2 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/livewire-v3-performance-computed-properties-dehydration-budgets-and-wiremodel-lazy) [ ![Filament v3 Table Tricks: Deferred Loading, Live Search, and Custom Filter Forms](https://cdn.msaied.com/617/2c4c33f76e69e2d61f0b6cf2918a8ad2.png) filament laravel livewire 

### Filament v3 Table Tricks: Deferred Loading, Live Search, and Custom Filter Forms

Go beyond the defaults with Filament v3 tables: wire up deferred loading for heavy datasets, build live search...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 1 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/filament-v3-table-tricks-deferred-loading-live-search-and-custom-filter-forms) [ ![MKSine: A Filament CMS with Plugins, Themes, and Blocks for Laravel](https://cdn.msaied.com/619/a6bec1a59695b3d3ffb212492862d25b.png) Laravel Filament CMS 

### MKSine: A Filament CMS with Plugins, Themes, and Blocks for Laravel

MKSine is a community-built Filament CMS that adds pages, posts, a block-based page builder, themes, menus, a...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 1 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/mksine-a-filament-cms-with-plugins-themes-and-blocks-for-laravel) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
