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

Eloquent: початок роботи

Вступ

Laravel містить Eloquent - об'єктно-реляційний мапер (ORM), який робить роботу з базою даних приємною. Коли ви користуєтеся Eloquent, кожній таблиці бази даних відповідає своя «модель», через яку ви з цією таблицею працюєте. Крім читання записів, моделі Eloquent дозволяють вставляти, оновлювати та видаляти записи в таблиці.

Перш ніж почати, обов'язково налаштуйте підключення до бази даних у файлі config/database.php вашого застосунку. Детальніше про налаштування бази читайте в документації з конфігурації бази даних.

Створення класів моделей

Для початку створімо модель Eloquent. Моделі зазвичай лежать у каталозі app\Models і розширюють клас Illuminate\Database\Eloquent\Model. Щоб згенерувати нову модель, скористайтеся artisan-командою make:model:

php artisan make:model Flight

Якщо ви хочете разом із моделлю згенерувати міграцію бази даних, скористайтеся опцією --migration або -m:

php artisan make:model Flight --migration

Разом із моделлю можна згенерувати й інші класи: фабрики, сідери, політики, контролери та form request. Ці опції можна поєднувати, щоб створити кілька класів одразу:

# Generate a model and a FlightFactory class...
php artisan make:model Flight --factory
php artisan make:model Flight -f

# Generate a model and a FlightSeeder class...
php artisan make:model Flight --seed
php artisan make:model Flight -s

# Generate a model and a FlightController class...
php artisan make:model Flight --controller
php artisan make:model Flight -c

# Generate a model, FlightController resource class, and form request classes...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR

# Generate a model and a FlightPolicy class...
php artisan make:model Flight --policy

# Generate a model and a migration, factory, seeder, and controller...
php artisan make:model Flight -mfsc

# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests...
php artisan make:model Flight --all
php artisan make:model Flight -a

# Generate a pivot model...
php artisan make:model Member --pivot
php artisan make:model Member -p

Огляд моделей

Іноді буває непросто зрозуміти всі доступні атрибути та зв'язки моделі, лише проглянувши її код. Натомість спробуйте artisan-команду model:show - вона дає зручний огляд усіх атрибутів і зв'язків моделі:

php artisan model:show Flight

Конвенції моделей Eloquent

Моделі, згенеровані командою make:model, потрапляють до каталогу app/Models. Розгляньмо простий клас моделі й обговорімо кілька ключових конвенцій Eloquent:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    // ...
}

Назви таблиць

Погляньте на приклад вище: ми ніде не сказали Eloquent, яка таблиця відповідає нашій моделі Flight. За конвенцією назвою таблиці стає назва класу в «snake case» і в множині - хіба що ви явно вказали іншу. Тож у цьому випадку Eloquent вважатиме, що модель Flight зберігає записи в таблиці flights, а модель AirTrafficController - у таблиці air_traffic_controllers.

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table('my_flights')]
class Flight extends Model
{
    // ...
}

Первинні ключі

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table(key: 'flight_id')]
class Flight extends Model
{
    // ...
}

Крім того, Eloquent вважає, що первинний ключ - це автоінкрементне ціле число, тож він автоматично приводить його до цілого. Якщо ви хочете скористатися неавтоінкрементним або нечисловим первинним ключем, задайте аргументи keyType та incrementing атрибута Table:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table(key: 'uuid', keyType: 'string', incrementing: false)]
class Flight extends Model
{
    // ...
}

Якщо вам потрібно лише вимкнути автоінкрементні ID, скористайтеся атрибутом WithoutIncrementing:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\WithoutIncrementing;
use Illuminate\Database\Eloquent\Model;

#[WithoutIncrementing]
class Flight extends Model
{
    // ...
}

«Складені» первинні ключі

Eloquent вимагає, щоб кожна модель мала щонайменше один унікальний «ID», який може служити первинним ключем. «Складені» первинні ключі моделі Eloquent не підтримують. Втім, ви вільні додавати до таблиць додаткові унікальні індекси з кількох стовпців - окрім самого первинного ключа, що унікально ідентифікує рядок.

Ключі UUID та ULID

Замість автоінкрементних цілих чисел як первинних ключів моделі Eloquent ви можете обрати UUID. UUID - це універсально унікальні буквенно-цифрові ідентифікатори довжиною 36 символів.

Якщо ви хочете, щоб модель використовувала ключ UUID замість автоінкрементного цілого, застосуйте до неї трейт Illuminate\Database\Eloquent\Concerns\HasUuids. Звісно, переконайтеся, що модель має стовпець первинного ключа, еквівалентний UUID:

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasUuids;

    // ...
}

$article = Article::create(['title' => 'Traveling to Europe']);

$article->id; // "018f2b5c-6a7f-7b12-9d6f-2f8a4e0c9c11"

За замовчуванням трейт HasUuids генерує для ваших моделей ідентифікатори UUIDv7. Такі UUID ефективніші для зберігання в проіндексованій базі, бо їх можна сортувати лексикографічно.

Ви можете перевизначити процес генерації UUID для конкретної моделі, описавши в ній метод newUniqueId. Крім того, методом uniqueIds можна вказати, які стовпці мають отримувати UUID:

use Ramsey\Uuid\Uuid;

/**
 * Generate a new UUID for the model.
 */
public function newUniqueId(): string
{
    return (string) Uuid::uuid4();
}

/**
 * Get the columns that should receive a unique identifier.
 *
 * @return array<int, string>
 */
public function uniqueIds(): array
{
    return ['id', 'discount_code'];
}

За бажанням ви можете скористатися «ULID» замість UUID. ULID схожі на UUID, але мають довжину лише 26 символів. Як і впорядковані UUID, ULID сортуються лексикографічно, що дає ефективне індексування в базі. Щоб скористатися ULID, застосуйте до моделі трейт Illuminate\Database\Eloquent\Concerns\HasUlids. Також переконайтеся, що модель має стовпець первинного ключа, еквівалентний ULID:

use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasUlids;

    // ...
}

$article = Article::create(['title' => 'Traveling to Asia']);

$article->id; // "01gd4d3tgrrfqeda94gdbtdk5c"

Часові позначки

За замовчуванням Eloquent очікує, що у відповідній таблиці моделі є стовпці created_at та updated_at. Eloquent автоматично задає їхні значення, коли модель створюється чи оновлюється. Якщо ви не хочете, щоб Eloquent керував цими стовпцями автоматично, задайте timestamps значення false в атрибуті Table вашої моделі:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table(timestamps: false)]
class Flight extends Model
{
    // ...
}

Якщо вам потрібно лише вимкнути часові позначки, скористайтеся атрибутом WithoutTimestamps:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Database\Eloquent\Model;

#[WithoutTimestamps]
class Flight extends Model
{
    // ...
}

Якщо вам потрібно змінити формат часових позначок моделі, скористайтеся аргументом dateFormat атрибута Table. Він визначає, як атрибути дати зберігаються в базі даних, а також їхній формат при серіалізації моделі в масив чи JSON:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table(dateFormat: 'U')]
class Flight extends Model
{
    // ...
}

Якщо вам потрібно задати лише формат дати, скористайтеся атрибутом DateFormat:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\DateFormat;
use Illuminate\Database\Eloquent\Model;

#[DateFormat('U')]
class Flight extends Model
{
    // ...
}

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

<?php

class Flight extends Model
{
    /**
     * The name of the "created at" column.
     *
     * @var string|null
     */
    public const CREATED_AT = 'creation_date';

    /**
     * The name of the "updated at" column.
     *
     * @var string|null
     */
    public const UPDATED_AT = 'updated_date';
}

Якщо ви хочете виконати операції з моделлю, не змінюючи її часову позначку updated_at, працюйте з моделлю в замиканні, переданому методу withoutTimestamps:

Model::withoutTimestamps(fn () => $post->increment('reads'));

Підключення до бази даних

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Connection;
use Illuminate\Database\Eloquent\Model;

#[Connection('mysql')]
class Flight extends Model
{
    // ...
}

Значення атрибутів за замовчуванням

За замовчуванням новостворений екземпляр моделі не містить жодних значень атрибутів. Якщо ви хочете задати значення за замовчуванням для деяких атрибутів моделі, опишіть у ній властивість $attributes. Значення в масиві $attributes мають бути в «сирому» форматі для зберігання - так, ніби їх щойно прочитали з бази даних:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * The model's default values for attributes.
     *
     * @var array<string, mixed>
     */
    protected $attributes = [
        'options' => '[]',
        'delayed' => false,
    ];
}

Налаштування строгості Eloquent

Laravel має кілька методів, якими можна налаштувати поведінку та «строгість» Eloquent у різних ситуаціях.

По-перше, метод preventLazyLoading приймає необов'язковий булевий аргумент, що вказує, чи слід забороняти ліниве завантаження (lazy loading). Наприклад, ви можете вимкнути ліниве завантаження лише в непродакшн-середовищах, щоб продакшн і далі працював нормально, навіть якщо в код випадково потрапило ліниве завантаження зв'язку. Зазвичай цей метод викликають у методі boot класу AppServiceProvider вашого застосунку:

use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

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

Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());

Отримання моделей

Коли ви створили модель і відповідну таблицю бази даних, можна починати діставати дані з бази. Уявляйте кожну модель Eloquent як потужний конструктор запитів, що дозволяє плавно робити запити до пов'язаної з моделлю таблиці. Метод all моделі дістає всі записи з відповідної таблиці:

use App\Models\Flight;

foreach (Flight::all() as $flight) {
    echo $flight->name;
}

Побудова запитів

Метод all в Eloquent повертає всі результати з таблиці моделі. Але оскільки кожна модель Eloquent є конструктором запитів, ви можете додати до запиту обмеження, а потім викликати метод get, щоб отримати результати:

$flights = Flight::where('active', 1)
    ->orderBy('name')
    ->limit(10)
    ->get();

Оскільки моделі Eloquent є конструкторами запитів, варто переглянути всі методи конструктора запитів Laravel. Будь-який із них можна використовувати у своїх запитах Eloquent.

Оновлення моделей

Якщо у вас уже є екземпляр моделі Eloquent, дістаний із бази даних, ви можете «оновити» його методами fresh і refresh. Метод fresh заново дістає модель із бази. Наявний екземпляр моделі при цьому не змінюється:

$flight = Flight::where('number', 'FR 900')->first();

$freshFlight = $flight->fresh();

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

$flight = Flight::where('number', 'FR 900')->first();

$flight->number = 'FR 456';

$flight->refresh();

$flight->number; // "FR 900"

Колекції

Як ми бачили, методи Eloquent на кшталт all і get дістають із бази кілька записів. Проте вони повертають не звичайний PHP-масив, а екземпляр Illuminate\Database\Eloquent\Collection.

Клас Collection в Eloquent розширює базовий клас Laravel Illuminate\Support\Collection, який має цілу низку корисних методів для роботи з наборами даних. Наприклад, методом reject можна прибрати з колекції моделі за результатом виклику замикання:

$flights = Flight::where('destination', 'Paris')->get();

$flights = $flights->reject(function (Flight $flight) {
    return $flight->cancelled;
});

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

Оскільки всі колекції Laravel реалізують інтерфейси ітерування PHP, ви можете обходити колекції в циклі, наче масиви:

foreach ($flights as $flight) {
    echo $flight->name;
}

Обробка результатів частинами

Якщо ви спробуєте завантажити десятки тисяч записів Eloquent методами all чи get, вашому застосунку може забракнути пам'яті. Замість них скористайтеся методом chunk - він опрацьовує велику кількість моделей ефективніше.

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

use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;

Flight::chunk(200, function (Collection $flights) {
    foreach ($flights as $flight) {
        // ...
    }
});

Перший аргумент методу chunk - кількість записів, які ви хочете отримувати в кожній «частині». Замикання, передане другим аргументом, буде викликано для кожної частини, дістаної з бази. Щоб отримати кожну частину записів для замикання, виконується окремий запит до бази.

Якщо ви фільтруєте результати методу chunk за стовпцем, який заодно оновлюєте під час обходу, скористайтеся методом chunkById. Метод chunk у таких випадках може дати неочікувані й неузгоджені результати. Усередині метод chunkById завжди дістає моделі зі значенням стовпця id, більшим за останню модель попередньої частини:

Flight::where('departed', true)
    ->chunkById(200, function (Collection $flights) {
        $flights->each->update(['departed' => false]);
    }, column: 'id');

Оскільки методи chunkById і lazyById додають до запиту власні умови «where», свої умови варто логічно згрупувати в замиканні:

Flight::where(function ($query) {
    $query->where('delayed', true)->orWhere('cancelled', true);
})->chunkById(200, function (Collection $flights) {
    $flights->each->update([
        'departed' => false,
        'cancelled' => true
    ]);
}, column: 'id');

Обробка частинами через ліниві колекції

Метод lazy працює схоже на метод chunk у тому, що всередині виконує запит частинами. Але замість того щоб передавати кожну частину прямо в колбек, метод lazy повертає «розплющену» LazyCollection моделей Eloquent, і ви працюєте з результатами як з єдиним потоком:

use App\Models\Flight;

foreach (Flight::lazy() as $flight) {
    // ...
}

Якщо ви фільтруєте результати методу lazy за стовпцем, який заодно оновлюєте під час обходу, скористайтеся методом lazyById. Усередині метод lazyById завжди дістає моделі зі значенням стовпця id, більшим за останню модель попередньої частини:

Flight::where('departed', true)
    ->lazyById(200, column: 'id')
    ->each->update(['departed' => false]);

Ви можете фільтрувати результати за спаданням id методом lazyByIdDesc.

Курсори

Як і метод lazy, метод cursor значно зменшує споживання пам'яті вашим застосунком, коли ви обходите десятки тисяч записів моделей Eloquent.

Метод cursor виконує лише один запит до бази даних; проте окремі моделі Eloquent не наповнюються даними, аж поки ви до них не дійдете в обході. Тому в будь-який момент обходу курсора в пам'яті тримається лише одна модель Eloquent.

Оскільки метод cursor тримає в пам'яті лише одну модель Eloquent за раз, він не може жадібно завантажувати зв'язки. Якщо вам потрібне жадібне завантаження, розгляньте метод lazy.

Усередині метод cursor реалізує це через генератори PHP:

use App\Models\Flight;

foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) {
    // ...
}

cursor повертає екземпляр Illuminate\Support\LazyCollection. Ліниві колекції дозволяють користуватися багатьма методами звичайних колекцій Laravel, тримаючи в пам'яті лише одну модель за раз:

use App\Models\User;

$users = User::cursor()->filter(function (User $user) {
    return $user->id > 500;
});

foreach ($users as $user) {
    echo $user->id;
}

Хоч метод cursor споживає значно менше пам'яті, ніж звичайний запит (бо тримає в пам'яті лише одну модель Eloquent за раз), пам'ять усе одно колись закінчиться. Причина в тому, що драйвер PDO у PHP усередині кешує всі сирі результати запиту у своєму буфері. Якщо ви маєте справу з дуже великою кількістю записів Eloquent, розгляньте метод lazy.

Складніші підзапити

Підзапити у SELECT

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

За допомогою підзапитів у методах select і addSelect конструктора запитів ми можемо одним запитом вибрати всі destinations і назву рейсу, який прибув у цей пункт останнім:

use App\Models\Destination;
use App\Models\Flight;

return Destination::addSelect(['last_flight' => Flight::select('name')
    ->whereColumn('destination_id', 'destinations.id')
    ->orderByDesc('arrived_at')
    ->limit(1)
])->get();

Сортування за підзапитом

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

return Destination::orderByDesc(
    Flight::select('arrived_at')
        ->whereColumn('destination_id', 'destinations.id')
        ->orderByDesc('arrived_at')
        ->limit(1)
)->get();

Отримання окремих моделей та агрегатів

Крім отримання всіх записів, що відповідають запиту, ви можете дістати окремі записи методами find, first або firstWhere. Замість колекції моделей вони повертають один екземпляр моделі:

use App\Models\Flight;

// Retrieve a model by its primary key...
$flight = Flight::find(1);

// Retrieve the first model matching the query constraints...
$flight = Flight::where('active', 1)->first();

// Alternative to retrieving the first model matching the query constraints...
$flight = Flight::firstWhere('active', 1);

Іноді ви хочете виконати якусь іншу дію, якщо результатів не знайдено. Методи findOr і firstOr повертають один екземпляр моделі або, якщо результатів немає, виконують задане замикання. Значення, яке повернуло замикання, стане результатом методу:

$flight = Flight::findOr(1, function () {
    // ...
});

$flight = Flight::where('legs', '>', 3)->firstOr(function () {
    // ...
});

Винятки «не знайдено»

Іноді ви хочете викинути виняток, якщо модель не знайдено. Це особливо зручно в маршрутах і контролерах. Методи findOrFail і firstOrFail дістають перший результат запиту; але якщо результату немає, буде викинуто Illuminate\Database\Eloquent\ModelNotFoundException:

$flight = Flight::findOrFail(1);

$flight = Flight::where('legs', '>', 3)->firstOrFail();

Якщо ModelNotFoundException не перехоплено, клієнту автоматично надсилається HTTP-відповідь 404:

use App\Models\Flight;

Route::get('/api/flights/{id}', function (string $id) {
    return Flight::findOrFail($id);
});

Отримання або створення моделей

Метод firstOrCreate спробує знайти запис у базі за заданими парами «стовпець - значення». Якщо моделі в базі немає, буде вставлено запис з атрибутами, які утворюються об'єднанням першого масиву-аргументу з необов'язковим другим.

Метод firstOrNew, як і firstOrCreate, спробує знайти в базі запис, що відповідає заданим атрибутам. Але якщо моделі не знайдено, буде повернуто новий екземпляр моделі. Зверніть увагу: модель, яку повернув firstOrNew, ще не збережена в базі. Щоб зберегти її, потрібно вручну викликати метод save:

use App\Models\Flight;

// Retrieve flight by name or create it if it doesn't exist...
$flight = Flight::firstOrCreate([
    'name' => 'London to Paris'
]);

// Retrieve flight by name or create it with the name, delayed, and arrival_time attributes...
$flight = Flight::firstOrCreate(
    ['name' => 'London to Paris'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

// Retrieve flight by name or instantiate a new Flight instance...
$flight = Flight::firstOrNew([
    'name' => 'London to Paris'
]);

// Retrieve flight by name or instantiate with the name, delayed, and arrival_time attributes...
$flight = Flight::firstOrNew(
    ['name' => 'Tokyo to Sydney'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

Отримання агрегатів

Працюючи з моделями Eloquent, ви можете також користуватися методами count, sum, max та іншими агрегатними методами конструктора запитів Laravel. Як і слід очікувати, вони повертають скалярне значення, а не екземпляр моделі Eloquent:

$count = Flight::where('active', 1)->count();

$max = Flight::where('active', 1)->max('price');

Вставка та оновлення моделей

Вставка

Звісно, працюючи з Eloquent, ми не лише дістаємо моделі з бази - нам потрібно вставляти й нові записи. На щастя, Eloquent робить це просто. Щоб вставити новий запис, створіть екземпляр моделі й задайте йому атрибути. Далі викличте на екземплярі метод save:

<?php

namespace App\Http\Controllers;

use App\Models\Flight;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class FlightController extends Controller
{
    /**
     * Store a new flight in the database.
     */
    public function store(Request $request): RedirectResponse
    {
        // Validate the request...

        $flight = new Flight;

        $flight->name = $request->name;

        $flight->save();

        return redirect('/flights');
    }
}

У цьому прикладі ми присвоюємо поле name із вхідного HTTP-запиту атрибуту name екземпляра моделі App\Models\Flight. Коли ми викликаємо метод save, у базу даних вставляється запис. Часові позначки created_at та updated_at буде задано автоматично при виклику save, тож задавати їх вручну не потрібно.

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

$flight->saveOrFail();

Або ж ви можете скористатися методом create, щоб «зберегти» нову модель однією PHP-інструкцією. Метод create поверне вам вставлений екземпляр моделі:

use App\Models\Flight;

$flight = Flight::create([
    'name' => 'London to Paris',
]);

Проте, перш ніж користуватися методом create, вам потрібно задати в класі моделі атрибут Fillable або Guarded. Ці атрибути обов'язкові, бо всі моделі Eloquent за замовчуванням захищені від вразливостей масового призначення. Детальніше про це читайте в документації з масового призначення.

Оновлення

Методом save можна оновлювати й моделі, які вже є в базі даних. Щоб оновити модель, дістаньте її та задайте атрибути, які хочете змінити. Далі викличте метод save. І знову ж таки, часову позначку updated_at буде оновлено автоматично, тож задавати її вручну не потрібно:

use App\Models\Flight;

$flight = Flight::find(1);

$flight->name = 'Paris to London';

$flight->save();

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

$flight->updateOrFail(['name' => 'Paris to London']);

Іноді вам потрібно оновити наявну модель або створити нову, якщо відповідної немає. Як і firstOrCreate, метод updateOrCreate зберігає модель, тож викликати save вручну не потрібно.

У прикладі нижче, якщо існує рейс із пунктом відправлення Oakland і пунктом призначення San Diego, буде оновлено його стовпці price та discounted. Якщо такого рейсу немає, буде створено новий з атрибутами, які утворюються об'єднанням першого масиву-аргументу з другим:

$flight = Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99, 'discounted' => 1]
);

Користуючись методами на кшталт firstOrCreate чи updateOrCreate, ви можете не знати, чи створено нову модель, чи оновлено наявну. Властивість wasRecentlyCreated вказує, чи була модель створена протягом її поточного життєвого циклу:

$flight = Flight::updateOrCreate(
    // ...
);

if ($flight->wasRecentlyCreated) {
    // New flight record was inserted...
}

Масові оновлення

Оновлення можна виконувати й над моделями, що відповідають заданому запиту. У цьому прикладі всі рейси, що є active і мають destination зі значенням San Diego, буде позначено як затримані:

Flight::where('active', 1)
    ->where('destination', 'San Diego')
    ->update(['delayed' => 1]);

Метод update очікує масив пар «стовпець - значення», які вказують, що саме оновити. Метод update повертає кількість зачеплених рядків.

Коли ви робите масове оновлення через Eloquent, події моделі saving, saved, updating та updated для оновлених моделей не спрацюють. Причина в тому, що при масовому оновленні моделі насправді ніколи не дістаються з бази.

Перевірка змін атрибутів

Eloquent має методи isDirty, isClean і wasChanged, щоб перевірити внутрішній стан моделі й визначити, як змінилися її атрибути з моменту, коли модель дістали з бази.

Метод isDirty визначає, чи змінився хоч якийсь атрибут моделі відтоді, як її дістали. Ви можете передати isDirty назву конкретного атрибута або масив атрибутів, щоб перевірити, чи є серед них «брудні». Метод isClean визначає, чи лишився атрибут незмінним відтоді, як модель дістали. Він теж приймає необов'язковий аргумент-атрибут:

use App\Models\User;

$user = User::create([
    'first_name' => 'Taylor',
    'last_name' => 'Otwell',
    'title' => 'Developer',
]);

$user->title = 'Painter';

$user->isDirty(); // true
$user->isDirty('title'); // true
$user->isDirty('first_name'); // false
$user->isDirty(['first_name', 'title']); // true

$user->isClean(); // false
$user->isClean('title'); // false
$user->isClean('first_name'); // true
$user->isClean(['first_name', 'title']); // false

$user->save();

$user->isDirty(); // false
$user->isClean(); // true

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

$user = User::create([
    'first_name' => 'Taylor',
    'last_name' => 'Otwell',
    'title' => 'Developer',
]);

$user->title = 'Painter';

$user->save();

$user->wasChanged(); // true
$user->wasChanged('title'); // true
$user->wasChanged(['title', 'slug']); // true
$user->wasChanged('first_name'); // false
$user->wasChanged(['first_name', 'title']); // true

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

$user = User::find(1);

$user->name; // John
$user->email; // john@example.com

$user->name = 'Jack';
$user->name; // Jack

$user->getOriginal('name'); // John
$user->getOriginal(); // Array of original attributes...

Метод getChanges повертає масив атрибутів, які змінилися при останньому збереженні моделі, а метод getPrevious - масив значень атрибутів, які були до останнього збереження:

$user = User::find(1);

$user->name; // John
$user->email; // john@example.com

$user->update([
    'name' => 'Jack',
    'email' => 'jack@example.com',
]);

$user->getChanges();

/*
    [
        'name' => 'Jack',
        'email' => 'jack@example.com',
    ]
*/

$user->getPrevious();

/*
    [
        'name' => 'John',
        'email' => 'john@example.com',
    ]
*/

Масове призначення

Ви можете скористатися методом create, щоб «зберегти» нову модель однією PHP-інструкцією. Метод поверне вам вставлений екземпляр моделі:

use App\Models\Flight;

$flight = Flight::create([
    'name' => 'London to Paris',
]);

Проте, перш ніж користуватися методом create, вам потрібно задати в класі моделі атрибут Fillable або Guarded. Ці атрибути обов'язкові, бо всі моделі Eloquent за замовчуванням захищені від вразливостей масового призначення.

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

Отже, для початку вам слід описати, які атрибути моделі мають бути доступні для масового призначення. Це робиться атрибутом Fillable на моделі. Наприклад, зробімо атрибут name нашої моделі Flight доступним для масового призначення:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;

#[Fillable(['name'])]
class Flight extends Model
{
    // ...
}

Коли ви вказали, які атрибути доступні для масового призначення, можна вставляти новий запис у базу методом create. Метод create повертає щойно створений екземпляр моделі:

$flight = Flight::create(['name' => 'London to Paris']);

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

$flight->fill(['name' => 'Amsterdam to Frankfurt']);

Масове призначення та стовпці JSON

Присвоюючи стовпці JSON, кожен ключ, доступний для масового призначення, потрібно вказати в атрибуті Fillable вашої моделі. З міркувань безпеки Laravel не підтримує оновлення вкладених атрибутів JSON при використанні атрибута Guarded:

use Illuminate\Database\Eloquent\Attributes\Fillable;

#[Fillable(['options->enabled'])]
class Flight extends Model
{
    // ...
}

Дозвіл масового призначення

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Unguarded;
use Illuminate\Database\Eloquent\Model;

#[Unguarded]
class Flight extends Model
{
    // ...
}

Винятки при масовому призначенні

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

За бажанням ви можете наказати Laravel викидати виняток при спробі заповнити атрибут, недоступний для заповнення, - для цього викличте метод preventSilentlyDiscardingAttributes. Зазвичай цей метод викликають у методі boot класу AppServiceProvider вашого застосунку:

use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}

Upsert

Метод upsert в Eloquent оновлює або створює записи однією атомарною операцією. Перший аргумент методу - значення для вставки чи оновлення, другий - стовпці, які унікально ідентифікують записи у відповідній таблиці. Третій і останній аргумент - масив стовпців, які слід оновити, якщо відповідний запис у базі вже є. Метод upsert автоматично задасть часові позначки created_at і updated_at, якщо вони увімкнені на моделі:

Flight::upsert([
    ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
    ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], uniqueBy: ['departure', 'destination'], update: ['price']);

Усі бази даних, окрім SQL Server, вимагають, щоб стовпці з другого аргументу методу upsert мали індекс «primary» або «unique». Крім того, драйвери MariaDB і MySQL ігнорують другий аргумент upsert і завжди використовують індекси «primary» та «unique» таблиці, щоб виявити наявні записи.

Видалення моделей

Щоб видалити модель, викличте на її екземплярі метод delete:

use App\Models\Flight;

$flight = Flight::find(1);

$flight->delete();

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

$flight->deleteOrFail();

Видалення наявної моделі за первинним ключем

У прикладі вище ми дістаємо модель із бази, перш ніж викликати метод delete. Але якщо ви знаєте первинний ключ моделі, її можна видалити, не дістаючи явно, - методом destroy. Крім одного первинного ключа, метод destroy приймає кілька первинних ключів, масив первинних ключів або колекцію первинних ключів:

Flight::destroy(1);

Flight::destroy(1, 2, 3);

Flight::destroy([1, 2, 3]);

Flight::destroy(collect([1, 2, 3]));

Якщо ви користуєтеся м'яким видаленням моделей, ви можете видалити моделі назавжди методом forceDestroy:

Flight::forceDestroy(1);

Метод destroy завантажує кожну модель окремо й викликає метод delete, щоб події deleting та deleted коректно надсилалися для кожної моделі.

Видалення моделей запитами

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

$deleted = Flight::where('active', 0)->delete();

Щоб видалити всі моделі в таблиці, виконайте запит без жодних умов:

$deleted = Flight::query()->delete();

Коли ви виконуєте масове видалення через Eloquent, події моделі deleting та deleted для видалених моделей не надсилаються. Причина в тому, що при виконанні запиту на видалення моделі насправді ніколи не дістаються з бази.

М'яке видалення

Крім справжнього видалення записів із бази даних, Eloquent уміє «м'яко видаляти» моделі (soft delete). Коли модель видалено м'яко, вона насправді лишається в базі. Натомість їй задається атрибут deleted_at з датою й часом «видалення». Щоб увімкнути м'яке видалення для моделі, додайте до неї трейт Illuminate\Database\Eloquent\SoftDeletes:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Flight extends Model
{
    use SoftDeletes;
}

Трейт SoftDeletes автоматично приведе атрибут deleted_at до екземпляра DateTime / Carbon.

Вам також слід додати до таблиці стовпець deleted_at. Конструктор схеми Laravel має для цього допоміжний метод:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('flights', function (Blueprint $table) {
    $table->softDeletes();
});

Schema::table('flights', function (Blueprint $table) {
    $table->dropSoftDeletes();
});

Тепер, коли ви викликаєте на моделі метод delete, стовпцю deleted_at буде задано поточну дату й час. Проте запис моделі лишиться в таблиці. Коли ви робите запит до моделі з м'яким видаленням, м'яко видалені моделі автоматично виключаються з усіх результатів.

Щоб визначити, чи був конкретний екземпляр моделі м'яко видалений, скористайтеся методом trashed:

if ($flight->trashed()) {
    // ...
}

Відновлення м'яко видалених моделей

Іноді вам потрібно «скасувати видалення» м'яко видаленої моделі. Щоб відновити її, викличте на екземплярі моделі метод restore. Метод restore задасть стовпцю deleted_at значення null:

$flight->restore();

Метод restore можна використати й у запиті, щоб відновити кілька моделей. І знову ж таки, як і інші «масові» операції, це не надішле жодних подій моделі для відновлених моделей:

Flight::withTrashed()
    ->where('airline_id', 1)
    ->restore();

Метод restore можна також використовувати при побудові запитів до зв'язків:

$flight->history()->restore();

Остаточне видалення моделей

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

$flight->forceDelete();

Метод forceDelete можна також використовувати при побудові запитів до зв'язків Eloquent:

$flight->history()->forceDelete();

Запити до м'яко видалених моделей

Включення м'яко видалених моделей

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

use App\Models\Flight;

$flights = Flight::withTrashed()
    ->where('account_id', 1)
    ->get();

Метод withTrashed можна викликати й при побудові запиту до зв'язку:

$flight->history()->withTrashed()->get();

Отримання лише м'яко видалених моделей

Метод onlyTrashed дістане лише м'яко видалені моделі:

$flights = Flight::onlyTrashed()
    ->where('airline_id', 1)
    ->get();

Очищення моделей

Іноді вам потрібно періодично видаляти моделі, які більше не потрібні. Для цього додайте до таких моделей трейт Illuminate\Database\Eloquent\Prunable або Illuminate\Database\Eloquent\MassPrunable. Після цього реалізуйте метод prunable, що повертає конструктор запитів Eloquent, який відбирає непотрібні моделі:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;

class Flight extends Model
{
    use Prunable;

    /**
     * Get the prunable model query.
     */
    public function prunable(): Builder
    {
        return static::where('created_at', '<=', now()->minus(months: 1));
    }
}

Позначаючи моделі як Prunable, ви можете також описати в моделі метод pruning. Його буде викликано перед видаленням моделі. Це стане в пригоді, щоб видалити пов'язані з моделлю додаткові ресурси - наприклад, збережені файли, - перш ніж модель назавжди зникне з бази:

/**
 * Prepare the model for pruning.
 */
protected function pruning(): void
{
    // ...
}

Налаштувавши модель для очищення, заплануйте artisan-команду model:prune у файлі routes/console.php вашого застосунку. Ви вільні обрати доречний інтервал запуску цієї команди:

use Illuminate\Support\Facades\Schedule;

Schedule::command('model:prune')->daily();

Усередині команда model:prune автоматично знаходить моделі «Prunable» у каталозі app/Models вашого застосунку. Якщо ваші моделі лежать деінде, вкажіть назви класів опцією --model:

Schedule::command('model:prune', [
    '--model' => [Address::class, Flight::class],
])->daily();

Якщо ви хочете виключити певні моделі з очищення, а решту знайдених очищати, скористайтеся опцією --except:

Schedule::command('model:prune', [
    '--except' => [Address::class, Flight::class],
])->daily();

Перевірити свій запит prunable можна, виконавши команду model:prune з опцією --pretend. У цьому режимі команда model:prune просто повідомить, скільки записів було б видалено, якби вона справді відпрацювала:

php artisan model:prune --pretend

Моделі з м'яким видаленням буде видалено назавжди (forceDelete), якщо вони відповідають запиту очищення.

Масове очищення

Коли моделі позначені трейтом Illuminate\Database\Eloquent\MassPrunable, вони видаляються з бази запитами масового видалення. Тому метод pruning не буде викликано, а події моделі deleting та deleted не надішлються. Причина в тому, що моделі насправді ніколи не дістаються перед видаленням, і саме тому очищення відбувається значно ефективніше:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\MassPrunable;

class Flight extends Model
{
    use MassPrunable;

    /**
     * Get the prunable model query.
     */
    public function prunable(): Builder
    {
        return static::where('created_at', '<=', now()->minus(months: 1));
    }
}

Копіювання моделей

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

use App\Models\Address;

$shipping = Address::create([
    'type' => 'shipping',
    'line_1' => '123 Example Street',
    'city' => 'Victorville',
    'state' => 'CA',
    'postcode' => '90001',
]);

$billing = $shipping->replicate()->fill([
    'type' => 'billing'
]);

$billing->save();

Щоб виключити один чи кілька атрибутів із копіювання в нову модель, передайте методу replicate масив:

$flight = Flight::create([
    'destination' => 'LAX',
    'origin' => 'LHR',
    'last_flown' => '2020-03-04 11:00:00',
    'last_pilot_id' => 747,
]);

$flight = $flight->replicate([
    'last_flown',
    'last_pilot_id'
]);

Скопи запитів

Глобальні скопи

Глобальні скопи (global scope) дозволяють додати обмеження до всіх запитів певної моделі. Власна функція м'якого видалення в Laravel користується глобальними скопами, щоб діставати з бази лише «невидалені» моделі. Власні глобальні скопи дають зручний і простий спосіб гарантувати, що кожен запит до певної моделі отримає потрібні обмеження.

Створення скопів

Щоб згенерувати новий глобальний скоп, викличте artisan-команду make:scope - вона покладе згенерований скоп у каталог app/Models/Scopes вашого застосунку:

php artisan make:scope AncientScope

Написання глобальних скопів

Написати глобальний скоп просто. Спершу командою make:scope згенеруйте клас, що реалізує інтерфейс Illuminate\Database\Eloquent\Scope. Інтерфейс Scope вимагає реалізувати один метод - apply. Метод apply може додавати до запиту обмеження where чи інші потрібні вирази:

<?php

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class AncientScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('created_at', '<', now()->minus(years: 2000));
    }
}

Якщо ваш глобальний скоп додає стовпці до виразу select запиту, користуйтеся методом addSelect, а не select. Так ви не заміните ненавмисно наявний вираз select запиту.

Застосування глобальних скопів

Щоб призначити моделі глобальний скоп, просто поставте на ній атрибут ScopedBy:

<?php

namespace App\Models;

use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([AncientScope::class])]
class User extends Model
{
    //
}

Або ж ви можете зареєструвати глобальний скоп вручну, перевизначивши метод booted моделі й викликавши метод addGlobalScope. Метод addGlobalScope приймає єдиний аргумент - екземпляр вашого скопа:

<?php

namespace App\Models;

use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::addGlobalScope(new AncientScope);
    }
}

Після додавання скопа з прикладу вище до моделі App\Models\User виклик методу User::all() виконає такий SQL-запит:

select * from `users` where `created_at` < 0021-02-18 00:00:00

Анонімні глобальні скопи

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::addGlobalScope('ancient', function (Builder $builder) {
            $builder->where('created_at', '<', now()->minus(years: 2000));
        });
    }
}

Прибирання глобальних скопів

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

User::withoutGlobalScope(AncientScope::class)->get();

Або, якщо ви описали глобальний скоп замиканням, передайте рядкову назву, яку йому призначили:

User::withoutGlobalScope('ancient')->get();

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

// Remove all of the global scopes...
User::withoutGlobalScopes()->get();

// Remove some of the global scopes...
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

// Remove all global scopes except the given ones...
User::withoutGlobalScopesExcept([
    SecondScope::class,
])->get();

Локальні скопи

Локальні скопи дозволяють описати типові набори обмежень запиту, які легко перевикористовувати в усьому застосунку. Наприклад, вам може часто знадобитися діставати всіх користувачів, що вважаються «популярними». Щоб описати скоп, додайте до методу Eloquent атрибут Scope.

Скопи завжди мають повертати той самий екземпляр конструктора запитів або void:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include popular users.
     */
    #[Scope]
    protected function popular(Builder $query): void
    {
        $query->where('votes', '>', 100);
    }

    /**
     * Scope a query to only include active users.
     */
    #[Scope]
    protected function active(Builder $query): void
    {
        $query->where('active', 1);
    }
}

Використання локального скопа

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

use App\Models\User;

$users = User::popular()->active()->orderBy('created_at')->get();

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

$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

Але оскільки це громіздко, Laravel має метод orWhere «вищого порядку», який дозволяє плавно зчіплювати скопи без замикань:

$users = User::popular()->orWhere->active()->get();

Динамічні скопи

Іноді вам потрібен скоп, що приймає параметри. Для цього просто додайте свої додаткові параметри до сигнатури методу скопа. Параметри скопа описуються після параметра $query:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include users of a given type.
     */
    #[Scope]
    protected function ofType(Builder $query, string $type): void
    {
        $query->where('type', $type);
    }
}

Коли ви додали очікувані аргументи до сигнатури методу скопа, їх можна передавати при виклику скопа:

$users = User::ofType('admin')->get();

Методи скопів з атрибутом мають бути protected. Викликаючи такий скоп зсередини класу моделі, робіть це через екземпляр конструктора запитів - наприклад, static::query()->ofType('admin'), - щоб виклик пройшов через механізм обробки скопів Eloquent.

Відкладені атрибути

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Scope the query to only include drafts.
     */
    #[Scope]
    protected function draft(Builder $query): void
    {
        $query->withAttributes([
            'hidden' => true,
        ]);
    }
}

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

$draft = Post::draft()->create(['title' => 'In Progress']);

$draft->hidden; // true

Щоб метод withAttributes не додавав до запиту умов where, задайте аргументу asConditions значення false:

$query->withAttributes([
    'hidden' => true,
], asConditions: false);

Порівняння моделей

Іноді вам потрібно визначити, чи є дві моделі «однаковими». Методи is та isNot швидко перевіряють, чи мають дві моделі однакові первинний ключ, таблицю й підключення до бази даних:

if ($post->is($anotherPost)) {
    // ...
}

if ($post->isNot($anotherPost)) {
    // ...
}

Методи is та isNot доступні також при роботі зі зв'язками belongsTo, hasOne, morphTo і morphOne. Це особливо зручно, коли ви хочете порівняти пов'язану модель, не виконуючи запиту для її отримання:

if ($post->author()->is($user)) {
    // ...
}

Події

Хочете транслювати події Eloquent прямо до клієнтської частини застосунку? Погляньте на бродкастинг подій моделей у Laravel.

Моделі Eloquent надсилають кілька подій, що дозволяють вклинитися в такі моменти життєвого циклу моделі: retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, trashed, forceDeleting, forceDeleted, restoring, restored і replicating.

Подія retrieved надсилається, коли наявну модель дістають із бази даних. Коли нову модель зберігають уперше, надсилаються події creating та created. Події updating / updated надсилаються, коли наявну модель змінюють і викликають метод save. Події saving / saved надсилаються, коли модель створюють або оновлюють - навіть якщо атрибути моделі не змінилися. Назви подій, що закінчуються на -ing, надсилаються до того, як зміни моделі збережено, а ті, що закінчуються на -ed, - після збереження змін.

Щоб почати слухати події моделі, опишіть у моделі Eloquent властивість $dispatchesEvents. Вона зіставляє різні моменти життєвого циклу моделі з вашими власними класами подій. Кожен клас події моделі має очікувати екземпляр відповідної моделі у своєму конструкторі:

<?php

namespace App\Models;

use App\Events\UserDeleted;
use App\Events\UserSaved;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The event map for the model.
     *
     * @var array<string, string>
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

Описавши та зіставивши події Eloquent, ви можете обробляти їх слухачами подій.

Коли ви виконуєте масове оновлення чи видалення через Eloquent, події моделі saved, updated, deleting та deleted для зачеплених моделей не надсилаються. Причина в тому, що при масових оновленнях чи видаленнях моделі насправді ніколи не дістаються з бази.

Використання замикань

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::created(function (User $user) {
            // ...
        });
    }
}

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

use function Illuminate\Events\queueable;

static::created(queueable(function (User $user) {
    // ...
}));

Спостерігачі

Опис спостерігачів

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

php artisan make:observer UserObserver --model=User

Ця команда покладе нового спостерігача в каталог app/Observers. Якщо каталогу немає, Artisan створить його за вас. Ваш новий спостерігач виглядатиме так:

<?php

namespace App\Observers;

use App\Models\User;

class UserObserver
{
    /**
     * Handle the User "created" event.
     */
    public function created(User $user): void
    {
        // ...
    }

    /**
     * Handle the User "updated" event.
     */
    public function updated(User $user): void
    {
        // ...
    }

    /**
     * Handle the User "deleted" event.
     */
    public function deleted(User $user): void
    {
        // ...
    }

    /**
     * Handle the User "restored" event.
     */
    public function restored(User $user): void
    {
        // ...
    }

    /**
     * Handle the User "forceDeleted" event.
     */
    public function forceDeleted(User $user): void
    {
        // ...
    }
}

Щоб зареєструвати спостерігача, поставте на відповідній моделі атрибут ObservedBy:

use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
    //
}

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

use App\Models\User;
use App\Observers\UserObserver;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    User::observe(UserObserver::class);
}

Спостерігач може слухати й додаткові події - наприклад, saving і retrieved. Ці події описано в документації з подій.

Спостерігачі та транзакції

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

<?php

namespace App\Observers;

use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;

class UserObserver implements ShouldHandleEventsAfterCommit
{
    /**
     * Handle the User "created" event.
     */
    public function created(User $user): void
    {
        // ...
    }
}

Вимкнення подій

Іноді вам потрібно тимчасово «вимкнути» всі події, які надсилає модель. Це робиться методом withoutEvents. Метод withoutEvents приймає єдиний аргумент - замикання. Будь-який код, виконаний у цьому замиканні, не надсилатиме подій моделі, а значення, яке поверне замикання, поверне й метод withoutEvents:

use App\Models\User;

$user = User::withoutEvents(function () {
    User::findOrFail(1)->delete();

    return User::find(2);
});

Збереження однієї моделі без подій

Іноді ви хочете «зберегти» певну модель, не надсилаючи жодних подій. Це робиться методом saveQuietly:

$user = User::findOrFail(1);

$user->name = 'Victoria Faith';

$user->saveQuietly();

Ви можете також «оновити», «видалити», «м'яко видалити», «відновити» та «скопіювати» модель без надсилання подій:

$user->deleteQuietly();
$user->forceDeleteQuietly();
$user->restoreQuietly();