Вывод товарного каталога: PHP-методы и примеры кода

Раздел: Магазин -> Каталогизация

Основное решение: вывод каталога через PDO с разделением логики и представления

Наиболее эффективный подход для магазина – отделить запросы к базе данных от HTML-шаблонов. Это повышает безопасность, читаемость и упрощает поддержку. Используются классы для соединения с БД, репозиторий товаров и файл шаблона.


<?php
// db.php – подключение к БД через PDO
$host = 'localhost';
$dbname = 'shop';
$user = 'root';
$pass = '';
try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die('Ошибка соединения: ' . $e->getMessage());
}
?>

Catalog php view (просмотр каталога php)


<?php
// catalog.php – получение товаров с пагинацией
require 'db.php';

$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = 10;
$offset = ($page - 1) * $limit;

$sql = "SELECT id, name, price, category FROM products LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);

$totalStmt = $pdo->query('SELECT COUNT(*) FROM products');
$total = $totalStmt->fetchColumn();
$pages = ceil($total / $limit);

<!-- template.php – шаблон вывода -->
<h2 class="fw-bold">Каталог товаров</h2>
<?php foreach ($products as $product): ?>
    <div class="product">
        <span class="fw-bold"><?= htmlspecialchars($product['name']); ?></span>
        <span>Цена: <?= number_format($product['price'], 2); ?> руб.</span>
        <span>Категория: <?= htmlspecialchars($product['category']); ?></span>
    </div>
<?php endforeach; ?>

<div class="pagination">
    <?php for ($i = 1; $i <= $pages; $i++): ?>
        <a href="?page=<?= $i; ?>"><?= $i; ?></a>
    <?php endfor; ?>
</div>

Типичные ошибки:

  • Неэкранирование вывода – приводит к XSS-уязвимости. Всегда используйте htmlspecialchars().
  • Прямое использование $_GET['page'] без приведения к числу – возможность SQL-инъекции при неправильном запросе.
  • Отсутствие обработки ошибок PDO – скрывает проблемы с БД.

Как вывести товары простым циклом без разделения?

Самый быстрый способ для прототипа – смешать PHP и HTML в одном файле.


<?php
$products = $pdo->query('SELECT * FROM products')->fetchAll();
?>
<?php foreach ($products as $product): ?>
    <p><?= $product['name']; ?></p>
<?php endforeach; ?>

Вопрос: Как получить первый результат без лишних классов? Ответ: используйте foreach сразу после fetchAll(). Проблема: отсутствие защиты и сложности при изменении дизайна.

Как добавить постраничную навигацию?

К базовому запросу добавляются LIMIT и OFFSET, а также вычисляется общее количество страниц.


$page = isset($_GET['p']) ? (int)$_GET['p'] : 1;
$perPage = 5;
$offset = ($page - 1) * $perPage;
$stmt = $pdo->prepare('SELECT * FROM products LIMIT :lim OFFSET :off');
$stmt->execute([':lim' => $perPage, ':off' => $offset]);
$items = $stmt->fetchAll();
$total = $pdo->query('SELECT COUNT(*) FROM products')->fetchColumn();
$pages = ceil($total / $perPage);

Вопрос: Как избежать дублирования кода подсчёта страниц? Ответ: вынести логику в отдельную функцию или класс.

Типичная ошибка: забыть про fetchColumn() для подсчёта, использовать fetchAll() на больших таблицах – снижение производительности.

Как реализовать фильтрацию по категории?

Добавляется условие WHERE и передача параметра через подготовленный запрос.


$category = isset($_GET['cat']) ? (int)$_GET['cat'] : 0;
$sql = 'SELECT * FROM products';
$params = [];
if ($category > 0) {
    $sql .= ' WHERE category_id = :cat';
    $params[':cat'] = $category;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$products = $stmt->fetchAll();

Вопрос: Как сделать фильтр по диапазону цен? Ответ: добавить два параметра price_min и price_max.

Как организовать сортировку по цене или названию?

Переменная сортировки проверяется на допустимые значения, затем подставляется в ORDER BY.


$sort = 'name';
$order = 'ASC';
$allowedSort = ['name', 'price', 'id'];
if (in_array($_GET['sort'] ?? '', $allowedSort)) {
    $sort = $_GET['sort'];
}
$sql = "SELECT * FROM products ORDER BY $sort $order";

Вопрос: Как защититься от SQL-инъекции в ORDER BY? Ответ: проверять через белый список, как в примере.

Как применить шаблонизатор Twig?

После установки Twig (через Composer) определяются шаблоны с синтаксисом {{ }}.


require_once 'vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader('templates');
$twig = new \Twig\Environment($loader);
echo $twig->render('catalog.twig', [
    'products' => $products,
    'pages' => $pages
]);

Шаблон catalog.twig:


{% for product in products %}
    <div>{{ product.name }} – {{ product.price }}</div>
{% endfor %}

Вопрос: Зачем усложнять, если можно обойтись чистым PHP? Ответ: Twig автоматически экранирует вывод, отделяет логику, облегчает работу дизайнерам.

Как обновлять каталог без перезагрузки страницы (AJAX)?

Создаётся PHP-скрипт, возвращающий JSON, и на клиенте JavaScript обрабатывает ответ.


// ajax_catalog.php?page=2
header('Content-Type: application/json');
echo json_encode($products);

// JS (пример с fetch)
fetch('ajax_catalog.php?page=2')
    .then(res => res.json())
    .then(data => {
        // рендер на странице
    });

Вопрос: Как обработать ошибки AJAX-запроса? Ответ: добавить блок .catch() и проверку HTTP-статуса.

Расширенные примеры с пошаговыми пояснениями

Пример 1: Полный скрипт с пагинацией, фильтром по категории и сортировкой

Объединим все элементы в один безопасный скрипт. Используется PDO, белый список для сортировки, экранирование вывода.

Пример

<?php
require 'db.php';

$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 6;
$offset = ($page - 1) * $perPage;

$category = isset($_GET['cat']) ? (int)$_GET['cat'] : 0;
$sort = 'name';
$order = 'ASC';
$allowedSort = ['name', 'price', 'id'];
$allowedOrder = ['ASC', 'DESC'];

if (in_array($_GET['sort'] ?? '', $allowedSort)) {
    $sort = $_GET['sort'];
}
if (in_array(strtoupper($_GET['order'] ?? ''), $allowedOrder)) {
    $order = strtoupper($_GET['order']);
}

$sql = "SELECT * FROM products WHERE 1=1";
$params = [];

if ($category > 0) {
    $sql .= " AND category_id = :cat";
    $params[':cat'] = $category;
}

// Подсчёт общего числа
$countSql = str_replace('SELECT *', 'SELECT COUNT(*)', $sql);
$countStmt = $pdo->prepare($countSql);
$countStmt->execute($params);
$total = $countStmt->fetchColumn();

$sql .= " ORDER BY $sort $order LIMIT :limit OFFSET :offset";
$params[':limit'] = $perPage;
$params[':offset'] = $offset;

$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);

$pages = ceil($total / $perPage);
?>
<!DOCTYPE html>
<html>
<head><title>Каталог</title></head>
<body>
<h2 class="fw-bold">Товары</h2>
<form method="get">
    Категория: <select name="cat">
        <option value="0">Все</option>
        <option value="1"<?= $category==1?' selected':'' ?>>Одежда</option>
        <option value="2"<?= $category==2?' selected':'' ?>>Электроника</option>
    </select>
    Сортировать: <select name="sort">
        <option value="name"<?= $sort=='name'?' selected':'' ?>>Название</option>
        <option value="price"<?= $sort=='price'?' selected':'' ?>>Цена</option>
    </select>
    <select name="order">
        <option value="ASC"<?= $order=='ASC'?' selected':'' ?>>По возрастанию</option>
        <option value="DESC"<?= $order=='DESC'?' selected':'' ?>>По убыванию</option>
    </select>
    <button type="submit">Применить</button>
</form>
<?php foreach ($products as $product): ?>
    <div class="product">
        <span class="fw-bold"><?= htmlspecialchars($product['name']); ?></span> 
        <?= number_format($product['price'], 2); ?> руб.
    </div>
<?php endforeach; ?>
<div class="pagination">
    <?php for ($i = 1; $i <= $pages; $i++): ?>
        <a href="?page=<?= $i ?>&cat=<?= $category ?>&sort=<?= $sort ?>&order=<?= $order ?>"><?= $i ?></a>
    <?php endfor; ?>
</div>
</body>
</html>

Результат выполнения (часть вывода):

Товары
Форма фильтрации
[Одежда] [Название] [По возрастанию] [Применить]

Футболка белая 450.00 руб.
Джинсы синие 1200.00 руб.
...
Страницы: 1 2 3

Пример 2: Вывод каталога в виде таблицы с группировкой по категориям

Иногда требуется сгруппировать товары по категориям. Используется подзапрос или два цикла.

Пример

<?php
$categories = $pdo->query('SELECT id, name FROM categories')->fetchAll(PDO::FETCH_ASSOC);
?>
<table>
    <?php foreach ($categories as $cat): ?>
        <tr><th colspan="3" class="fw-bold"><?= htmlspecialchars($cat['name']); ?></th></tr>
        <?php
        $stmt = $pdo->prepare('SELECT name, price, stock FROM products WHERE category_id = ?');
        $stmt->execute([$cat['id']]);
        $products = $stmt->fetchAll();
        ?>
        <?php foreach ($products as $p): ?>
        <tr>
            <td><?= htmlspecialchars($p['name']); ?></td>
            <td><?= $p['price']; ?></td>
            <td><?= $p['stock']; ?> шт.</td>
        </tr>
        <?php endforeach; ?>
    <?php endforeach; ?>
</table>

Результат:

| Одежда |          |      |
| Футболка | 450    | 10   |
| Джинсы   | 1200   | 5    |
| Электроника |      |      |
| Наушники | 1500   | 3    |
| Колонка  | 2000   | 7    |

Пример 3: Вывод каталога с использованием шаблонизатора Twig (расширенный)

Установка: composer require twig/twig. Создаётся папка templates/ с файлом catalog.twig.

Пример

// index.php
require 'vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader('templates');
$twig = new \Twig\Environment($loader, [
    'cache' => 'cache', // опционально
]);

// Подготовка данных (аналогично Примеру 1)
// $products, $pages, $category, $sort, $order...

echo $twig->render('catalog.twig', [
    'products' => $products,
    'pages' => $pages,
    'currentPage' => $page,
    'category' => $category,
    'sort' => $sort,
    'order' => $order
]);
Пример

{# templates/catalog.twig #}
<h2 class="fw-bold">Каталог товаров</h2>

<form method="get">
    <select name="cat">
        <option value="0" {% if category == 0 %}selected{% endif %}>Все</option>
        <option value="1" {% if category == 1 %}selected{% endif %}>Одежда</option>
        <option value="2" {% if category == 2 %}selected{% endif %}>Электроника</option>
    </select>
    <select name="sort">
        <option value="name" {% if sort == 'name' %}selected{% endif %}>Название</option>
        <option value="price" {% if sort == 'price' %}selected{% endif %}>Цена</option>
    </select>
    <select name="order">
        <option value="ASC" {% if order == 'ASC' %}selected{% endif %}>По возрастанию</option>
        <option value="DESC" {% if order == 'DESC' %}selected{% endif %}>По убыванию</option>
    </select>
    <button type="submit">Применить</button>
</form>

{% for product in products %}
    <div class="product">
        <span class="fw-bold">{{ product.name }}</span>
        {{ product.price|number_format(2, '.', ' ') }} руб.
    </div>
{% endfor %}

<div class="pagination">
    {% for i in 1..pages %}
        <a href="?page={{ i }}&cat={{ category }}&sort={{ sort }}&order={{ order }}">{{ i }}</a>
    {% endfor %}
</div>

Результат будет идентичен Примеру 1, но код чище и автоматически экранирует переменные.

Пример 4: AJAX-загрузка товаров с бесконечным скроллом

PHP-скрипт возвращает JSON с товарами и информацией о наличии следующих страниц.

Пример

// load_products.php
$page = (int)($_GET['page'] ?? 1);
$limit = 8;
$offset = ($page-1)*$limit;
$stmt = $pdo->prepare("SELECT id, name, price, image FROM products LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
$total = $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
$hasMore = ($page * $limit) < $total;

header('Content-Type: application/json');
echo json_encode([
    'items' => $products,
    'hasMore' => $hasMore,
    'nextPage' => $page + 1
]);
Пример

// frontend.js (фрагмент)
let page = 1;
function loadMore() {
    fetch('load_products.php?page=' + page)
        .then(r => r.json())
        .then(data => {
            data.items.forEach(p => {
                const div = document.createElement('div');
                div.innerHTML = `${p.name} – ${p.price}`;
                document.getElementById('catalog').appendChild(div);
            });
            if (data.hasMore) {
                page = data.nextPage;
                // привязка к скроллу или кнопке
            }
        });
}

Результат: динамическое добавление элементов на страницу без перезагрузки.

Просмотр каталога PHP - comments

En
Catalog php view (php)