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

Питання на співбесіді рівня Junior

Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів

52 питань

Mailable - це лист. Notification - це повідомлення, яке може піти листом, у базу, в Slack чи кудись ще, і канал вибирається окремо від змісту.

Створення:

php artisan make:notification VacancyPublished
class VacancyPublished extends Notification
{
    public function __construct(public Vacancy $vacancy)
    {
    }

    public function via(object $notifiable): array
    {
        return ['mail', 'database'];
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Вашу вакансію опубліковано')
            ->line("«{$this->vacancy->title}» тепер видно всім.")
            ->action('Переглянути', route('vacancies.show', $this->vacancy));
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(object $notifiable): array
    {
        return ['vacancy_id' => $this->vacancy->id];
    }
}

Відправлення:

$user->notify(new VacancyPublished($vacancy));

// або багатьом
Notification::send($users, new VacancyPublished($vacancy));

Метод notify() доступний завдяки трейту Notifiable на моделі.

Що дає канал database: сповіщення зберігається в таблиці notifications, і з нього робиться «дзвіночок» в інтерфейсі - $user->unreadNotifications.

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

Докладніше в документації: Сповіщення

Драйвер задається в config/cache.php і змінною CACHE_STORE. Вибір визначає не швидкість, а те, чи бачать процеси спільний кеш.

array - у памʼяті одного процесу, зникає після запиту. Стандартний драйвер для тестів: кеш не тягнеться між тестами й нічого не треба чистити.

file - файли в storage/framework/cache. Працює без жодного налаштування, тому зручний локально. На кількох серверах кожен матиме свій кеш, а очищення тегів файловий драйвер не підтримує.

database - таблиця в базі. Спільний для всіх серверів, але кожне читання - це запит до тієї самої бази, яку кеш і мав розвантажити.

redis / memcached - окремий сервер у памʼяті. Спільний для всіх процесів, витримує теги й атомарні блокування. Це стандартний вибір для проду.

Практичне правило: локально file, у тестах array, у проді redis.

Що важливо пам'ятати про різницю. Код, написаний під Redis, може мовчки не працювати на file:

Cache::tags(['posts'])->put('list', $posts, 600);   // file-драйвер кине виняток

Блокування (Cache::lock()) файловий драйвер підтримує, але блокування живе на диску одного сервера - на кількох вузлах воно вже нічого не гарантує.

Тому середовища краще тримати на однаковому драйвері - інакше помилка знайдеться вже в проді.

Докладніше в документації: Кеш

Найпростіший пошук - фільтр за кількома колонками, і для більшості сайтів цього достатньо надовго.

Запит:

public function index(Request $request)
{
    $term = $request->string('q')->trim()->toString();

    $vacancies = Vacancy::query()
        ->when($term !== '', function ($query) use ($term) {
            $query->where(function ($query) use ($term) {
                $query->where('title', 'like', "%{$term}%")
                    ->orWhere('description', 'like', "%{$term}%");
            });
        })
        ->paginate(15)
        ->withQueryString();

    return view('vacancies.index', compact('vacancies'));
}

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

1. Дужки навколо orWhere. Без вкладеного замикання умова «розклеїться»: where(A)->orWhere(B) разом з іншими фільтрами дасть фільтр AND A OR B, і пошук почне повертати чуже. Вкладене замикання групує умови правильно.

2. Порожній запит. Без when() пошук за порожнім рядком перетвориться на like '%%' - формально працює, але виконує зайву роботу.

Про безпеку: підставляти значення в where() безпечно - Eloquent параметризує запит. Небезпечним є лише whereRaw("title like '%{$term}%'") зі вставкою рядка напряму.

Коли цього перестане вистачати: like '%...%' не використовує індекс і не знає словоформ, тож на десятках тисяч записів пошук стає повільним, а «розробники» не знаходять «розробник». Тоді переходять на повнотекстовий індекс бази або Laravel Scout.

Докладніше в документації: Запити до бази даних

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

Як виглядає змішування:

public function store(Request $request)
{
    $order = Order::create($request->validated());
    $order->items()->createMany($request->input('items'));

    $total = collect($request->input('items'))->sum(fn ($i) => $i['price'] * $i['qty']);
    $order->update(['total' => $total]);

    Mail::to($order->email)->send(new OrderPlaced($order));
    Http::post('https://warehouse.example/orders', $order->toArray());

    return redirect()->route('orders.show', $order);
}

Тут порахунок суми, лист і виклик складу існують лише всередині HTTP-запиту.

Те саме з сервісом:

class PlaceOrder
{
    public function handle(array $data): Order
    {
        return DB::transaction(function () use ($data) {
            $order = Order::create($data);
            $order->items()->createMany($data['items']);
            $order->update(['total' => $this->total($data['items'])]);

            OrderPlaced::dispatch($order);

            return $order;
        });
    }
}

public function store(StoreOrderRequest $request, PlaceOrder $placeOrder)
{
    $order = $placeOrder->handle($request->validated());

    return redirect()->route('orders.show', $order);
}

Що це дає:

  • Ту саму логіку можна викликати з Artisan-команди, черги чи імпорту - не тільки з форми.
  • Тестувати можна без HTTP-запиту: створили масив, викликали handle(), перевірили результат.
  • Транзакція охоплює всю операцію цілком.
  • Контролер знову читається за кілька секунд.

Не поспішайте. Для Post::create($request->validated()) окремий сервіс - зайвий шар. Виносити варто тоді, коли з'являється щось із трьох: кілька моделей в одній операції, потреба викликати логіку не з HTTP, або контролер перестав вміщатися на екран.

Докладніше в документації: Контролери

Обидва кажуть контейнеру, як створювати сервіс. Різниця - у тому, скільки разів це станеться.

bind() створює новий екземпляр щоразу:

$this->app->bind(ReportBuilder::class, function ($app) {
    return new ReportBuilder($app->make(Connection::class));
});

app(ReportBuilder::class) === app(ReportBuilder::class);   // false

singleton() створює один раз і повертає той самий обʼєкт до кінця запиту:

$this->app->singleton(WeatherClient::class, function ($app) {
    return new WeatherClient(config('services.weather.key'));
});

app(WeatherClient::class) === app(WeatherClient::class);   // true

Є ще scoped() - як singleton, але скидається на кожному запиті. Це важливо для Octane, де застосунок живе між запитами й звичайний singleton зберігав би стан довше, ніж треба.

Що обирати. Singleton доречний, коли створення дороге (клієнт із конфігурацією, зʼєднання) або коли стан має бути спільним. bind() - коли обʼєкт накопичує стан і два різних місця не повинні його ділити.

Головна пастка singleton - утримання стану. Сервіс, що накопичує щось усередині, віддасть ці дані наступному, хто його запросить:

class Basket
{
    private array $items = [];

    public function add(Item $item): void
    {
        $this->items[] = $item;   // у singleton лишиться на весь запит
    }
}

Під Octane це вже не «на запит», а між запитами - і дані одного користувача протікають до іншого. Тому в singleton кладуть сервіси без стану, а стан тримають явно.

Докладніше в документації: Сервіс-контейнер

PHP дозволяє оголошувати типи аргументів, повернення та властивостей - і перевіряє їх у рантаймі.

class VacancyService
{
    public function __construct(
        private VacancyRepository $repository,
    ) {
    }

    public function publish(int $id, ?string $comment = null): Vacancy
    {
        // ...
    }
}

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

declare(strict_types=1) змінює поведінку перевірки. Без нього PHP приводить типи мовчки:

function repeat(int $times): string { /* ... */ }

repeat('5');     // без strict_types: '5' стане 5, викликається нормально
repeat('5');     // зі strict_types: TypeError

Саме мовчазне приведення небезпечне: 'abc' перетвориться на 0, а '5 котів' - на 5, і помилка проявиться далеко від місця, де виникла.

Важлива деталь: директива діє на файл, де вона написана, і стосується викликів з нього, а не в нього. Тому її ставлять у кожен файл, першим рядком після <?php.

Корисні типи PHP 8:

  • ?string - рядок або null.
  • int|string - обʼєднання типів.
  • never - функція не повертає керування (кидає виняток чи завершує процес).

У Laravel-проєктах declare(strict_types=1) зазвичай вимагається стилем коду й перевіряється Pint.

Докладніше в документації: Стандарти коду

Ресурс - це опис того, як модель виглядає в адмінці: таблиця, форма й сторінки навколо них.

php artisan make:filament-resource Vacancy --generate

Команда створює клас ресурсу, сторінки List/Create/Edit, а форму й таблицю виносить в окремі файли (Schemas/VacancyForm.php, Tables/VacancyTable.php). Прапорці --embed-schemas і --embed-table лишають їх усередині класу ресурсу.

Дві головні частини - форма й таблиця:

public static function form(Schema $schema): Schema
{
    return $schema->components([
        TextInput::make('title')
            ->required()
            ->maxLength(255),
        Select::make('level')
            ->options(VacancyLevel::class)
            ->required(),
        Toggle::make('is_published'),
    ]);
}

public static function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('title')->searchable()->sortable(),
            TextColumn::make('company.name')->label('Компанія'),
            IconColumn::make('is_published')->boolean(),
        ])
        ->filters([
            SelectFilter::make('level')->options(VacancyLevel::class),
        ]);
}

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

На чому побудовано: Livewire відповідає за реактивність без написання JS, Alpine - за дрібні взаємодії в браузері, Tailwind - за оформлення. Тому Filament - це PHP-код, а не окремий фронтенд-застосунок.

Що варто знати одразу: форма й таблиця - це звичайні PHP-масиви обʼєктів, тож видимість поля, опції списку чи доступність дії задаються замиканнями й можуть залежати від користувача та стану запису.

Вакансії Laravel рівня Junior

Усі вакансії Laravel Junior
Bulls Media Worldgroup
20 днів тому

Junior+ Full Stack Developer

Junior+ Full Stack розробник з досвідом 1+ років для розробки та підтримки backend (PHP/Laravel) та frontend (JavaScript/HTML/CSS) компонентів веб-додатків. Основні обов'язки: розробка нових функцій, робота з БД, REST API, дебаг коду, code review. Вимогає PHP, Laravel, SQL, Git, розуміння REST/MVC/OOP та англійської Intermediate+.

Bulls Media Worldgroup
20 днів тому

Junior + Backend Developer

Junior+ Backend Developer для розробки та підтримки веб-проєктів. Основні завдання: розробка backend-функціональності, робота з базами даних, інтеграція API, виправлення помилок. Вимоги: 1+ рік комерційного досвіду, PHP, Laravel (базовий-середній рівень), SQL, Git, Linux, REST API, MVC/OOP. Англійська intermediate або вище.

AniArt
28 днів тому

Junior, Middle PHP-розробник

Laravel WordPress Київ Офіс B2B Junior ₴20,000 - ₴40,000

Junior або Middle PHP-розробник для розробки сайтів малого та середнього бізнесу. Необхідний досвід з Laravel та/або WordPress, знання Git, Composer, ООП. Робота в невеликій команді в офісі на умовах ФОП з пн-пт 9:00-18:00 або 10:00-19:00. Досвід з e-commerce проектами буде плюсом.

Питання рівня Junior з реальних співбесід Laravel і PHP - 52 питання у 35 темах, розібраних із відповідями. Нижче - теми цього рівня та сусідні рівні, якщо хочете звузити або розширити підготовку.

Інші рівні
Middle 57 Senior 62