Создание индексной страницы каталога на PHP

Раздел: Разработка каталогов на 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 мс при наличии кэша.
- Catalog index php (индексная страница каталога php)

Индексная страница каталога PHP - comments

En
Catalog index php (php)