Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама
Новини 02 вересня 2026

Compoships: Eloquent-зв'язки на основі кількох колонок

Деякі Laravel-застосунки працюють зі схемами баз даних, які ніхто з команди не проєктував, де зв'язок між двома таблицями здійснюється через пару колонок, а не один зовнішній ключ. Eloquent за замовчуванням порівнює лише одну колонку, а звичайний обхідний шлях - додавання where() до hasMany() - повертає неправильні рядки при eager loading. Laravel будує eager-loaded зв'язок з нового порожнього екземпляра моделі, тому батьківський атрибут, на який ви посилаєтесь у where(), є null. Compoships від Claudin J. Daniel дозволяє передавати масив колонок там, де Eloquent очікує назву ключа.

Визначення зв'язку на основі двох колонок

Обидві моделі у зв'язку потребують трейту Awobaz\Compoships\Compoships, або можуть успадковувати Awobaz\Compoships\Database\Eloquent\Model, який є підкласом базової моделі Eloquent. Після цього методи зв'язків приймають масиви замість рядків.

Розглянемо таблицю замовлень, імпортовану з системи бухгалтерського обліку, де номер замовлення є унікальним лише в межах коду компанії. Для доступу до рядків замовлення потрібно порівнювати обидві колонки:

namespace App\Models;
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']
        );
    }
}

Зворотний зв'язок має таку саму структуру:

class OrderLine extends Model
{
    use Compoships;
    
    public function order()
    {
        return $this->belongsTo(
            Order::class,
            ['company_code', 'order_no'],
            ['company_code', 'order_no']
        );
    }
}

Методи hasOne, hasMany, belongsTo та belongsToMany приймають масиви колонок. Nullable-колонки також підтримуються, хоча зв'язок, у якому всі ключові колонки є null, не повертає нічого.

Зв'язкиMany-to-Many через проміжну таблицю

Метод belongsToMany приймає назву pivot-таблиці, а потім чотири масиви: колонки pivot, що вказують на кожну сторону, та локальні ключові колонки на кожній моделі. У прикладі склад і перевізник ідентифікуються кодом регіону плюс коротким кодом:

class Warehouse extends Model
{
    use Compoships;
    
    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(), withTimestamps(), has() та whereHas() працюють з таким зв'язком. Там, де Laravel приймає список ідентифікаторів, Compoships приймає список кортежів з одним значенням для кожної колонки пов'язаного pivot-ключа:

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

Для індивідуальних атрибутів pivot ключ масиву - це кортеж, перетворений через json_encode(), що замінює форму [id => attributes]:

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

Асоціативний ключ, який не є JSON-кортежем правильної довжини, викликає виняток Awobaz\Compoships\Exceptions\InvalidUsageException. Користувацькі pivot-моделі підтримуються через using(), якщо pivot-клас успадковує Awobaz\Compoships\Database\Eloquent\Relations\Pivot.

Складені первинні ключі на шляху запису

Для таблиці з ключем (invoice_no, company_code) збереження гідратованої моделі створює запит UPDATE ... WHERE invoice_no = ?. Той самий номер рахунку може існувати під іншим кодом компанії, тому такий запит може оновити неправильний рядок. Compoships обмежує шлях запису всіма ключовими колонками після оголошення $compositeKey, тоді як $primaryKey залишається скалярною назвою колонки:

class Invoice extends Model
{
    use Compoships;
    
    protected $primaryKey = 'invoice_no';
    public $incrementing = false;
    protected $keyType = 'string';
    protected $compositeKey = ['invoice_no', 'company_code'];
}

Методи save(), update(), delete() (включно з м'яким видаленням), refresh() та fresh() тоді будують WHERE-умову з обох колонок:

$invoice = Invoice::where('invoice_no', 'INV-4471')
    ->where('company_code', 'DE01')
    ->first();
$invoice->status = 'paid';
$invoice->save();
// UPDATE invoices SET status = ?
// WHERE invoice_no = ? AND company_code = ?

Усе, що прив'язане до скалярної колонки, зберігає стандартну поведінку Eloquent: Model::find($id), route model binding та хелпери на кшталт firstOrCreate() і updateOrCreate(), які будують свої власні умови з переданих параметрів.

Коли збережене значення ключової колонки є null, трейт пише WHERE column IS NULL замість прив'язки null до перевірки на рівність, тому $compositeKey також охоплює таблиці з унікальним індексом з nullable-дискримінатором. Якщо ви змінюєте ключову колонку в пам'яті перед викликом save(), WHERE-умова використовує оригінальне значення зі сховища, тоді як SET-умова записує нове.

Моделі в чергах

Одна модель зі складеним ключем у властивості завдання переживає повний цикл через чергу. SerializesModels викликає getQueueableId(), який повертає JSON-закодовані ключові колонки, а worker декодує це в запит, обмежений усіма ними. Payload-и, поставлені в чергу до появи цієї функції, все ще відновлюються через власний шлях Laravel, тому можна оновлюватись, маючи завдання в черзі.

Колекції потребують обгортки. Метод restoreCollection Laravel переіндексує завантажені моделі за їхнім скалярним ключем і шукає їх за поставленими в чергу ідентифікаторами, які для цих моделей є JSON-рядками, тому колекція повертається порожньою. QueueableCompositeCollection захоплює кортежі ключів під час відправки і перезавантажує рядки одним запитом:

use Awobaz\Compoships\Queue\QueueableCompositeCollection;

class ExportInvoices
{
    use SerializesModels;
    
    public QueueableCompositeCollection $invoices;
    
    public function __construct(Collection $invoices)
    {
        $this->invoices = QueueableCompositeCollection::for($invoices);
    }
    
    public function handle(): void
    {
        $invoices = $this->invoices->restore();
    }
}

Обгортка зберігає оригінальний порядок, eager-loaded зв'язки та з'єднання. Колекції зі змішаними класами викликають LogicException, а неправильний $compositeKey викликає InvalidUsageException - обидва у момент обгортання.

Встановлення

Compoships вимагає PHP 8.2 та Laravel 12 або 13. Встановлення через Composer:

composer require awobaz/compoships

Пакет закриває два прогалини і залишає решту Eloquent без змін. Один скалярний первинний ключ залишається кращим за замовчуванням для схеми, яку ви контролюєте, а Compoships призначений для випадків, коли база даних походить з іншого джерела, або коли зв'язку потрібно більше однієї колонки для порівняння. Вихідний код разом із Docker-скриптом, який локально запускає повну матрицю тестів Laravel і PHP, доступний на GitHub.

0

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

PayZephyr
Новини 12 вересня 2026

PayZephyr: Єдиний API для роботи з Stripe, Paystack та PayPal

PayZephyr - Laravel-пакет від Nwaneri Chukwunyere Kenneth, який об'єднує вісім платіжних провайдерів під одним зручним API. Підтримує автоматичне перемикання, захист від подвійної оплати, підписки та повернення коштів.

2
Laravel Telescope
Новини 11 вересня 2026

Artisan-команди для дебагу в Laravel Telescope 5.24.0

Laravel Telescope 5.24.0 додає дві нові Artisan-команди для роботи із записами із терміналу. Тепер можна переглядати запити, винятки, джоби та запити до бази даних без відкриття веб-інтерфейсу, а також отримувати дані у форматі JSON для скриптів та AI-агентів.

3

Вакансії за темою

Гарант-Інфо
4 дні тому

Middle PHP Developer (Laravel)

Middle PHP Developer для міжнародного B2B-продукту у телекомунікаціях. Розробка нового функціоналу на PHP 8.x/Laravel, підтримка кодової бази, робота з REST API, платіжними інтеграціями та MySQL. Вимоги: 2+ років комерційного досвіду PHP, впевнене знання Laravel, ООП, SOLID, тестування (PHPUnit/Pest), Git, Docker.

Пакети за темою

Laravel Medialibrary

spatie/laravel-medialibrary

Пакет для асоціювання файлів з Eloquent-моделями. Надає зручний інтерфейс для управління медіа-файлами, пов'язаними з вашими моделями бази даних.

6,163 11.23.7 13 8

Laravel Query Builder

spatie/laravel-query-builder

Легко будуйте Eloquent-запити на основі запитів від API.

4,469 7.3.5 13 11