v1.0.0.0
StocketBase
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/database.php';
- require_once __DIR__ . '/site-front.php';
- require_once __DIR__ . '/currency.php';
-
- final class Cart
- {
- private const SESSION_KEY = 'cart';
-
- public static function boot(): void
- {
- if (session_status() !== PHP_SESSION_ACTIVE) {
- session_set_cookie_params([
- 'lifetime' => 0,
- 'path' => '/',
- 'secure' => Config::get('SESSION_SECURE', '1') === '1',
- 'httponly' => true,
- 'samesite' => 'Lax',
- ]);
- session_start();
- }
-
- if (!isset($_SESSION[self::SESSION_KEY]) || !is_array($_SESSION[self::SESSION_KEY])) {
- $_SESSION[self::SESSION_KEY] = [];
- }
- }
-
- private static function lineKey(int $productId, ?int $variantId): string
- {
- return $productId . ':' . ($variantId ?? 0);
- }
-
- public static function add(int $productId, ?int $variantId, int $quantity): void
- {
- self::boot();
-
- $quantity = max(1, $quantity);
- $key = self::lineKey($productId, $variantId);
- $existing = $_SESSION[self::SESSION_KEY][$key] ?? null;
- $newQuantity = $quantity + (is_array($existing) ? (int) $existing['quantity'] : 0);
-
- $_SESSION[self::SESSION_KEY][$key] = [
- 'product_id' => $productId,
- 'variant_id' => $variantId,
- 'quantity' => min(999, $newQuantity),
- ];
- }
-
- public static function updateQuantity(int $productId, ?int $variantId, int $quantity): void
- {
- self::boot();
-
- $key = self::lineKey($productId, $variantId);
-
- if ($quantity <= 0) {
- unset($_SESSION[self::SESSION_KEY][$key]);
- return;
- }
-
- if (!isset($_SESSION[self::SESSION_KEY][$key])) {
- return;
- }
-
- $_SESSION[self::SESSION_KEY][$key]['quantity'] = min(999, $quantity);
- }
-
- public static function remove(int $productId, ?int $variantId): void
- {
- self::boot();
- unset($_SESSION[self::SESSION_KEY][self::lineKey($productId, $variantId)]);
- }
-
- public static function clear(): void
- {
- self::boot();
- $_SESSION[self::SESSION_KEY] = [];
- unset($_SESSION['cart_discount_code']);
- }
-
- public static function count(): int
- {
- self::boot();
-
- $count = 0;
- foreach ($_SESSION[self::SESSION_KEY] as $row) {
- $count += (int) ($row['quantity'] ?? 0);
- }
-
- return $count;
- }
-
- public static function isEmpty(): bool
- {
- self::boot();
- return empty($_SESSION[self::SESSION_KEY]);
- }
-
- public static function items(): array
- {
- self::boot();
-
- $entries = $_SESSION[self::SESSION_KEY];
- if (empty($entries)) {
- return [];
- }
-
- $db = Database::site();
- $storeCurrency = (string) (SiteFront::settings()['store_currency'] ?? Currency::DEFAULT_CODE);
- $items = [];
-
- foreach ($entries as $key => $entry) {
- $productId = (int) ($entry['product_id'] ?? 0);
- $variantId = isset($entry['variant_id']) && $entry['variant_id'] !== null ? (int) $entry['variant_id'] : null;
- $quantity = max(1, (int) ($entry['quantity'] ?? 1));
-
- $statement = $db->prepare(
- "SELECT id, title, slug, cover_image_path, price_cents, currency, track_inventory, stock_quantity, status, product_kind
- FROM products WHERE id = :id LIMIT 1"
- );
- $statement->execute(['id' => $productId]);
- $product = $statement->fetch();
-
- if (!$product || $product['status'] !== 'published') {
- unset($_SESSION[self::SESSION_KEY][$key]);
- continue;
- }
-
- $variant = null;
- if ($variantId !== null) {
- $variantStatement = $db->prepare(
- 'SELECT id, name, sku, price_cents, stock_quantity FROM product_variants WHERE id = :id AND product_id = :product_id LIMIT 1'
- );
- $variantStatement->execute(['id' => $variantId, 'product_id' => $productId]);
- $variant = $variantStatement->fetch() ?: null;
- if (!$variant) {
- $variantId = null;
- }
- }
-
- $unitPriceCents = ($variant && $variant['price_cents'] !== null)
- ? (int) $variant['price_cents']
- : (int) ($product['price_cents'] ?? 0);
- $currency = (string) ($product['currency'] ?: $storeCurrency);
- $stockQuantity = ($variant && $variant['stock_quantity'] !== null) ? $variant['stock_quantity'] : $product['stock_quantity'];
-
- if ($unitPriceCents <= 0) {
- unset($_SESSION[self::SESSION_KEY][$key]);
- continue;
- }
-
- if ((int) ($product['track_inventory'] ?? 0) === 1 && $stockQuantity !== null) {
- if ((int) $stockQuantity <= 0) {
- unset($_SESSION[self::SESSION_KEY][$key]);
- continue;
- }
- if ($quantity > (int) $stockQuantity) {
- $quantity = (int) $stockQuantity;
- $_SESSION[self::SESSION_KEY][$key]['quantity'] = $quantity;
- }
- }
-
- $items[] = [
- 'key' => $key,
- 'product_id' => $productId,
- 'variant_id' => $variantId,
- 'title' => $product['title'],
- 'slug' => $product['slug'],
- 'variant_name' => $variant['name'] ?? null,
- 'cover_image_path' => $product['cover_image_path'],
- 'unit_price_cents' => $unitPriceCents,
- 'currency' => $currency,
- 'quantity' => $quantity,
- 'total_cents' => $unitPriceCents * $quantity,
- 'product_kind' => (string) ($product['product_kind'] ?? 'physical'),
- 'track_inventory' => (bool) ($product['track_inventory'] ?? 0),
- 'stock_quantity' => $stockQuantity !== null ? (int) $stockQuantity : null,
- ];
- }
-
- return $items;
- }
-
- public static function requiresShipping(?array $items = null): bool
- {
- foreach ($items ?? self::items() as $item) {
- if (($item['product_kind'] ?? 'physical') === 'physical') {
- return true;
- }
- }
-
- return false;
- }
-
- public static function subtotalCents(): int
- {
- $total = 0;
- foreach (self::items() as $item) {
- $total += $item['total_cents'];
- }
-
- return $total;
- }
-
- public static function currency(): string
- {
- foreach (self::items() as $item) {
- return $item['currency'];
- }
-
- return (string) (SiteFront::settings()['store_currency'] ?? Currency::DEFAULT_CODE);
- }
-
- public static function findDiscount(string $code, ?string $currency = null): ?array
- {
- $code = trim($code);
- if ($code === '') {
- return null;
- }
-
- $statement = Database::site()->prepare('SELECT * FROM discounts WHERE UPPER(code) = UPPER(:code) LIMIT 1');
- $statement->execute(['code' => $code]);
- $discount = $statement->fetch();
-
- if (!$discount || (int) $discount['is_active'] !== 1) {
- return null;
- }
-
- $now = date('Y-m-d H:i:s');
- if (!empty($discount['starts_at']) && (string) $discount['starts_at'] > $now) {
- return null;
- }
- if (!empty($discount['ends_at']) && (string) $discount['ends_at'] < $now) {
- return null;
- }
- if ($discount['max_redemptions'] !== null && (int) $discount['times_redeemed'] >= (int) $discount['max_redemptions']) {
- return null;
- }
- if ($currency !== null && !empty($discount['currency']) && strtoupper((string) $discount['currency']) !== strtoupper($currency)) {
- return null;
- }
-
- return $discount;
- }
-
- public static function discountAmountCents(array $discount, int $subtotalCents): int
- {
- if ($discount['min_order_cents'] !== null && $subtotalCents < (int) $discount['min_order_cents']) {
- return 0;
- }
-
- if ($discount['discount_type'] === 'percentage') {
- $amount = (int) round($subtotalCents * (((int) $discount['value']) / 100));
- } else {
- $amount = (int) $discount['value'];
- }
-
- return max(0, min($amount, $subtotalCents));
- }
-
- public static function appliedDiscountCode(): ?string
- {
- self::boot();
- $code = $_SESSION['cart_discount_code'] ?? null;
-
- return is_string($code) && $code !== '' ? $code : null;
- }
-
- public static function setDiscountCode(?string $code): void
- {
- self::boot();
- if ($code === null || $code === '') {
- unset($_SESSION['cart_discount_code']);
- return;
- }
-
- $_SESSION['cart_discount_code'] = $code;
- }
- }
-