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

Питання на співбесіді: MVC та архітектура

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

3 питання

MVC (Model-View-Controller) - це архітектурний патерн, який розділяє застосунок на три шари, аби логіка обробки даних, бізнес-правила та відображення не змішувались між собою. Laravel побудований навколо MVC, тож кожному шару відповідає своя директорія.

Три складові:

  • Model - відповідає за дані та бізнес-логіку: спілкування з базою, зв'язки, валідаційні правила домену. У Laravel моделі розширюють Eloquent і лежать у app/Models.
  • View - відповідає лише за відображення. У Laravel це Blade-шаблони у resources/views; вони не повинні містити складної логіки.
  • Controller - приймає HTTP-запит, координує роботу моделей, готує дані й повертає View або відповідь. Лежать у app/Http/Controllers.

Як це працює разом: запит потрапляє на маршрут (routes/web.php), той викликає метод контролера. Контролер звертається до моделі за даними й передає їх у view:

class PostController extends Controller
{
    // Route Model Binding: $post резолвиться автоматично
    public function show(Post $post)
    {
        // контролер координує: бере дані з моделі й віддає у view
        return view('posts.show', ['post' => $post]);
    }
}

Навіщо: розділення відповідальностей спрощує тестування, підтримку й командну роботу - верстку можна змінити, не чіпаючи запити до БД, і навпаки. Laravel розширює класичний MVC сервісним контейнером, middleware та провайдерами.

Докладніше в документації: Архітектура та структура

Контролер - це шар 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, або контролер перестав вміщатися на екран.

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

«Тонкий контролер» - це той, що лише координує: приймає запит, викликає потрібне й повертає відповідь. Усе інше живе деінде.

Що виносять із контролера і куди:

Що Куди
Правила валідації Form Request
Перевірка прав Policy або authorize() у Form Request
Бізнес-логіка Service чи Action-клас
Складна вибірка Локальний скоп на моделі
Формат відповіді API Resource
Побічні ефекти (лист, індексація) Слухач події або джоба в черзі

Було:

public function store(Request $request)
{
    $data = $request->validate([...]);          // валідація

    if (! $request->user()->can('create', Post::class)) {   // права
        abort(403);
    }

    $post = Post::create($data);                 // логіка
    $post->tags()->sync($data['tags']);
    Mail::to($post->author)->send(new PostCreated($post));  // побічний ефект

    return response()->json([                     // формат
        'id' => $post->id,
        'title' => $post->title,
    ]);
}

Стало:

public function store(StorePostRequest $request, CreatePost $createPost)
{
    return new PostResource($createPost->handle($request->validated()));
}

Валідація й права спрацювали до входу в метод, логіка з побічними ефектами - усередині CreatePost, формат - у ресурсі.

Навіщо це насправді. Не заради краси: логіку з контролера не викликати ні з Artisan-команди, ні з черги, ні з тесту без HTTP-запиту. Щойно та сама операція знадобилась у другому місці, тонкий контролер перестає бути питанням смаку.

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

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