Контролери
Контролер — клас, який обробляє HTTP-запит після middleware і повертає
Psr\Http\Message\ResponseInterface. У Concept Core немає обов’язкового
базового класу: це звичайний PHP-клас із публічними методами, прив’язаними до маршрутів.
Див. також: маршрутизація, життєвий цикл → контролер, middleware, route interceptors.
Прив’язка до маршруту
Callable маршруту — масив [Клас::class, 'метод']:
use Concept\App\Controllers\IndexController;
$router->get('/', [IndexController::class, 'index'])->setName('home');
$router->get('/projects/{id:number}', [CabinetProjectController::class, 'show'])
->setName('cabinet.project.show');
League Route бере контролер із DI-контейнера; залежності конструктора підставляються автоматично
(автовайринг). Метод action викликає RouteStrategy.
Де розміщувати
Типові варіанти (шляхи налаштовуються через paths.php):
- Застосунок —
App/Controllers/→ namespaceConcept\App\Controllers - Компонент —
Components/{Name}/Controllers/→Concept\Components\{Name}\Controllers, маршрути вroutes.phpкомпонента
Детальніше про дерева каталогів і namespaces — у
структурі каталогів.
Один контролер — одна зона відповідальності (кабінет, адмін, публічна вітрина).
Складну логіку виносьте в Services/.
Мінімальний приклад
<?php declare(strict_types=1);
namespace Concept\App\Controllers;
use Concept\Core\Services\View\Contracts\ViewResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
class IndexController
{
public function __construct(
private readonly ViewResponseFactoryInterface $viewResponse,
) {}
public function index(): ResponseInterface
{
return $this->viewResponse->create('@frontend/index');
}
}
Шаблон @frontend/index — логічне ім’я Twig (префікс компонента/модуля).
Детальніше — у розділі Views.
Повний приклад
UserController компонента AuthAdmin — типовий CRUD: список, форми, FormRequest,
flash + redirect після POST, окремий JSON-endpoint:
namespace Concept\Components\AuthAdmin\Controllers;
class UserController
{
public function __construct(
private readonly ResponseFactoryInterface $response,
private readonly ViewResponseFactoryInterface $viewResponse,
private readonly ConfigInterface $config,
private readonly AuthService $authService,
private readonly FlashBagInterface $flashBag,
private readonly UserModel $userModel,
) {}
public function store(StoreUserRequest $request): ResponseInterface
{
$this->userModel
->newQuery()
->create($this->normalizeUserPayload($request->validated()));
$this->flashBag->addSuccess(, 'User successfully created');
return $this->response->redirectByName('admin.users');
}
public function updatePassword(UpdateUserPasswordRequest $request, int $id): ResponseInterface
{
// ...
$data = $request->toDto();
$user->update([UserModel::FIELD_PASSWORD => $data->password]);
// ...
}
public function generateTokenApi(): ResponseInterface
{
return $this->response->json(['token' => bin2hex(random_bytes(32))]);
}
}
RouteStrategy: від маршруту до action
Після middleware League Route викликає RouteStrategy::invokeRouteCallable().
Порядок усередині стратегії:
- Route interceptors — глобальні перевірки (ACL) до handler
- prepareRequest() — сегменти URL → атрибути PSR-7; оновлення
ServerRequestInterfaceу контейнері - getCallable() — резолв контролера з DI (constructor injection)
- resolveArguments() — FormRequest,
ServerRequestInterface, route params, defaults - invokeArgs() — виклик методу action
Телеметрія HTTP_ROUTE_CALLABLE_INVOKED охоплює крок 5.
Детальніше — життєвий цикл.
Автовайринг і конструктор
Під час bootstrap створюється League Container із делегатом
ReflectionContainer: якщо клас не зареєстрований явно, контейнер аналізує
конструктор і рекурсивно створює залежності за type-hint.
// Concept\Core\App::__construct()
$this->container->delegate(new ReflectionContainer(cacheResolutions: true));
Для маршруту [UserController::class, 'index'] League Route робить приблизно
$container->get(UserController::class) — один екземпляр контролера на запит
(shared за замовчуванням для того самого класу в межах одного HTTP-запиту).
Що передавати в конструктор
Оголошуйте залежності явно — через типи параметрів. Контролер «просить» те, що йому потрібно:
| Тип залежності | Приклад | Коментар |
|---|---|---|
| Контракти ядра | ResponseFactoryInterface, ConfigInterface |
Реєстрація в service providers Concept Core |
| Сервіси застосунку / компонента | UserAuthService, AuthService |
Бізнес-логіка; краще залежність від інтерфейсу, якщо є |
| Моделі, репозиторії | UserModel, ProjectModel |
Допустимо в MVC-стилі; для складних доменів — обгортка-сервіс |
| Інфраструктура HTTP/сесії | FlashBagInterface |
Для flash-повідомлень після redirect |
public function __construct(
private readonly ResponseFactoryInterface $response,
private readonly ViewResponseFactoryInterface $viewResponse,
private readonly UserAuthService $auth,
private readonly FlashBagInterface $flashBag,
private readonly ProjectModel $projectModel,
) {}
Якщо клас не резолвиться (немає binding, циклічна залежність) — помилка контейнера на етапі запиту.
Власні реалізації реєструйте в ServiceProvider компонента або застосунку
(детальніше — Контейнер сервісів,
Service providers).
Не використовуйте контейнер як service locator
У контейнері зареєстровано ContainerInterface, але не інжектуйте його в контролер
і не викликайте $this->container->get(...) у методах action.
Це приховує реальні залежності, ускладнює тести й рефакторинг.
Погано (service locator)
public function __construct(
private readonly ContainerInterface $container,
) {}
public function index(): ResponseInterface
{
$auth = $this->container->get(UserAuthService::class);
// ...
}
Добре (явні залежності)
public function __construct(
private readonly UserAuthService $auth,
private readonly ViewResponseFactoryInterface $viewResponse,
) {}
public function index(): ResponseInterface
{
$user = $this->auth->user();
// ...
}
Що краще не передавати в конструктор
-
ContainerInterface,League\Container\Container— антипатерн service locator (див. вище) -
Router— для URL єUrlGeneratorInterface/redirectByName() -
Суперглобали (
$_GET,$_POST) — лише через FormRequest або PSR-7
Контейнер у App::getContainer() — для bootstrap, CLI і service providers.
У контролерах, middleware і сервісах домену покладайтеся на constructor injection конкретних типів.
Відповіді
Кожен action повертає ResponseInterface. Дві основні фабрики:
| Інтерфейс | Коли використовувати |
|---|---|
ViewResponseFactoryInterface |
HTML: create('@cabinet/index', ['user' => $user]) |
ResponseFactoryInterface |
JSON, redirect, back() |
// Redirect за іменем маршруту
return $this->response->redirectByName('cabinet.login');
// JSON (API) — json(), jsonSuccess(), jsonError()
return $this->response->json(['token' => $token]);
return $this->response->jsonSuccess(['id' => $id]);
// Flash + redirect після POST
$this->flashBag->set('success', 'Збережено');
return $this->response->redirectByName('admin.users');
Повний опис — Відповіді; Twig-шаблони — Views.
Аргументи методу
RouteStrategy заповнює параметри action у такому пріоритеті:
- FormRequest — створення з контейнера, валідація до входу в метод
- ServerRequestInterface — поточний PSR-7 запит
- Параметри URL — ім’я параметра = ім’я в
{id:number}, з приведенням типів черезCaster - Значення за замовчуванням у сигнатурі PHP
public function show(int $id): ResponseInterface { /* ... */ }
public function update(UpdateUserRequest $request, int $id): ResponseInterface { /* ... */ }
public function handle(ServerRequestInterface $request): ResponseInterface { /* ... */ }
Сервіси (UserModel, ResponseFactoryInterface тощо) — через
конструктор (автовайринг), не через ContainerInterface::get().
У параметри методу — лише FormRequest, ServerRequestInterface і сегменти URL.
FormRequest
Type-hint нащадка FormRequest у action — RouteStrategy створює
екземпляр з контейнера і викликає validate() до входу в метод.
При помилці — ValidationException, яку перехоплює
HandleValidationExceptionMiddleware (JSON 422 або flash + back()).
class StoreUserRequest extends FormRequest
{
protected ?string $dtoClass = StoreUserDto::class;
protected array $except = ['password_confirmation'];
public function rules(): array
{
return [
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'min:8'],
'password_confirmation' => ['required', 'same:password'],
];
}
}
public function store(StoreUserRequest $request): ResponseInterface
{
$this->userModel->newQuery()->create($request->validated());
return $this->response->redirectByName('admin.users');
}
Повний опис правил, DTO, except/only — у розділі
Запити та валідація.
Параметри маршруту
Сегмент {id:number} потрапляє в метод як int $id (якщо імена збігаються).
Той самий id доступний і як атрибут запиту:
$request->getAttribute('id').
$router->get('/edit/{id:number}', [UserController::class, 'edit'])
->setName('admin.user.edit');
Route interceptors
Глобальні ACL-перевірки за setName() маршруту виконуються в
RouteStrategy до autowire action — через класи з
routes.interceptors. Типова відмова — throw винятку;
redirect або JSON 403 — у middleware, що обгортає handler.
Детальні приклади — Route interceptors.
Авторизація
Розділіть рівні перевірки:
- Middleware на групі — «чи залогінений», redirect на login (приклади)
- Route interceptors — ACL за named route (детальніше)
- Контролер — бізнес-обмеження (ресурс належить поточному користувачу)
public function show(int $id): ResponseInterface
{
$user = $this->auth->user();
if (!$user instanceof UserModel) {
return $this->response->redirectByName('cabinet.login');
}
$project = $this->findUserProject($user, $id);
if ($project === null) {
$this->flashBag->addError(, 'Order not found.');
return $this->response->redirectByName('cabinet.projects');
}
return $this->viewResponse->create('@projects/cabinet/show', ['project' => $project]);
}
Рекомендовані патерни
- Тонкий контролер — orchestration: запит → сервіс → відповідь
- Один action — одна відповідальність —
showLogin,login, не «універсальний» метод - POST/PUT/PATCH/DELETE — FormRequest + redirect (PRG), не повторний рендер форми
- Іменовані маршрути —
redirectByName()замість жорстких URL
Closure та invokable
Маршрут може вказувати на closure або клас з __invoke.
RouteStrategy autowire для них обмежений: FormRequest,
ServerRequestInterface, сегменти URL і значення за замовчуванням.
Довільні сервіси (UserModel тощо) у параметрах closure не
резолвляться — для DI використовуйте класи контролерів.
Діагностика
php bin/console.php route:list