Laravel 包

Compoships: Eloquent Relationships on Multiple Columns

发布
Compoships: Eloquent Relationships on Multiple Columns image

Some Laravel applications run against a schema nobody on the team designed, where the link between two tables is a pair of columns rather than one foreign key. Eloquent matches a single column, and the usual workaround of chaining a where() onto hasMany() returns the wrong rows under 急切加载 . Laravel builds an eager-loaded relationship from a new empty model instance, so the parent attribute you reference in the where() is null. Compoships, by Claudin J. Daniel , lets you pass an array of columns where Eloquent expects a key name.

Defining a Relationship on Two Columns

Both models in the relationship need the Awobaz\Compoships\Compoships trait, or they can extend Awobaz\Compoships\Database\Eloquent\Model , which subclasses Eloquent's base model. After that the relationship methods take arrays instead of strings.

Take an order table imported from an accounting system, where an order number is only unique within a company code. Reaching the order lines means matching both columns:

命名空间 应用程序\模型 ;
使用 Awobaz\Compoships\Compoships ;
使用 照亮\数据库\雄辩\模型 ;
班级 命令 延伸 模型
{
使用 Compoships ;
民众 功能 线 ()
{
返回 $this -> hasMany
OrderLine ::班级 ,
[ 'company_code' , 'order_no' ],
[ 'company_code' , 'order_no' ]
(英文):
}
}

The inverse uses the same shape:

班级 OrderLine 延伸 模型
{
使用 Compoships ;
民众 功能 命令 ()
{
返回 $this -> 属于
命令 ::班级 ,
[ 'company_code' , 'order_no' ],
[ 'company_code' , 'order_no' ]
(英文):
}
}

hasOne , hasMany , belongsTo , 和 belongsToMany accept column arrays. Nullable columns are handled, though a relationship whose key columns are all null returns nothing.

Many-to-Many Through a Pivot Table

belongsToMany takes the pivot table name, then four arrays: the pivot columns pointing at each side, and the local key columns on each model. Here a warehouse and a carrier are both identified by a region code plus a short code:

班级 仓库 延伸 模型
{
使用 Compoships ;
民众 功能 carriers ()
{
返回 $this -> 属于多
Carrier ::班级 ,
'carrier_warehouse' ,
[ 'warehouse_region_code' , 'warehouse_code' ],
[ 'carrier_region_code' , 'carrier_code' ],
[ 'region_code' , '代码' ],
[ 'region_code' , '代码' ]
(英文):
}
}

attach() , detach() , sync() , toggle() , withPivot() , withTimestamps() , has() , 和 whereHas() all work on that relationship. Where Laravel takes a list of ids, Compoships takes a list of tuples with one value per related pivot key column:

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

For per-row pivot attributes, the array key is the tuple run through json_encode() , which stands in for the [id => attributes] 形式:

$warehouse -> carriers () -> ([
json_encode ([ 'EU' , 'DHL' ]) => [ '优先事项' => 1 ],
json_encode ([ 'EU' , 'UPS' ]) => [ '优先事项' => 2 ],
], [ 'contract_year' => 2026 ]);

An associative key that is not a JSON tuple of the right length raises Awobaz\Compoships\Exceptions\InvalidUsageException . Custom pivot models are supported through using() , as long as the pivot class extends Awobaz\Compoships\Database\Eloquent\Relations\Pivot

Composite Primary Keys on the Write Path

On a table keyed by (invoice_no, company_code) , saving a hydrated model produces UPDATE ... WHERE invoice_no = ? . The same invoice number can exist under a different company code, so that statement can update the wrong row. Compoships scopes the write path by every key column once you declare $compositeKey , while $primaryKey stays a scalar column name:

班级 发票 延伸 模型
{
使用 Compoships ;
受保护 $primaryKey = 'invoice_no' ;
民众 $增加 = 错误的 ;
受保护 $keyType = '细绳' ;
受保护 $compositeKey = [ 'invoice_no' , 'company_code' ];
}

save() , update() , delete() including soft deletes, refresh() , 和 fresh() then build their WHERE clause from both columns:

$invoice = 发票 :: 在哪里 'invoice_no' , 'INV-4471'
-> 在哪里 'company_code' , 'DE01'
-> 第一的 ();
$invoice -> 地位 = '有薪酬的' ;
$invoice -> 节省 ();
// UPDATE invoices SET status = ?
// WHERE invoice_no = ? AND company_code = ?

Everything keyed off the scalar column keeps stock Eloquent behavior: Model::find($id) , route model binding, and helpers like firstOrCreate()updateOrCreate() that build their own conditions from what you pass them.

When the stored value of a key column is null, the trait writes WHERE column IS NULL instead of binding null into an equality check, so $compositeKey also covers tables that use a unique index with a nullable discriminator. If you change a key column in memory before calling save() , the WHERE clause uses the original value from storage while the SET clause writes the new one.

Queued Models

A single composite-keyed model on a job property survives a round trip through the queueSerializesModels 调用 getQueueableId() , which returns the JSON-encoded key columns, and the worker decodes it into a query scoped by all of them. Payloads queued before the feature existed still restore through Laravel's own path, so you can upgrade with jobs already on the queue.

Collections need a wrapper. Laravel's restoreCollection re-keys the loaded models by their scalar key and looks them up by the queued ids, which for these models are JSON strings, so the collection comes back empty. QueueableCompositeCollection captures the key tuples at dispatch time and reloads the rows in one query:

使用 Awobaz\Compoships\Queue\QueueableCompositeCollection ;
班级 ExportInvoices
{
使用 序列化模型 ;
民众 QueueableCompositeCollection $invoices;
民众 功能 __构造 收藏 $invoices)
{
$this -> 发票 = QueueableCompositeCollection :: 为了 ($invoices);
}
民众 功能 处理 () : 空白
{
$invoices = $this -> 发票 -> 恢复 ();
}
}

The wrapper keeps the original order, the eager-loaded relations, and the connection. Mixed-class collections raise a LogicException and a bad $compositeKey raises InvalidUsageException , both at the point you wrap.

安装

Compoships requires PHP 8.2 and Laravel 12 or 13. Install it with Composer:

作曲家 要求 awobaz/compoships

The package covers two gaps and leaves the rest of Eloquent alone. A single scalar primary key is still the better default for a schema you control, and Compoships is for the cases where the database comes from somewhere else, or where a relationship needs more than one column to match. The source, along with a Docker script that runs the full Laravel and PHP test matrix locally, is on GitHub

Yannick Lyn Fatt 的照片

Laravel News 的特约撰稿人和全栈 Web 开发人员。

赞助

laravelcloud logo
Laravel 云

轻松创建和管理服务器,并在几秒钟内部署 Laravel 应用程序。

访问 Laravel Cloud
MKSine: A Filament CMS with Plugins, Themes, and Blocks image

MKSine: A Filament CMS with Plugins, Themes, and Blocks

阅读文章
Cancel In-Flight Form Submissions in Inertia.js v3.7 image

Cancel In-Flight Form Submissions in Inertia.js v3.7

阅读文章
Laravel Starter Kits Now Ship with Vite+ image

Laravel Starter Kits Now Ship with Vite+

阅读文章
Pessimistic Locking in Laravel Eloquent with refreshForUpdate() image

Pessimistic Locking in Laravel Eloquent with refreshForUpdate()

阅读文章
Mask Query Bindings in Laravel Exception Messages image

Mask Query Bindings in Laravel Exception Messages

阅读文章
whereBinary(): Case-Sensitive MySQL Queries in Laravel image

whereBinary(): Case-Sensitive MySQL Queries in Laravel

阅读文章