Питання
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
171 питань
Починається все з простого $user->is_admin, далі зʼявляється редактор, потім модератор - і умови розповзаються по коду.
Крок перший - роль як enum:
enum Role: string
{
case Admin = 'admin';
case Editor = 'editor';
case Author = 'author';
}
Це вже краще за рядки, але перевірки виду $user->role === Role::Editor розкидані по контролерах ламаються, щойно права ролі змінюються.
Крок другий - права, а не ролі. Код питає «чи можна публікувати», а не «чи ти редактор»:
Gate::define('publish', fn (User $user) => $user->hasPermission('publish'));
Роль стає лише набором прав, і зміна набору не потребує правок у коді.
Policy для дій над моделлю:
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->hasPermission('posts.update')
|| $post->author_id === $user->id;
}
}
before() для суперкористувача - щоб не дублювати перевірку в кожному методі:
public function before(User $user): ?bool
{
return $user->isAdmin() ? true : null;
}
Повертати треба саме null, а не false: false заборонить дію остаточно й не дасть іншим методам відпрацювати.
Коли брати пакет. spatie/laravel-permission дає ролі, права й кеш перевірок з коробки. Він доречний, коли набір прав змінюють з адмінки; якщо ролей три й вони зашиті в код, enum із Policy простіший.
Що не забути: перевірки прав кешуються не самі - на кожен запит це кілька звернень до бази, тому права користувача варто завантажувати разом із ним.
Filament будує таблицю з Eloquent-запиту, тож усі звичайні проблеми продуктивності лишаються - але ховаються за декларативним синтаксисом.
N+1 у колонках. Колонка зі звʼязком робить запит на рядок:
TextColumn::make('company.name') // + 1 запит на кожен рядок
Лікується модифікацією запиту таблиці:
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()->with(['company', 'technologies']);
}
Лічильники. ->counts('vacancies') замість завантаження всієї колекції заради count().
Дорогі обчислення в state(). Замикання виконується для кожного рядка, тож звернення до бази всередині - те саме N+1, лише написане інакше.
Сортування й пошук за звʼязком роблять join, і без індексу на зовнішньому ключі це помітно:
TextColumn::make('company.name')
->searchable() // where по приєднаній таблиці
->sortable()
Що ще сповільнює адмінку:
->paginated([10, 25, 50, 'all'])з опцієюallна великій таблиці вивантажує все в памʼять одним кліком користувача.- Глобальний пошук по багатьох ресурсах виконує запит на кожен зареєстрований ресурс.
- Мініатюри без кешу:
ImageColumn, що генерує прев'ю на льоту, робить це для кожного рядка. - Опції
Selectбез->searchable()вивантажують усі записи довідника у форму.
Як шукати причину: Debugbar або Telescope на сторінці ресурсу покажуть кількість запитів. Таблиця на 25 рядків має вкладатися в одиниці запитів; десятки - ознака незавантаженого звʼязку.
Enum у коді змінюється легко, а от рядки, які вже лежать у базі, - ні. Саме тут зʼявляються помилки після деплою.
Найнебезпечніше - перейменувати кейс:
// було
case Middle = 'middle';
// стало
case Mid = 'mid';
Код збереться, а всі наявні рядки зі значенням middle перестануть кастуватися: ValueError: "middle" is not a valid backing value. Впаде не міграція, а звичайна сторінка.
Правильний порядок для перейменування:
- Додати новий кейс, лишивши старий.
- Міграцією перевести дані:
Vacancy::where('level', 'middle')->update(['level' => 'mid']). - Наступним релізом прибрати старий кейс.
Додати новий кейс - безпечно, якщо колонка varchar. Але коли в базі використано нативний тип enum, потрібна ще й міграція самої колонки, а Schema::table()->change() для нативних enum працює не в усіх драйверах - подекуди доводиться писати DB::statement().
Тому колонку під enum майже завжди роблять string: перелік живе в PHP, база зберігає рядок, і зміни не потребують ALTER на великій таблиці.
Захист від падіння на невідомому значенні:
// null замість винятку, коли в базі щось несподіване
$level = VacancyLevel::tryFrom($vacancy->getRawOriginal('level'));
І ще одне: якщо enum використовується у валідації через Rule::enum(), видалений кейс одразу зробить старі збережені записи невалідними при редагуванні - про це згадують уже після скарг користувачів.
Довідники - ролі, статуси, категорії, налаштування - це дані застосунку, а не демо. Вони потрібні на проді, і сідер для них має витримувати повторний запуск.
Не так:
Role::create(['slug' => 'admin', 'name' => 'Адміністратор']);
Другий запуск або впаде на унікальному індексі, або створить дубль.
Так:
foreach ([
['slug' => 'admin', 'name' => 'Адміністратор'],
['slug' => 'editor', 'name' => 'Редактор'],
] as $role) {
Role::updateOrCreate(['slug' => $role['slug']], $role);
}
Ключ пошуку - стабільний ідентифікатор, а не id: автоінкремент на різних середовищах різний.
Обережно з updateOrCreate на довідниках, які редагують з адмінки. Він перезапише зміни, зроблені руками. Якщо назву дозволено міняти, оновлювати варто лише технічні поля:
Role::firstOrCreate(['slug' => 'editor'], ['name' => 'Редактор']);
firstOrCreate створює запис, якщо його немає, і не чіпає наявний.
Як запускати на проді:
php artisan db:seed --class=RolesSeeder --force
--force потрібен, бо в продакшн-середовищі Artisan питає підтвердження. Викликати db:seed без --class на проді небезпечно: DatabaseSeeder зазвичай тягне ще й демо-дані.
Альтернатива - зробити це міграцією. Тоді заповнення виконається рівно один раз і саме в потрібний момент деплою, а не за окремою командою, яку легко забути. Для довідника, від якого залежить код нового випуску, це надійніше.
Мову зазвичай визначає middleware - з URL, налаштувань користувача чи заголовка браузера:
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = $request->route('locale')
?? $request->user()?->locale
?? $request->getPreferredLanguage(['uk', 'en']);
App::setLocale($locale);
return $next($request);
}
}
Де це стикається з кешем - три різні місця:
1. Кеш застосунку. Значення, покладене під ключем nav.items, буде віддане й іншій мові. Ключ має включати локаль:
Cache::remember('nav.items.'.app()->getLocale(), 3600, fn () => ...);
2. Кеш HTTP і CDN. Якщо мова визначається заголовком Accept-Language, а не URL, то одна адреса віддає різний вміст - і проксі роздасть усім ту версію, яка потрапила в кеш першою. Рятує або Vary: Accept-Language, або мова в URL.
3. Кеш маршрутів. route:cache фіксує маршрути один раз, тож локаль не може бути частиною визначення маршруту - лише параметром.
Чому мову краще тримати в URL. Окремі адреси на кожну мову - це єдиний варіант, який нормально індексується: у пошуку зʼявляються обидві версії, hreflang їх звʼязує, а посилання веде туди, куди вело в того, хто ним поділився. Мова в сесії всього цього не дає.
Дрібниця, яку часто пропускають: App::setLocale() не змінює локаль Carbon - для дат потрібен окремий Carbon::setLocale().
Кожен провайдер завантажується на кожному запиті. Якщо він лише реєструє сервіс, потрібний в одному місці з сотні, це марна робота на всіх інших запитах.
Відкладений провайдер реєструється лише тоді, коли його сервіс справді резолвлять:
use Illuminate\Contracts\Support\DeferrableProvider;
class WeatherServiceProvider extends ServiceProvider implements DeferrableProvider
{
public function register(): void
{
$this->app->singleton(WeatherClient::class, function ($app) {
return new WeatherClient(config('services.weather.key'));
});
}
/**
* @return array<int, class-string>
*/
public function provides(): array
{
return [WeatherClient::class];
}
}
Laravel запамʼятовує зіставлення «сервіс → провайдер» у маніфесті й піднімає провайдер лише за потреби.
Умови, за яких це працює:
- Провайдер має лише
register(). Якщо єboot(), слухачі подій, маршрути чи публікація ресурсів - вони не виконаються, поки сервіс ніхто не запросить, тобто фактично ніколи. provides()мусить перелічувати всі привʼязки. Забута привʼязка просто не знайдеться в контейнері.
Коли не варто: якщо провайдер реєструє щось, що потрібне майже завжди, відкладення дає нуль виграшу й додає місце для помилки.
Скільки це коштує. Виграш помітний на застосунку з десятками провайдерів; на невеликому - в межах похибки. Перед тим як відкладати, варто виміряти профайлером, а не робити це «про всяк випадок».
Питання з реальних співбесід Laravel і PHP - 171 питання у 41 темі, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
- Теми
- Eloquent 19 Архітектура 12 Database 10 Performance 7 Routing 5 Безпека 5 Blade 5 API 5
Готуєтесь до співбесіди не просто так: зараз на сайті 167 відкритих вакансій Laravel і PHP. Переглянути вакансії