Контроллер в PHP: от простого index.php до профессионального роутера

Раздел: Веб-разработка -> MVC архитектура

Основной подход: 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.

Контроллер в PHP (MVC) - comments

En
Index php controller (php)