575 lines
18 KiB
PHP
575 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Copyright seit 2024 Webshop System
|
|
*
|
|
* MultiShop Core-Klasse für das Webshop-System
|
|
*
|
|
* @author Webshop System
|
|
* @license GPL v3
|
|
*/
|
|
|
|
namespace App\Core;
|
|
|
|
use Doctrine\DBAL\DriverManager;
|
|
use Doctrine\DBAL\Exception;
|
|
|
|
class MultiShop
|
|
{
|
|
private $conn;
|
|
private $config;
|
|
private $currentShop;
|
|
private $shops = [];
|
|
private $defaultShop;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->conn = DriverManager::getConnection([
|
|
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
|
|
]);
|
|
$this->config = new Configuration();
|
|
$this->loadShops();
|
|
$this->detectCurrentShop();
|
|
}
|
|
|
|
/**
|
|
* Alle Shops laden
|
|
*/
|
|
private function loadShops()
|
|
{
|
|
try {
|
|
$stmt = $this->conn->prepare('
|
|
SELECT s.*, d.domain, d.ssl_enabled, d.force_ssl
|
|
FROM ws_shop s
|
|
LEFT JOIN ws_shop_domain d ON s.id = d.shop_id
|
|
WHERE s.active = 1
|
|
ORDER BY s.sort_order ASC, s.name ASC
|
|
');
|
|
$stmt->execute();
|
|
$shops = $stmt->fetchAllAssociative();
|
|
|
|
foreach ($shops as $shop) {
|
|
$this->shops[$shop['id']] = $shop;
|
|
|
|
if ($shop['is_default']) {
|
|
$this->defaultShop = $shop;
|
|
}
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
error_log('Error loading shops: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aktuellen Shop basierend auf Domain erkennen
|
|
*/
|
|
private function detectCurrentShop()
|
|
{
|
|
$host = $_SERVER['HTTP_HOST'] ?? '';
|
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
|
|
|
// Shop-ID aus URL-Parameter
|
|
if (preg_match('/\/shop\/(\d+)/', $requestUri, $matches)) {
|
|
$shopId = intval($matches[1]);
|
|
if (isset($this->shops[$shopId])) {
|
|
$this->currentShop = $this->shops[$shopId];
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Shop basierend auf Domain
|
|
foreach ($this->shops as $shop) {
|
|
if ($shop['domain'] && $host === $shop['domain']) {
|
|
$this->currentShop = $shop;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback auf Standard-Shop
|
|
$this->currentShop = $this->defaultShop ?? reset($this->shops);
|
|
}
|
|
|
|
/**
|
|
* Aktuellen Shop abrufen
|
|
*/
|
|
public function getCurrentShop()
|
|
{
|
|
return $this->currentShop;
|
|
}
|
|
|
|
/**
|
|
* Shop-ID abrufen
|
|
*/
|
|
public function getCurrentShopId()
|
|
{
|
|
return $this->currentShop['id'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Alle Shops abrufen
|
|
*/
|
|
public function getAllShops()
|
|
{
|
|
return $this->shops;
|
|
}
|
|
|
|
/**
|
|
* Shop nach ID abrufen
|
|
*/
|
|
public function getShop($id)
|
|
{
|
|
return $this->shops[$id] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Shop nach Domain abrufen
|
|
*/
|
|
public function getShopByDomain($domain)
|
|
{
|
|
foreach ($this->shops as $shop) {
|
|
if ($shop['domain'] === $domain) {
|
|
return $shop;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Shop-Konfiguration abrufen
|
|
*/
|
|
public function getShopConfig($key, $default = null)
|
|
{
|
|
if (!$this->currentShop) {
|
|
return $default;
|
|
}
|
|
|
|
try {
|
|
$stmt = $this->conn->prepare('
|
|
SELECT value FROM ws_shop_config
|
|
WHERE shop_id = ? AND config_key = ?
|
|
');
|
|
$stmt->execute([$this->currentShop['id'], $key]);
|
|
$result = $stmt->fetchAssociative();
|
|
|
|
return $result ? $result['value'] : $default;
|
|
|
|
} catch (Exception $e) {
|
|
error_log('Error getting shop config: ' . $e->getMessage());
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shop-Konfiguration setzen
|
|
*/
|
|
public function setShopConfig($key, $value)
|
|
{
|
|
if (!$this->currentShop) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$stmt = $this->conn->prepare('
|
|
INSERT INTO ws_shop_config (shop_id, config_key, value, updated_at)
|
|
VALUES (?, ?, ?, NOW())
|
|
ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = NOW()
|
|
');
|
|
$stmt->execute([$this->currentShop['id'], $key, $value]);
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
error_log('Error setting shop config: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shop erstellen
|
|
*/
|
|
public function createShop($data)
|
|
{
|
|
try {
|
|
$this->conn->beginTransaction();
|
|
|
|
// Shop erstellen
|
|
$stmt = $this->conn->prepare('
|
|
INSERT INTO ws_shop (name, description, active, is_default, sort_order, created_at)
|
|
VALUES (?, ?, ?, ?, ?, NOW())
|
|
');
|
|
$stmt->execute([
|
|
$data['name'],
|
|
$data['description'] ?? '',
|
|
$data['active'] ?? true,
|
|
$data['is_default'] ?? false,
|
|
$data['sort_order'] ?? 0
|
|
]);
|
|
|
|
$shopId = $this->conn->lastInsertId();
|
|
|
|
// Domain hinzufügen
|
|
if (!empty($data['domain'])) {
|
|
$stmt = $this->conn->prepare('
|
|
INSERT INTO ws_shop_domain (shop_id, domain, ssl_enabled, force_ssl, created_at)
|
|
VALUES (?, ?, ?, ?, NOW())
|
|
');
|
|
$stmt->execute([
|
|
$shopId,
|
|
$data['domain'],
|
|
$data['ssl_enabled'] ?? false,
|
|
$data['force_ssl'] ?? false
|
|
]);
|
|
}
|
|
|
|
// Standard-Konfiguration kopieren
|
|
$this->copyDefaultConfig($shopId);
|
|
|
|
$this->conn->commit();
|
|
|
|
// Shops neu laden
|
|
$this->loadShops();
|
|
|
|
return $shopId;
|
|
|
|
} catch (Exception $e) {
|
|
$this->conn->rollBack();
|
|
error_log('Error creating shop: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shop aktualisieren
|
|
*/
|
|
public function updateShop($id, $data)
|
|
{
|
|
try {
|
|
$this->conn->beginTransaction();
|
|
|
|
// Shop aktualisieren
|
|
$stmt = $this->conn->prepare('
|
|
UPDATE ws_shop
|
|
SET name = ?, description = ?, active = ?, sort_order = ?, updated_at = NOW()
|
|
WHERE id = ?
|
|
');
|
|
$stmt->execute([
|
|
$data['name'],
|
|
$data['description'] ?? '',
|
|
$data['active'] ?? true,
|
|
$data['sort_order'] ?? 0,
|
|
$id
|
|
]);
|
|
|
|
// Domain aktualisieren
|
|
if (isset($data['domain'])) {
|
|
$stmt = $this->conn->prepare('
|
|
INSERT INTO ws_shop_domain (shop_id, domain, ssl_enabled, force_ssl, created_at)
|
|
VALUES (?, ?, ?, ?, NOW())
|
|
ON DUPLICATE KEY UPDATE
|
|
domain = VALUES(domain),
|
|
ssl_enabled = VALUES(ssl_enabled),
|
|
force_ssl = VALUES(force_ssl),
|
|
updated_at = NOW()
|
|
');
|
|
$stmt->execute([
|
|
$id,
|
|
$data['domain'],
|
|
$data['ssl_enabled'] ?? false,
|
|
$data['force_ssl'] ?? false
|
|
]);
|
|
}
|
|
|
|
$this->conn->commit();
|
|
|
|
// Shops neu laden
|
|
$this->loadShops();
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->conn->rollBack();
|
|
error_log('Error updating shop: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shop löschen
|
|
*/
|
|
public function deleteShop($id)
|
|
{
|
|
try {
|
|
$this->conn->beginTransaction();
|
|
|
|
// Prüfen ob Shop Daten hat
|
|
$stmt = $this->conn->prepare('
|
|
SELECT COUNT(*) as count FROM ws_order WHERE shop_id = ?
|
|
');
|
|
$stmt->execute([$id]);
|
|
$orderCount = $stmt->fetchAssociative()['count'];
|
|
|
|
if ($orderCount > 0) {
|
|
throw new Exception('Shop kann nicht gelöscht werden - hat Bestellungen');
|
|
}
|
|
|
|
// Shop-Daten löschen
|
|
$tables = [
|
|
'ws_shop_config',
|
|
'ws_shop_domain',
|
|
'ws_shop_currency',
|
|
'ws_shop_language'
|
|
];
|
|
|
|
foreach ($tables as $table) {
|
|
$stmt = $this->conn->prepare("DELETE FROM $table WHERE shop_id = ?");
|
|
$stmt->execute([$id]);
|
|
}
|
|
|
|
// Shop löschen
|
|
$stmt = $this->conn->prepare('DELETE FROM ws_shop WHERE id = ?');
|
|
$stmt->execute([$id]);
|
|
|
|
$this->conn->commit();
|
|
|
|
// Shops neu laden
|
|
$this->loadShops();
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->conn->rollBack();
|
|
error_log('Error deleting shop: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Standard-Konfiguration kopieren
|
|
*/
|
|
private function copyDefaultConfig($shopId)
|
|
{
|
|
$defaultConfigs = [
|
|
'SHOP_NAME' => 'Neuer Shop',
|
|
'SHOP_DESCRIPTION' => 'Beschreibung des Shops',
|
|
'SHOP_EMAIL' => 'info@example.com',
|
|
'SHOP_PHONE' => '',
|
|
'SHOP_ADDRESS' => '',
|
|
'SHOP_CITY' => '',
|
|
'SHOP_POSTAL_CODE' => '',
|
|
'SHOP_COUNTRY' => 'DE',
|
|
'SHOP_CURRENCY' => 'EUR',
|
|
'SHOP_LANGUAGE' => 'de',
|
|
'SHOP_TIMEZONE' => 'Europe/Berlin',
|
|
'SHOP_DATE_FORMAT' => 'd.m.Y',
|
|
'SHOP_TIME_FORMAT' => 'H:i',
|
|
'SHOP_TAX_RATE' => '19.00',
|
|
'SHOP_SHIPPING_COST' => '5.90',
|
|
'SHOP_FREE_SHIPPING_THRESHOLD' => '50.00',
|
|
'SHOP_MIN_ORDER_AMOUNT' => '0.00',
|
|
'SHOP_MAX_ORDER_AMOUNT' => '0.00',
|
|
'SHOP_STOCK_WARNING' => '5',
|
|
'SHOP_REVIEWS_ENABLED' => '1',
|
|
'SHOP_NEWSLETTER_ENABLED' => '1',
|
|
'SHOP_MAINTENANCE_MODE' => '0',
|
|
'SHOP_MAINTENANCE_MESSAGE' => 'Shop ist zurzeit nicht verfügbar'
|
|
];
|
|
|
|
foreach ($defaultConfigs as $key => $value) {
|
|
$this->setShopConfig($key, $value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shop-Statistiken abrufen
|
|
*/
|
|
public function getShopStatistics($shopId = null)
|
|
{
|
|
$shopId = $shopId ?: $this->getCurrentShopId();
|
|
|
|
if (!$shopId) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
$stats = [];
|
|
|
|
// Bestellungen
|
|
$stmt = $this->conn->prepare('
|
|
SELECT COUNT(*) as total_orders,
|
|
SUM(total_amount) as total_revenue,
|
|
COUNT(CASE WHEN status = "completed" THEN 1 END) as completed_orders,
|
|
COUNT(CASE WHEN status = "pending" THEN 1 END) as pending_orders
|
|
FROM ws_order
|
|
WHERE shop_id = ?
|
|
');
|
|
$stmt->execute([$shopId]);
|
|
$orderStats = $stmt->fetchAssociative();
|
|
$stats['orders'] = $orderStats;
|
|
|
|
// Produkte
|
|
$stmt = $this->conn->prepare('
|
|
SELECT COUNT(*) as total_products,
|
|
COUNT(CASE WHEN active = 1 THEN 1 END) as active_products,
|
|
COUNT(CASE WHEN stock <= 0 THEN 1 END) as out_of_stock
|
|
FROM ws_product
|
|
WHERE shop_id = ?
|
|
');
|
|
$stmt->execute([$shopId]);
|
|
$productStats = $stmt->fetchAssociative();
|
|
$stats['products'] = $productStats;
|
|
|
|
// Kunden
|
|
$stmt = $this->conn->prepare('
|
|
SELECT COUNT(*) as total_customers,
|
|
COUNT(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as new_customers_30d
|
|
FROM ws_customer
|
|
WHERE shop_id = ?
|
|
');
|
|
$stmt->execute([$shopId]);
|
|
$customerStats = $stmt->fetchAssociative();
|
|
$stats['customers'] = $customerStats;
|
|
|
|
// Bewertungen
|
|
$stmt = $this->conn->prepare('
|
|
SELECT COUNT(*) as total_reviews,
|
|
AVG(rating) as avg_rating
|
|
FROM ws_review r
|
|
JOIN ws_product p ON r.product_id = p.id
|
|
WHERE p.shop_id = ?
|
|
');
|
|
$stmt->execute([$shopId]);
|
|
$reviewStats = $stmt->fetchAssociative();
|
|
$stats['reviews'] = $reviewStats;
|
|
|
|
return $stats;
|
|
|
|
} catch (Exception $e) {
|
|
error_log('Error getting shop statistics: ' . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shop-Domain validieren
|
|
*/
|
|
public function validateShopDomain($domain, $excludeShopId = null)
|
|
{
|
|
if (empty($domain)) {
|
|
return true; // Leere Domain ist erlaubt
|
|
}
|
|
|
|
// Domain-Format prüfen
|
|
if (!filter_var('http://' . $domain, FILTER_VALIDATE_URL)) {
|
|
return false;
|
|
}
|
|
|
|
// Prüfen ob Domain bereits verwendet wird
|
|
try {
|
|
$stmt = $this->conn->prepare('
|
|
SELECT shop_id FROM ws_shop_domain
|
|
WHERE domain = ? AND shop_id != ?
|
|
');
|
|
$stmt->execute([$domain, $excludeShopId]);
|
|
|
|
return $stmt->rowCount() === 0;
|
|
|
|
} catch (Exception $e) {
|
|
error_log('Error validating shop domain: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shop-URL generieren
|
|
*/
|
|
public function getShopUrl($shopId = null, $path = '')
|
|
{
|
|
$shop = $shopId ? $this->getShop($shopId) : $this->currentShop;
|
|
|
|
if (!$shop) {
|
|
return '/';
|
|
}
|
|
|
|
$baseUrl = $shop['domain'] ? 'https://' . $shop['domain'] : '';
|
|
$shopPath = $shop['is_default'] ? '' : '/shop/' . $shop['id'];
|
|
|
|
return $baseUrl . $shopPath . $path;
|
|
}
|
|
|
|
/**
|
|
* Shop-Wechsel
|
|
*/
|
|
public function switchShop($shopId)
|
|
{
|
|
if (isset($this->shops[$shopId])) {
|
|
$this->currentShop = $this->shops[$shopId];
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Shop-Kontext für Datenbank-Queries
|
|
*/
|
|
public function getShopContext()
|
|
{
|
|
return [
|
|
'shop_id' => $this->getCurrentShopId(),
|
|
'shop_name' => $this->currentShop['name'] ?? '',
|
|
'shop_domain' => $this->currentShop['domain'] ?? '',
|
|
'is_default' => $this->currentShop['is_default'] ?? false
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Shop-spezifische Datenbank-Query
|
|
*/
|
|
public function addShopFilter($query, $shopId = null)
|
|
{
|
|
$shopId = $shopId ?: $this->getCurrentShopId();
|
|
|
|
if (!$shopId) {
|
|
return $query;
|
|
}
|
|
|
|
// Prüfen ob Query bereits WHERE hat
|
|
if (stripos($query, 'WHERE') !== false) {
|
|
return str_replace('WHERE', "WHERE shop_id = $shopId AND", $query);
|
|
} else {
|
|
return $query . " WHERE shop_id = $shopId";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shop-Konfiguration für Template
|
|
*/
|
|
public function getShopTemplateConfig()
|
|
{
|
|
return [
|
|
'shop_name' => $this->getShopConfig('SHOP_NAME', 'Webshop'),
|
|
'shop_description' => $this->getShopConfig('SHOP_DESCRIPTION', ''),
|
|
'shop_email' => $this->getShopConfig('SHOP_EMAIL', ''),
|
|
'shop_phone' => $this->getShopConfig('SHOP_PHONE', ''),
|
|
'shop_address' => $this->getShopConfig('SHOP_ADDRESS', ''),
|
|
'shop_city' => $this->getShopConfig('SHOP_CITY', ''),
|
|
'shop_postal_code' => $this->getShopConfig('SHOP_POSTAL_CODE', ''),
|
|
'shop_country' => $this->getShopConfig('SHOP_COUNTRY', 'DE'),
|
|
'shop_currency' => $this->getShopConfig('SHOP_CURRENCY', 'EUR'),
|
|
'shop_language' => $this->getShopConfig('SHOP_LANGUAGE', 'de'),
|
|
'shop_timezone' => $this->getShopConfig('SHOP_TIMEZONE', 'Europe/Berlin'),
|
|
'shop_date_format' => $this->getShopConfig('SHOP_DATE_FORMAT', 'd.m.Y'),
|
|
'shop_time_format' => $this->getShopConfig('SHOP_TIME_FORMAT', 'H:i'),
|
|
'shop_tax_rate' => floatval($this->getShopConfig('SHOP_TAX_RATE', '19.00')),
|
|
'shop_shipping_cost' => floatval($this->getShopConfig('SHOP_SHIPPING_COST', '5.90')),
|
|
'shop_free_shipping_threshold' => floatval($this->getShopConfig('SHOP_FREE_SHIPPING_THRESHOLD', '50.00')),
|
|
'shop_min_order_amount' => floatval($this->getShopConfig('SHOP_MIN_ORDER_AMOUNT', '0.00')),
|
|
'shop_max_order_amount' => floatval($this->getShopConfig('SHOP_MAX_ORDER_AMOUNT', '0.00')),
|
|
'shop_stock_warning' => intval($this->getShopConfig('SHOP_STOCK_WARNING', '5')),
|
|
'shop_reviews_enabled' => boolval($this->getShopConfig('SHOP_REVIEWS_ENABLED', '1')),
|
|
'shop_newsletter_enabled' => boolval($this->getShopConfig('SHOP_NEWSLETTER_ENABLED', '1')),
|
|
'shop_maintenance_mode' => boolval($this->getShopConfig('SHOP_MAINTENANCE_MODE', '0')),
|
|
'shop_maintenance_message' => $this->getShopConfig('SHOP_MAINTENANCE_MESSAGE', 'Shop ist zurzeit nicht verfügbar')
|
|
];
|
|
}
|
|
}
|