Каталог товаров на PHP: от базовых решений до продвинутых методов
Создание каталога товаров на PHP с использованием архитектуры MVC и PDO
Основной подход
Данное решение предполагает разделение логики на модель (Model), представление (View) и контроллер (Controller). Для работы с базой данных применяется расширение PDO, которое обеспечивает безопасную работу с запросами и защиту от SQL-инъекций. Каталог строится на основе таблицы 'products' в MySQL.
// Пример структуры таблицы
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
description TEXT,
category_id INT,
image VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Для подключения к базе данных используется класс Database с синглтоном.
class Database {
private static $instance = null;
private $pdo;
private function __construct() {
$dsn = 'mysql:host=localhost;dbname=catalog;charset=utf8';
$this->pdo = new PDO($dsn, 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getPdo() {
return $this->pdo;
}
}
Модель ProductModel отвечает за получение данных из БД.
class ProductModel {
public function getAllProducts($categoryId = null) {
$pdo = Database::getInstance()->getPdo();
$sql = 'SELECT * FROM products';
$params = [];
if ($categoryId) {
$sql .= ' WHERE category_id = :cat_id';
$params[':cat_id'] = $categoryId;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
public function getProductById($id) {
$pdo = Database::getInstance()->getPdo();
$stmt = $pdo->prepare('SELECT * FROM products WHERE id = :id');
$stmt->execute([':id' => $id]);
return $stmt->fetch();
}
}
Контроллер ProductController обрабатывает запросы.
class ProductController {
public function index($categoryId = null) {
$model = new ProductModel();
$products = $model->getAllProducts($categoryId);
include 'views/catalog.php';
}
public function show($id) {
$model = new ProductModel();
$product = $model->getProductById($id);
if (!$product) {
http_response_code(404);
echo 'Товар не найден';
return;
}
include 'views/product.php';
}
}
Представление catalog.php выводит список товаров.
<!DOCTYPE html>
<html>
<head>
<title>Каталог товаров</title>
</head>
<body>
<h2>Товары</h2>
<div class="products">
<?php foreach ($products as $product): ?>
<div class="product-card">
<h3><?= htmlspecialchars($product['name']) ?></h3>
<p>Цена: <?= number_format($product['price'], 2) ?> руб.</p>
<a href="?action=show&id=<?= $product['id'] ?>">Подробнее</a>
</div>
<?php endforeach; ?>
</div>
</body>
</html>
Типичные проблемы и их решения:
- Ошибка подключения: проверьте имя базы данных, пользователя и пароль. Используйте try-catch при создании PDO.
- SQL-инъекции: всегда используйте подготовленные запросы. Не конкатенируйте переменные в SQL.
- Кодировка: укажите charset=utf8 в DSN.
- Отображение ошибок: включите display_errors для разработки, но отключите на production.
Основное преимущество этого подхода - безопасность, переиспользуемость кода и лёгкая поддержка.
Как реализовать каталог без базы данных, используя массив?
Для очень маленьких проектов или прототипов можно хранить товары в PHP-массиве прямо в коде или в отдельном файле конфигурации.
// config/products.php
return [
['id' => 1, 'name' => 'Футболка', 'price' => 1500],
['id' => 2, 'name' => 'Джинсы', 'price' => 3500],
['id' => 3, 'name' => 'Кроссовки', 'price' => 5000]
];
// catalog.php
$products = include 'config/products.php';
?>
<?php foreach ($products as $product): ?>
<p><?= $product['name'] ?> - <?= $product['price'] ?> руб.</p>
<?php endforeach; ?>
Проблемы: данные не сохраняются между запросами, отсутствует масштабируемость. Решение: использовать только для статичного контента.
Как использовать MySQLi вместо PDO для работы с каталогом?
MySQLi - альтернативный способ подключения к MySQL. Он также поддерживает подготовленные запросы.
$mysqli = new mysqli('localhost', 'root', '', 'catalog');
if ($mysqli->connect_error) {
die('Ошибка подключения: ' . $mysqli->connect_error);
}
$stmt = $mysqli->prepare('SELECT * FROM products WHERE price > ?');
$stmt->bind_param('d', $minPrice);
$minPrice = 1000;
$stmt->execute();
$result = $stmt->get_result();
$products = $result->fetch_all(MYSQLI_ASSOC);
Ошибки: путаница с bind_param (типы: i, d, s, b). Решение: внимательно указывать тип параметра. MySQLi не поддерживает именованные плейсхолдеры, только позиционные.
Как организовать каталог с файловой системой (JSON-файлы)?
Можно хранить каждый товар в отдельном JSON-файле или в одном общем файле. Удобно для небольших каталогов, где не нужна реляционная БД.
// products.json
[
{"id":1,"name":"Ноутбук","price":45000},
{"id":2,"name":"Мышь","price":1200}
]
$json = file_get_contents('products.json');
$products = json_decode($json, true);
foreach ($products as $product) {
echo $product['name'] . ' - ' . $product['price'] . PHP_EOL;
}
Проблема с конкурентным доступом: при одновременной записи файл может повредиться. Решение: использовать блокировки flock или перейти к БД.
Как добавить пагинацию в каталог на PHP?
Пагинация необходима при большом количестве товаров. Реализуется с помощью LIMIT и OFFSET в SQL.
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 10;
$offset = ($page - 1) * $perPage;
$stmt = $pdo->prepare('SELECT * FROM products LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll();
// подсчёт общего количества
$countStmt = $pdo->query('SELECT COUNT(*) FROM products');
$total = $countStmt->fetchColumn();
$totalPages = ceil($total / $perPage);
В представлении выводится нумерация страниц.
Ошибка: неправильный тип при bindValue - LIMIT требует целое число. Решение: явно указывать PDO::PARAM_INT. Также стоит проверять, что $page > 0 и $page <= $totalPages.
Расширенные примеры для каталога на PHP
Пример 1: Фильтрация товаров по категории и цене с PDO
// Контроллер с фильтрами
public function filter($categoryId, $minPrice, $maxPrice) {
$pdo = Database::getInstance()->getPdo();
$sql = 'SELECT * FROM products WHERE 1=1';
$params = [];
if ($categoryId) {
$sql .= ' AND category_id = :cat';
$params[':cat'] = $categoryId;
}
if ($minPrice !== null) {
$sql .= ' AND price >= :min';
$params[':min'] = $minPrice;
}
if ($maxPrice !== null) {
$sql .= ' AND price <= :max';
$params[':max'] = $maxPrice;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
Вызов: $products = $controller->filter(2, 1000, 5000);
Результат: массив товаров категории 2 с ценой от 1000 до 5000.
Array
(
[0] => Array
(
[id] => 5
[name] => Смартфон
[price] => 2500.00
[category_id] => 2
)
[1] => Array
(
[id] => 8
[name] => Планшет
[price] => 3200.00
[category_id] => 2
)
)
Пример 2: Поиск товаров по названию с использованием LIKE
public function search($query) {
$pdo = Database::getInstance()->getPdo();
$stmt = $pdo->prepare('SELECT * FROM products WHERE name LIKE :query');
$stmt->execute([':query' => '%' . $query . '%']);
return $stmt->fetchAll();
}
Поиск 'ноут' вернёт все товары с 'ноутбук', 'ноутбук игровой' и т.д.
Array
(
[0] => Array
(
[id] => 12
[name] => Ноутбук Lenovo
[price] => 45000.00
)
[1] => Array
(
[id] => 15
[name] => Ноутбук HP
[price] => 52000.00
)
)
Пример 3: Кэширование результатов каталога с помощью файлового кэша
public function getCachedProducts($categoryId = null) {
$cacheKey = 'products_' . ($categoryId ?? 'all');
$cacheFile = __DIR__ . '/cache/' . $cacheKey . '.cache';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
return unserialize(file_get_contents($cacheFile));
}
$products = $this->getAllProducts($categoryId);
file_put_contents($cacheFile, serialize($products));
return $products;
}
Этот метод ускоряет загрузку каталога при неизменных данных.
Пример 4: Вывод товаров с использованием шаблонизатора Twig
// composer require twig/twig
require_once 'vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader('templates');
$twig = new \Twig\Environment($loader);
echo $twig->render('catalog.html.twig', ['products' => $products]);
Шаблон catalog.html.twig:
<h2>Товары</h2>
<ul>
{% for product in products %}
<li>{{ product.name }} - {{ product.price|number_format(2, '.', ' ') }} руб.</li>
{% endfor %}
</ul>
Результат: HTML список с отформатированными ценами.
Пример 5: Использование AJAX для подгрузки товаров при скролле (бесконечная лента)
// PHP endpoint: ajax-products.php?page=2
$page = $_GET['page'] ?? 1;
$perPage = 5;
$offset = ($page - 1) * $perPage;
$stmt = $pdo->prepare('SELECT * FROM products LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll();
header('Content-Type: application/json');
echo json_encode($products);
JavaScript на клиенте:
let page = 1;
window.addEventListener('scroll', () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
page++;
fetch('ajax-products.php?page=' + page)
.then(response => response.json())
.then(data => {
data.forEach(product => {
// добавить карточку товара на страницу
});
});
}
});
Результат: динамическая подгрузка без перезагрузки страницы.