Питання на співбесіді: 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 або контролер перестав вміщатися на екран.