Создание страницы категории на PHP: от простого к сложному
Реализация страницы категории на PHP
Основной эффективный вариант: централизованный роутинг с ЧПУ
Цель: организовать страницу категории так, чтобы URL были читаемыми (например, /category/novosti), а логика обрабатывалась единым контроллером. Этот подход считается стандартом в современных PHP-приложениях, так как он безопасен, гибок и легко поддерживается.
Как это работает:
- Все запросы перенаправляются на
index.phpчерез.htaccess. - В
index.phpанализируется путь (request_uri) и извлекается сегмент категории. - Выполняется запрос к базе данных для получения записей этой категории.
- Результат передается в шаблон для отображения.
Пример минимальной структуры:
/.htaccess
/index.php
/app/controllers/CategoryController.php
/app/models/Category.php
/app/views/category.php
Файл .htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
Файл index.php
<?php
$url = $_GET['url'] ?? '';
$segments = explode('/', trim($url, '/'));
if (isset($segments[0]) && $segments[0] === 'category' && isset($segments[1])) {
$categorySlug = $segments[1];
require 'app/controllers/CategoryController.php';
$controller = new CategoryController();
$controller->show($categorySlug);
} else {
// 404 или другая обработка
header('HTTP/1.0 404 Not Found');
echo 'Страница не найдена';
}
Файл app/controllers/CategoryController.php
<?php
class CategoryController {
public function show($slug) {
$categoryModel = new Category();
$category = $categoryModel->getBySlug($slug);
if (!$category) {
header('HTTP/1.0 404 Not Found');
echo 'Категория не найдена';
exit;
}
$posts = $categoryModel->getPosts($category['id']);
include 'app/views/category.php';
}
}
Файл app/models/Category.php
<?php
class Category {
public function getBySlug($slug) {
// Пример с PDO
$db = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$stmt = $db->prepare('SELECT * FROM categories WHERE slug = ?');
$stmt->execute([$slug]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function getPosts($categoryId) {
$db = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$stmt = $db->prepare('SELECT * FROM posts WHERE category_id = ?');
$stmt->execute([$categoryId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
Файл app/views/category.php
<h2><?= htmlspecialchars($category['name']) ?></h2>
<?php foreach ($posts as $post): ?>
<article>
<h3><?= htmlspecialchars($post['title']) ?></h3>
<p><?= nl2br(htmlspecialchars($post['content'])) ?></p>
</article>
<?php endforeach; ?>
Типичные ошибки и их решения
- Ошибка 500 из-за синтаксиса в .htaccess (например, лишних пробелов). Решение: проверить файл через онлайн-валидатор mod_rewrite.
- Запросы выполняются медленно из-за отсутствия индексов в БД. Решение: добавить индекс на поле
slugиcategory_id. - SQL-инъекция при прямом встраивании переменных. Решение: всегда использовать подготовленные запросы (PDO или mysqli).
- Проблемы с кодировкой (кракозябры). Решение: установить
charset=utf8в DSN и заголовокContent-Type: text/html; charset=utf-8.
Как отобразить записи определенной категории с помощью GET-параметра?
Самый простой способ: передавать идентификатор категории через ?cat=ID. Подходит для небольших проектов или прототипов, но неудобен для пользователей и SEO.
<?php
$catId = (int)($_GET['cat'] ?? 0);
if ($catId > 0) {
$db = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$stmt = $db->prepare('SELECT * FROM posts WHERE category_id = ?');
$stmt->execute([$catId]);
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($posts as $post) {
echo '<h3>' . htmlspecialchars($post['title']) . '</h3>';
}
}
?>
Проблемы
- URL неинформативен:
?cat=1не говорит о сущности категории. - Легко поддается атакам (SQL-инъекции при неправильной обработке).
- Нет возможности сделать человеко-понятные ссылки.
Решение: все же использовать ЧПУ и валидировать входные данные.
Как создать ЧПУ для страницы категории с помощью mod_rewrite?
Этот вариант улучшает первый, добавляя правило перезаписи URL. В итоге у вас есть ЧПУ, но логика остается в одном файле.
RewriteEngine On
RewriteRule ^category/([a-z0-9-]+)$ index.php?cat_slug=$1 [L,QSA]
В index.php обрабатываем $_GET['cat_slug'] аналогично примеру выше.
Ошибка: если правило написано неверно, может возникнуть цикл перенаправления или некорректное срабатывание. Решение: использовать флаг [L] и проверять URL в браузере.
Как организовать страницу категории в MVC-архитектуре?
Полноценный MVC подразумевает разделение на модели, контроллеры и представления. В этом варианте каждый компонент отвечает за свою часть. Приведенный в основном решении код и есть пример простого MVC. Главное преимущество: легко добавлять новые типы страниц и поддерживать код.
// Дополнительный пример с наследованием контроллера
class CategoryController extends BaseController {
public function show($slug) {
$this->view->render('category', ['category' => $this->model->getBySlug($slug)]);
}
}
Ошибка: слишком много классов для маленького проекта. Решение: не злоупотреблять MVC, если приложение состоит из 2-3 страниц.
Расширенные примеры и сценарии
Пример 1: Страница категории с пагинацией
<?php
// В контроллере
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 10;
$offset = ($page - 1) * $perPage;
$posts = $categoryModel->getPostsPaginated($categoryId, $perPage, $offset);
$totalPosts = $categoryModel->countPosts($categoryId);
$totalPages = ceil($totalPosts / $perPage);
// В представлении
for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="?page=<?= $i ?>"><?= $i ?></a>
<?php endfor; ?>
Вывод: ссылки на страницы (1 2 3 ...) внизу списка записей.
Пример 2: Обработка пустой категории
<?php
if (empty($posts)) {
echo '<p class="fw-bold">В этой категории пока нет записей.</p>';
} else {
foreach ($posts as $post) {
echo '<div>' . htmlspecialchars($post['title']) . '</div>';
}
}
Если записей нет, выводится сообщение, иначе список.
Пример 3: Сортировка записей (по дате, по названию)
<?php
$order = $_GET['order'] ?? 'date_desc';
switch ($order) {
case 'date_asc':
$sqlOrder = 'created_at ASC';
break;
case 'title_asc':
$sqlOrder = 'title ASC';
break;
default:
$sqlOrder = 'created_at DESC';
}
$stmt = $db->prepare("SELECT * FROM posts WHERE category_id = ? ORDER BY $sqlOrder");
$stmt->execute([$catId]);
Пользователь может переключать сортировку через параметр ?order=title_asc.
Пример 4: Использование подготовленных запросов для предотвращения SQL-инъекций
<?php
$db = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$slug = $_GET['slug'] ?? '';
$stmt = $db->prepare('SELECT * FROM categories WHERE slug = :slug');
$stmt->bindParam(':slug', $slug, PDO::PARAM_STR);
$stmt->execute();
$category = $stmt->fetch(PDO::FETCH_ASSOC);
В случае передачи вредоносного значения в slug запрос останется безопасным.
Пример 5: Обработка ошибки 404 для несуществующей категории
<?php
$categoryModel = new Category();
$category = $categoryModel->getBySlug($slug);
if (!$category) {
http_response_code(404);
include 'errors/404.php';
exit;
}
Браузер получит статус 404, что важно для SEO.
Пример 6: Создание URL для категории в шаблоне
<?php
function categoryUrl($slug) {
return '/category/' . urlencode($slug);
}
// Использование
?>
<a href="<?= categoryUrl($category['slug']) ?>"><?= htmlspecialchars($category['name']) ?></a>
Генерируется правильный ЧПУ-URL.
Пример 7: Использование кэширования для страниц категорий
<?php
$cacheKey = 'category_' . $slug . '_page_' . $page;
$cached = apcu_fetch($cacheKey);
if ($cached === false) {
// Генерация содержимого
ob_start();
// ... вывод представления
$content = ob_get_clean();
apcu_store($cacheKey, $content, 3600); // кэш на 1 час
echo $content;
} else {
echo $cached;
}
Повышает производительность при большом количестве посещений.