Реализация класса каталога в PHP для e-commerce проектов
Реализация класса каталога товаров в PHP
Класс каталога - центральный компонент интернет-магазина, отвечающий за управление списком товаров, их фильтрацию, сортировку и постраничный вывод. В зависимости от размера проекта и требований к производительности можно выбрать различные подходы. Далее рассмотрено основное эффективное решение и несколько альтернатив.
Класс Catalog на PDO с гибкими методами
Наиболее универсальный вариант - класс, использующий PDO для работы с реляционной базой данных. Он поддерживает фильтрацию по категории, цене, бренду, а также пагинацию и сортировку.
class Catalog {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function getProducts(
int $page = 1,
int $perPage = 12,
?int $categoryId = null,
?string $sort = 'name_asc',
?float $minPrice = null,
?float $maxPrice = null
): array {
$offset = ($page - 1) * $perPage;
$where = [];
$params = [];
if ($categoryId) {
$where[] = 'category_id = :category_id';
$params[':category_id'] = $categoryId;
}
if ($minPrice) {
$where[] = 'price >= :min_price';
$params[':min_price'] = $minPrice;
}
if ($maxPrice) {
$where[] = 'price <= :max_price';
$params[':max_price'] = $maxPrice;
}
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$order = match($sort) {
'price_asc' => 'ORDER BY price ASC',
'price_desc' => 'ORDER BY price DESC',
'name_desc' => 'ORDER BY name DESC',
default => 'ORDER BY name ASC'
};
$sql = "SELECT * FROM products {$whereClause} {$order} LIMIT :limit OFFSET :offset";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function countProducts(...$filters): int {
// аналогичная логика для COUNT(*)
}
}
Catalog php class (класс каталога в php)
Цель:
обеспечить производительную и безопасную выборку товаров с динамическими условиями. Использование: крупные магазины с тысячами товаров и сложными фильтрами.Типичные ошибки:
- Отсутствие экранирования параметров - использование подготовленных выражений решает проблему.
- Неэффективная пагинация на больших объёмах - для больших таблиц стоит добавить индексы.
- Забывают считать общее количество товаров для пагинации - требуется отдельный метод countProducts.
Вариант 1. Как сделать простой класс каталога без базы данных?
Для небольших каталогов или временных прототипов можно хранить товары в массиве внутри класса.
class InMemoryCatalog {
private array $products;
public function __construct(array $products = []) {
$this->products = $products;
}
public function filterByCategory(int $catId): array {
return array_filter($this->products, fn($p) => $p['category_id'] === $catId);
}
}
Вопрос: какой случай использования у такого подхода?
Ответ: демонстрация функционала, тестирование, магазины с десятком товаров без необходимости сохранять изменения.
Проблемы:
- Данные не сохраняются между запросами.
- Ограниченный объём памяти для больших наборов.
- Нет возможности выполнять сложные SQL-подобные запросы.
Вариант 2. Как интегрировать класс каталога с ORM (например, Eloquent)?
В современных фреймворках используют ORM для работы с БД. Класс каталога становится сервисом, работающим с моделью Product.
class CatalogService {
public function getFilteredProducts(array $filters): LengthAwarePaginator {
$query = Product::query();
if (!empty($filters['category_id'])) {
$query->where('category_id', $filters['category_id']);
}
return $query->paginate($filters['per_page'] ?? 12);
}
}
Вопрос: когда использовать ORM вместо прямого PDO?
Ответ: когда проект уже использует фреймворк (Laravel, Symfony) - ORM ускоряет разработку и предоставляет встроенные возможности пагинации/фильтрации.
Типичные ошибки:
- N+1 запросов при получении связанных данных (используйте with()).
- Неоптимальная производительность при сложных фильтрах - лучше перейти на сырые запросы.
Вариант 3. Как реализовать класс каталога с паттерном Repository?
Repository абстрагирует логику доступа к данным, упрощая тестирование и замену хранилища.
interface ProductRepositoryInterface {
public function findById(int $id): ?Product;
public function findAll(array $criteria): Collection;
}
class Catalog {
public function __construct(private ProductRepositoryInterface $repository) {}
public function getProductsByCategory(int $categoryId): array {
return $this->repository->findAll(['category_id' => $categoryId]);
}
}
Вопрос: какая выгода от использования Repository?
Ответ: код становится независимым от конкретной реализации БД, легко подменить на mock в тестах или переключиться с MySQL на MongoDB.
Проблемы:
- Увеличение количества классов.
- Избыточность для простых сценариев.
- Необходимость чётко определить интерфейс, чтобы не потерять гибкость.
Расширенные примеры кода для класса каталога
Пример 1. Класс с кешированием результатов
Добавление мемкеша или файлового кеша для снижения нагрузки на базу данных.
class CachedCatalog extends Catalog {
private CacheInterface $cache;
private int $ttl;
public function getProducts(int $page = 1, int $perPage = 12, ?int $categoryId = null): array {
$cacheKey = "products_{$page}_{$perPage}_" . ($categoryId ?? 'all');
if ($cached = $this->cache->get($cacheKey)) {
return $cached;
}
$products = parent::getProducts($page, $perPage, $categoryId);
$this->cache->set($cacheKey, $products, $this->ttl);
return $products;
}
}
// Использование
$catalog = new CachedCatalog($pdo, new FileCache(), 3600);
$products = $catalog->getProducts(1, 12, 5);
Результат:
повторный запрос к тому же набору товаров не вызывает SQL-запрос, данные берутся из кеша.Пример 2. Класс с поддержкой составных фильтров и полнотекстового поиска
class AdvancedCatalog {
private PDO $pdo;
public function searchByText(string $query, int $limit = 10): array {
$sql = "SELECT *, MATCH(name, description) AGAINST(:query IN BOOLEAN MODE) AS relevance
FROM products
WHERE MATCH(name, description) AGAINST(:query IN BOOLEAN MODE)
ORDER BY relevance DESC
LIMIT :limit";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':query', $query, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
Результат:
при поиске "красное платье" вернутся товары с упоминанием этих слов в названии или описании, отсортированные по релевантности.Пример 3. Агрегация данных - получение минимальной и максимальной цены в категории
public function getPriceRange(?int $categoryId = null): array {
$sql = "SELECT MIN(price) AS min_price, MAX(price) AS max_price FROM products";
$params = [];
if ($categoryId) {
$sql .= " WHERE category_id = :cat";
$params[':cat'] = $categoryId;
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// Вызов
$range = $catalog->getPriceRange(5);
// $range = ['min_price' => 199.00, 'max_price' => 2999.00]
Результат:
массив с границами цен, используется для построения слайдера фильтра.Пример 4. Интеграция с внешним API (например, поставщик товаров)
class ApiCatalog {
private HttpClient $client;
public function getProductsFromApi(array $filters): array {
$response = $this->client->get('https://supplier.example.com/products', [
'query' => $filters
]);
return json_decode($response->getBody(), true);
}
}
Результат:
класс работает как прокси - получает данные от внешнего сервиса, может кешировать и трансформировать их в массив товаров.