Индексный контроллер PHP: варианты построения и примеры кода

Раздел: Программирование PHP -> Архитектура MVC

Индексный контроллер в структуре MVC

Индексный контроллер (Index Controller) обрабатывает корневой или стартовый маршрут приложения. В PHP‑фреймворках он часто именуется IndexController или HomeController. Выбор реализации влияет на удобство поддержки, тестирование и производительность. Рассмотрим несколько способов его создания.

Как организовать индексный контроллер с использованием PSR‑7 и контейнера зависимостей?

Наиболее эффективное решение - использование современного роутера, поддерживающего invokable классы, и внедрение зависимостей через конструктор. Это позволяет легко тестировать и заменять компоненты. Пример:

// index.php (точка входа)
require 'vendor/autoload.php';

$container = new League\Container\Container;
$container->add(IndexController::class)->addArgument(Twig_Environment::class);

$router = new League\Route\Router;
$router->get('/', IndexController::class);

$response = $router->dispatch($request);
echo $response->getBody();
// IndexController.php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Twig\Environment;

class IndexController
{
private $twig;
public function __construct(Environment $twig) { $this->twig = $twig; }
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$html = $this->twig->render('home.html', ['title' => 'Главная']);
return new Zend\Diactoros\Response\HtmlResponse($html);
}
}
Результат: в браузере отобразится страница home.html с переданным заголовком.

Проблема: если контейнер не настроен корректно, возникает ошибка разрешения зависимостей. Решение: проверьте регистрацию всех сервисов в контейнере. Типичная ошибка - забыть добавить сам контроллер в контейнер.


Вариант 1: простой класс со статическим методом

Как реализовать индексный контроллер без фреймворка?

class IndexController {
public static function index() {
echo '<h1>Добро пожаловать</h1>';
}
}
// index.php
IndexController::index();

Ошибка: статические методы усложняют тестирование и переопределение поведения. При изменении поля вывода придётся менять логику в каждом месте вызова.


Вариант 2: анонимная функция в роутере

Как обработать корневой маршрут без отдельного класса?

$router->get('/', function () {
$html = file_get_contents('home.html');
return new Response($html);
});

Проблема: при росте логики функция становится громоздкой, её сложно переиспользовать и тестировать.


Вариант 3: контроллер с наследованием от базового класса

Как переиспользовать общую логику (рендеринг, работа с запросом)?

abstract class AbstractController {
protected function render($template, $data) { /* … */ }
}

class IndexController extends AbstractController {
public function index() {
return $this->render('home', ['title' => 'Главная']);
}
}

Ошибка: жёсткая связь через наследование. Изменение базового класса может нарушить работу потомков. Альтернатива - композиция или трейты.


Вариант 4: внедрение зависимостей через конструктор

Как сделать контроллер независимым от глобальных состояний?

class IndexController {
private $repository;
public function __construct(UserRepository $repo) { $this->repository = $repo; }
public function index() {
$users = $this->repository->findAll();
return view('users', compact('users'));
}
}

Проблема: контейнер автоматически не создаёт контроллер. Необходимо настроить роутер на вызов метода или invokable.


Вариант 5: invokable класс (рекомендовано)

Как объединить логику одного маршрута в единый класс?

class IndexController {
public function __invoke() {
return new JsonResponse(['status' => 'ok']);
}
}
// Роутер: $router->get('/', IndexController::class);

Ошибка: если метод __invoke не реализован, PHP выдаст фатальную ошибку. Всегда проверяйте наличие метода.


Вариант 6: middleware перед контроллером

Как добавить проверку аутентификации перед отдачей главной страницы?

$router->get('/', [AuthMiddleware::class, IndexController::class]);
// или цепочка: $router->group('/')->middleware(Auth::class)->controller(IndexController::class);

Проблема: порядок middleware важен. Если middleware возвращает ответ раньше, контроллер не выполняется. Следует документировать цепочку.

Расширенные примеры индексного контроллера

Пример A: поддержка разных HTTP методов

Как заставить один контроллер обрабатывать GET и POST запросы для главной страницы?

Пример
class IndexController {
public function handle(ServerRequestInterface $request): ResponseInterface
{
switch ($request->getMethod()) {
case 'GET':
return $this->showForm();
case 'POST':
return $this->processForm($request);
default:
return new Response('Method not allowed', 405);
}
}
private function showForm() {
$html = '<form method="POST"><input name="name"><button>Send</button></form>';
return new HtmlResponse($html);
}
private function processForm($request) {
$name = $request->getParsedBody()['name'] ?? 'Guest';
return new JsonResponse(['message' => "Hello, $name"]);
}
}
Результат GET: HTML форма. Результат POST: JSON приветствие.

Ошибка: незащищённый POST без CSRF‑токена. Решение: добавить проверку токена в middleware или внутри контроллера.


Пример B: вывод списка статей с пагинацией

Как реализовать главную страницу, показывающую последние новости?

Пример
class IndexController {
private $articleRepository;
private $paginator;
public function __construct(ArticleRepository $repo, Paginator $paginator) {
$this->articleRepository = $repo;
$this->paginator = $paginator;
}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$page = (int)($request->getQueryParams()['page'] ?? 1);
$articles = $this->articleRepository->findAll($page);
$total = $this->articleRepository->count();
$links = $this->paginator->generate($page, $total);
// рендеринг Twig
$html = $this->twig->render('index.html', [
'articles' => $articles,
'pages' => $links
]);
return new HtmlResponse($html);
}
}
Вывод: страница списка статей с номерами страниц.

Проблема: отсутствие кэширования. Каждый запрос загружает данные из базы. Решение: применить кэш для репозитория или использовать HTTP‑кэширование.


Пример C: кэширование ответа на стороне сервера

Как уменьшить нагрузку на базу данных для главной страницы?

Пример
use Psr\SimpleCache\CacheInterface;

class CachedIndexController {
private $cache;
private $innerController;
public function __construct(CacheInterface $cache, callable $innerController) {
$this->cache = $cache;
$this->innerController = $innerController;
}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$key = 'index_page_' . md5($request->getUri());
if ($cached = $this->cache->get($key)) {
return new HtmlResponse($cached); // или unserialize
}
$response = ($this->innerController)($request);
$this->cache->set($key, (string)$response->getBody(), 3600);
return $response;
}
}
При первом запросе генерируется страница и сохраняется в кэш. Последующие запросы отдают кэш до истечения часа.

Ошибка: кэширование может отдавать устаревшие данные. Решение: добавить инвалидацию при обновлении новостей.


Пример D: тестирование индексного контроллера с PHPUnit

Как протестировать контроллер, не запуская веб‑сервер?

Пример
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;

class IndexControllerTest extends TestCase
{
public function testInvokeReturnsSuccessfulResponse()
{
$twig = $this->createMock(Twig_Environment::class);
$twig->method('render')->willReturn('<h1>Home</h1>');
$controller = new IndexController($twig);
$request = $this->createMock(ServerRequestInterface::class);
$response = $controller($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertStringContainsString('<h1>Home</h1>', (string)$response->getBody());
}
}
Результат: тест пройден, контроллер возвращает статус 200 и корректный HTML.

Проблема: сложность тестирования, если контроллер использует глобальные функции. Решение: изолировать зависимости через интерфейсы и подменять их моками.


Пример E: защита от CSRF на главной странице с формой

Как внедрить CSRF‑защиту в индексный контроллер?

Пример
class IndexController {
private $csrfProvider;
public function __construct(CsrfProviderInterface $csrfProvider) {
$this->csrfProvider = $csrfProvider;
}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
if ($request->getMethod() === 'GET') {
$token = $this->csrfProvider->generate();
$html = '<form><input type="hidden" name="_token" value="'.$token.'">…</form>';
return new HtmlResponse($html);
}
$submittedToken = $request->getParsedBody()['_token'] ?? '';
if (!$this->csrfProvider->validate($submittedToken)) {
return new HtmlResponse('Invalid CSRF token', 403);
}
// обработка формы
}
}
GET: форма с CSRF‑токеном. POST: проверка токена, при неверном - 403.

Ошибка: хранение токена в скрытом поле уязвимо к перехвату. Решение: токен должен быть привязан к сессии и быть одноразовым.

Индексный контроллер PHP - comments

En
Php controllers index (php)