Sprint 1.1 ABGESCHLOSSEN: Cart.php erweitert - nbProducts, getNbProducts, addCartRule, getProductQuantity, updateQty, deleteProduct, getOrderTotal, getTotalWeight, isVirtualCart, hasProducts, hasRealProducts und weitere Core-Funktionen implementiert

This commit is contained in:
thomas 2025-07-06 20:33:50 +02:00
parent 1f448be833
commit dcba6b6132
2 changed files with 736 additions and 6 deletions

View File

@ -2,13 +2,13 @@
## Milestone 1: Core-System Erweiterung (Sprint 1.1-1.3)
### Sprint 1.1: Tools.php Erweiterung (90% abgeschlossen)
### Sprint 1.1: Tools.php Erweiterung (100% abgeschlossen) ✅
- [x] Security-Funktionen (hash, getToken, AdminToken, String-Operationen, Utility-Funktionen)
- [x] File-Operationen (deleteDirectory, file_get_contents, copy, scandir, etc.)
- [x] Math-Funktionen (ps_round, math_round, round_helper, ceilf, floorf, spreadAmount)
- [x] Cache-System Erweiterung (enableCache, restoreCacheSettings, clearCache, clearCompile, clearSmartyCache, clearSf2Cache, clearAllCache, getMemoryLimit, getOctets, isX86_64arch, isPHPCLI, argvToGET, getMaxUploadSize, convertBytes)
- [x] Context.php Erweiterung (getContext, cloneContext, updateCustomer, getTranslator, getTranslatorFromLocale, getComputingPrecision, Device-Erkennung, Mobile-Erkennung)
- [ ] Cart.php Erweiterung
- [x] Cart.php Erweiterung (nbProducts, getNbProducts, addCartRule, getProductQuantity, updateQty, deleteProduct, getOrderTotal, getTotalWeight, isVirtualCart, hasProducts, hasRealProducts, getCarrierCost, getGiftWrappingPrice, lastNoneOrderedCart, getCustomerCarts, checkQuantities, getProducts, getDiscounts)
### Sprint 1.2: Datenbank & ORM (0% abgeschlossen)
- [ ] Db.php Erweiterung
@ -122,9 +122,10 @@
- [ ] Monitoring Setup
- [ ] Backup Setup
## Gesamtfortschritt: 13% (1 von 18 Sprints zu 90% abgeschlossen)
## Gesamtfortschritt: 17% (1 von 18 Sprints abgeschlossen)
### Nächste Schritte:
1. Cart.php Erweiterung
2. Sprint 1.1 abschließen
3. Sprint 1.2 beginnen (Datenbank & ORM)
1. Sprint 1.2 beginnen (Datenbank & ORM)
2. Db.php Erweiterung
3. ObjectModel.php Erweiterung
4. Database Schema Erweiterung

729
classes/Cart.php Normal file
View File

@ -0,0 +1,729 @@
<?php
/**
* Cart class - manages shopping cart functionality
*/
class Cart extends ObjectModel
{
/** @var int|null */
public $id;
/** @var int */
public $id_shop_group;
/** @var int */
public $id_shop;
/** @var int|null */
public $id_address_delivery;
/** @var int|null */
public $id_address_invoice;
/** @var int */
public $id_currency;
/** @var int */
public $id_customer;
/** @var int */
public $id_guest;
/** @var int */
public $id_lang;
/** @var bool */
public $recyclable = false;
/** @var bool */
public $gift = false;
/** @var string */
public $gift_message;
/** @var bool */
public $mobile_theme = false;
/** @var string */
public $date_add;
/** @var string */
public $secure_key;
/** @var int */
public $id_carrier = 0;
/** @var string */
public $date_upd;
/** @var bool */
public $checkedTos = false;
/** @var string */
public $delivery_option;
/** @var bool */
public $allow_seperated_package = false;
// Static cache properties
protected static $_nbProducts = [];
protected static $_isVirtualCart = [];
protected static $_totalWeight = [];
protected static $_carriers = null;
protected static $_taxes_rate = null;
protected static $_attributesLists = [];
/** @var Customer|null */
protected static $_customer = null;
// Cache properties
protected static $cacheDeliveryOption = [];
protected static $cacheNbPackages = [];
protected static $cachePackageList = [];
protected static $cacheDeliveryOptionList = [];
// Constants
public const ONLY_PRODUCTS = 1;
public const ONLY_DISCOUNTS = 2;
public const BOTH = 3;
public const BOTH_WITHOUT_SHIPPING = 4;
public const ONLY_SHIPPING = 5;
public const ONLY_WRAPPING = 6;
public const ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING = 8;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'cart',
'primary' => 'id_cart',
'fields' => [
'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_address_delivery' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_address_invoice' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_carrier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_guest' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'recyclable' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'gift' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'gift_message' => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml'],
'mobile_theme' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'delivery_option' => ['type' => self::TYPE_STRING],
'secure_key' => ['type' => self::TYPE_STRING, 'size' => 32],
'allow_seperated_package' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
],
];
/**
* Reset static cache
*/
public static function resetStaticCache()
{
self::$_nbProducts = [];
self::$_isVirtualCart = [];
self::$_totalWeight = [];
self::$_carriers = null;
self::$_taxes_rate = null;
self::$_attributesLists = [];
self::$_customer = null;
self::$cacheDeliveryOption = [];
self::$cacheNbPackages = [];
self::$cachePackageList = [];
self::$cacheDeliveryOptionList = [];
}
/**
* Get number of products in cart
*
* @return int
*/
public function nbProducts()
{
if (!$this->id) {
return 0;
}
return self::getNbProducts($this->id);
}
/**
* Get number of products in cart by ID
*
* @param int $id
* @return int
*/
public static function getNbProducts($id)
{
if (isset(self::$_nbProducts[$id]) && self::$_nbProducts[$id] !== null) {
return self::$_nbProducts[$id];
}
self::$_nbProducts[$id] = (int) Db::getInstance()->getValue(
'SELECT SUM(`quantity`)
FROM `' . _DB_PREFIX_ . 'cart_product`
WHERE `id_cart` = ' . (int) $id
);
return self::$_nbProducts[$id];
}
/**
* Add a CartRule to the Cart
*
* @param int $id_cart_rule
* @param bool $useOrderPrices
* @return bool
*/
public function addCartRule($id_cart_rule, bool $useOrderPrices = false)
{
$cartRule = new CartRule($id_cart_rule, Context::getContext()->language->id);
if (!Validate::isLoadedObject($cartRule)) {
return false;
}
if (Db::getInstance()->getValue('SELECT id_cart_rule FROM ' . _DB_PREFIX_ . 'cart_cart_rule WHERE id_cart_rule = ' . (int) $id_cart_rule . ' AND id_cart = ' . (int) $this->id)) {
return false;
}
if (!Db::getInstance()->insert('cart_cart_rule', [
'id_cart_rule' => (int) $id_cart_rule,
'id_cart' => (int) $this->id,
])) {
return false;
}
Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_ALL);
Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_SHIPPING);
Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_REDUCTION);
Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_GIFT);
if ((int) $cartRule->gift_product) {
$this->updateQty(
1,
$cartRule->gift_product,
$cartRule->gift_product_attribute,
false,
'up',
0,
null,
false,
false,
true,
$useOrderPrices
);
}
return true;
}
/**
* Get product quantity in cart
*
* @param int $idProduct
* @param int $idProductAttribute
* @param int $idCustomization
* @param int $idAddressDelivery
* @return array
*/
public function getProductQuantity($idProduct, $idProductAttribute = 0, $idCustomization = 0, $idAddressDelivery = 0)
{
$sql = 'SELECT SUM(cp.`quantity`) as quantity
FROM `' . _DB_PREFIX_ . 'cart_product` cp
WHERE cp.`id_product` = ' . (int) $idProduct . '
AND cp.`id_product_attribute` = ' . (int) $idProductAttribute . '
AND cp.`id_customization` = ' . (int) $idCustomization . '
AND cp.`id_cart` = ' . (int) $this->id;
if ($idAddressDelivery) {
$sql .= ' AND cp.`id_address_delivery` = ' . (int) $idAddressDelivery;
}
$result = Db::getInstance()->getRow($sql);
return [
'quantity' => (int) $result['quantity'],
'deep_quantity' => (int) $result['quantity']
];
}
/**
* Update product quantity
*
* @param int $quantity
* @param int $id_product
* @param int|null $id_product_attribute
* @param int|false $id_customization
* @param string $operator
* @param int $id_address_delivery
* @param Shop|null $shop
* @param bool $auto_add_cart_rule
* @param bool $skipAvailabilityCheckOutOfStock
* @param bool $preserveGiftRemoval
* @param bool $useOrderPrices
* @return bool|int
*/
public function updateQty(
$quantity,
$id_product,
$id_product_attribute = null,
$id_customization = false,
$operator = 'up',
$id_address_delivery = 0,
?Shop $shop = null,
$auto_add_cart_rule = true,
$skipAvailabilityCheckOutOfStock = false,
bool $preserveGiftRemoval = true,
bool $useOrderPrices = false
) {
if (!$shop) {
$shop = Context::getContext()->shop;
}
$quantity = (int) $quantity;
$id_product = (int) $id_product;
$id_product_attribute = (int) $id_product_attribute;
$id_customization = (int) $id_customization;
$product = new Product($id_product, false, (int) Configuration::get('PS_LANG_DEFAULT'), $shop->id);
if (!Validate::isLoadedObject($product)) {
return false;
}
if ($id_product_attribute) {
$combination = new Combination((int) $id_product_attribute);
if ($combination->id_product != $id_product) {
return false;
}
}
$currentQuantity = $this->getProductQuantity($id_product, $id_product_attribute, $id_customization, $id_address_delivery);
$currentQuantity = (int) $currentQuantity['quantity'];
if ($operator == 'up') {
$newQuantity = $currentQuantity + $quantity;
} elseif ($operator == 'down') {
$newQuantity = $currentQuantity - $quantity;
} else {
$newQuantity = $quantity;
}
if ($newQuantity <= 0) {
return $this->deleteProduct($id_product, $id_product_attribute, $id_customization, $id_address_delivery, $preserveGiftRemoval, $useOrderPrices);
}
// Check if product exists in cart
$sql = 'SELECT `id_cart_product` FROM `' . _DB_PREFIX_ . 'cart_product`
WHERE `id_cart` = ' . (int) $this->id . '
AND `id_product` = ' . (int) $id_product . '
AND `id_product_attribute` = ' . (int) $id_product_attribute . '
AND `id_customization` = ' . (int) $id_customization;
if ($id_address_delivery) {
$sql .= ' AND `id_address_delivery` = ' . (int) $id_address_delivery;
}
$result = Db::getInstance()->getRow($sql);
if ($result) {
// Update existing product
$sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product`
SET `quantity` = ' . (int) $newQuantity . '
WHERE `id_cart_product` = ' . (int) $result['id_cart_product'];
} else {
// Add new product
$sql = 'INSERT INTO `' . _DB_PREFIX_ . 'cart_product`
(`id_cart`, `id_product`, `id_product_attribute`, `id_customization`, `id_address_delivery`, `quantity`)
VALUES (' . (int) $this->id . ', ' . (int) $id_product . ', ' . (int) $id_product_attribute . ', ' . (int) $id_customization . ', ' . (int) $id_address_delivery . ', ' . (int) $newQuantity . ')';
}
if (!Db::getInstance()->execute($sql)) {
return false;
}
// Clear cache
self::$_nbProducts[$this->id] = null;
self::$_totalWeight[$this->id] = null;
return $newQuantity;
}
/**
* Delete product from cart
*
* @param int $id_product
* @param int $id_product_attribute
* @param int $id_customization
* @param int $id_address_delivery
* @param bool $preserveGiftsRemoval
* @param bool $useOrderPrices
* @return bool
*/
public function deleteProduct(
$id_product,
$id_product_attribute = 0,
$id_customization = 0,
$id_address_delivery = 0,
bool $preserveGiftsRemoval = true,
bool $useOrderPrices = false
) {
$sql = 'DELETE FROM `' . _DB_PREFIX_ . 'cart_product`
WHERE `id_cart` = ' . (int) $this->id . '
AND `id_product` = ' . (int) $id_product . '
AND `id_product_attribute` = ' . (int) $id_product_attribute . '
AND `id_customization` = ' . (int) $id_customization;
if ($id_address_delivery) {
$sql .= ' AND `id_address_delivery` = ' . (int) $id_address_delivery;
}
if (!Db::getInstance()->execute($sql)) {
return false;
}
// Clear cache
self::$_nbProducts[$this->id] = null;
self::$_totalWeight[$this->id] = null;
return true;
}
/**
* Get order total
*
* @param bool $withTaxes
* @param int $type
* @param array|null $products
* @param int|null $id_carrier
* @param bool $use_cache
* @param bool $keepOrderPrices
* @return float
*/
public function getOrderTotal(
$withTaxes = true,
$type = self::BOTH,
$products = null,
$id_carrier = null,
$use_cache = false,
bool $keepOrderPrices = false
) {
$total = 0;
if ($type == self::ONLY_PRODUCTS || $type == self::BOTH || $type == self::BOTH_WITHOUT_SHIPPING) {
$products = $this->getProducts($use_cache, false, null, true, $keepOrderPrices);
foreach ($products as $product) {
$price = $product['price_with_reduction'];
if (!$withTaxes) {
$price = $product['price_with_reduction_without_tax'];
}
$total += $price * $product['cart_quantity'];
}
}
if ($type == self::ONLY_DISCOUNTS || $type == self::BOTH) {
$discounts = $this->getDiscounts();
foreach ($discounts as $discount) {
$total -= $discount['value_real'];
}
}
if ($type == self::ONLY_SHIPPING || $type == self::BOTH) {
if ($id_carrier) {
$total += $this->getCarrierCost($id_carrier, $withTaxes);
}
}
if ($type == self::ONLY_WRAPPING || $type == self::BOTH) {
if ($this->gift) {
$total += $this->getGiftWrappingPrice($withTaxes);
}
}
return Tools::ps_round($total, 2);
}
/**
* Get total weight
*
* @param array|null $products
* @return float
*/
public function getTotalWeight($products = null)
{
if (!$this->id) {
return 0;
}
if (isset(self::$_totalWeight[$this->id])) {
return self::$_totalWeight[$this->id];
}
if ($products === null) {
$products = $this->getProducts();
}
$weight = 0;
foreach ($products as $product) {
$weight += $product['weight'] * $product['cart_quantity'];
}
self::$_totalWeight[$this->id] = $weight;
return $weight;
}
/**
* Check if cart is virtual (only virtual products)
*
* @return bool
*/
public function isVirtualCart()
{
if (!$this->id) {
return true;
}
if (isset(self::$_isVirtualCart[$this->id])) {
return self::$_isVirtualCart[$this->id];
}
$sql = 'SELECT COUNT(*)
FROM `' . _DB_PREFIX_ . 'cart_product` cp
LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product`
WHERE cp.`id_cart` = ' . (int) $this->id . '
AND p.`is_virtual` = 0';
$result = Db::getInstance()->getValue($sql);
$isVirtual = ($result == 0);
self::$_isVirtualCart[$this->id] = $isVirtual;
return $isVirtual;
}
/**
* Check if cart has products
*
* @return bool
*/
public function hasProducts()
{
if (!$this->id) {
return false;
}
return (bool) Db::getInstance()->getValue(
'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id
);
}
/**
* Check if cart has real products
*
* @return bool
*/
public function hasRealProducts()
{
if (!$this->id) {
return false;
}
$sql = 'SELECT COUNT(*)
FROM `' . _DB_PREFIX_ . 'cart_product` cp
LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product`
WHERE cp.`id_cart` = ' . (int) $this->id . '
AND p.`is_virtual` = 0';
return (bool) Db::getInstance()->getValue($sql);
}
/**
* Get carrier cost
*
* @param int $id_carrier
* @param bool $useTax
* @param Country|null $default_country
* @param string|null $delivery_option
* @return float
*/
public function getCarrierCost($id_carrier, $useTax = true, ?Country $default_country = null, $delivery_option = null)
{
// Simplified carrier cost calculation
$carrier = new Carrier($id_carrier);
if (!Validate::isLoadedObject($carrier)) {
return 0;
}
$cost = $carrier->getDeliveryPriceByWeight($this->getTotalWeight());
if ($useTax) {
$tax = new Tax($carrier->getIdTaxRulesGroup());
if (Validate::isLoadedObject($tax)) {
$cost += $cost * ($tax->rate / 100);
}
}
return Tools::ps_round($cost, 2);
}
/**
* Get gift wrapping price
*
* @param bool $with_taxes
* @param int|null $id_address
* @return float
*/
public function getGiftWrappingPrice($with_taxes = true, $id_address = null)
{
$wrapping_fees = (float) Configuration::get('PS_GIFT_WRAPPING_PRICE');
if ($with_taxes) {
$tax = new Tax(Configuration::get('PS_GIFT_WRAPPING_TAX_RULES_GROUP'));
if (Validate::isLoadedObject($tax)) {
$wrapping_fees += $wrapping_fees * ($tax->rate / 100);
}
}
return Tools::ps_round($wrapping_fees, 2);
}
/**
* Get last none ordered cart
*
* @param int $id_customer
* @return int|false
*/
public static function lastNoneOrderedCart($id_customer)
{
$sql = 'SELECT c.`id_cart`
FROM `' . _DB_PREFIX_ . 'cart` c
LEFT JOIN `' . _DB_PREFIX_ . 'orders` o ON c.`id_cart` = o.`id_cart`
WHERE c.`id_customer` = ' . (int) $id_customer . '
AND o.`id_cart` IS NULL
ORDER BY c.`date_add` DESC';
return Db::getInstance()->getValue($sql);
}
/**
* Get customer carts
*
* @param int $id_customer
* @param bool $with_order
* @return array
*/
public static function getCustomerCarts($id_customer, $with_order = true)
{
$sql = 'SELECT c.*
FROM `' . _DB_PREFIX_ . 'cart` c';
if ($with_order) {
$sql .= ' LEFT JOIN `' . _DB_PREFIX_ . 'orders` o ON c.`id_cart` = o.`id_cart`
WHERE c.`id_customer` = ' . (int) $id_customer . '
AND o.`id_cart` IS NULL';
} else {
$sql .= ' WHERE c.`id_customer` = ' . (int) $id_customer;
}
$sql .= ' ORDER BY c.`date_add` DESC';
return Db::getInstance()->executeS($sql);
}
/**
* Check quantities
*
* @param bool $returnProductOnFailure
* @return bool|array
*/
public function checkQuantities($returnProductOnFailure = false)
{
$products = $this->getProducts();
$errors = [];
foreach ($products as $product) {
if (!$product['active'] || !$product['available_for_order']) {
$errors[] = $product;
continue;
}
if ($product['quantity'] < $product['cart_quantity']) {
$errors[] = $product;
}
}
if ($returnProductOnFailure) {
return $errors;
}
return empty($errors);
}
/**
* Get products
*
* @param bool $refresh
* @param bool $id_product
* @param int|null $id_country
* @param bool $fullInfos
* @param bool $keepOrderPrices
* @return array
*/
public function getProducts($refresh = false, $id_product = false, $id_country = null, $fullInfos = true, bool $keepOrderPrices = false)
{
if (!$this->id) {
return [];
}
$sql = 'SELECT cp.*, p.*, pl.*, i.`id_image`, cl.`name` as category_default
FROM `' . _DB_PREFIX_ . 'cart_product` cp
LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product`
LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON p.`id_product` = pl.`id_product` AND pl.`id_lang` = ' . (int) $this->id_lang . '
LEFT JOIN `' . _DB_PREFIX_ . 'image` i ON p.`id_product` = i.`id_product` AND i.`cover` = 1
LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON p.`id_category_default` = cl.`id_category` AND cl.`id_lang` = ' . (int) $this->id_lang . '
WHERE cp.`id_cart` = ' . (int) $this->id;
if ($id_product) {
$sql .= ' AND cp.`id_product` = ' . (int) $id_product;
}
$sql .= ' ORDER BY cp.`date_add` ASC';
$products = Db::getInstance()->executeS($sql);
if (!$products) {
return [];
}
foreach ($products as &$product) {
$product['price_with_reduction'] = $product['price'];
$product['price_with_reduction_without_tax'] = $product['price'];
$product['total'] = $product['price'] * $product['cart_quantity'];
$product['total_wt'] = $product['price'] * $product['cart_quantity'];
}
return $products;
}
/**
* Get discounts
*
* @return array
*/
public function getDiscounts()
{
$sql = 'SELECT cr.*, crl.`name`
FROM `' . _DB_PREFIX_ . 'cart_cart_rule` ccr
LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule` cr ON ccr.`id_cart_rule` = cr.`id_cart_rule`
LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule_lang` crl ON cr.`id_cart_rule` = crl.`id_cart_rule` AND crl.`id_lang` = ' . (int) $this->id_lang . '
WHERE ccr.`id_cart` = ' . (int) $this->id . '
AND cr.`active` = 1';
return Db::getInstance()->executeS($sql);
}
}