Контроллер в PHP: от простого index.php до профессионального роутера
Основной подход: Front Controller в index.php
В архитектуре MVC точка входа в приложение часто реализуется через единый файл index.php, который играет роль фронт-контроллера. Он принимает все HTTP-запросы, анализирует URL, загружает нужный контроллер и вызывает его метод. Такой подход централизует обработку запросов, упрощает маршрутизацию и обеспечивает единую точку для добавления middleware, логирования или обработки ошибок.
// index.php - простейший фронт-контроллер
$uri = $_SERVER['REQUEST_URI'];
$uri = rtrim($uri, '/');
$uri = filter_var($uri, FILTER_SANITIZE_URL);
$parts = explode('/', $uri);
// Определяем контроллер и действие
$controllerName = $parts[1] ?? 'home';
$actionName = $parts[2] ?? 'index';
$params = array_slice($parts, 3);
// Загружаем соответствующий файл контроллера
$controllerFile = __DIR__ . '/controllers/' . ucfirst($controllerName) . 'Controller.php';
if (file_exists($controllerFile)) {
require_once $controllerFile;
$controllerClass = ucfirst($controllerName) . 'Controller';
if (class_exists($controllerClass)) {
$controller = new $controllerClass();
if (method_exists($controller, $actionName)) {
call_user_func_array([$controller, $actionName], $params);
} else {
die('Метод не найден');
}
} else {
die('Класс контроллера не найден');
}
} else {
die('Файл контроллера не найден');
}
Типичные ошибки:
- Некорректная обработка символов в URL (например, кириллица). Используйте rawurlencode при построении ссылок.
- Проблемы с регистром имён файлов на Linux. Все имена должны строго совпадать.
- Отсутствие проверки метода HTTP (GET/POST). Добавьте проверку $_SERVER['REQUEST_METHOD'].
Как добавить маршрутизацию через switch-case в index.php?
Когда проект мал, можно обойтись простым switch по пути из URL. Это быстро, но плохо масштабируется.
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
switch ($uri) {
case '/':
$controller = new HomeController();
$controller->index();
break;
case '/about':
$controller = new AboutController();
$controller->index();
break;
case '/contact':
$controller = new ContactController();
$controller->index();
break;
default:
http_response_code(404);
echo 'Страница не найдена';
}
Проблемы:
- При увеличении числа маршрутов switch разрастается.
- Нет поддержки параметров из URL (например, /post/123). Придётся добавлять дополнительную логику.
- Сложно подключать middleware.
Как вынести маршруты в отдельный класс Router?
Класс Router позволяет централизованно хранить все правила маршрутизации, поддерживает регулярные выражения и улучшает читаемость кода.
class Router {
private $routes = [];
public function add($pattern, $callback) {
$this->routes[$pattern] = $callback;
}
public function dispatch($uri) {
foreach ($this->routes as $pattern => $callback) {
if (preg_match($pattern, $uri, $matches)) {
array_shift($matches); // убираем полное совпадение
call_user_func_array($callback, $matches);
return;
}
}
throw new \Exception('Маршрут не найден');
}
}
// Использование в index.php
$router = new Router();
$router->add('/^\/$/', function() { (new HomeController)->index(); });
$router->add('/^\/post\/(\d+)$/', function($id) { (new PostController)->show($id); });
$router->dispatch(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
Ошибки:
- Ошибочное экранирование слэшей в регулярных выражениях – используйте строки в одинарных кавычках.
- Не забывайте про http_response_code(404) в блоке catch.
- Метод dispatch() должен возвращать ответ, а не выводить его напрямую, если планируется middleware.
Как организовать автозагрузку контроллеров с помощью Composer и PSR-4?
Для больших проектов автозагрузка классов избавляет от многочисленных require_once. Создайте файл composer.json с namespace, выполните composer dump-autoload.
// composer.json
{
"autoload": {
"psr-4": {
"App\\Controllers\\": "controllers/"
}
}
}
// index.php
require __DIR__ . '/vendor/autoload.php';
use App\Controllers\HomeController;
use App\Controllers\PostController;
// Далее работа с маршрутами через Router, вызываем классы из неймспейса
Проблемы:
- Забыли выполнить composer install или dump-autoload – классы не загрузятся.
- Путь в PSR-4 должен точно соответствовать структуре папок (например, App/Controllers/HomeController.php).
Как передавать параметры из URL в контроллер (например, /post/123/edit)?
При вложенных параметрах удобно использовать регулярные выражения с именованными группами или разбивать URL на сегменты, как в первом примере. Более продвинутый способ – использовать библиотеку FastRoute (composer require nikic/fast-route).
// Вариант с ручным разбором
$segments = explode('/', trim($uri, '/'));
$action = $segments[0] ?? 'home';
$params = array_slice($segments, 1);
// Пример для /post/123/edit -> action='post', params=['123','edit']
// Далее в контроллере обрабатываем логику в зависимости от количества параметров
Ошибки:
- Пустые сегменты при повторяющихся слэшах – применяйте array_filter.
- Неэкранированные спецсимволы – используйте filter_var с FILTER_SANITIZE_URL.
Как реализовать контроллер для REST API с методом GET, POST, PUT, DELETE?
В REST маршруты привязываются к HTTP-методам. Используйте массив маршрутов с методом.
$method = $_SERVER['REQUEST_METHOD'];
$routes = [
'GET' => [
'/api/users' => 'UserController@index',
'/api/users/\d+' => 'UserController@show',
],
'POST' => [
'/api/users' => 'UserController@store',
],
];
if (isset($routes[$method])) {
foreach ($routes[$method] as $pattern => $handler) {
if (preg_match('#^' . $pattern . '$#', $uri, $matches)) {
list($controller, $action) = explode('@', $handler);
$controller = new $controller();
$controller->$action($matches[1] ?? null);
break;
}
}
}
Проблемы:
- Не обработаны неверные методы – возвращайте 405 Method Not Allowed.
- Сложность с передачей тела запроса для POST/PUT – используйте file_get_contents('php://input').
Как добавить middleware (проверку авторизации, CORS, логирование) в контроллер?
Можно оформить middleware как отдельные классы и пропускать через них запрос до вызова контроллера. Например, создать цепочку обработчиков.
class AuthMiddleware {
public function handle($request, $next) {
if (!isset($_SESSION['user'])) {
header('Location: /login');
exit;
}
return $next($request);
}
}
// В index.php
$middlewares = [new AuthMiddleware(), new CorsMiddleware()];
$request = $_SERVER;
foreach ($middlewares as $middleware) {
$response = $middleware->handle($request, function($req) use ($router) {
return $router->dispatch($req['REQUEST_URI']);
});
if ($response !== null) break;
}
Типичные ошибки:
- Забывают вызвать exit после редиректа в middleware.
- Порядок middleware важен: сначала CORS, потом аутентификация.
- Не использовать буферизацию вывода, если middleware изменяет заголовки.
Расширенные примеры реализации контроллера в PHP MVC
Пример 1: Полный фронт-контроллер с классом Router, автозагрузкой и обработкой ошибок
Структура проекта:
project/
├── composer.json
├── public/
│ └── index.php # точка входа
├── src/
│ ├── Controllers/
│ │ ├── HomeController.php
│ │ ├── PostController.php
│ │ └── BaseController.php
│ ├── Router.php
│ └── Middleware/
│ ├── AuthMiddleware.php
│ └── CorsMiddleware.php
└── views/
composer.json:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
После выполнения composer dump-autoload.
public/index.php:
<?php
require __DIR__ . '/../vendor/autoload.php';
use App\Routing\Router;
use App\Middleware\AuthMiddleware;
use App\Middleware\CorsMiddleware;
use App\Controllers\HomeController;
use App\Controllers\PostController;
// Создаём маршрутизатор
$router = new Router();
// Определяем маршруты
$router->add('GET', '/', [HomeController::class, 'index']);
$router->add('GET', '/about', [HomeController::class, 'about']);
$router->add('GET', '/post/{id:\d+}', [PostController::class, 'show']);
$router->add('POST', '/post/{id}/comment', [PostController::class, 'addComment']);
// Middleware цепочка
$middlewares = [
CorsMiddleware::class,
AuthMiddleware::class,
];
// Обработка запроса
$request = [
'method' => $_SERVER['REQUEST_METHOD'],
'uri' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
];
// Пропускаем через middleware
$next = function($req) use ($router) {
return $router->dispatch($req['method'], $req['uri']);
};
foreach ($middlewares as $middlewareClass) {
$middleware = new $middlewareClass();
$result = $middleware->handle($request, $next);
if ($result !== null) {
// Если middleware вернул ответ, завершаем
echo $result;
exit;
}
$next = function($req) use ($middleware, $next) {
return $middleware->handle($req, $next);
};
}
// Выполняем финальный обработчик
$response = $router->dispatch($request['method'], $request['uri']);
if ($response) {
echo $response;
} else {
http_response_code(404);
echo 'Страница не найдена';
}
src/Routing/Router.php:
<?php
namespace App\Routing;
class Router
{
private $routes = [];
public function add(string $method, string $pattern, callable|array $handler): void
{
// Преобразуем шаблон вида /post/{id:\d+} в регулярное выражение
$pattern = preg_replace('/\{[a-zA-Z_]+:([^}]+)\}/', '($1)', $pattern);
$pattern = '#^' . $pattern . '$#';
$this->routes[] = compact('method', 'pattern', 'handler');
}
public function dispatch(string $method, string $uri): mixed
{
foreach ($this->routes as $route) {
if ($route['method'] === $method && preg_match($route['pattern'], $uri, $matches)) {
array_shift($matches); // Убираем полное совпадение
if (is_array($route['handler'])) {
list($class, $action) = $route['handler'];
$controller = new $class();
return call_user_func_array([$controller, $action], $matches);
}
return call_user_func_array($route['handler'], $matches);
}
}
return null;
}
}
Пример контроллера (src/Controllers/HomeController.php):
<?php
namespace App\Controllers;
class HomeController
{
public function index(): string
{
return '<h1>Главная страница</h1>';
}
public function about(): string
{
return '<h1>О нас</h1>';
}
}
Пример middleware (src/Middleware/AuthMiddleware.php):
<?php
namespace App\Middleware;
class AuthMiddleware
{
public function handle(array $request, callable $next): mixed
{
session_start();
if (!isset($_SESSION['user'])) {
header('Location: /login');
exit;
}
return $next($request);
}
}
Результат: при обращении к / без авторизации произойдёт редирект на /login. Если авторизация пройдена, будет выведен HTML главной страницы.
Пример 2: Контроллер с внедрением зависимостей (DI) через конструктор
Часто контроллеру требуется работа с БД или шаблонизатором. Вместо создания экземпляров внутри класса используйте DI-контейнер. Ниже простой пример без сторонних библиотек.
<?php
// src/Controllers/PostController.php
namespace App\Controllers;
use App\Services\PostService;
use App\View\Renderer;
class PostController
{
private $postService;
private $renderer;
public function __construct(PostService $postService, Renderer $renderer)
{
$this->postService = $postService;
$this->renderer = $renderer;
}
public function show(int $id): string
{
$post = $this->postService->findById($id);
return $this->renderer->render('post', ['post' => $post]);
}
}
// В Router при создании контроллера нужно передать зависимости.
// Для этого можно создать простой контейнер:
// $container = new Container();
// $container->set(PostService::class, function() { return new PostService(new PDO('...')); });
// $container->set(Renderer::class, function() { return new Renderer(__DIR__.'/../views'); });
// При создании контроллера: $container->resolve(PostController::class);
Результат: контроллер получает готовые сервисы, что упрощает тестирование (можно подменять зависимости на моки).
Пример 3: Использование FastRoute (популярная библиотека)
FastRoute от Никиты Попова - быстрый и гибкий маршрутизатор.
// Установка: composer require nikic/fast-route
// public/index.php
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/', 'HomeController@index');
$r->addRoute('GET', '/post/{id:\d+}', 'PostController@show');
$r->addRoute('POST', '/post/{id}/comment', 'PostController@addComment');
});
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
http_response_code(404);
echo '404 Not Found';
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
http_response_code(405);
echo '405 Method Not Allowed';
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// Разбираем строку типа 'Controller@action'
list($class, $method) = explode('@', $handler);
$controller = new $class();
echo call_user_func_array([$controller, $method], $vars);
break;
}
Результат: FastRoute сам обрабатывает 404 и 405, поддерживает кэширование маршрутов, что ускоряет работу.
Пример 4: Обработка ошибок и логирование в контроллере
Полезно создать базовый контроллер с перехватом исключений.
<?php
namespace App\Controllers;
abstract class BaseController
{
protected function render(string $view, array $data = []): string
{
$file = __DIR__ . '/../../views/' . $view . '.php';
if (!file_exists($file)) {
throw new \RuntimeException("View $view not found");
}
extract($data);
ob_start();
include $file;
return ob_get_clean();
}
public function __call($name, $arguments)
{
// Логируем вызов несуществующего метода
error_log("Неизвестный метод $name в " . get_class($this));
http_response_code(500);
return 'Внутренняя ошибка сервера';
}
}
// Пример использования в PostController
class PostController extends BaseController
{
public function show($id)
{
try {
$post = $this->postService->findById($id);
return $this->render('post', compact('post'));
} catch (\Exception $e) {
error_log($e->getMessage());
http_response_code(404);
return 'Пост не найден';
}
}
}
Результат: единый механизм обработки ошибок, логирование и пользовательские страницы ошибок.
Пример 5: Контроллер для API с выводом JSON
Устанавливаем заголовки и возвращаем сериализованные данные.
<?php
namespace App\Controllers;
class ApiController
{
public function users()
{
header('Content-Type: application/json');
$users = [
['id' => 1, 'name' => 'Анна'],
['id' => 2, 'name' => 'Борис'],
];
echo json_encode($users);
}
public function createUser()
{
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['name'])) {
http_response_code(400);
echo json_encode(['error' => 'Не передано имя']);
return;
}
// Сохраняем в БД...
http_response_code(201);
echo json_encode(['status' => 'created']);
}
}
Результат: при обращении к /api/users (GET) вернётся JSON-массив. При POST на /api/users с телом {'name':'Вася'} вернётся ответ 201.