Eloquent: Відносини

Вступ

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

Визначення Відносин

Відносини Eloquent визначаються як методи у ваших класах моделей Eloquent. Оскільки відносини також служать потужними конструкторами запитів, визначення відносин як методів надає потужні можливості ланцюжка методів і запитів. Наприклад, ми можемо додати додаткові обмеження запиту до цього відношення posts:

$user->posts()->where('active', 1)->get();

Але перш ніж заглиблюватися у використання відносин, давайте дізнаємося, як визначити кожен тип відносин, підтримуваних Eloquent.

Один до одного / Має один

Одно-до-одного відношення є дуже базовим типом відношення в базі даних. Наприклад, модель User може бути пов'язана з однією моделлю Phone. Щоб визначити це відношення, ми розмістимо метод phone у моделі User. Метод phone повинен викликати метод hasOne і повертати його результат. Метод hasOne доступний вашій моделі через базовий клас моделі Illuminate\Database\Eloquent\Model:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
 
class User extends Model
{
    /**
     * Отримати телефон, пов'язаний з користувачем.
     */
    public function phone(): HasOne
    {
        return $this->hasOne(Phone::class);
    }
}

Перший аргумент, переданий методу hasOne, це ім'я пов'язаної моделі класу. Після визначення відношення, ми можемо отримати пов'язаний запис, використовуючи динамічні властивості Eloquent. Динамічні властивості дозволяють вам отримувати доступ до методів відношення так, ніби вони були властивостями, визначеними в моделі:

$phone = User::find(1)->phone;

Eloquent визначає зовнішній ключ відношення на основі імені батьківської моделі. У цьому випадку модель Phone автоматично вважається такою, що має зовнішній ключ user_id. Якщо ви бажаєте перевизначити цю конвенцію, ви можете передати другий аргумент методу hasOne:

return $this->hasOne(Phone::class, 'foreign_key');

Крім того, Eloquent припускає, що зовнішній ключ повинен мати значення, яке відповідає стовпцю первинного ключа батьківського елемента. Іншими словами, Eloquent шукатиме значення стовпця id користувача в стовпці user_id запису Phone. Якщо ви хочете, щоб відношення використовувало значення первинного ключа, відмінне від id або властивості $primaryKey вашої моделі, ви можете передати третій аргумент методу hasOne:

return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

Визначення зворотного зв’язку "Один до одного"

Отже, ми можемо отримати доступ до моделі Phone з нашої моделі User. Далі, давайте визначимо відношення в моделі Phone, яке дозволить нам отримати доступ до користувача, якому належить телефон. Ми можемо визначити обернене відношення hasOne за допомогою методу belongsTo:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Phone extends Model
{
/**
* Отримати користувача, якому належить телефон.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

Коли викликається метод user, Eloquent спробує знайти модель User, яка має id, що відповідає стовпцю user_id у моделі Phone.

Eloquent визначає ім'я зовнішнього ключа, досліджуючи ім'я методу відношення та додаючи до імені методу суфікс _id. Отже, в цьому випадку Eloquent припускає, що модель Phone має стовпець user_id. Однак, якщо зовнішній ключ у моделі Phone не є user_id, ви можете передати власне ім'я ключа як другий аргумент до методу belongsTo:

/**
* Отримати користувача, якому належить телефон.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}

Якщо батьківська модель не використовує id як свій первинний ключ, або ви бажаєте знайти пов'язану модель, використовуючи інший стовпець, ви можете передати третій аргумент методу belongsTo, вказуючи власний ключ батьківської таблиці:

/**
* Отримати користувача, якому належить телефон.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}

Один до багатьох / Має багато

Відношення "один-до-багатьох" використовується для визначення відносин, де одна модель є батьківською для однієї або більше дочірніх моделей. Наприклад, блог-пост може мати нескінченну кількість коментарів. Як і всі інші відношення Eloquent, відношення "один-до-багатьох" визначаються шляхом визначення методу у вашій моделі Eloquent:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Post extends Model
{
/**
* Отримати коментарі до допису в блозі.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}

Пам'ятайте, Eloquent автоматично визначить правильну колонку зовнішнього ключа для моделі Comment. За конвенцією, Eloquent візьме назву батьківської моделі в "snake case" і додасть до неї суфікс _id. Отже, в цьому прикладі, Eloquent припускатиме, що колонка зовнішнього ключа в моделі Comment є post_id.

Після того як метод відношення було визначено, ми можемо отримати доступ до колекції пов'язаних коментарів, звертаючись до властивості comments. Пам'ятайте, оскільки Eloquent надає "динамічні властивості відношень", ми можемо отримати доступ до методів відношень так, ніби вони були визначені як властивості моделі:

use App\Models\Post;
 
$comments = Post::find(1)->comments;
 
foreach ($comments as $comment) {
    // ...
}

Оскільки всі відносини також слугують конструкторами запитів, ви можете додати подальші обмеження до запиту відносин, викликавши метод comments і продовжуючи додавати умови до запиту:

$comment = Post::find(1)->comments()
    ->where('title', 'foo')
    ->first();

Як і метод hasOne, ви також можете перевизначити зовнішні та локальні ключі, передавши додаткові аргументи до методу hasMany:

return $this->hasMany(Comment::class, 'foreign_key');
 
return $this->hasMany(Comment::class, 'foreign_key', 'local_key');

Автоматичне підключення батьківських моделей до дочірніх

Навіть при використанні Eloquent eager loading, проблеми з запитами "N + 1" можуть виникати, якщо ви намагаєтеся отримати доступ до батьківської моделі з дочірньої моделі під час перебору дочірніх моделей:

$posts = Post::with('comments')->get();
 
foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->post->title;
    }
}

У наведеному вище прикладі виникла проблема запиту "N + 1", оскільки, хоча коментарі були завантажені заздалегідь для кожної моделі Post, Eloquent не автоматично заповнює батьківську модель Post для кожної дочірньої моделі Comment.

Якщо ви хочете, щоб Eloquent автоматично заповнював батьківські моделі їхніми дочірніми елементами, ви можете викликати метод chaperone при визначенні відношення hasMany:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Post extends Model
{
/**
* Отримати коментарі до допису в блозі.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->chaperone();
}
}

Або, якщо ви хочете увімкнути автоматичне заповнення батьків під час виконання, ви можете викликати модель chaperone при жадібному завантаженні відношення:

use App\Models\Post;
 
$posts = Post::with([
    'comments' => fn ($comments) => $comments->chaperone(),
])->get();

Один до багатьох (зворотний) / Належить до

Тепер, коли ми можемо отримати доступ до всіх коментарів поста, давайте визначимо відношення, яке дозволить коментарю отримати доступ до свого батьківського поста. Щоб визначити обернене відношення hasMany, визначте метод відношення в дочірній моделі, який викликає метод belongsTo:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Comment extends Model
{
/**
* Отримати допис, якому належить коментар.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}

Після визначення відношення ми можемо отримати батьківський пост коментаря, звернувшись до "динамічної властивості відношення" post:

use App\Models\Comment;
 
$comment = Comment::find(1);
 
return $comment->post->title;

У наведеному вище прикладі Eloquent спробує знайти модель Post, яка має id, що відповідає стовпцю post_id у моделі Comment.

Eloquent визначає ім'я зовнішнього ключа за замовчуванням, досліджуючи ім'я методу відношення та додаючи до імені методу суфікс _, за яким слідує ім'я стовпця первинного ключа батьківської моделі. Отже, в цьому прикладі Eloquent припускатиме, що зовнішній ключ моделі Post у таблиці comments є post_id.

Однак, якщо зовнішній ключ для вашого відношення не відповідає цим конвенціям, ви можете передати власне ім'я зовнішнього ключа як другий аргумент методу belongsTo:

/**
* Отримати допис, якому належить коментар.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}

Якщо ваша батьківська модель не використовує id як первинний ключ, або ви бажаєте знайти пов'язану модель, використовуючи інший стовпець, ви можете передати третій аргумент методу belongsTo, вказуючи власний ключ вашої батьківської таблиці:

/**
* Отримати допис, якому належить коментар.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

Моделі за замовчуванням

Відношення belongsTo, hasOne, hasOneThrough та morphOne дозволяють визначити модель за замовчуванням, яка буде повернена, якщо дане відношення є null. Цей шаблон часто називають Null Object pattern і він може допомогти усунути умовні перевірки у вашому коді. У наступному прикладі відношення user поверне порожню модель App\Models\User, якщо жоден користувач не приєднаний до моделі Post:

/**
 * Отримати автора публікації.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault();
}

Щоб заповнити модель за замовчуванням атрибутами, ви можете передати масив або замикання в метод withDefault:

/**
 * Отримати автора публікації.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault([
        'name' => 'Guest Author',
    ]);
}
 
/**
 * Отримати автора публікації.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
        $user->name = 'Guest Author';
    });
}

Запити до відносин Belongs To

Коли виконуєте запит на отримання дочірніх елементів відношення "належить до", ви можете вручну створити where вираз для отримання відповідних моделей Eloquent:

use App\Models\Post;
 
$posts = Post::where('user_id', $user->id)->get();

Однак, вам може бути зручніше використовувати метод whereBelongsTo, який автоматично визначить відповідні відношення та зовнішній ключ для даної моделі:

$posts = Post::whereBelongsTo($user)->get();

Ви також можете надати екземпляр колекції методу whereBelongsTo. При цьому Laravel отримає моделі, які належать будь-якій з батьківських моделей у колекції:

$users = User::where('vip', true)->get();
 
$posts = Post::whereBelongsTo($users)->get();

За замовчуванням, Laravel визначить зв'язок, пов'язаний з даною моделлю, на основі імені класу моделі; однак, ви можете вказати ім'я зв'язку вручну, надавши його як другий аргумент методу whereBelongsTo:

$posts = Post::whereBelongsTo($user, 'author')->get();

Має Один з Багатьох

Іноді модель може мати багато пов'язаних моделей, але ви хочете легко отримати "найновішу" або "найстарішу" пов'язану модель відносин. Наприклад, модель User може бути пов'язана з багатьма моделями Order, але ви хочете визначити зручний спосіб взаємодії з найновішим замовленням, яке користувач зробив. Ви можете досягти цього, використовуючи тип відносин hasOne у поєднанні з методами ofMany:

/**
 * Отримати останнє замовлення користувача.
 */
public function latestOrder(): HasOne
{
    return $this->hasOne(Order::class)->latestOfMany();
}

Так само, ви можете визначити метод для отримання "найстарішої" або першої пов'язаної моделі відношення:

/**
 * Отримати найстаріше замовлення користувача.
 */
public function oldestOrder(): HasOne
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

За замовчуванням методи latestOfMany та oldestOfMany отримуватимуть останню або найстарішу пов'язану модель на основі первинного ключа моделі, який має бути сортувальним. Однак іноді ви можете захотіти отримати одну модель з більшого відношення, використовуючи інші критерії сортування.

Наприклад, використовуючи метод ofMany, ви можете отримати найдорожче замовлення користувача. Метод ofMany приймає стовпець для сортування як перший аргумент і яку агрегатну функцію (min або max) застосувати при запиті пов'язаної моделі:

/**
 * Отримати найбільше замовлення користувача.
 */
public function largestOrder(): HasOne
{
    return $this->hasOne(Order::class)->ofMany('price', 'max');
}

Оскільки PostgreSQL не підтримує виконання функції MAX для стовпців UUID, наразі неможливо використовувати відносини "один з багатьох" у поєднанні зі стовпцями UUID PostgreSQL.

Перетворення відносин "Багато" на відносини "Має Один"

Часто, коли ви отримуєте одну модель за допомогою методів latestOfMany, oldestOfMany або ofMany, у вас вже визначено відношення "has many" для тієї ж моделі. Для зручності, Laravel дозволяє легко перетворити це відношення у відношення "has one", викликавши метод one на відношенні:

/**
 * Отримати замовлення користувача.
 */
public function orders(): HasMany
{
    return $this->hasMany(Order::class);
}
 
/**
 * Отримати найбільше замовлення користувача.
 */
public function largestOrder(): HasOne
{
    return $this->orders()->one()->ofMany('price', 'max');
}

Ви також можете використовувати метод one для перетворення відносин HasManyThrough на відносини HasOneThrough:

public function latestDeployment(): HasOneThrough
{
    return $this->deployments()->one()->latestOfMany();
}

Розширені Відносини "Має Один з Багатьох"

Можливо створити більш складні відносини "має один з багатьох". Наприклад, модель Product може мати багато пов'язаних моделей Price, які зберігаються в системі навіть після публікації нових цін. Крім того, нові дані про ціни для продукту можуть бути опубліковані заздалегідь, щоб набрати чинності в майбутньому через стовпець published_at.

Отже, підсумовуючи, нам потрібно отримати останню опубліковану ціну, де дата публікації не є в майбутньому. Крім того, якщо дві ціни мають однакову дату публікації, ми віддамо перевагу ціні з найбільшим ID. Для цього ми повинні передати масив до методу ofMany, який містить стовпці для сортування, що визначають останню ціну. Крім того, замикання буде надано як другий аргумент до методу ofMany. Це замикання буде відповідальним за додавання додаткових обмежень дати публікації до запиту відношення:

/**
 * Отримати поточну ціну продукту.
 */
public function currentPricing(): HasOne
{
    return $this->hasOne(Price::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function (Builder $query) {
        $query->where('published_at', '<', now());
    });
}

Має Один Через

Відношення "has-one-through" визначає відношення один-до-одного з іншою моделлю. Однак, це відношення вказує на те, що модель, яка його оголошує, може бути пов'язана з одним екземпляром іншої моделі, проходячи через третю модель.

Наприклад, у застосунку для автомайстерні кожна модель Mechanic може бути пов'язана з однією моделлю Car, і кожна модель Car може бути пов'язана з однією моделлю Owner. Хоча механік і власник не мають прямого зв'язку в базі даних, механік може отримати доступ до власника через модель Car. Давайте розглянемо таблиці, необхідні для визначення цього зв'язку:

mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

Тепер, коли ми розглянули структуру таблиці для відношення, давайте визначимо відношення в моделі Mechanic:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
 
class Mechanic extends Model
{
/**
* Отримати власника автомобіля.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}
}

Першим аргументом, переданим методу hasOneThrough, є назва кінцевої моделі, до якої ми хочемо отримати доступ, тоді як другим аргументом є назва проміжної моделі.

Або, якщо відповідні відносини вже визначені на всіх моделях, що беруть участь у відносинах, ви можете плавно визначити відношення "has-one-through", викликавши метод through і вказавши імена цих відносин. Наприклад, якщо модель Mechanic має відношення cars, а модель Car має відношення owner, ви можете визначити відношення "has-one-through", що з'єднує механіка та власника, наступним чином:

// Синтаксис на основі рядків...
return $this->through('cars')->has('owner');
 
// Динамічний синтаксис...
return $this->throughCars()->hasOwner();

Ключові Конвенції

Типові угоди про зовнішні ключі Eloquent будуть використовуватися при виконанні запитів відношення. Якщо ви хочете налаштувати ключі відношення, ви можете передати їх як третій і четвертий аргументи методу hasOneThrough. Третій аргумент - це назва зовнішнього ключа на проміжній моделі. Четвертий аргумент - це назва зовнішнього ключа на кінцевій моделі. П'ятий аргумент - це локальний ключ, а шостий аргумент - це локальний ключ проміжної моделі:

class Mechanic extends Model
{
/**
* Отримати власника автомобіля.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(
Owner::class,
Car::class,
'mechanic_id', // Зовнішній ключ у таблиці cars...
'car_id', // Зовнішній ключ у таблиці owners...
'id', // Локальний ключ у таблиці mechanics...
'id' // Локальний ключ у таблиці cars...
);
}
}

Або, як обговорювалося раніше, якщо відповідні відносини вже визначені на всіх моделях, що беруть участь у відносинах, ви можете плавно визначити відношення "has-one-through", викликавши метод through і вказавши імена цих відносин. Цей підхід пропонує перевагу повторного використання ключових умовностей, вже визначених на існуючих відносинах:

// Синтаксис на основі рядків...
return $this->through('cars')->has('owner');
 
// Динамічний синтаксис...
return $this->throughCars()->hasOwner();

Має Багато Через

Відношення "has-many-through" надає зручний спосіб доступу до віддалених зв'язків через проміжне відношення. Наприклад, припустимо, що ми створюємо платформу розгортання, таку як Laravel Cloud. Модель Application може отримувати доступ до багатьох моделей Deployment через проміжну модель Environment. Використовуючи цей приклад, ви можете легко зібрати всі розгортання для даного застосунку. Давайте розглянемо таблиці, необхідні для визначення цього відношення:

applications
    id - integer
    name - string

environments
    id - integer
    application_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

Тепер, коли ми розглянули структуру таблиці для відношення, давайте визначимо відношення в моделі Application:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
 
class Application extends Model
{
    /**
     * Отримати всі розгортання для застосунку.
     */
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(Deployment::class, Environment::class);
    }
}

Першим аргументом, переданим методу hasManyThrough, є ім'я кінцевої моделі, до якої ми бажаємо отримати доступ, тоді як другим аргументом є ім'я проміжної моделі.

Або, якщо відповідні відносини вже визначені на всіх моделях, що беруть участь у відносинах, ви можете плавно визначити відношення "has-many-through", викликавши метод through і вказавши імена цих відносин. Наприклад, якщо модель Application має відношення environments, а модель Environment має відношення deployments, ви можете визначити відношення "has-many-through", що з'єднує застосунок і розгортання, наступним чином:

// Синтаксис на основі рядків...
return $this->through('environments')->has('deployments');
 
// Динамічний синтаксис...
return $this->throughEnvironments()->hasDeployments();

Хоча таблиця моделі Deployment не містить стовпця application_id, відношення hasManyThrough надає доступ до розгортань застосунку через $application->deployments. Щоб отримати ці моделі, Eloquent перевіряє стовпець application_id у таблиці проміжної моделі Environment. Після знаходження відповідних ідентифікаторів середовища вони використовуються для запиту таблиці моделі Deployment.

Ключові Конвенції

Типові угоди про зовнішні ключі Eloquent будуть використовуватися при виконанні запитів відношення. Якщо ви хочете налаштувати ключі відношення, ви можете передати їх як третій і четвертий аргументи методу hasManyThrough. Третій аргумент - це назва зовнішнього ключа на проміжній моделі. Четвертий аргумент - це назва зовнішнього ключа на кінцевій моделі. П'ятий аргумент - це локальний ключ, а шостий аргумент - це локальний ключ проміжної моделі:

class Application extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'application_id', // Зовнішній ключ у таблиці environments...
'environment_id', // Зовнішній ключ у таблиці deployments...
'id', // Локальний ключ у таблиці applications...
'id' // Локальний ключ у таблиці environments...
);
}
}

Або, як обговорювалося раніше, якщо відповідні відносини вже визначені на всіх моделях, що беруть участь у відносинах, ви можете плавно визначити відношення "has-many-through", викликавши метод through і вказавши імена цих відносин. Цей підхід пропонує перевагу повторного використання ключових умовностей, вже визначених на існуючих відносинах:

// Синтаксис на основі рядків...
return $this->through('environments')->has('deployments');
 
// Динамічний синтаксис...
return $this->throughEnvironments()->hasDeployments();

Відносини з обмеженням області

Зазвичай до моделей додають додаткові методи, які обмежують відносини. Наприклад, ви можете додати метод featuredPosts до моделі User, який обмежує ширші відносини posts додатковим обмеженням where:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class User extends Model
{
    /**
     * Отримати пости користувача.
     */
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class)->latest();
    }
 
    /**
     * Отримати рекомендовані пости користувача.
     */
    public function featuredPosts(): HasMany
    {
        return $this->posts()->where('featured', true);
    }
}

Однак, якщо ви спробуєте створити модель через метод featuredPosts, її атрибут featured не буде встановлено в true. Якщо ви хочете створювати моделі через методи відносин і також вказати атрибути, які повинні бути додані до всіх моделей, створених через цю відносину, ви можете використовувати метод withAttributes при побудові запиту відносини:

/**
 * Отримати рекомендовані пости користувача.
 */
public function featuredPosts(): HasMany
{
    return $this->posts()->withAttributes(['featured' => true]);
}

Метод withAttributes додасть умови where до запиту, використовуючи задані атрибути, і також додасть задані атрибути до будь-яких моделей, створених через метод відношення:

$post = $user->featuredPosts()->create(['title' => 'Featured Post']);
 
$post->featured; // true

Щоб вказати методу withAttributes не додавати умови where до запиту, ви можете встановити аргумент asConditions в false:

return $this->posts()->withAttributes(['featured' => true], asConditions: false);

Багато до Багатьох Відносини

Багато-до-багатьох відносини є трохи складнішими, ніж відносини hasOne та hasMany. Прикладом відносин багато-до-багатьох є користувач, який має багато ролей, і ці ролі також поділяються з іншими користувачами в застосунку. Наприклад, користувачу може бути призначена роль "Автор" і "Редактор"; однак ці ролі можуть бути призначені й іншим користувачам. Отже, користувач має багато ролей, а роль має багато користувачів.

Структура таблиці

Щоб визначити цей зв'язок, потрібні три таблиці бази даних: users, roles і role_user. Таблиця role_user утворена з алфавітного порядку імен пов'язаних моделей і містить стовпці user_id і role_id. Ця таблиця використовується як проміжна таблиця, що з'єднує користувачів і ролі.

Пам'ятайте, оскільки роль може належати багатьом користувачам, ми не можемо просто розмістити стовпець user_id у таблиці roles. Це означало б, що роль може належати лише одному користувачу. Щоб забезпечити підтримку призначення ролей кільком користувачам, потрібна таблиця role_user. Ми можемо підсумувати структуру таблиці відносин таким чином:

users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

Структура моделі

Багато-до-багатьох відносини визначаються шляхом написання методу, який повертає результат методу belongsToMany. Метод belongsToMany надається базовим класом Illuminate\Database\Eloquent\Model, який використовується всіма Eloquent моделями вашого застосунку. Наприклад, давайте визначимо метод roles у нашій моделі User. Перший аргумент, переданий цьому методу, - це ім'я класу пов'язаної моделі:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class User extends Model
{
    /**
     * Ролі, які належать користувачу.
     */
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
    }
}

Після визначення відношення, ви можете отримати доступ до ролей користувача, використовуючи динамічну властивість відношення roles:

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->roles as $role) {
    // ...
}

Оскільки всі відносини також служать як конструктори запитів, ви можете додати подальші обмеження до запиту відносин, викликавши метод roles і продовжуючи додавати умови до запиту:

$roles = User::find(1)->roles()->orderBy('name')->get();

Щоб визначити назву таблиці проміжної таблиці відношення, Eloquent об'єднає назви двох пов'язаних моделей в алфавітному порядку. Однак ви можете змінити цю конвенцію. Ви можете зробити це, передавши другий аргумент методу belongsToMany:

return $this->belongsToMany(Role::class, 'role_user');

Крім налаштування назви проміжної таблиці, ви також можете налаштувати назви стовпців ключів у таблиці, передаючи додаткові аргументи до методу belongsToMany. Третій аргумент — це назва зовнішнього ключа моделі, для якої ви визначаєте зв'язок, а четвертий аргумент — це назва зовнішнього ключа моделі, до якої ви приєднуєтеся:

return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');

Визначення зворотного зв'язку

Щоб визначити "зворотний" бік відношення багато-до-багатьох, ви повинні визначити метод у пов'язаній моделі, який також повертає результат методу belongsToMany. Щоб завершити наш приклад користувач / роль, давайте визначимо метод users у моделі Role:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Role extends Model
{
    /**
     * Користувачі, які належать до ролі.
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class);
    }
}

Як ви можете бачити, відношення визначено точно так само, як і його аналог у моделі User, за винятком посилання на модель App\Models\User. Оскільки ми повторно використовуємо метод belongsToMany, всі звичайні параметри налаштування таблиць і ключів доступні при визначенні "зворотного" відношення багато-до-багатьох.

Отримання Стовпців Проміжної Таблиці

Як ви вже дізналися, робота з відносинами "багато-до-багатьох" вимагає наявності проміжної таблиці. Eloquent надає кілька дуже корисних способів взаємодії з цією таблицею. Наприклад, припустимо, що наша модель User має багато моделей Role, з якими вона пов'язана. Після доступу до цього відношення ми можемо отримати доступ до проміжної таблиці, використовуючи атрибут pivot на моделях:

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

Зверніть увагу, що кожна модель Role, яку ми отримуємо, автоматично отримує атрибут pivot. Цей атрибут містить модель, що представляє проміжну таблицю.

За замовчуванням на моделі pivot будуть присутні лише ключі моделі. Якщо ваша проміжна таблиця містить додаткові атрибути, ви повинні вказати їх при визначенні відношення:

return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');

Якщо ви хочете, щоб ваша проміжна таблиця мала часові мітки created_at та updated_at, які автоматично підтримуються Eloquent, викличте метод withTimestamps при визначенні відношення:

return $this->belongsToMany(Role::class)->withTimestamps();

Проміжні таблиці, які використовують автоматично підтримувані часові мітки Eloquent, повинні мати обидва стовпці часових міток created_at та updated_at.

Налаштування імені атрибута pivot

Як зазначалося раніше, атрибути з проміжної таблиці можуть бути доступні в моделях через атрибут pivot. Однак ви можете змінити назву цього атрибута, щоб краще відобразити його призначення у вашому застосунку.

Наприклад, якщо ваш застосунок містить користувачів, які можуть підписуватися на подкасти, у вас, ймовірно, є зв'язок багато-до-багатьох між користувачами та подкастами. Якщо це так, ви можете захотіти перейменувати атрибут проміжної таблиці на subscription замість pivot. Це можна зробити за допомогою методу as при визначенні зв'язку:

return $this->belongsToMany(Podcast::class)
    ->as('subscription')
    ->withTimestamps();

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

$users = User::with('podcasts')->get();
 
foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->subscription->created_at;
}

Фільтрація запитів через стовпці проміжної таблиці

Ви також можете фільтрувати результати, повернені запитами відносин belongsToMany, використовуючи методи wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNull та wherePivotNotNull при визначенні відносин:

return $this->belongsToMany(Role::class)
    ->wherePivot('approved', 1);
 
return $this->belongsToMany(Role::class)
    ->wherePivotIn('priority', [1, 2]);
 
return $this->belongsToMany(Role::class)
    ->wherePivotNotIn('priority', [1, 2]);
 
return $this->belongsToMany(Podcast::class)
    ->as('subscriptions')
    ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
 
return $this->belongsToMany(Podcast::class)
    ->as('subscriptions')
    ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
 
return $this->belongsToMany(Podcast::class)
    ->as('subscriptions')
    ->wherePivotNull('expired_at');
 
return $this->belongsToMany(Podcast::class)
    ->as('subscriptions')
    ->wherePivotNotNull('expired_at');

Метод wherePivot додає обмеження у вигляді виразу where до запиту, але не додає вказане значення при створенні нових моделей через визначений зв'язок. Якщо вам потрібно як запитувати, так і створювати зв'язки з певним значенням у таблиці зв'язків, ви можете використовувати метод withPivotValue:

return $this->belongsToMany(Role::class)
    ->withPivotValue('approved', 1);

Упорядкування запитів через стовпці проміжної таблиці

Ви можете впорядкувати результати, повернені запитами відносин belongsToMany, використовуючи метод orderByPivot. У наступному прикладі ми отримаємо всі останні значки для користувача:

return $this->belongsToMany(Badge::class)
    ->where('rank', 'gold')
    ->orderByPivot('created_at', 'desc');

Визначення Користувацьких Моделей Проміжних Таблиць

Якщо ви хочете визначити власну модель для представлення проміжної таблиці вашого зв'язку "багато-до-багатьох", ви можете викликати метод using при визначенні зв'язку. Власні моделі з'єднання дають вам можливість визначити додаткову поведінку на моделі з'єднання, таку як методи та касти.

Користувацькі моделі з'єднання "багато-до-багатьох" повинні наслідувати клас Illuminate\Database\Eloquent\Relations\Pivot, тоді як користувацькі поліморфні моделі з'єднання "багато-до-багатьох" повинні наслідувати клас Illuminate\Database\Eloquent\Relations\MorphPivot. Наприклад, ми можемо визначити модель Role, яка використовує користувацьку модель з'єднання RoleUser:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Role extends Model
{
    /**
     * Користувачі, які належать до ролі.
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class)->using(RoleUser::class);
    }
}

Коли визначаєте модель RoleUser, ви повинні успадкувати клас Illuminate\Database\Eloquent\Relations\Pivot:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Relations\Pivot;
 
class RoleUser extends Pivot
{
    // ...
}

Моделі з'єднання не можуть використовувати трейта SoftDeletes. Якщо вам потрібно м'яко видаляти записи з'єднання, розгляньте можливість перетворення вашої моделі з'єднання на фактичну модель Eloquent.

Користувацькі моделі Pivot та інкрементні ID

Якщо ви визначили відношення "багато-до-багатьох", яке використовує користувацьку проміжну модель, і ця проміжна модель має автоінкрементний первинний ключ, ви повинні переконатися, що ваш клас користувацької проміжної моделі визначає властивість incrementing, яка встановлена в true.

/**
 * Вказує, чи є ID автоінкрементними.
 *
 * @var bool
 */
public $incrementing = true;

Поліморфні відносини

Поліморфний зв'язок дозволяє дочірній моделі належати до більше ніж одного типу моделі, використовуючи єдину асоціацію. Наприклад, уявіть, що ви створюєте застосунок, який дозволяє користувачам ділитися блог-постами та відео. У такому застосунку модель Comment може належати як до моделі Post, так і до моделі Video.

Один до одного (Поліморфний)

Структура таблиці

Одно-до-одного поліморфний зв'язок схожий на типовий одно-до-одного зв'язок; однак, дочірня модель може належати до більш ніж одного типу моделі, використовуючи єдину асоціацію. Наприклад, блог Post і User можуть мати поліморфний зв'язок з моделлю Image. Використання одно-до-одного поліморфного зв'язку дозволяє мати єдину таблицю унікальних зображень, які можуть бути пов'язані з постами та користувачами. Спочатку давайте розглянемо структуру таблиці:

posts
    id - integer
    name - string

users
    id - integer
    name - string

images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

Зверніть увагу на стовпці imageable_id та imageable_type у таблиці images. Стовпець imageable_id міститиме значення ID поста або користувача, тоді як стовпець imageable_type міститиме ім'я класу батьківської моделі. Стовпець imageable_type використовується Eloquent для визначення, який "тип" батьківської моделі повернути при доступі до відношення imageable. У цьому випадку стовпець міститиме або App\Models\Post, або App\Models\User.

Структура моделі

Далі давайте розглянемо визначення моделей, необхідні для побудови цього відношення:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Image extends Model
{
    /**
     * Отримати батьківську модель imageable (користувач або пост).
     */
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
 
class Post extends Model
{
    /**
     * Отримати зображення публікації.
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
 
class User extends Model
{
    /**
     * Отримати зображення користувача.
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

Отримання Відношення

Після того як ваша таблиця бази даних і моделі визначені, ви можете отримати доступ до відносин через ваші моделі. Наприклад, щоб отримати зображення для поста, ми можемо звернутися до динамічної властивості відносин image:

use App\Models\Post;
 
$post = Post::find(1);
 
$image = $post->image;

Ви можете отримати батьківську модель поліморфної моделі, звернувшись до назви методу, який виконує виклик до morphTo. У цьому випадку це метод imageable у моделі Image. Отже, ми будемо звертатися до цього методу як до динамічної властивості відношення:

use App\Models\Image;
 
$image = Image::find(1);
 
$imageable = $image->imageable;

Відношення imageable у моделі Image поверне або екземпляр Post, або User, залежно від того, який тип моделі володіє зображенням.

Ключові Конвенції

Якщо необхідно, ви можете вказати назву стовпців "id" та "type", які використовуються вашим поліморфним дочірнім моделлю. Якщо ви це робите, переконайтеся, що завжди передаєте назву відношення як перший аргумент методу morphTo. Зазвичай, це значення має відповідати назві методу, тому ви можете використовувати константу PHP __FUNCTION__:

/**
 * Отримати модель, до якої належить зображення.
 */
public function imageable(): MorphTo
{
    return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}

Один до багатьох (Поліморфний)

Структура таблиці

Одновідношення поліморфного зв'язку схоже на типове одновідношення; однак, дочірня модель може належати до більше ніж одного типу моделі, використовуючи єдину асоціацію. Наприклад, уявіть, що користувачі вашого застосунку можуть "коментувати" пости та відео. Використовуючи поліморфні зв'язки, ви можете використовувати єдину таблицю comments для зберігання коментарів як для постів, так і для відео. Спочатку давайте розглянемо структуру таблиці, необхідну для побудови цього зв'язку:

posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

Структура моделі

Далі давайте розглянемо визначення моделей, необхідні для побудови цього відношення:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Comment extends Model
{
    /**
     * Отримати батьківську модель, що може коментуватися (пост або відео).
     */
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Post extends Model
{
    /**
     * Отримати всі коментарі до публікації.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Video extends Model
{
    /**
     * Отримати всі коментарі до відео.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Отримання Відношення

Після того як ваша таблиця бази даних і моделі визначені, ви можете отримати доступ до зв'язків через динамічні властивості зв'язків вашої моделі. Наприклад, щоб отримати доступ до всіх коментарів для поста, ми можемо використовувати динамічну властивість comments:

use App\Models\Post;
 
$post = Post::find(1);
 
foreach ($post->comments as $comment) {
    // ...
}

Ви також можете отримати батьківську модель поліморфної дочірньої моделі, звернувшись до назви методу, який виконує виклик до morphTo. У цьому випадку це метод commentable у моделі Comment. Отже, ми звернемося до цього методу як до динамічної властивості відношення, щоб отримати доступ до батьківської моделі коментаря:

use App\Models\Comment;
 
$comment = Comment::find(1);
 
$commentable = $comment->commentable;

Відношення commentable у моделі Comment поверне або екземпляр Post, або Video, залежно від того, який тип моделі є батьківським для коментаря.

Автоматичне підключення батьківських моделей до дочірніх

Навіть при використанні Eloquent eager loading, проблеми з запитами "N + 1" можуть виникати, якщо ви намагаєтеся отримати доступ до батьківської моделі з дочірньої моделі під час перебору дочірніх моделей:

$posts = Post::with('comments')->get();
 
foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->commentable->title;
    }
}

У наведеному вище прикладі виникла проблема запиту "N + 1", оскільки, навіть якщо коментарі були завантажені заздалегідь для кожної моделі Post, Eloquent не автоматично заповнює батьківську модель Post для кожної дочірньої моделі Comment.

Якщо ви хочете, щоб Eloquent автоматично заповнював батьківські моделі їхніми дочірніми елементами, ви можете викликати метод chaperone при визначенні відношення morphMany:

class Post extends Model
{
    /**
     * Отримати всі коментарі до публікації.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable')->chaperone();
    }
}

Або, якщо ви хочете увімкнути автоматичне заповнення батьків під час виконання, ви можете викликати модель chaperone при жадібному завантаженні відношення:

use App\Models\Post;
 
$posts = Post::with([
    'comments' => fn ($comments) => $comments->chaperone(),
])->get();

Один з багатьох (Поліморфний)

Іноді модель може мати багато пов'язаних моделей, але ви хочете легко отримати "найновішу" або "найстарішу" пов'язану модель відносин. Наприклад, модель User може бути пов'язана з багатьма моделями Image, але ви хочете визначити зручний спосіб взаємодії з найновішим зображенням, яке користувач завантажив. Ви можете досягти цього, використовуючи тип відносин morphOne у поєднанні з методами ofMany:

/**
 * Отримати найновіше зображення користувача.
 */
public function latestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

Так само, ви можете визначити метод для отримання "найстарішої" або першої пов'язаної моделі відношення:

/**
 * Отримати найстаріше зображення користувача.
 */
public function oldestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

За замовчуванням методи latestOfMany та oldestOfMany отримуватимуть останню або найстарішу пов'язану модель на основі первинного ключа моделі, який має бути сортувальним. Однак іноді ви можете захотіти отримати одну модель з більшого відношення, використовуючи інші критерії сортування.

Наприклад, використовуючи метод ofMany, ви можете отримати найбільш "улюблене" зображення користувача. Метод ofMany приймає стовпець для сортування як свій перший аргумент і яку агрегатну функцію (min або max) застосувати при запиті пов'язаної моделі:

/**
 * Отримати найпопулярніше зображення користувача.
 */
public function bestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}

Можна створити більш складні відносини "один з багатьох". Для отримання додаткової інформації, будь ласка, зверніться до документації "has one of many".

Багато до Багатьох (Поліморфний)

Структура таблиці

Багато-до-багатьох поліморфні відносини є трохи складнішими, ніж відносини "morph one" і "morph many". Наприклад, модель Post і модель Video можуть мати поліморфне відношення до моделі Tag. Використання багато-до-багатьох поліморфного відношення в цій ситуації дозволить вашому застосунку мати єдину таблицю унікальних тегів, які можуть бути пов'язані з постами або відео. Спочатку давайте розглянемо структуру таблиці, необхідну для побудови цього відношення:

posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string

Перш ніж зануритися в поліморфні відносини "багато-до-багатьох", вам може бути корисно ознайомитися з документацією про типові відносини "багато-до-багатьох".

Структура Моделі

Далі ми готові визначити відносини в моделях. Моделі Post та Video обидві міститимуть метод tags, який викликає метод morphToMany, наданий базовим класом моделі Eloquent.

Метод morphToMany приймає назву пов'язаної моделі, а також "назву відношення". Виходячи з назви, яку ми призначили нашій проміжній таблиці та ключам, які вона містить, ми будемо посилатися на відношення як "taggable":

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
 
class Post extends Model
{
    /**
     * Отримати всі теги для поста.
     */
    public function tags(): MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

Визначення зворотнього відношення

Далі, у моделі Tag, ви повинні визначити метод для кожної з її можливих батьківських моделей. Отже, в цьому прикладі ми визначимо метод posts та метод videos. Обидва ці методи повинні повертати результат методу morphedByMany.

Метод morphedByMany приймає назву пов'язаної моделі, а також "назву відношення". Виходячи з назви, яку ми призначили нашій проміжній таблиці та ключам, які вона містить, ми будемо посилатися на відношення як "taggable":

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
 
class Tag extends Model
{
    /**
     * Отримати всі пости, які призначені цьому тегу.
     */
    public function posts(): MorphToMany
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
 
    /**
     * Отримати всі відео, яким призначено цей тег.
     */
    public function videos(): MorphToMany
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

Отримання Відношення

Після того як ваша таблиця бази даних і моделі визначені, ви можете отримати доступ до зв'язків через ваші моделі. Наприклад, щоб отримати доступ до всіх тегів для поста, ви можете використовувати динамічну властивість зв'язку tags:

use App\Models\Post;
 
$post = Post::find(1);
 
foreach ($post->tags as $tag) {
    // ...
}

Ви можете отримати батьківську модель поліморфного зв'язку з поліморфної дочірньої моделі, звернувшись до назви методу, який виконує виклик до morphedByMany. У цьому випадку це методи posts або videos у моделі Tag:

use App\Models\Tag;
 
$tag = Tag::find(1);
 
foreach ($tag->posts as $post) {
    // ...
}
 
foreach ($tag->videos as $video) {
    // ...
}

Користувацькі поліморфні типи

За замовчуванням Laravel використовуватиме повністю кваліфіковане ім'я класу для зберігання "типу" пов'язаної моделі. Наприклад, у наведеному вище прикладі відношення "один до багатьох", де модель Comment може належати до моделі Post або Video, значення за замовчуванням для commentable_type буде або App\Models\Post, або App\Models\Video відповідно. Однак, ви можете бажати відокремити ці значення від внутрішньої структури вашого застосунку.

Наприклад, замість використання назв моделей як "типу", ми можемо використовувати прості рядки, такі як post і video. Роблячи так, значення поліморфного стовпця "тип" у нашій базі даних залишаться дійсними, навіть якщо моделі будуть перейменовані:

use Illuminate\Database\Eloquent\Relations\Relation;
 
Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);

Ви можете викликати метод enforceMorphMap у методі boot вашого класу App\Providers\AppServiceProvider або створити окремий сервіс-провайдер, якщо бажаєте.

Ви можете визначити морф-аліас даної моделі під час виконання, використовуючи метод моделі getMorphClass. Навпаки, ви можете визначити повністю кваліфіковане ім'я класу, пов'язане з морф-аліасом, використовуючи метод Relation::getMorphedModel:

use Illuminate\Database\Eloquent\Relations\Relation;
 
$alias = $post->getMorphClass();
 
$class = Relation::getMorphedModel($alias);

Коли додаєте "morph map" до вашого існуючого застосунку, кожне значення стовпця *_type у вашій базі даних, яке все ще містить повністю кваліфікований клас, потрібно буде перетворити на його ім'я з "map".

Динамічні Відносини

Ви можете використовувати метод resolveRelationUsing для визначення відносин між моделями Eloquent під час виконання. Хоча це зазвичай не рекомендується для звичайної розробки застосунків, це може бути корисним при розробці пакетів Laravel.

Метод resolveRelationUsing приймає бажане ім'я відношення як свій перший аргумент. Другий аргумент, переданий методу, повинен бути замиканням, яке приймає екземпляр моделі та повертає дійсне визначення відношення Eloquent. Зазвичай, ви повинні налаштовувати динамічні відношення в межах методу boot у Сервіс-провайдері:

use App\Models\Order;
use App\Models\Customer;
 
Order::resolveRelationUsing('customer', function (Order $orderModel) {
    return $orderModel->belongsTo(Customer::class, 'customer_id');
});

Коли визначаєте динамічні відносини, завжди надавайте явні аргументи імен ключів методам відносин Eloquent.

Запити до відносин

Оскільки всі відносини Eloquent визначаються через методи, ви можете викликати ці методи, щоб отримати екземпляр відносин без фактичного виконання запиту для завантаження пов'язаних моделей. Крім того, всі типи відносин Eloquent також слугують конструкторами запитів, дозволяючи вам продовжувати додавати обмеження до запиту відносин перед остаточним виконанням SQL-запиту до вашої бази даних.

Наприклад, уявіть собі блог-застосунок, в якому модель User має багато пов'язаних моделей Post:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class User extends Model
{
    /**
     * Отримати всі пости для користувача.
     */
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

Ви можете виконати запит до відношення posts і додати додаткові обмеження до відношення наступним чином:

use App\Models\User;
 
$user = User::find(1);
 
$user->posts()->where('active', 1)->get();

Ви можете використовувати будь-які методи конструктора запитів Laravel у відношенні, тому обов'язково ознайомтеся з документацією конструктора запитів, щоб дізнатися про всі доступні вам методи.

Зв'язування виразів orWhere після відносин

Як показано в наведеному вище прикладі, ви можете додавати додаткові обмеження до відносин під час запиту до них. Однак будьте обережні, коли додаєте вирази orWhere до відносин, оскільки вирази orWhere будуть логічно згруповані на тому ж рівні, що й обмеження відносин:

$user->posts()
    ->where('active', 1)
    ->orWhere('votes', '>=', 100)
    ->get();

Приклад вище згенерує наступний SQL. Як ви можете бачити, вираз or вказує запиту повернути будь-який пост з більше ніж 100 голосами. Запит більше не обмежений конкретним користувачем:

select *
from posts
where user_id = ? and active = 1 or votes >= 100

У більшості випадків слід використовувати логічні групи для групування умовних перевірок у дужках:

use Illuminate\Database\Eloquent\Builder;
 
$user->posts()
    ->where(function (Builder $query) {
        return $query->where('active', 1)
            ->orWhere('votes', '>=', 100);
    })
    ->get();

Наведений вище приклад створить наступний SQL. Зверніть увагу, що логічне групування правильно згрупувало обмеження, і запит залишається обмеженим до конкретного користувача:

select *
from posts
where user_id = ? and (active = 1 or votes >= 100)

Методи Відносин vs. Динамічні Властивості

Якщо вам не потрібно додавати додаткові обмеження до запиту відношення Eloquent, ви можете отримати доступ до відношення так, ніби це властивість. Наприклад, продовжуючи використовувати наші моделі User та Post, ми можемо отримати доступ до всіх постів користувача ось так:

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->posts as $post) {
    // ...
}

Динамічні властивості відносин виконують "ліниве завантаження", тобто вони завантажують дані відносин лише тоді, коли ви фактично до них звертаєтеся. Через це розробники часто використовують жадібне завантаження для попереднього завантаження відносин, які вони знають, що будуть доступні після завантаження моделі. Жадібне завантаження забезпечує значне зменшення кількості SQL-запитів, які потрібно виконати для завантаження відносин моделі.

Запит на існування відносин

Коли ви отримуєте записи моделі, можливо, ви захочете обмежити результати на основі існування відношення. Наприклад, уявіть, що ви хочете отримати всі пости блогу, які мають принаймні один коментар. Для цього ви можете передати назву відношення методам has та orHas:

use App\Models\Post;
 
// Отримати всі пости, які мають принаймні один коментар...
$posts = Post::has('comments')->get();

Ви також можете вказати оператор і значення кількості для подальшого налаштування запиту:

// Отримати всі пости, які мають три або більше коментарів...
$posts = Post::has('comments', '>=', 3)->get();

Вкладені інструкції has можуть бути побудовані з використанням нотації "крапка". Наприклад, ви можете отримати всі пости, які мають принаймні один коментар, що має принаймні одне зображення:

// Отримати пости, які мають принаймні один коментар з зображеннями...
$posts = Post::has('comments.images')->get();

Якщо вам потрібно ще більше можливостей, ви можете використовувати методи whereHas та orWhereHas для визначення додаткових обмежень запиту у ваших запитах has, наприклад, для перевірки вмісту коментаря:

use Illuminate\Database\Eloquent\Builder;
 
// Отримати пости з принаймні одним коментарем, що містить слова на кшталт code%...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();
 
// Отримати пости з принаймні десятьма коментарями, що містять слова на кшталт code%...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
}, '>=', 10)->get();

Eloquent наразі не підтримує запити на існування відносин між різними базами даних. Відносини повинні існувати в межах однієї бази даних.

Запити на існування відношень "багато до багатьох"

Метод whereAttachedTo може бути використаний для запиту моделей, які мають зв'язок "багато до багатьох" з моделлю або колекцією моделей:

$users = User::whereAttachedTo($role)->get();

Ви також можете надати екземпляр колекції методу whereAttachedTo. При цьому Laravel отримає моделі, які прикріплені до будь-якої з моделей у колекції:

$tags = Tag::whereLike('name', '%laravel%')->get();
 
$posts = Post::whereAttachedTo($tags)->get();

Запити на існування відносин вбудовані в рядок

Якщо ви хочете виконати запит на існування відношення з однією простою умовою where, прикріпленою до запиту відношення, вам може бути зручніше використовувати методи whereRelation, orWhereRelation, whereMorphRelation та orWhereMorphRelation. Наприклад, ми можемо виконати запит на всі пости, які мають непідтверджені коментарі:

use App\Models\Post;
 
$posts = Post::whereRelation('comments', 'is_approved', false)->get();

Звичайно, як і виклики методу where конструктора запитів, ви також можете вказати оператор:

$posts = Post::whereRelation(
    'comments', 'created_at', '>=', now()->subHour()
)->get();

Запити на відсутність відносин

Коли ви отримуєте записи моделі, можливо, ви захочете обмежити результати на основі відсутності відношення. Наприклад, уявіть, що ви хочете отримати всі пости блогу, які не мають жодних коментарів. Для цього ви можете передати назву відношення до методів doesntHave та orDoesntHave:

use App\Models\Post;
 
$posts = Post::doesntHave('comments')->get();

Якщо вам потрібно ще більше можливостей, ви можете використовувати методи whereDoesntHave та orWhereDoesntHave для додавання додаткових обмежень запиту до ваших запитів doesntHave, наприклад, для перевірки вмісту коментаря:

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::whereDoesntHave('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

Ви можете використовувати нотацію "dot" для виконання запиту до вкладених відносин. Наприклад, наступний запит отримає всі пости, які не мають коментарів, а також пости, які мають коментарі, де жоден з коментарів не від заблокованих користувачів:

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
    $query->where('banned', 1);
})->get();

Запити до відносин Morph To

Щоб виконати запит на існування відносин "morph to", ви можете використовувати методи whereHasMorph та whereDoesntHaveMorph. Ці методи приймають назву відносин як свій перший аргумент. Далі методи приймають назви пов'язаних моделей, які ви бажаєте включити в запит. Нарешті, ви можете надати замикання, яке налаштовує запит відносин:

use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;
 
// Отримати коментарі, пов'язані з постами або відео, з заголовком, схожим на code%...
$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();
 
// Отримати коментарі, пов'язані з постами, заголовок яких не схожий на code%...
$comments = Comment::whereDoesntHaveMorph(
    'commentable',
    Post::class,
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

Ви можете час від часу потребувати додати обмеження запиту на основі "типу" пов'язаної поліморфної моделі. Замикання, передане методу whereHasMorph, може отримати значення $type як другий аргумент. Цей аргумент дозволяє вам перевірити "тип" запиту, який будується:

use Illuminate\Database\Eloquent\Builder;
 
$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query, string $type) {
        $column = $type === Post::class ? 'content' : 'title';
 
        $query->where($column, 'like', 'code%');
    }
)->get();

Іноді ви можете захотіти виконати запит для дочірніх елементів батьківського елемента відношення "morph to". Ви можете досягти цього, використовуючи методи whereMorphedTo та whereNotMorphedTo, які автоматично визначать відповідне відображення morph типу для даної моделі. Ці методи приймають назву відношення morphTo як свій перший аргумент і пов'язану батьківську модель як свій другий аргумент:

$comments = Comment::whereMorphedTo('commentable', $post)
    ->orWhereMorphedTo('commentable', $video)
    ->get();

Замість передачі масиву можливих поліморфних моделей, ви можете надати * як значення-узагальнення. Це вкаже Laravel отримати всі можливі поліморфні типи з бази даних. Laravel виконає додатковий запит для виконання цієї операції:

use Illuminate\Database\Eloquent\Builder;
 
$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
    $query->where('title', 'like', 'foo%');
})->get();

Іноді ви можете захотіти підрахувати кількість пов'язаних моделей для заданого відношення, не завантажуючи самі моделі. Для цього ви можете використовувати метод withCount. Метод withCount додасть атрибут {relation}_count до отриманих моделей:

use App\Models\Post;
 
$posts = Post::withCount('comments')->get();
 
foreach ($posts as $post) {
    echo $post->comments_count;
}

Передаючи масив методу withCount, ви можете додати "кількість" для декількох відносин, а також додати додаткові обмеження до запитів:

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
    $query->where('content', 'like', 'code%');
}])->get();
 
echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

Ви також можете задати псевдонім для результату підрахунку відносин, що дозволяє виконувати кілька підрахунків для тих самих відносин:

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::withCount([
    'comments',
    'comments as pending_comments_count' => function (Builder $query) {
        $query->where('approved', false);
    },
])->get();
 
echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;

Відкладене завантаження підрахунку

Використовуючи метод loadCount, ви можете завантажити кількість відносин після того, як батьківська модель вже була отримана:

$book = Book::first();
 
$book->loadCount('genres');

Якщо вам потрібно встановити додаткові обмеження запиту на підрахунок, ви можете передати масив, ключами якого є відносини, які ви бажаєте підрахувати. Значення масиву повинні бути замиканнями, які отримують екземпляр конструктора запитів:

$book->loadCount(['reviews' => function (Builder $query) {
    $query->where('rating', 5);
}])

Підрахунок Відносин та Користувацькі Інструкції Select

Якщо ви поєднуєте withCount з інструкцією select, переконайтеся, що ви викликаєте withCount після методу select:

$posts = Post::select(['title', 'body'])
    ->withCount('comments')
    ->get();

Інші агрегатні функції

На додаток до методу withCount, Eloquent надає методи withMin, withMax, withAvg, withSum та withExists. Ці методи розмістять атрибут {relation}_{function}_{column} на ваших отриманих моделях:

use App\Models\Post;
 
$posts = Post::withSum('comments', 'votes')->get();
 
foreach ($posts as $post) {
    echo $post->comments_sum_votes;
}

Якщо ви бажаєте отримати доступ до результату агрегатної функції під іншим ім'ям, ви можете вказати свій власний псевдонім:

$posts = Post::withSum('comments as total_comments', 'votes')->get();
 
foreach ($posts as $post) {
    echo $post->total_comments;
}

Як і метод loadCount, відкладені версії цих методів також доступні. Ці додаткові агрегатні операції можуть виконуватися на моделях Eloquent, які вже були отримані:

$post = Post::first();
 
$post->loadSum('comments', 'votes');

Якщо ви поєднуєте ці методи агрегації з інструкцією select, переконайтеся, що ви викликаєте методи агрегації після методу select:

$posts = Post::select(['title', 'body'])
    ->withExists('comments')
    ->get();

Якщо ви хочете завантажити "morph to" відношення заздалегідь, а також підрахунки пов'язаних моделей для різних сутностей, які можуть бути повернені цим відношенням, ви можете скористатися методом with у поєднанні з методом morphWithCount відношення morphTo.

У цьому прикладі припустимо, що моделі Photo та Post можуть створювати моделі ActivityFeed. Ми припустимо, що модель ActivityFeed визначає відношення "morph to" під назвою parentable, яке дозволяє нам отримати батьківську модель Photo або Post для даного екземпляра ActivityFeed. Додатково, припустимо, що моделі Photo "мають багато" моделей Tag, а моделі Post "мають багато" моделей Comment.

Тепер уявімо, що ми хочемо отримати екземпляри ActivityFeed і завантажити з нетерпінням батьківські моделі parentable для кожного екземпляра ActivityFeed. Крім того, ми хочемо отримати кількість тегів, які пов'язані з кожною батьківською фотографією, і кількість коментарів, які пов'язані з кожним батьківським постом:

use Illuminate\Database\Eloquent\Relations\MorphTo;
 
$activities = ActivityFeed::with([
    'parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWithCount([
            Photo::class => ['tags'],
            Post::class => ['comments'],
        ]);
    }])->get();

Відкладене Завантаження Кількості

Давайте припустимо, що ми вже отримали набір моделей ActivityFeed і тепер хочемо завантажити кількість вкладених відносин для різних моделей parentable, пов'язаних з активними стрічками. Ви можете використовувати метод loadMorphCount для цього:

$activities = ActivityFeed::with('parentable')->get();
 
$activities->loadMorphCount('parentable', [
    Photo::class => ['tags'],
    Post::class => ['comments'],
]);

Жадне завантаження

Коли ви отримуєте доступ до відносин Eloquent як до властивостей, пов'язані моделі завантажуються "ліниво". Це означає, що дані відносин фактично не завантажуються, поки ви вперше не звернетеся до властивості. Однак, Eloquent може "жадібно завантажувати" відносини в момент, коли ви запитуєте батьківську модель. Жадібне завантаження полегшує проблему запитів "N + 1". Щоб проілюструвати проблему запитів N + 1, розгляньте модель Book, яка "належить" до моделі Author:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Book extends Model
{
    /**
     * Отримати автора, який написав книгу.
     */
    public function author(): BelongsTo
    {
        return $this->belongsTo(Author::class);
    }
}

Тепер давайте отримаємо всі книги та їх авторів:

use App\Models\Book;
 
$books = Book::all();
 
foreach ($books as $book) {
    echo $book->author->name;
}

Цей цикл виконає один запит для отримання всіх книг з таблиці бази даних, а потім ще один запит для кожної книги, щоб отримати автора книги. Отже, якщо у нас є 25 книг, наведений вище код виконає 26 запитів: один для отримання всіх книг і 25 додаткових запитів для отримання автора кожної книги.

На щастя, ми можемо використовувати жадібне завантаження, щоб зменшити цю операцію до лише двох запитів. Під час побудови запиту ви можете вказати, які відносини слід завантажувати жадібно, використовуючи метод with:

$books = Book::with('author')->get();
 
foreach ($books as $book) {
    echo $book->author->name;
}

Для цієї операції буде виконано лише два запити - один запит для отримання всіх книг і один запит для отримання всіх авторів для всіх книг:

select * from books
 
select * from authors where id in (1, 2, 3, 4, 5, ...)

Жадібне завантаження кількох відносин

Іноді вам може знадобитися завантажити кілька різних відносин заздалегідь. Для цього просто передайте масив відносин у метод with:

$books = Book::with(['author', 'publisher'])->get();

Вкладене Жадібне Завантаження

Щоб завантажити відносини відносин заздалегідь, ви можете використовувати синтаксис "крапка". Наприклад, давайте заздалегідь завантажимо всіх авторів книги та всі особисті контакти автора:

$books = Book::with('author.contacts')->get();

Альтернативно, ви можете вказати вкладені жадібно завантажені відносини, надаючи вкладений масив методу with, що може бути зручним при жадібному завантаженні декількох вкладених відносин:

$books = Book::with([
    'author' => [
        'contacts',
        'publisher',
    ],
])->get();

Вкладене Жадібне Завантаження Відносин morphTo

Якщо ви хочете завантажити заздалегідь відношення morphTo, а також вкладені відношення для різних сутностей, які можуть бути повернені цим відношенням, ви можете використовувати метод with у поєднанні з методом morphWith відношення morphTo. Щоб проілюструвати цей метод, розглянемо наступну модель:

<?php
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class ActivityFeed extends Model
{
    /**
     * Отримати батьківський запис стрічки активності.
     */
    public function parentable(): MorphTo
    {
        return $this->morphTo();
    }
}

У цьому прикладі припустимо, що моделі Event, Photo та Post можуть створювати моделі ActivityFeed. Додатково, припустимо, що моделі Event належать до моделі Calendar, моделі Photo асоційовані з моделями Tag, а моделі Post належать до моделі Author.

Використовуючи ці визначення моделей та відносини, ми можемо отримати екземпляри моделі ActivityFeed та завантажити всі моделі parentable і їх відповідні вкладені відносини:

use Illuminate\Database\Eloquent\Relations\MorphTo;
 
$activities = ActivityFeed::query()
    ->with(['parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWith([
            Event::class => ['calendar'],
            Photo::class => ['tags'],
            Post::class => ['author'],
        ]);
    }])->get();

Жадне завантаження конкретних стовпців

Вам може не завжди знадобитися кожен стовпець з відносин, які ви отримуєте. З цієї причини Eloquent дозволяє вам вказати, які стовпці відносин ви хотіли б отримати:

$books = Book::with('author:id,name,book_id')->get();

Коли використовуєте цю функцію, завжди слід включати стовпець id та будь-які відповідні зовнішні ключі у список стовпців, які ви бажаєте отримати.

Попереднє завантаження за замовчуванням

Іноді ви можете захотіти завжди завантажувати деякі зв'язки при отриманні моделі. Щоб досягти цього, ви можете визначити властивість $with у моделі:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Book extends Model
{
    /**
     * Відносини, які завжди повинні бути завантажені.
     *
     * @var array
     */
    protected $with = ['author'];
 
    /**
     * Отримати автора, який написав книгу.
     */
    public function author(): BelongsTo
    {
        return $this->belongsTo(Author::class);
    }
 
    /**
     * Отримати жанр книги.
     */
    public function genre(): BelongsTo
    {
        return $this->belongsTo(Genre::class);
    }
}

Якщо ви хочете видалити елемент з властивості $with для одного запиту, ви можете використовувати метод without:

$books = Book::without('author')->get();

Якщо ви хочете перевизначити всі елементи в межах властивості $with для одного запиту, ви можете використовувати метод withOnly:

$books = Book::withOnly('genre')->get();

Обмеження Жадібного Завантаження

Іноді ви можете захотіти завантажити відношення заздалегідь, але також вказати додаткові умови запиту для запиту завантаження. Ви можете досягти цього, передавши масив відношень до методу with, де ключ масиву є назвою відношення, а значення масиву - це замикання, яке додає додаткові обмеження до запиту завантаження:

use App\Models\User;
use Illuminate\Contracts\Database\Eloquent\Builder;
 
$users = User::with(['posts' => function (Builder $query) {
    $query->where('title', 'like', '%code%');
}])->get();

У цьому прикладі Eloquent буде завантажувати з нетерпінням лише ті пости, де стовпець title містить слово code. Ви можете викликати інші методи конструктора запитів, щоб додатково налаштувати операцію завантаження з нетерпінням:

$users = User::with(['posts' => function (Builder $query) {
    $query->orderBy('created_at', 'desc');
}])->get();

Обмеження жадібного завантаження відносин morphTo

Якщо ви використовуєте eager loading для відношення morphTo, Eloquent виконає декілька запитів для отримання кожного типу пов'язаної моделі. Ви можете додати додаткові обмеження до кожного з цих запитів, використовуючи метод constrain відношення MorphTo:

use Illuminate\Database\Eloquent\Relations\MorphTo;
 
$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
    $morphTo->constrain([
        Post::class => function ($query) {
            $query->whereNull('hidden_at');
        },
        Video::class => function ($query) {
            $query->where('type', 'educational');
        },
    ]);
}])->get();

У цьому прикладі Eloquent буде завантажувати з нетерпінням лише ті пости, які не були приховані, та відео, які мають значення type "educational".

Обмеження Жадібного Завантаження Існуванням Відносин

Ви можете іноді виявити, що вам потрібно перевірити існування відношення, одночасно завантажуючи відношення на основі тих самих умов. Наприклад, ви можете бажати отримати лише моделі User, які мають дочірні моделі Post, що відповідають заданій умові запиту, одночасно завантажуючи відповідні пости. Ви можете досягти цього, використовуючи метод withWhereHas:

use App\Models\User;
 
$users = User::withWhereHas('posts', function ($query) {
    $query->where('featured', true);
})->get();

Ліниве Жадібне Завантаження

Іноді вам може знадобитися завантажити відношення з нетерпінням після того, як батьківська модель вже була отримана. Наприклад, це може бути корисно, якщо вам потрібно динамічно вирішити, чи завантажувати пов'язані моделі:

use App\Models\Book;
 
$books = Book::all();
 
if ($someCondition) {
    $books->load('author', 'publisher');
}

Якщо вам потрібно встановити додаткові обмеження запиту на запит з нетерпінням, ви можете передати масив, ключами якого є відносини, які ви хочете завантажити. Значення масиву повинні бути екземплярами замикань, які отримують екземпляр запиту:

$author->load(['books' => function (Builder $query) {
    $query->orderBy('published_date', 'asc');
}]);

Щоб завантажити відношення лише тоді, коли воно ще не було завантажене, використовуйте метод loadMissing:

$book->loadMissing('author');

Вкладене ліниве жадібне завантаження і morphTo

Якщо ви хочете завантажити заздалегідь відношення morphTo, а також вкладені відношення на різних сутностях, які можуть бути повернені цим відношенням, ви можете використовувати метод loadMorph.

Цей метод приймає назву відношення morphTo як свій перший аргумент, а масив пар модель / відношення як свій другий аргумент. Щоб проілюструвати цей метод, розглянемо наступну модель:

<?php
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class ActivityFeed extends Model
{
    /**
     * Отримати батьківський запис стрічки активності.
     */
    public function parentable(): MorphTo
    {
        return $this->morphTo();
    }
}

У цьому прикладі припустимо, що моделі Event, Photo та Post можуть створювати моделі ActivityFeed. Додатково, припустимо, що моделі Event належать до моделі Calendar, моделі Photo асоційовані з моделями Tag, а моделі Post належать до моделі Author.

Використовуючи ці визначення моделей та відносини, ми можемо отримати екземпляри моделі ActivityFeed та завантажити всі моделі parentable і їх відповідні вкладені відносини:

$activities = ActivityFeed::with('parentable')
    ->get()
    ->loadMorph('parentable', [
        Event::class => ['calendar'],
        Photo::class => ['tags'],
        Post::class => ['author'],
    ]);

Автоматичне Жадібне Завантаження

Ця функція наразі знаходиться в бета-версії, щоб зібрати відгуки від спільноти. Поведінка та функціональність цієї функції можуть змінюватися навіть у патч-релізах.

У багатьох випадках Laravel може автоматично завантажувати відносини, до яких ви звертаєтеся. Щоб увімкнути автоматичне завантаження відносин, ви повинні викликати метод Model::automaticallyEagerLoadRelationships у методі boot вашого AppServiceProvider застосунку:

use Illuminate\Database\Eloquent\Model;
 
/**
 * Завантажте будь-які сервіси застосунку.
 */
public function boot(): void
{
    Model::automaticallyEagerLoadRelationships();
}

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

use App\Models\User;
 
$users = User::all();
 
foreach ($users as $user) {
    foreach ($user->posts as $post) {
        foreach ($post->comments as $comment) {
            echo $comment->content;
        }
    }
}

Зазвичай, наведений вище код виконує запит для кожного користувача, щоб отримати їхні пости, а також запит для кожного поста, щоб отримати його коментарі. Однак, коли функція automaticallyEagerLoadRelationships увімкнена, Laravel автоматично ліниво завантажує пости для всіх користувачів у колекції користувачів, коли ви намагаєтеся отримати доступ до постів будь-якого з отриманих користувачів. Так само, коли ви намагаєтеся отримати доступ до коментарів будь-якого отриманого поста, всі коментарі будуть ліниво завантажені для всіх постів, які були спочатку отримані.

Якщо ви не хочете глобально вмикати автоматичне завантаження зв'язків, ви все ще можете увімкнути цю функцію для окремого екземпляра колекції Eloquent, викликавши метод withRelationshipAutoloading на колекції:

$users = User::where('vip', true)->get();
 
return $users->withRelationshipAutoloading();

Запобігання ледачому завантаженню

Як обговорювалося раніше, завчасне завантаження зв'язків може часто надавати значні переваги продуктивності вашому застосунку. Тому, якщо ви бажаєте, ви можете вказати Laravel завжди запобігати відкладеному завантаженню зв'язків. Для цього ви можете викликати метод preventLazyLoading, запропонований базовим класом моделі Eloquent. Зазвичай, ви повинні викликати цей метод у межах методу boot класу AppServiceProvider вашого застосунку.

Метод preventLazyLoading приймає необов'язковий булевий аргумент, який вказує, чи слід запобігати відкладеному завантаженню. Наприклад, ви можете бажати вимкнути відкладене завантаження лише в непроизводчих середовищах, щоб ваше виробниче середовище продовжувало функціонувати нормально, навіть якщо відкладене завантаження випадково присутнє у виробничому коді:

use Illuminate\Database\Eloquent\Model;
 
/**
 * Завантажте будь-які сервіси застосунку.
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

Після запобігання ледачому завантаженню, Eloquent викине виняток Illuminate\Database\LazyLoadingViolationException, коли ваш застосунок спробує ледаче завантаження будь-якого відношення Eloquent.

Ви можете налаштувати поведінку порушень відкладеного завантаження за допомогою методу handleLazyLoadingViolationsUsing. Наприклад, використовуючи цей метод, ви можете вказати, щоб порушення відкладеного завантаження лише записувалися в журнал замість переривання виконання застосунку винятками:

Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
    $class = $model::class;
 
    info("Attempted to lazy load [{$relation}] on model [{$class}].");
});

Метод save

Eloquent надає зручні методи для додавання нових моделей до відносин. Наприклад, можливо, вам потрібно додати новий коментар до поста. Замість того, щоб вручну встановлювати атрибут post_id на моделі Comment, ви можете вставити коментар, використовуючи метод save відносин:

use App\Models\Comment;
use App\Models\Post;
 
$comment = new Comment(['message' => 'A new comment.']);
 
$post = Post::find(1);
 
$post->comments()->save($comment);

Зверніть увагу, що ми не зверталися до відношення comments як до динамічної властивості. Натомість ми викликали метод comments, щоб отримати екземпляр відношення. Метод save автоматично додасть відповідне значення post_id до нової моделі Comment.

Якщо вам потрібно зберегти декілька пов'язаних моделей, ви можете використовувати метод saveMany:

$post = Post::find(1);
 
$post->comments()->saveMany([
    new Comment(['message' => 'A new comment.']),
    new Comment(['message' => 'Another new comment.']),
]);

Методи save та saveMany збережуть надані екземпляри моделі, але не додадуть нові збережені моделі до жодних завантажених у пам'ять відносин, які вже завантажені на батьківську модель. Якщо ви плануєте звертатися до відносин після використання методів save або saveMany, можливо, ви захочете використати метод refresh для перезавантаження моделі та її відносин:

$post->comments()->save($comment);
 
$post->refresh();
 
// Усі коментарі, включаючи новозбережений коментар...
$post->comments;

Рекурсивне Збереження Моделей та Відносин

Якщо ви хочете зберегти вашу модель та всі її пов'язані відносини, ви можете використовувати метод push. У цьому прикладі модель Post буде збережена разом з її коментарями та авторами коментарів:

$post = Post::find(1);
 
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
 
$post->push();

Метод pushQuietly може бути використаний для збереження моделі та її пов'язаних відносин без виклику будь-яких подій:

$post->pushQuietly();

Метод create

На додаток до методів save та saveMany, ви також можете використовувати метод create, який приймає масив атрибутів, створює модель та вставляє її в базу даних. Різниця між save та create полягає в тому, що save приймає повну екземпляр моделі Eloquent, тоді як create приймає звичайний PHP array. Нещодавно створена модель буде повернена методом create:

use App\Models\Post;
 
$post = Post::find(1);
 
$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

Ви можете використовувати метод createMany для створення декількох пов'язаних моделей:

$post = Post::find(1);
 
$post->comments()->createMany([
    ['message' => 'A new comment.'],
    ['message' => 'Another new comment.'],
]);

Методи createQuietly та createManyQuietly можуть бути використані для створення моделі(ей) без відправлення будь-яких подій:

$user = User::find(1);
 
$user->posts()->createQuietly([
    'title' => 'Post title.',
]);
 
$user->posts()->createManyQuietly([
    ['title' => 'First post.'],
    ['title' => 'Second post.'],
]);

Ви також можете використовувати методи findOrNew, firstOrNew, firstOrCreate та updateOrCreate для створення та оновлення моделей у відносинах.

Перш ніж використовувати метод create, обов'язково перегляньте документацію з масового призначення.

Відношення "Належить До"

Якщо ви хочете призначити дочірню модель новій батьківській моделі, ви можете використовувати метод associate. У цьому прикладі модель User визначає відношення belongsTo до моделі Account. Цей метод associate встановить зовнішній ключ на дочірній моделі:

use App\Models\Account;
 
$account = Account::find(10);
 
$user->account()->associate($account);
 
$user->save();

Щоб видалити батьківську модель з дочірньої моделі, ви можете використовувати метод dissociate. Цей метод встановить зовнішній ключ відношення на null:

$user->account()->dissociate();
 
$user->save();

Багато до Багатьох Відносини

Прикріплення / Відкріплення

Eloquent також надає методи для зручнішої роботи з відносинами "багато-до-багатьох". Наприклад, уявімо, що користувач може мати багато ролей, а роль може мати багато користувачів. Ви можете використовувати метод attach, щоб прикріпити роль до користувача, вставивши запис у проміжну таблицю відносин:

use App\Models\User;
 
$user = User::find(1);
 
$user->roles()->attach($roleId);

Коли ви додаєте відношення до моделі, ви також можете передати масив додаткових даних, які будуть вставлені в проміжну таблицю:

$user->roles()->attach($roleId, ['expires' => $expires]);

Іноді може бути необхідно видалити роль у користувача. Щоб видалити запис з відношенням "багато-до-багатьох", використовуйте метод detach. Метод detach видалить відповідний запис з проміжної таблиці; однак, обидві моделі залишаться в базі даних:

// Від'єднати одну роль від користувача...
$user->roles()->detach($roleId);
 
// Від'єднати всі ролі від користувача...
$user->roles()->detach();

Для зручності, attach і detach також приймають масиви ідентифікаторів як вхідні дані:

$user = User::find(1);
 
$user->roles()->detach([1, 2, 3]);
 
$user->roles()->attach([
    1 => ['expires' => $expires],
    2 => ['expires' => $expires],
]);

Синхронізація Асоціацій

Ви також можете використовувати метод sync для створення зв'язків "багато-до-багатьох". Метод sync приймає масив ID, які потрібно розмістити в проміжній таблиці. Будь-які ID, які не входять до вказаного масиву, будуть видалені з проміжної таблиці. Отже, після завершення цієї операції в проміжній таблиці залишаться лише ID з вказаного масиву:

$user->roles()->sync([1, 2, 3]);

Ви також можете передати додаткові значення проміжної таблиці з ID:

$user->roles()->sync([1 => ['expires' => true], 2, 3]);

Якщо ви хочете вставити ті самі значення проміжної таблиці з кожним із синхронізованих ідентифікаторів моделі, ви можете використовувати метод syncWithPivotValues:

$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);

Якщо ви не хочете від'єднувати існуючі ID, які відсутні в даному масиві, ви можете використовувати метод syncWithoutDetaching:

$user->roles()->syncWithoutDetaching([1, 2, 3]);

Перемикання Асоціацій

Багато-до-багатьох відношення також надає метод toggle, який "перемикає" статус прикріплення вказаних ідентифікаторів пов'язаних моделей. Якщо вказаний ідентифікатор наразі прикріплений, він буде від'єднаний. Так само, якщо він наразі від'єднаний, він буде прикріплений:

$user->roles()->toggle([1, 2, 3]);

Ви також можете передати додаткові значення проміжної таблиці разом з ID:

$user->roles()->toggle([
    1 => ['expires' => true],
    2 => ['expires' => true],
]);

Оновлення запису в проміжній таблиці

Якщо вам потрібно оновити існуючий рядок у проміжній таблиці вашого зв'язку, ви можете використовувати метод updateExistingPivot. Цей метод приймає зовнішній ключ проміжного запису та масив атрибутів для оновлення:

$user = User::find(1);
 
$user->roles()->updateExistingPivot($roleId, [
    'active' => false,
]);

Оновлення часових міток батьківського запису

Коли модель визначає відношення belongsTo або belongsToMany до іншої моделі, наприклад, Comment, який належить до Post, іноді буває корисно оновити часову мітку батьківської моделі, коли дочірня модель оновлюється.

Наприклад, коли модель Comment оновлюється, ви можете захотіти автоматично "торкнутися" позначки часу updated_at власника Post, щоб вона була встановлена на поточну дату та час. Щоб досягти цього, ви можете додати властивість touches до вашої дочірньої моделі, що містить імена відносин, які повинні мати оновлені позначки часу updated_at, коли дочірня модель оновлюється:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Comment extends Model
{
    /**
     * Усі відносини, які потрібно оновити.
     *
     * @var array
     */
    protected $touches = ['post'];
 
    /**
     * Отримати пост, до якого належить коментар.
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

Мітки часу батьківської моделі будуть оновлені лише в тому випадку, якщо дочірня модель оновлюється за допомогою методу Eloquent save.