State - поведінка залежно від стану
Змінює поведінку об'єкта залежно від стану, прибираючи каскади if. Замовлення, підписки, оплати та пакет spatie/laravel-model-states.
Почніть вводити, щоб шукати по статтях, вакансіях, компаніях, проєктах, ресурсах, пакетах, подіях і користувачах.
@oksana-kovalchuk
Редакторка Laravel Ukraine. Стежу за новинами екосистеми та перекладаю найцікавіше для спільноти.
Змінює поведінку об'єкта залежно від стану, прибираючи каскади if. Замовлення, підписки, оплати та пакет spatie/laravel-model-states.
Треба знайти користувачів, у яких є пости? Або тільки з опублікованими постами?
has() перевіряє факт наявності зв'язку.
whereHas() дозволяє додати умови до зв'язку.
Різниця:
has('posts') - користувачі з будь-якими постамиwhereHas('posts', fn($q) => ...) - користувачі з конкретними постамиПорада по продуктивності:
Використовуйте has(), коли немає додаткових умов - він простіший.
// Користувачі з будь-якими постами
$users = User::has('posts')->get();
// Користувачі з мінімум 5 постами
$users = User::has('posts', '>=', 5)->get();
// Користувачі тільки з опублікованими постами
$users = User::whereHas('posts', function ($query) {
$query->where('published', true);
})->get();
// Більш складні умови
$users = User::whereHas('posts', function ($query) {
$query->where('published', true)
->where('views', '>', 1000);
})->get();
// Користувачі без постів
$users = User::doesntHave('posts')->get();
// Комбінування кількох зв'язків
$users = User::has('posts')
->whereHas('comments', fn($q) => $q->where('spam', false))
->get();
Фільтруєте за зв'язком через where('user_id', $user->id)? Можна краще.
whereBelongsTo() сам підставляє зовнішній ключ.
Переваги:
Бонус: працює з масивом моделей.
Порада: дуже зручно в policy-методах, де фільтруєте по auth()->user().
// ПОГАНО: ручний foreign key
$posts = Post::where('user_id', $user->id)->get();
$comments = Comment::where('post_id', $post->id)->get();
// ДОБРЕ: Laravel сам визначає ключ
$posts = Post::whereBelongsTo($user)->get();
$comments = Comment::whereBelongsTo($post)->get();
// Кілька зв'язків
$comments = Comment::whereBelongsTo($user)
->whereBelongsTo($post)
->get();
// Кастомна назва зв'язку
$posts = Post::whereBelongsTo($user, 'author')->get();
// У policy - максимально чисто
public function update(User $user, Post $post)
{
return Post::whereBelongsTo($user)->exists();
}
Laravel 13.20 представив власний компонент обробки зображень Illuminate\Image. Тепер фреймворк пропонує вбудований fluent API для зміни розміру, обрізки, конвертації форматів, контролю якості та збереження результатів без сторонніх пакетів.
Зберігає знімок стану об'єкта і відновлює його, не порушуючи інкапсуляцію. Основа для undo/redo: ролі Originator, Memento та Caretaker.
Треба надіслати email після реєстрації? Або залогувати видалення поста?
Model Events дозволяють підключатися до create/update/delete.
Доступні події:
creating / createdupdating / updateddeleting / deletedsaving / savedДва підходи:
booted() у моделі (швидкий варіант)Порада: використовуйте creating для генерації UUID/slug до збереження.
База зберігає JSON як рядок. Ви всюди вручну робите decode.
Casts виконують перетворення типів автоматично. Ви працюєте з нативними типами PHP.
Поширені касти:
array / json - авто-encode/decodeboolean - 0/1 → true/falsedate / datetime - об'єкти Carbonencrypted - автоматичне шифрування/дешифруванняКастомні касти:
Можна створювати власні для складніших перетворень.
Порада: використовуйте encrypted для чутливих даних (напр. номери карток).
class User extends Model
{
protected $casts = [
'email_verified_at' => 'datetime',
'settings' => 'array',
'is_admin' => 'boolean',
'metadata' => 'json',
'birth_date' => 'date',
'salary' => 'decimal:2',
'credit_card' => 'encrypted',
];
}
// JSON cast - працюємо з масивами
$user->settings = ['theme' => 'dark', 'lang' => 'en'];
$user->save(); // Зберігається як JSON-рядок
$theme = $user->settings['theme']; // Авто-декод
// Datetime cast - Carbon
echo $user->email_verified_at->diffForHumans();
// Boolean cast
if ($user->is_admin) {
// 1/0 у базі, але в коді - true/false
}
Створюєте фільтр пошуку з купою умов if? Це важко читати.
when() додає умову до запиту тільки тоді, коли вона потрібна.
Переваги:
Просунутий варіант:
Є callback для "else".
Порада: Ідеально для API-фільтрів, де параметри опціональні.
// ПОГАНО
$query = User::query();
if ($request->status) {
$query->where('status', $request->status);
}
if ($request->role) {
$query->where('role', $request->role);
}
$users = $query->get();
// ДОБРЕ
$users = User::query()
->when($request->status,
fn($q, $v) => $q->where('status', $v))
->when($request->role,
fn($q, $v) => $q->where('role', $v))
->get();
// З "else"
$users = User::when(
$request->sort === 'name',
fn($q) => $q->orderBy('name'),
fn($q) => $q->orderBy('created_at')
)->get();
Vocalizer - нативне PHP-розширення для локального синтезу мовлення без API-викликів. Підтримує вісім сімейств моделей, клонування голосу з 3-10 секунд запису, захист від артефактів та асинхронну генерацію аудіо.
HTTP-метод QUERY - нове доповнення до специфікації HTTP (RFC 10008), призначене спеціально для запитів. Розглянемо, як побудувати пошуковий ендпоінт на основі Laravel Scout із використанням цього методу.
Перевіряєте, чи існує запис, а потім оновлюєте або створюєте? Забагато коду.
updateOrCreate() виконує обидві операції за один раз.
Як працює:
Магія:
Порада: Ідеально підходить для інтеграцій з API.
// ПОГАНО: два запити, зайвий код
$user = User::where('email', 'john@example.com')->first();
if ($user) {
$user->update(['name' => 'John Doe']);
} else {
User::create([
'email' => 'john@example.com',
'name' => 'John Doe'
]);
}
// ДОБРЕ: один метод, один запит
User::updateOrCreate(
['email' => 'john@example.com'],
['name' => 'John Doe', 'verified' => true]
);
// Синхронізація даних з API
$apiUsers = ExternalAPI::getUsers();
foreach ($apiUsers as $apiUser) {
User::updateOrCreate(
['external_id' => $apiUser['id']],
['name' => $apiUser['name'], 'email' => $apiUser['email']]
);
}
Вийшов Laravel 13.20, який додає вбудовану обробку зображень через новий Image-фасад, атрибут WithoutMiddleware для контролерів, окремий префікс сесій у Redis, тихе збільшення/зменшення в Eloquent та підтримку enum у WithoutOverlapping.
Пишете where('status', 'active') у 20 контролерах? Перестаньте дублювати код.
Query Scopes дозволяють повторно використовувати логіку запитів. Описали один раз - застосовуєте всюди.
Два типи:
Чому варто використовувати скоупи:
Порада: Методи локальних скоупів починаються зі слова scope - Laravel автоматично прибирає його.
// User Model
public function scopeActive($query)
{
return $query->where('status', 'active');
}
public function scopeVerified($query)
{
return $query->whereNotNull('email_verified_at');
}
public function scopeCreatedAfter($query, $date)
{
return $query->where('created_at', '>', $date);
}
// Використання - чисто та читабельно
$users = User::active()->verified()->get();
$recentUsers = User::active()
->createdAfter(now()->subDays(7))
->get();
// Комбінування зі звичайними методами запиту
$admins = User::active()
->where('role', 'admin')
->orderBy('name')
->get();
Laravel Quota - новий пакет для відстеження та контролю накопичувальних лімітів споживання ресурсів. На відміну від RateLimiter, він рахує витрати відносно бюджету, що скидається на межі календарного періоду, і може зберігати дані у базі даних.
Uvora - безкоштовний macOS-застосунок у menu bar, який індексує Laravel-проєкти на вашому комп'ютері й дозволяє миттєво переходити до них через клавіатурний шорткат. Індексує маршрути, контролери, моделі та компоненти, забезпечуючи результати пошуку менш ніж за 50 мілісекунд.
Новий пакет Email Magic Link для Laravel дозволяє користувачам входити в систему через посилання або одноразовий код, надісланий електронною поштою. Підтримує інтеграцію з Laravel Fortify, захист від сканерів електронної пошти та обмеження повторних надсилань.
Laravel AI Tasks - пакет, що надбудовується над Laravel AI SDK, додаючи операційні можливості: багаторазові класи завдань, три режими виконання (синхронний, черговий, потоковий), дашборд з аудитом, підтримку восьми провайдерів та контроль бюджету на рівні орендарів.
phpcpd-next - CLI-інструмент для виявлення скопійованих блоків коду у PHP-проєктах. Підтримує три алгоритми детекції, включно з пошуком переупорядкованих та змінених фрагментів, інтеграцію з GitHub Code Scanning, вбудований API та PHPUnit-трейт для перевірки дублювання у тестах.
Фільтруєш користувачів які мають пости? whereHas() створює підзапит.
whereRelation() використовує join. Швидше на великих таблицях.
Різниця в продуктивності:
whereHas() - підзапит для перевірки кожного рядкаwhereRelation() - одна join операціяКоли використовувати:
Використовуй whereRelation() для простих умов на пов'язаних моделях.
Використовуй whereHas() коли потрібна складна логіка або підрахунок.
Покращення Laravel 11:
whereRelation() додано в Laravel 8.57. Читабельніше ніж ручні join'и.
Порада: Для простих фільтрів зв'язків завжди віддавай перевагу whereRelation() над whereHas().
Економить пам'ять, повторно використовуючи спільні екземпляри замість тисяч однакових. Розділення стану на внутрішній і зовнішній на прикладі кешу довідників.
USAIGE - Laravel-пакет для моніторингу AI-запитів через Laravel AI SDK. Записує кожен запит з підрахунком токенів, витрат, деталями провайдера та моделі, таймінгами і статусом помилок. Усі дані доступні через вбудовану веб-панель.
Yammi Audit Log - новий пакет для Laravel, який вирішує проблему аудиту змін у моделях через черги. На відміну від звичайних рішень, він зберігає інформацію про того, хто ініціював зміну, навіть якщо вона відбулася через кілька черг або фонових завдань.
Watchtower - новий Laravel-пакет для централізованого моніторингу scheduled tasks, черг, job'ів та винятків на єдиній панелі. Підтримує будь-які драйвери черг, багатобазові та multi-tenant налаштування, а також включає інструменти для негайного запуску задач та сповіщення про збої.
Laravel 13.17.0 додає першокласну підтримку метаданих маршрутів з dot-нотацією та каскадним успадкуванням, інтеграцію з PostgreSQL transaction pooler, нову команду dev:list для перегляду dev-процесів та обробник винятків Should Not Retry для черг.
NationForge - це Laravel 12 застосунок з відкритим кодом для управління діяльністю громадських організацій, політичних рухів та ком'юніті-груп. Поєднує CRM, події, email-кампанії, донати та проєкти в одній панелі на власному сервері.
Lattice - це server-driven UI фреймворк для Laravel, що дозволяє описувати сторінки, форми, таблиці, дії та меню в PHP, а рендерити їх як React-компоненти через Inertia. Сервер стає єдиним джерелом правди для інтерфейсу, а клієнт лише відповідає за його відображення.
При створенні 1,000 записів в циклі? Кожен create() звертається до бази окремо.
insert() надсилає всі записи одним запитом. В 100 разів швидше.
Компроміс:
create() запускає події, заповнює timestamps, повертає модельinsert() пропускає все, просто сирий SQL insertКоли використовувати insert():
Порада: Використовувати upsert() для масових операцій insert-or-update в Laravel 8+.
// ПОГАНО: 1000 окремих запитів
foreach ($records as $record) {
User::create($record); // Повільно
}
// ДОБРЕ: Один запит для всіх
User::insert($records);
// З timestamps
$now = now();
$records = collect($records)->map(function ($record) use ($now) {
return array_merge($record, [
'created_at' => $now,
'updated_at' => $now,
]);
})->toArray();
User::insert($records);
// Upsert - вставити або оновити
User::upsert(
[
['email' => 'john@example.com', 'name' => 'John'],
['email' => 'jane@example.com', 'name' => 'Jane'],
],
['email'], // Унікальна колонка
['name'] // Оновити ці колонки
);
Патерн Prototype створює нові об'єкти через клонування шаблону. Це доречно, коли ініціалізація об'єкта дорога або складна.
Патерн Builder корисний, коли об'єкт має багато опцій і варіантів. Він дозволяє зібрати об'єкт поетапно і уникнути довгих конструкторів із десятком аргументів.
Патерн Abstract Factory створює сім'ї пов'язаних об'єктів без прив'язки до конкретних класів. Він корисний, коли потрібно підтримувати кілька конфігурацій або провайдерів, які мають узгоджуватись між собою (наприклад, пошта і нотифікації одного хмарного провайдера).
Перевірка чи користувач має пости? Використання $user->posts->count() > 0 завантажує всі пости.
exists() зупиняється на першому збігу. Набагато швидше.
Різниця:
count() підраховує всі записиexists() зупиняється коли знаходить першийВплив на продуктивність:
З 10,000 постів - count() завантажує всі, exists() знаходить один і зупиняється.
Порада: Використання exists() для булевих перевірок, count() тільки коли потрібне фактичне число.
// ПОГАНО: Завантажує всі пости, рахує їх
if ($user->posts->count() > 0) {
echo "Користувач має пости";
}
// ДОБРЕ: Зупиняється на першому пості
if ($user->posts()->exists()) {
echo "Користувач має пости";
}
// Перевірка чи зв'язок порожній
if ($user->posts()->doesntExist()) {
echo "Пости не знайдено";
}
// Рівень запиту
if (Post::where('published', true)->exists()) {
// Принаймні один опублікований пост існує
}
// Коли справді потрібна кількість
$postCount = $user->posts()->count();
echo "Користувач має {$postCount} постів";
// Перевірка порожнього на завантаженому зв'язку
if ($user->posts->isEmpty()) {
// Вже завантажено, без додаткових запитів
}
При завантаженні 50,000 записів для показу 10 на екрані.
Додаток зависає. Серверу бракує пам'яті. Користувач закриває браузер через 30 секунд.
Проблема:
Без limit(), Eloquent завантажує все в пам'ять. Пагінація допомагає, але все одно потрібен limit() в багатьох випадках.
Коли використовувати:
Порада: Поєднуй з orderBy() для консистентних результатів. Без сортування limit() повертає випадкову підмножину.
// ПОГАНО: Завантажує все
$products = Product::where('active', true)->get();
// Повертає 50,000 продуктів в пам'яті
// ДОБРЕ: Тільки те що потрібно
$featured = Product::where('featured', true)
->orderBy('created_at', 'desc')
->limit(5)
->get();
// Останні 10 замовлень
$recent = Order::latest()->limit(10)->get();
// Топ 3 користувачі за балами
$leaders = User::orderBy('points', 'desc')->limit(3)->get();
// Попередній перегляд дашборду
$stats = [
'recent_orders' => Order::latest()->limit(5)->get(),
'top_products' => Product::orderBy('sales', 'desc')
->limit(10)->get(),
'new_users' => User::latest()->limit(20)->get(),
];
// Без orderBy - непередбачувані результати
$random = Post::limit(5)->get(); // ПОГАНО
$latest = Post::latest()->limit(5)->get(); // ДОБРЕ
Завантаження повного рядка користувача з 20 колонками. Потрібні тільки ім'я та email.
База даних надсилає всі дані. Пам'ять витрачається. Запит повільніший.
Рішення:
Використовувати select() щоб вказати точні колонки. Швидші запити, менше пам'яті.
Важливо:
Завжди включай первинний ключ (id) щоб зв'язки працювали.
Просунуте:
Використовувати addSelect() щоб додати колонки до існуючого select.
Порада: Особливо важливо для таблиць з TEXT/BLOB колонками або таблиць з 20+ колонками.
// ПОГАНО: Завантажує всі колонки
$users = User::all();
foreach ($users as $user) {
echo $user->name; // Інші 18 колонок витрачені даремно
}
// ДОБРЕ: Тільки те що потрібно
$users = User::select('id', 'name', 'email')->get();
// Додавати колонки динамічно
$query = User::select('id', 'name');
if ($needEmail) {
$query->addSelect('email');
}
// Зі зв'язками - потрібні зовнішні ключі
$posts = Post::select('id', 'title', 'user_id')
->with('author:id,name')
->get();
// Агрегація без завантаження моделей
$totalViews = Post::sum('views');
$avgRating = Post::avg('rating');
$maxPrice = Product::max('price');
Переказ $100 між рахунками. Гроші списані з Аліси. Сервер падає. Боб їх ніколи не отримує.
Без транзакцій часткові оновлення пошкодять дані.
Як це працює:
Всі запити виконуються як одна атомарна одиниця. Якщо щось не вдається - все відкочується назад. База даних ніколи не опиняється в неузгодженому стані.
Три способи використання:
Важливо:
Транзакції працюють тільки з InnoDB таблицями. Тримати їх короткими - вони блокують рядки. Довгі операції блокують інші запити.
Не змішувати виклики зовнішніх API з транзакціями бази даних.
// ПОГАНО: Часткове збереження при помилці
$user = User::create($data);
$profile = Profile::create(['user_id' => $user->id]);
// Якщо це не вдається, користувач існує без профілю
// ДОБРЕ: Все або нічого
DB::transaction(function () use ($data) {
$user = User::create($data);
$profile = Profile::create(['user_id' => $user->id]);
$user->sendWelcomeEmail();
});
// Ручний контроль
DB::beginTransaction();
try {
$order = Order::create($orderData);
$payment = Payment::create($paymentData);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
// З повтором при deadlock
DB::transaction(function () {
// Код
}, 3); // Повторити 3 рази якщо deadlock
Сторінка завантажує 10 постів. База даних робить 101 запит.
1 запит для постів. 100 запитів для авторів. Класична проблема N+1.
Рішення: Eager Loading
Використання with() для завантаження зв'язків наперед. Один запит стає двома.
Порада: Laravel Debugbar показує всі запити. Спочатку встановити його.
Коли НЕ використовувати eager loading:
Просунутий трюк:
Використовувати withCount() коли потрібна тільки кількість, а не повний зв'язок.
// ПОГАНО: 101 запит
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Запит на кожен пост
}
// ДОБРЕ: 2 запити
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name; // Без додаткових запитів
}
// Завантажити декілька зв'язків
$posts = Post::with(['author', 'comments', 'tags'])->get();
// Вкладені зв'язки
$posts = Post::with('comments.author')->get();
// Потрібна тільки кількість?
$posts = Post::withCount('comments')->get();
echo $posts[0]->comments_count; // Зв'язок не завантажений
NativePHP for Mobile тепер доступна безкоштовно: core і ключові плагіни відкриті під MIT, тож почати розробку мобільних застосунків на Laravel можна без платного входу.
На Laracon India 2026 Тейлор Отвелл показав перше демо Laravel AI SDK - нового шару для роботи з AI-провайдерами у звичному стилі Laravel. Публічний реліз очікується вже наступного тижня.
Сьогодні команда Laravel офіційно випустила Laravel Boost 2.0 - важливе оновлення пакету, що значно розширює можливості AI-асистованої розробки в екосистемі Laravel.