Создание маршрутов продуктов: лучшие практики PHP
Основные подходы к маршрутизации продуктов
Наиболее эффективное решение - использование библиотеки FastRoute с групповыми маршрутами и внедрением контроллеров.
Этот вариант обеспечивает высокую производительность, читаемость и поддержку RESTful практик. Маршруты группируются по префиксу /products, каждый HTTP-метод обрабатывается отдельным обработчиком.
// composer require nikic/fast-route
// config/routes.php
use FastRoute\RouteCollector;
return function (RouteCollector $r) {
$r->addGroup('/products', function (RouteCollector $r) {
$r->addRoute('GET', '', 'ProductController::index'); // список
$r->addRoute('GET', '/{id:\d+}', 'ProductController::show'); // один продукт
$r->addRoute('POST', '', 'ProductController::store'); // создание
$r->addRoute('PUT', '/{id:\d+}', 'ProductController::update');
$r->addRoute('DELETE', '/{id:\d+}', 'ProductController::destroy');
});
};
Обработчик получает параметры маршрута и может вызывать соответствующий метод контроллера. Библиотека сама разбирает путь и выполняет подстановку параметров.
Частая ошибка:
Неправильное использование регулярных выражений в шаблоне {id:\d+} приводит к тому, что маршрут не срабатывает для числовых идентификаторов. Решение - всегда экранировать обратную косую черту в PHP-строке: '{id:\\d+}'.
Как реализовать маршруты продуктов без внешних библиотек, используя только встроенные функции?
Можно написать простой маршрутизатор на основе parse_url и регулярных выражений. Это подходит для маленьких проектов, где не требуется высокая производительность.
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
// База маршрутов
$routes = [
'GET /products' => 'indexAction',
'GET /products/(\\d+)' => 'showAction',
'POST /products' => 'storeAction',
];
$matched = false;
foreach ($routes as $pattern => $action) {
list($routeMethod, $routePath) = explode(' ', $pattern, 2);
if ($method !== $routeMethod) continue;
$regex = '#^' . $routePath . '$#';
if (preg_match($regex, $uri, $matches)) {
array_shift($matches); // удаляем полное совпадение
call_user_func_array($action, $matches);
$matched = true;
break;
}
}
if (!$matched) {
header('HTTP/1.1 404 Not Found');
echo 'Страница не найдена';
}
Проблема такого подхода:
Нет поддержки сложной вложенности и middleware. Порядок маршрутов важен - более конкретные должны быть объявлены раньше общих. Ошибка - забыть удалить полное совпадение из $matches, что передаёт лишний параметр в функцию.
Как добавить вложенные маршруты для продуктов, связанных с категориями?
Использование групповых маршрутов в FastRoute с вложенными префиксами:
$r->addGroup('/categories/{catId:\\d+}', function (RouteCollector $r) {
$r->addGroup('/products', function (RouteCollector $r) {
$r->addRoute('GET', '', 'CategoryProductController::index');
$r->addRoute('GET', '/{prodId:\\d+}', 'CategoryProductController::show');
});
});
Параметры catId и prodId передаются обработчику. В другом варианте (чистый PHP) придётся вручную извлекать сегменты URI.
Типичная ошибка:
Неверная вложенность group - забывают передать RouteCollector в анонимную функцию. Решение - всегда проверять сигнатуру колбэка.
Как маршрутизировать продукты с использованием query-параметров (например, фильтрация)?
Метод GET /products?category=1&page=2 обрабатывается тем же маршрутом, параметры извлекаются из $_GET.
$r->addRoute('GET', '/products', 'ProductController::index');
// В контроллере:
function index() {
$category = $_GET['category'] ?? null;
$page = $_GET['page'] ?? 1;
// ... логика
}
Проблема:
Смешивание логики маршрутизации и бизнес-логики. Лучше передавать параметры через сам маршрут (например, /products/category/1/page/2) или использовать middleware для валидации.
Как обеспечить RESTful маршруты для продуктов с авторизацией?
Добавление middleware (проверку токена или сессии) в группу маршрутов:
// Используя FastRoute и собственный диспетчер
$dispatcher = FastRoute\simpleDispatcher(function(RouteCollector $r) {
$r->addGroup('/api/products', ['middleware' => 'auth'], function (RouteCollector $r) {
$r->addRoute('GET', '', ['ProductController', 'index']);
$r->addRoute('POST', '', ['ProductController', 'store']);
});
});
В реальных фреймворках (Laravel, Symfony) это делается через группы middleware.
Ошибка:
Пытаться реализовать middleware внутри анонимной функции без понимания порядка вызовов. В FastRoute нет встроенного middleware, поэтому требуется обёртка.
Какие проблемы возникают при использовании симфони-роутинга в чистом PHP?
Symfony Routing требует PSR-7 сообщений и дополнительных абстракций, что усложняет простые проекты.
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
$routes = new RouteCollection();
$routes->add('product_index', new Route('/products', [
'_controller' => 'ProductController::index',
'methods' => ['GET']
]));
Проблема - необходимость настройки UrlMatcher и контекста запроса. Ошибки возникают при несовпадении формата параметров (например, {id} без ограничения типа).
Типичная ошибка:
Забывают указать метод HTTP в опциях маршрута, что приводит к тому, что POST-запрос обрабатывается как GET. Решение - всегда явно прописывать 'methods'.
Расширенные примеры маршрутов продуктов
Пример 1. Вложенные ресурсы с пагинацией и фильтрацией через сегменты URL
Маршрут: /api/v2/categories/{catId}/products?status=active&limit=10 и /api/v2/categories/{catId}/products/{prodId}
// FastRoute
$r->addGroup('/api/v2/categories/{catId:\d+}', function (RouteCollector $r) {
$r->addGroup('/products', function (RouteCollector $r) {
$r->addRoute('GET', '', 'CategoryProductController::index');
$r->addRoute('GET', '/{prodId:\d+}', 'CategoryProductController::show');
$r->addRoute('POST', '', 'CategoryProductController::store');
});
});
В контроллере:
class CategoryProductController {
public function index($catId) {
$status = $_GET['status'] ?? 'all';
$limit = (int)($_GET['limit'] ?? 20);
// выборка продуктов из категории $catId с фильтрацией
echo "Категория $catId, статус $status, лимит $limit";
}
public function show($catId, $prodId) {
echo "Продукт $prodId из категории $catId";
}
}
Результат вызова GET /api/v2/categories/5/products?status=active:
Категория 5, статус active, лимит 20
Пример 2. Использование именованных маршрутов для генерации URL продуктов
FastRoute не поддерживает именованные маршруты напрямую, но можно реализовать через хранение в массиве. В Symfony Routing это встроено.
// Симфони роутинг
$route = new Route('/products/{id}', [
'_controller' => 'ProductController::show',
], ['id' => '\d+']);
$route->setMethods(['GET']);
$routes->add('product_show', $route);
// Генерация URL
use Symfony\Component\Routing\Generator\UrlGenerator;
$generator = new UrlGenerator($routes, new RequestContext());
echo $generator->generate('product_show', ['id' => 42]);
/products/42
Пример 3. Маршруты с middleware для проверки прав доступа к продуктам
В самописном решении middleware оборачивает обработчик:
function authMiddleware(callable $handler) {
return function (...$params) use ($handler) {
if (!isset($_SESSION['user'])) {
header('HTTP/1.1 401 Unauthorized');
exit('Требуется авторизация');
}
return $handler(...$params);
};
}
$routes = [
'GET /products' => authMiddleware('ProductController::index'),
'POST /products' => authMiddleware('ProductController::store'),
];
Проблема: сложно компоновать несколько middleware. Решение - использовать цепочку вызовов или библиотеку Middleware.
Пример 4. Обработка ошибок 404 для несуществующих продуктов с кастомной страницей
$r->addRoute('GET', '/products/{id:\d+}', function ($id) {
$product = findProductById($id);
if (!$product) {
// выбросить исключение или вернуть 404
http_response_code(404);
echo json_encode(['error' => 'Продукт не найден']);
return;
}
echo json_encode($product);
});
Более продвинутый способ - использовать middleware для отлова исключений:
class NotFoundException extends \Exception {}
$container = function($uri, $method) {
try {
// диспетчеризация маршрута
} catch (NotFoundException $e) {
header('HTTP/1.1 404 Not Found');
echo '404 - Продукт не найден
';
}
};
Пример 5. Маршруты для массовых операций над продуктами (batch)
Маршрут POST /products/batch для массового создания/обновления:
$r->addRoute('POST', '/products/batch', 'ProductController::batchStore');
// в контроллере
function batchStore() {
$data = json_decode(file_get_contents('php://input'), true);
foreach ($data as $product) {
// сохранение
}
echo 'Обработано ' . count($data) . ' продуктов';
}
Вызов curl -X POST -H "Content-Type: application/json" -d '[{"name":"A"},{"name":"B"}]' http://example.com/products/batch
Обработано 2 продуктов
Пример 6. Динамический роутинг продуктов на основе slug вместо id
$r->addRoute('GET', '/products/{slug:[a-z0-9-]+}', 'ProductController::showBySlug');
function showBySlug($slug) {
$product = getProductBySlug($slug);
if (!$product) {
http_response_code(404);
echo 'Продукт с таким URL не найден';
return;
}
echo "Продукт: {$product['name']}";
}
Ошибка: забыть экранировать дефис в регулярном выражении. Правильно: [a-z0-9-]+ - дефис должен быть в конце или начале, иначе интерпретируется как диапазон.
Пример 7. Разделение маршрутов для админки и публичного API продуктов
$r->addGroup('/admin', function (RouteCollector $r) {
$r->addGroup('/products', function (RouteCollector $r) {
$r->addRoute('GET', '', 'Admin\ProductController::index');
$r->addRoute('POST', '', 'Admin\ProductController::store');
});
});
$r->addGroup('/api', function (RouteCollector $r) {
$r->addGroup('/products', function (RouteCollector $r) {
$r->addRoute('GET', '', 'Api\ProductController::index');
});
});
Разные пространства имён контроллеров упрощают поддержку.