Питання
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
171 питань
Їх часто плутають, бо обидва створюють записи. Різниця в тому, що саме вони створюють.
Фабрика описує, як виглядає один правдоподібний запис:
class VacancyFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->jobTitle(),
'level' => fake()->randomElement(VacancyLevel::cases()),
'is_published' => true,
];
}
public function draft(): static
{
return $this->state(fn () => ['is_published' => false]);
}
}
Використовується переважно в тестах, де потрібен запис із конкретною властивістю:
$vacancy = Vacancy::factory()->draft()->create();
Сідер заповнює базу набором даних - і зазвичай викликає фабрики:
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call(RolesSeeder::class); // довідник: конкретні значення
Vacancy::factory()->count(50)->create(); // демо-дані: випадкові
}
}
Коли що потрібне:
- Довідники - ролі, категорії, статуси, налаштування. Це реальні дані застосунку, у них немає випадковості, і вони мають бути ідемпотентними:
firstOrCreate()абоupdateOrCreate(), щоб повторний запуск не плодив дублів. - Демо-дані для локальної розробки - фабрики всередині сідера.
- Тести - фабрики напряму, без сідера: кожен тест створює рівно те, що перевіряє.
Головне правило: сідер із довідником має бути безпечним для повторного запуску, бо його виконують і на проді. Сідер з демо-даними на прод не потрапляє взагалі.
Підзапит - запит, вкладений в інший. Eloquent дозволяє вставляти їх у select, where, orderBy.
// додати останню дату входу кожного користувача одним запитом
User::addSelect(['last_login_at' => Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->limit(1),
])->get();
// сортування за підзапитом
Destination::orderByDesc(
Flight::select('arrived_at')->whereColumn('destination_id', 'destinations.id')->latest()->limit(1)
)->get();
Підзапити допомагають уникнути N+1 і зайвих операцій JOIN, обчислюючи похідні значення в межах одного запиту.
Scout - це загальний інтерфейс до пошукових рушіїв: модель позначається трейтом, а рушій (Meilisearch, Algolia, Typesense, база) підмінюється конфігурацією.
class Vacancy extends Model
{
use Searchable;
/**
* @return array<string, mixed>
*/
public function toSearchableArray(): array
{
return [
'title' => $this->title,
'description' => $this->description,
'company' => $this->company?->name,
];
}
}
Vacancy::search('laravel middle')->paginate(15);
Індексація відбувається автоматично на збереженні моделі, а масово - через php artisan scout:import.
Коли вистачить LIKE:
- Пошук по одній-двох колонках, записів - тисячі.
- Достатньо точного входження, без урахування словоформ.
- Не хочеться ще одного сервісу в інфраструктурі.
Vacancy::where('title', 'like', "%{$term}%")->get();
Де LIKE перестає працювати:
- Не використовує індекс із
%на початку - на великій таблиці це повний перебір. - Не знає словоформ. «розробник» не знайдеться за запитом «розробники», і жодних синонімів.
- Не ранжує. Збіг у заголовку та згадка в кінці опису однакові за вагою.
- Не прощає помилок - один зайвий символ, і результат порожній.
- Не шукає по кількох сутностях одразу.
Проміжний варіант - повнотекстовий індекс самої бази: whereFullText() у MySQL, tsvector у PostgreSQL. Він знімає перші три пункти без окремого сервісу, хоча за якістю ранжування поступається спеціалізованим рушіям.
Практичне правило: починайте з LIKE, переходьте на повнотекстовий індекс, коли обсяг виріс, і на Scout - коли потрібні релевантність, помилки в запиті й фасети.
php artisan route:cache компілює всі маршрути в один файл, тож фреймворк не реєструє їх заново на кожному запиті - це помітно пришвидшує бутстрап на застосунках із сотнями маршрутів.
php artisan route:cache # на деплої
php artisan route:clear # скинути
Лише для продакшену. Обмеження: маршрути на основі замикань кешувати не можна - потрібні контролери. Команда php artisan optimize кешує маршрути, конфіг та події разом.
Коротка відповідь: після php artisan config:cache функція env() поза конфігами повертає null.
Чому. Кешування конфігурації зберігає підсумковий масив у один PHP-файл. Файл .env після цього взагалі не читається - у проді його може й не бути. Значення, зчитані під час збирання кешу, всередині конфігів залишаються; будь-який виклик env() в іншому місці виконується вже без завантаженого середовища.
Як ламається:
class WeatherClient
{
public function __construct()
{
// У проді після config:cache тут null.
$this->key = env('WEATHER_KEY');
}
}
Найгірше, що локально все працює - кеш конфігурації зазвичай збирають лише в проді.
Правильно: env() живе тільки у файлах config/, а код читає конфіг.
// config/services.php
'weather' => [
'key' => env('WEATHER_KEY'),
],
// будь-де в застосунку
$this->key = config('services.weather.key');
Побічна вигода: конфіг можна підмінити в тестах, а .env - ні.
config(['services.weather.key' => 'test-key']);
Виняток - файли, що виконуються до завантаження застосунку (bootstrap/), і сам AppServiceProvider тут не виняток: у ньому теж потрібен config().
php artisan optimize виконує пакет кешувань, які прибирають повторну роботу з кожного запиту:
php artisan config:cache # усі config/*.php в один масив
php artisan route:cache # маршрути в серіалізований вигляд
php artisan view:cache # прекомпіляція Blade
php artisan event:cache # мапа подій і слухачів
Зворотна дія - php artisan optimize:clear.
Що це дає: застосунок перестає на кожному запиті обходити десятки файлів конфігурації, парсити маршрути й компілювати шаблони. На середньому проєкті це десятки мілісекунд, які видно на кожному запиті.
Коли шкодить:
- Локально під час розробки. Закешований конфіг не помічає змін у
.env, а закешовані маршрути - нових маршрутів. Півгодини пошуку помилки, якої немає. route:cacheне працює із замиканнями в маршрутах: команда впаде з помилкою. Маршрути мають вказувати на контролери.env()поза конфігами перестає працювати - див. окреме питання про це.- Якщо конфіг залежить від запиту. Кеш збирається один раз під час деплою, тож щось на кшталт мультитенантності за доменом у конфігу зафіксується у стані збирання.
Порядок під час деплою має значення: спершу викласти новий код, потім кешувати. Якщо кешувати до оновлення файлів, у кеш потрапить попередня версія.
Згенерувати клас, що реалізує ValidationRule:
php artisan make:rule Uppercase
class Uppercase implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) {
$fail('Поле :attribute має бути у верхньому регістрі.');
}
}
}
$request->validate(['code' => [new Uppercase]]);
Для разових перевірок можна передати замикання прямо в правило: 'field' => [fn ($attr, $value, $fail) => ...].
Підключення оголошуються в config/database.php. Вибір конкретного:
DB::connection('reporting')->table('events')->get();
class AnalyticsEvent extends Model
{
protected $connection = 'reporting'; // модель завжди на цьому з'єднанні
}
Типові сценарії:
- Read/Write splitting - окремі хости для читання й запису (Laravel сам маршрутизує
SELECTна репліку):'mysql' => ['read' => [...], 'write' => [...]], - Окрема аналітична або legacy-БД.
Laravel абстрагує файлові операції через фасад Storage поверх Flysystem. «Диски» (local, public, s3) налаштовуються в config/filesystems.php.
Storage::disk('s3')->put('reports/q1.pdf', $contents);
$url = Storage::disk('s3')->url('reports/q1.pdf');
$temporary = Storage::disk('s3')->temporaryUrl('reports/q1.pdf', now()->addMinutes(5));
php artisan storage:linkстворює символічне посиланняpublic/storage → storage/app/publicдля публічного доступу.- Зміна сховища (локально ↔ S3) не вимагає переписування коду - лише конфіг.
Драйвер задається в config/session.php. На одному сервері різниці майже немає, на кількох - вона вирішальна.
file - файли на диску сервера. За балансувальником користувач після кожного запиту може потрапити на інший сервер, де його сесії немає, тож його «розлогінює» через раз.
cookie - усе зберігається в самій куці, зашифрованій. Спільного сховища не треба, але обсяг обмежений ~4 КБ, і дані їздять у кожному запиті.
database - таблиця sessions, спільна для всіх серверів. Працює надійно, ціна - запит на кожен запит.
redis - спільне сховище в памʼяті. Стандартний вибір для кількох серверів: швидко й без навантаження на базу.
Що ще ламається на кількох серверах, крім драйвера:
APP_KEYмає бути однаковий на всіх серверах, інакше куки, зашифровані одним, не розшифрує інший.- Той самий Redis мають бачити всі вузли - окремі інстанси не допоможуть.
Дві деталі, що псують життя окремо від масштабування. Сесія блокується на час запиту, тож кілька паралельних AJAX-запитів шикуються в чергу - для читання це знімається ->block() або сесією без запису. І в API сесій зазвичай узагалі не має бути: токен не потребує стану на сервері, а SESSION_DRIVER для stateless-запитів - зайва робота.
Файли в lang/ перекладають інтерфейс - кнопки, підписи, помилки. Для контенту з бази - назв категорій, описів товарів - потрібен інший підхід, і вибір між трьома.
1. Колонки на кожну мову - найпростіше:
Schema::table('categories', function (Blueprint $table) {
$table->string('name_uk');
$table->string('name_en');
});
Працює, поки мов дві. Кожна нова - міграція й правка всіх запитів.
2. JSON-колонка:
$table->json('name'); // {"uk": "Вакансії", "en": "Jobs"}
protected function casts(): array
{
return ['name' => 'array'];
}
Нова мова не потребує міграції. Мінус - пошук і сортування за перекладом стають незручними, хоча PostgreSQL і MySQL уміють індексувати JSON-шляхи.
3. Окрема таблиця перекладів - рядок на пару «запис + мова». Найгнучкіше: пошук і сортування звичайні, мов скільки завгодно. Ціна - join у кожному запиті.
Що врахувати незалежно від вибору:
- Запасний варіант. Якщо перекладу немає, показувати мову за замовчуванням, а не порожнє місце.
- URL. Мову зазвичай виносять у шлях (
/en/jobs), бо це дає окремі адреси для індексації - на відміну від зберігання вибору лише в сесії. hreflangу розмітці, щоб пошук розумів звʼязок між версіями.- Не лише текст. Формати дат, чисел і валют теж локальні;
Carbon::setLocale()іNumber::format()це враховують.
Livewire дозволяє будувати реактивні інтерфейси на PHP без написання JavaScript. Компонент має серверний стан (public-властивості) та дії (методи).
class Counter extends Component
{
public int $count = 0;
public function increment(): void { $this->count++; }
public function render() { return view('livewire.counter'); }
}
<button wire:click="increment">+</button>
<span>{{ $count }}</span>
При взаємодії Livewire робить AJAX-запит, повторно рендерить компонент на сервері й оновлює лише змінений DOM. Підходить для форм, таблиць, дашбордів у Laravel-моноліті; для складної клієнтської логіки доповнюється Alpine.js.
wire:model двостороннє прив'язує поле форми до public-властивості компонента.
<input type="text" wire:model="search"> {{-- deferred --}}
<input type="text" wire:model.live="search"> {{-- одразу при вводі --}}
<input type="text" wire:model.live.debounce.300ms="search">
<input type="text" wire:model.blur="search"> {{-- при втраті фокуса --}}
- За замовчуванням значення синхронізується deferred - на наступній дії (наприклад, submit), що економить запити.
.liveсинхронізує одразу - потрібно для живого пошуку чи валідації, але створює запит на кожне натискання (часто комбінують із.debounce).
Filament - фреймворк Server-Driven UI для Laravel: інтерфейси (адмінки, панелі) описуються на PHP через структуровані об'єкти, а не верстку.
public static function form(Schema $schema): Schema
{
return $schema->components([
TextInput::make('title')->required(),
Select::make('status')->options(Status::class),
]);
}
- Побудований на Livewire, Alpine.js і Tailwind CSS.
- Будівельні блоки: Resources, Forms, Tables, Actions, Infolists, Widgets.
- Компоненти ініціалізуються статичними
make()-методами; динамічні значення задаються замиканнями з утилітамиGet/Set.
| Query Builder | Eloquent | |
|---|---|---|
| Повертає | stdClass / масиви |
моделі |
| Рівень | близько до SQL | ORM поверх QB |
| Зв'язки, події, касти | ні | так |
| Оверхед | мінімальний | невеликий |
// Query Builder
DB::table('users')->where('active', 1)->get();
// Eloquent
User::where('active', 1)->get();
Eloquent виразніший і зручніший для бізнес-логіки. Для важких масових операцій (мільйони рядків, складні агрегати) інколи свідомо обирають чистий Query Builder заради швидкості та меншого споживання пам'яті.
Питання з реальних співбесід Laravel і PHP - 171 питання у 41 темі, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
- Теми
- Eloquent 19 Архітектура 12 Database 10 Performance 7 Routing 5 Безпека 5 Blade 5 API 5
Готуєтесь до співбесіди не просто так: зараз на сайті 167 відкритих вакансій Laravel і PHP. Переглянути вакансії