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

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.

Докладніше в документації: 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, другий - про бізнес-логіку, яку можна викликати звідусіль.

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