Интернет магазины на PHP: практические примеры и сравнение подходов
Реализация интернет-магазина на PHP: подходы и примеры
Создание интернет-магазина на PHP требует выбора подходящей архитектуры. Ниже рассмотрено основное эффективное решение с использованием фреймворка Laravel, а также альтернативные варианты, каждый со своими вопросами, целями и типичными ошибками.
Основное решение: MVC на Laravel с готовой корзиной и каталогом
Laravel предоставляет структуру, ORM, миграции, шаблонизатор Blade, а также мощные средства для построения REST API. Это позволяет быстро создать масштабируемый магазин.
Как реализовать базовый каталог товаров с фильтрацией в Laravel?
// Маршруты (routes/web.php)
Route::get('/catalog', [CatalogController::class, 'index'])->name('catalog');
Route::get('/catalog/{category}', [CatalogController::class, 'byCategory']);
// Контроллер (app/Http/Controllers/CatalogController.php)
public function index(Request $request) {
$query = Product::query();
if ($request->filled('category')) {
$query->where('category_id', $request->category);
}
if ($request->filled('price_min')) {
$query->where('price', '>=', $request->price_min);
}
$products = $query->paginate(12);
$categories = Category::all();
return view('catalog.index', compact('products', 'categories'));
}
// Шаблон Blade (resources/views/catalog/index.blade.php)
@foreach($categories as $category)
<a href="{{ route('catalog', ['category' => $category->id]) }}">{{ $category->name }}</a>
@endforeach
@foreach($products as $product)
<div class="product-card">
<h3>{{ $product->name }}</h3>
<p>{{ $product->price }} руб.</p>
</div>
@endforeach
{{ $products->links() }}
Этот код демонстрирует получение товаров с фильтром по категории и цене, а также пагинацию.
Как добавить корзину с сохранением в сессии?
// Сервис корзины (app/Services/CartService.php)
class CartService {
public function add(int $productId, int $quantity = 1): void {
$cart = session()->get('cart', []);
if (isset($cart[$productId])) {
$cart[$productId]['quantity'] += $quantity;
} else {
$product = Product::findOrFail($productId);
$cart[$productId] = [
'name' => $product->name,
'price' => $product->price,
'quantity' => $quantity
];
}
session()->put('cart', $cart);
}
public function remove(int $productId): void {
$cart = session()->get('cart', []);
unset($cart[$productId]);
session()->put('cart', $cart);
}
public function total(): float {
$cart = session()->get('cart', []);
return array_sum(array_map(fn($item) => $item['price'] * $item['quantity'], $cart));
}
}
Использование сессий - простой способ для незарегистрированных пользователей. Для авторизованных можно сохранять корзину в БД.
Типичные проблемы:
- Потеря корзины при истечении сессии. Решение: дублировать корзину в таблице carts для авторизованных пользователей.
- Медленные запросы при большом количестве товаров. Используйте индексы в БД и кэширование (Redis).
Вариант 1: Чистый PHP без фреймворка
Когда оправдано написание магазина на чистом PHP, без фреймворков? Для очень простых витрин, учебных проектов или при жёстких требованиях к скорости работы без лишних зависимостей.
// Простой вывод списка товаров (index.php)
$db = new PDO('mysql:host=localhost;dbname=shop', 'root', '');
$stmt = $db->query('SELECT * FROM products');
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head><title>Магазин</title></head>
<body>
<h2>Товары</h2>
<?php foreach ($products as $product): ?>
<div>
<h3><?= htmlspecialchars($product['name']) ?></h3>
<p>Цена: <?= $product['price'] ?> руб.</p>
</div>
<?php endforeach; ?>
</body>
</html>
Этот способ прост для понимания, но при добавлении авторизации, корзины, админки код становится трудно поддерживаемым.
Проблемы чистой реализации:
- SQL-инъекции при отсутствии подготовленных запросов.
- Смешивание логики и представления.
- Трудности с расширением функционала.
Рекомендуется использовать минимальные практики безопасности: подготовленные выражения, фильтрация входных данных, а также организовать разделение на файлы (config, controllers, models).
Вариант 2: Использование микрофреймворка Slim
Как создать магазин с минимальным фреймворком, но с маршрутизацией и middleware? Slim подходит для быстрого прототипирования или API-бекенда.
// index.php
use Slim\Factory\AppFactory;
require __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/products', function ($request, $response, $args) {
$db = new PDO('mysql:host=localhost;dbname=shop', 'root', '');
$stmt = $db->query('SELECT * FROM products');
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
$payload = json_encode($products);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/json');
});
$app->run();
Здесь всё ещё необходимо вручную работать с БД, но маршрутизация и обработка запросов/ответов упрощены. Для полноценного магазина нужна самописная ORM или использование Doctrine.
Вариант 3: CMS на базе PHP (OpenCart, WooCommerce на WordPress)
Когда выгоднее использовать готовую CMS вместо разработки собственного магазина? Для быстрого запуска без глубоких кастомизаций, малого бизнеса.
OpenCart - специализированная PHP CMS для магазинов. WooCommerce - плагин для WordPress. Оба имеют готовые модули для корзины, платёжных систем, доставки. Главный минус - сложность внедрения нестандартной логики и производительность на больших каталогах.
Типичные ошибки при использовании CMS:
- Установка большого количества плагинов, замедляющих сайт.
- Игнорирование обновлений безопасности.
- Попытка изменить ядро CMS напрямую, что ломается при обновлении.
Лучше создавать дочерние темы (WordPress) или использовать переопределения (OpenCart).
Расширенные примеры кода для интернет-магазина на PHP
Далее приведены более сложные сценарии: работа с заказами, авторизация, интеграция платежей и админ-панель.
Реализация корзины с привязкой к пользователю (Eloquent ORM в Laravel)
// Миграция для таблицы carts
Schema::create('carts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
Schema::create('cart_items', function (Blueprint $table) {
$table->id();
$table->foreignId('cart_id')->constrained()->onDelete('cascade');
$table->foreignId('product_id')->constrained();
$table->integer('quantity');
$table->timestamps();
});
// Модель Cart с отношением
class Cart extends Model {
public function items() {
return $this->hasMany(CartItem::class);
}
public function user() {
return $this->belongsTo(User::class);
}
}
// Добавление товара в корзину (Auth::check() гарантирован)
$cart = Cart::firstOrCreate(['user_id' => Auth::id()]);
$item = $cart->items()->updateOrCreate(
['product_id' => $productId],
['quantity' => DB::raw('quantity + ' . $quantity)]
);
Результат: в базе создаётся запись корзины пользователя, а при повторном добавлении количество увеличивается.
Обработка заказа с транзакциями
use Illuminate\Support\Facades\DB;
try {
DB::beginTransaction();
$order = Order::create([
'user_id' => Auth::id(),
'total' => CartService::total(),
'status' => 'pending'
]);
$cart = Cart::where('user_id', Auth::id())->first();
foreach ($cart->items as $item) {
$order->items()->create([
'product_id' => $item->product_id,
'price' => $item->product->price,
'quantity' => $item->quantity
]);
// Уменьшаем остаток на складе
$item->product->decrement('stock', $item->quantity);
}
// Очищаем корзину
$cart->items()->delete();
DB::commit();
return redirect()->route('order.success', $order);
} catch (Exception $e) {
DB::rollBack();
Log::error('Order failed: ' . $e->getMessage());
return back()->with('error', 'Ошибка при оформлении заказа');
}
Результат: заказ создаётся только при успешном выполнении всех операций, иначе все изменения откатываются.
Фильтрация товаров с использованием AJAX (jQuery + Laravel)
// JavaScript (resources/js/filter.js)
$('.filter-checkbox').on('change', function() {
let categories = [];
$('input[name="category[]"]:checked').each(function() {
categories.push($(this).val());
});
$.ajax({
url: '/catalog/filter',
type: 'GET',
data: { categories: categories, min_price: $('#min_price').val(), max_price: $('#max_price').val() },
success: function(response) {
$('#product-list').html(response);
}
});
});
// Контроллер
public function filter(Request $request) {
$query = Product::query();
if ($request->has('categories')) {
$query->whereIn('category_id', $request->categories);
}
if ($request->filled('min_price')) {
$query->where('price', '>=', $request->min_price);
}
// ...
return view('catalog.partials.products', ['products' => $query->paginate(12)]);
}
Результат: список товаров обновляется без перезагрузки страницы.
Загрузка изображений товаров с валидацией (Laravel Storage)
// В контроллере админки
public function storeImage(Request $request, Product $product) {
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg|max:2048'
]);
$path = $request->file('image')->store('products', 'public');
$product->images()->create(['path' => $path]);
return back()->with('success', 'Изображение добавлено');
}
// Blade для вывода
<img src="{{ Storage::url($product->images->first()->path) }}" alt="{{ $product->name }}">
Результат: файл сохраняется в storage/app/public/products, возвращается URL через Symfony Filesystem.
Генерация sitemap.xml для SEO (на чистом PHP)
$dom = new DOMDocument('1.0', 'UTF-8');
$urlset = $dom->createElementNS('http://www.sitemaps.org/schemas/sitemap/0.9', 'urlset');
$dom->appendChild($urlset);
$products = $db->query('SELECT id, updated_at FROM products WHERE active=1')->fetchAll();
foreach ($products as $p) {
$url = $dom->createElement('url');
$loc = $dom->createElement('loc', 'https://example.com/product/' . $p['id']);
$lastmod = $dom->createElement('lastmod', date('Y-m-d', strtotime($p['updated_at'])));
$url->appendChild($loc);
$url->appendChild($lastmod);
$urlset->appendChild($url);
}
header('Content-Type: application/xml; charset=utf-8');
echo $dom->saveXML();
Результат: XML-файл со списком страниц товаров для поисковых систем.
Интеграция платежей через Stripe (Laravel Cashier)
// composer require laravel/cashier
// config/cashier.php - настройка ключей
// В контроллере оформления
$user = $request->user();
$payment = $user->charge(
100 * $order->total, // сумма в центах
$request->payment_method_id,
['description' => 'Заказ №'.$order->id]
);
$order->update(['payment_status' => 'paid', 'stripe_payment_id' => $payment->id]);
Результат: списание средств через Stripe, обновление статуса заказа.