Интернет магазины на PHP: практические примеры и сравнение подходов

Раздел: Веб-разработка на PHP -> Интернет-магазин на 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, обновление статуса заказа.

Интернет-магазины на PHP - comments

En
Shops php (php)