Каталог товаров на 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 => {
                    // добавить карточку товара на страницу
                });
            });
    }
});

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

Каталог на PHP - comments

En
Catalogue php page (php)