Создание индексной страницы каталога на PHP
Общие принципы построения индексной страницы каталога
Индексная страница каталога (catalog/index.php) является точкой входа для отображения списка товаров или категорий. Выбор архитектуры влияет на производительность, безопасность и удобство поддержки. Далее приведены варианты от простых до продвинутых с пояснением целей каждого подхода.
Основное решение: безопасный и производительный вывод с PDO, Twig и пагинацией
Как сделать надёжную и быструю индексную страницу каталога?
Рекомендуемый подход включает: использование PDO для работы с БД, шаблонизатора Twig для отделения логики от представления, пагинацию для больших наборов данных и фильтрацию через подготовленные запросы.
<?php
// index.php
require_once 'vendor/autoload.php';
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
$loader = new FilesystemLoader('templates');
$twig = new Environment($loader);
$dsn = 'mysql:host=localhost;dbname=catalog;charset=utf8';
$user = 'dbuser';
$pass = 'dbpass';
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 10;
$offset = ($page - 1) * $perPage;
$stmtCount = $pdo->query('SELECT COUNT(*) FROM products');
$total = $stmtCount->fetchColumn();
$stmt = $pdo->prepare('SELECT id, name, price FROM products ORDER BY id LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll();
echo $twig->render('catalog.html.twig', [
'products' => $products,
'currentPage' => $page,
'totalPages' => ceil($total / $perPage),
]);
} catch (Exception $e) {
error_log($e->getMessage());
echo 'Произошла ошибка, попробуйте позже.';
}
Типичные проблемы:
- Ошибка соединения с БД – проверьте dsn, логин и пароль.
- Неверные пути к шаблонам Twig – настройте FilesystemLoader с правильной директорией.
- SQL-инъекции – избегайте прямой подстановки переменных в запросы, используйте только bindValue.
- Пагинация при больших объёмах данных – добавьте индекс на поле сортировки и рассмотрите кэширование количества записей.
Вариант 1: простой вывод с устаревшими mysql_*
Как сделать простую индексную страницу каталога с помощью функций mysql_*?
Цель – быстрый старт без установки дополнительных библиотек. Не рекомендуется для новых проектов из-за уязвимостей и отсутствия поддержки.
<?php
$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('catalog');
$result = mysql_query('SELECT id, name FROM products');
echo '<ul>';
while ($row = mysql_fetch_assoc($result)) {
echo '<li>' . $row['name'] . '</li>';
}
echo '</ul>';
Ошибки и решения:
- Функции mysql_* удалены в PHP 7 – используйте mysqli или PDO.
- SQL-инъекции – экранирование через mysql_real_escape_string не спасает от всех атак, лучше перейти на подготовленные запросы.
- Смешивание HTML и PHP – затрудняет поддержку, рекомендуется хотя бы минимальное разделение.
Вариант 2: вывод через PDO без шаблонизатора
Как безопасно вывести каталог с использованием PDO?
Подходит, когда нет возможности использовать Twig или нужно минимизировать зависимости. Логика и представление разделены частично.
<?php
$pdo = new PDO('mysql:host=localhost;dbname=catalog', 'user', 'pass');
$stmt = $pdo->query('SELECT id, name, price FROM products');
?>
<!DOCTYPE html>
<html>
<head><title>Каталог</title></head>
<body>
<table>
<tr><th>ID</th><th>Название</th><th>Цена</th></tr>
<?php while ($row = $stmt->fetch()): ?>
<tr>
<td><?= $row['id'] ?></td>
<td><?= htmlspecialchars($row['name']) ?></td>
<td><?= $row['price'] ?></td>
</tr>
<?php endwhile; ?>
</table>
</body>
</html>
Возможные проблемы:
- XSS-уязвимости – всегда используйте htmlspecialchars для вывода данных.
- Большие объёмы данных – без пагинации страница будет медленно загружаться.
- Отсутствие шаблонизатора – код становится трудночитаемым при усложнении логики.
Вариант 3: реализация по шаблону MVC (самописный)
Как организовать индексную страницу каталога по шаблону MVC?
Цель – чёткое разделение модели (Model), представления (View) и контроллера (Controller). Пример с простыми классами.
// Model/Product.php
class Product {
public static function getAll(PDO $pdo, $page, $perPage) {
$offset = ($page - 1) * $perPage;
$stmt = $pdo->prepare('SELECT * FROM products LIMIT :limit OFFSET :offset');
$stmt->execute([':limit' => $perPage, ':offset' => $offset]);
return $stmt->fetchAll();
}
}
// Controller/CatalogController.php
class CatalogController {
public function index(PDO $pdo) {
$page = $_GET['page'] ?? 1;
$products = Product::getAll($pdo, $page, 10);
include 'views/catalog.php';
}
}
// index.php (front controller)
$controller = new CatalogController();
$controller->index($pdo);
Сложности:
- Необходимость ручного управления зависимостями – можно внедрить DI-контейнер.
- Автозагрузка классов – настройте spl_autoload_register или используйте Composer.
- Нарушение принципов MVC при недостатке опыта – строго разделяйте ответственность.
Вариант 4: динамическая подгрузка через AJAX
Как реализовать динамическую подгрузку товаров без перезагрузки страницы?
Используется для улучшения пользовательского опыта. Клиент отправляет AJAX-запрос на сервер, который возвращает HTML или JSON.
// index.php (загружает начальный список)
$products = $pdo->query('SELECT * FROM products LIMIT 10')->fetchAll();
?>
<div id="catalog-list">
<?php foreach ($products as $product): ?>
<div class="product"><?= $product['name'] ?></div>
<?php endforeach; ?>
</div>
<button id="load-more" data-page="2">Загрузить ещё</button>
// ajax.php (обработчик AJAX)
$page = $_GET['page'];
$offset = ($page - 1) * 10;
$stmt = $pdo->prepare('SELECT * FROM products LIMIT 10 OFFSET :offset');
$stmt->execute([':offset' => $offset]);
$products = $stmt->fetchAll();
foreach ($products as $product) {
echo '<div class="product">' . htmlspecialchars($product['name']) . '</div>';
}
Распространённые ошибки:
- Отсутствие XSS-защиты при возврате HTML – экранируйте данные.
- Некорректная обработка CORS при разделении frontend и backend.
- Дублирование кода пагинации – выносите общую логику в отдельный класс.
Вариант 5: кэширование результатов
Как ускорить загрузку каталога с помощью кэширования?
Кэширование уменьшает нагрузку на БД. Пример с файловым кэшем и Memcached.
$cacheFile = 'cache/catalog_page_' . $page . '.html';
$lifetime = 3600; // 1 час
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $lifetime)) {
readfile($cacheFile);
exit;
}
ob_start();
// ... генерация вывода (запросы, шаблонизация) ...
$html = ob_get_clean();
file_put_contents($cacheFile, $html);
echo $html;
Трудности:
- Устаревший кэш при изменении данных – реализуйте сброс кэша по событию (например, при добавлении товара).
- Большое количество файлов при пагинации – используйте Memcached или Redis.
- Проблемы с правами на запись кэш-директории – убедитесь, что веб-сервер имеет доступ.
Вариант 6: использование готового шаблонизатора Smarty
Как использовать шаблонизатор для отделения логики от представления?
Smarty – один из старейших шаблонизаторов. Подходит для проектов, где уже используется Smarty.
require_once 'libs/Smarty.class.php';
$smarty = new Smarty();
$smarty->template_dir = 'templates/';
$smarty->compile_dir = 'templates_c/';
$smarty->assign('products', $products);
$smarty->display('catalog.tpl');
Недостатки:
- Smarty требует компиляции шаблонов, что может замедлить первый запрос.
- Меньшая популярность по сравнению с Twig, сложнее найти готовые решения.
- Необходимость настройки прав на директорию компиляции.
Расширенные примеры индексной страницы каталога
Пример 1: полный скрипт с пагинацией, сортировкой и фильтрацией на PDO + Twig
Включает обработку GET-параметров sort, order, filter, защиту от инъекций, постраничную навигацию.
<?php
// index.php (полный пример)
require_once 'vendor/autoload.php';
use Twig\Loader\FilesystemLoader;
use Twig\Environment;
$loader = new FilesystemLoader('templates');
$twig = new Environment($loader);
$pdo = new PDO('mysql:host=localhost;dbname=catalog;charset=utf8', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 8;
$sort = in_array($_GET['sort'] ?? '', ['name', 'price', 'id']) ? $_GET['sort'] : 'id';
$order = strtoupper($_GET['order'] ?? 'ASC') === 'DESC' ? 'DESC' : 'ASC';
$categoryId = (int)($_GET['category'] ?? 0);
$where = '';
$params = [];
if ($categoryId > 0) {
$where = 'WHERE category_id = :cat';
$params[':cat'] = $categoryId;
}
$countSql = "SELECT COUNT(*) FROM products $where";
$stmtCount = $pdo->prepare($countSql);
$stmtCount->execute($params);
$total = $stmtCount->fetchColumn();
$sql = "SELECT id, name, price, image FROM products $where ORDER BY $sort $order LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', ($page - 1) * $perPage, PDO::PARAM_INT);
foreach ($params as $k => $v) $stmt->bindValue($k, $v);
$stmt->execute();
$products = $stmt->fetchAll();
$categories = $pdo->query('SELECT id, name FROM categories')->fetchAll();
echo $twig->render('catalog_with_filters.html.twig', [
'products' => $products,
'categories' => $categories,
'currentPage' => $page,
'totalPages' => ceil($total / $perPage),
'currentSort' => $sort,
'currentOrder' => $order,
'selectedCategory' => $categoryId,
]);
Результат: HTML-страница с каталогом, ссылками на сортировку и пагинацию, списком категорий для фильтрации.
Пример 2: AJAX-загрузка с использованием JSON и jQuery
Сервер возвращает JSON, клиент динамически строит DOM. Подходит для SPA-подобных интерфейсов.
// server.php (обработчик AJAX)
header('Content-Type: application/json');
$page = (int)($_GET['page'] ?? 1);
$stmt = $pdo->prepare('SELECT id, name, price FROM products LIMIT 6 OFFSET :offset');
$stmt->execute([':offset' => ($page - 1) * 6]);
$products = $stmt->fetchAll();
echo json_encode($products);
// index.html (фрагмент)
<script>
function loadPage(page) {
$.getJSON('server.php', {page: page}, function(data) {
$('#product-list').empty();
$.each(data, function(i, item) {
$('#product-list').append('<div class="product" data-id="' + item.id + '">' + item.name + ' - ' + item.price + '</div>');
});
});
}
</script>
Результат: динамическое обновление списка товаров без перезагрузки страницы.
Пример 3: кэширование с использованием Redis
Более производительное кэширование по сравнению с файловым. Требует установленного расширения phpredis.
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$cacheKey = 'catalog_page_' . $page;
if ($redis->exists($cacheKey)) {
echo $redis->get($cacheKey);
exit;
}
ob_start();
// ... генерация контента ...
$content = ob_get_clean();
$redis->setEx($cacheKey, 3600, $content);
echo $content;
Результат: страница отдаётся из Redis, время ответа снижено до 1-5 мс при наличии кэша.