Вывод товарного каталога: 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;
// привязка к скроллу или кнопке
}
});
}
Результат: динамическое добавление элементов на страницу без перезагрузки.