Middle: питання на співбесіді з теми «Архітектура»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Repository - патерн, що ховає доступ до даних за інтерфейсом: код працює з PostRepositoryInterface, не знаючи, звідки беруться записи.
interface PostRepositoryInterface
{
public function published(): Collection;
}
class EloquentPostRepository implements PostRepositoryInterface
{
public function published(): Collection
{
return Post::where('is_published', true)->get();
}
}
Питання зі співбесіди зазвичай про те, чи це потрібно тут. Аргумент «щоб замінити ORM» на практиці не спрацьовує: Eloquent - це Active Record, його моделі пронизують увесь застосунок, і підміна сховища все одно означала б переписування. Заміни ORM у живому проєкті майже не трапляється.
Аргумент «щоб тестувати» теж слабкий у Laravel: є RefreshDatabase і фабрики, тож тест із реальною базою пишеться просто й перевіряє більше, ніж мок репозиторію.
Що при цьому втрачається: зникають скопи, ліниві зв'язки й with() у місці виклику - або репозиторій обростає методами на кожну комбінацію фільтрів, або починає повертати Builder, і абстракція протікає.
Коли Repository справді доречний:
- Дані живуть не в Eloquent - зовнішнє API, Elasticsearch, кілька джерел за одним інтерфейсом.
- Домен свідомо відокремлений від фреймворку (DDD), і моделі домену не є Eloquent-моделями.
Що зазвичай працює краще: локальні скопи для повторюваних вибірок і Action/Service-класи для операцій. Вони дають ту саму зібраність без прошарку, який дублює Eloquent.
«Тонкий контролер» - це той, що лише координує: приймає запит, викликає потрібне й повертає відповідь. Усе інше живе деінде.
Що виносять із контролера і куди:
| Що | Куди |
|---|---|
| Правила валідації | 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 або контролер перестав вміщатися на екран.
У запит форми (Form Request) і політику - тоді контролер отримує вже перевірені дані.
class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Order::class);
}
public function rules(): array
{
return [
'items' => ['required', 'array', 'max:50'],
'items.*.product_id' => ['required', 'exists:products,id'],
];
}
}
class OrderController
{
public function store(StoreOrderRequest $request, PlaceOrder $placeOrder): RedirectResponse
{
$order = $placeOrder($request->user(), $request->validated());
return redirect()->route('orders.show', $order);
}
}
Розподіл відповідальності:
- Form Request - формат вхідних даних і, за потреби, доступ до дії;
- Політика - правила доступу до моделі (
update,delete), щоб їх перевикористовували контролери, Livewire, API й Blade (@can); - Дія чи сервіс - бізнес-логіка, яка не знає про HTTP;
- Контролер - лише склеює: приймає запит, викликає дію, повертає відповідь.
Перевага не лише в чистоті: запит форми й політику тестують окремо, а ту саму дію викликають з команди чи черги без підробки HTTP-запиту.
Ресурсний контролер описує CRUD над однією сутністю: index, show, store, update, destroy. Але не кожна дія вкладається в ці сім методів.
class PublishPostController
{
public function __invoke(Post $post, PublishPost $publish): RedirectResponse
{
Gate::authorize('publish', $post);
$publish($post);
return back()->with('status', 'Опубліковано');
}
}
Route::post('/posts/{post}/publish', PublishPostController::class);
Коли __invoke доречний:
- дія не є CRUD: «опублікувати», «скасувати замовлення», «експортувати звіт»;
- у дії свої залежності, які не потрібні іншим методам;
- ресурсний контролер розрісся до десятка нестандартних методів.
Коли ресурсний: стандартні операції над сутністю - так маршрути й імена передбачувані (posts.update), а Route::resource() реєструє їх одним рядком.
Типова еволюція: PostController з методами publish, unpublish, duplicate, export розбивають на ресурсний CRUD і кілька контролерів-дій. Кожен файл короткий, назва каже, що він робить, а залежності не змішуються.
Контролер-дія не замінює клас-дію: перший - про HTTP, другий - про бізнес-логіку, яку можна викликати звідусіль.