Phase 2 Start: PrestaShop-Modul-Kompatibilität implementiert - Hook-System mit Registry und Execution - Module-Base-Class mit Installation/Deinstallation - Module-Controller für Admin-Verwaltung - Datenbank-Schema für Module-System - Vollständiger Plan für Phase 2 erstellt

This commit is contained in:
thomas 2025-07-06 14:38:17 +02:00
parent 75fdb0d594
commit 93ace0c2ae
5 changed files with 1607 additions and 0 deletions

281
PHASE2_PLAN.md Normal file
View File

@ -0,0 +1,281 @@
# Phase 2 Entwicklungsplan: Nachbau aller fehlenden PrestaShop-Features + Modul-Kompatibilität
## Ziel
Alle wesentlichen Funktionen von PrestaShop werden nachgebaut UND das System wird PrestaShop-Module und Addons ohne Änderungen nutzen können. Dazu wird das komplette PrestaShop-Modul-System implementiert.
---
## Übersicht & Zeitrahmen
- **Gesamtdauer:** 8-10 Wochen (inkl. PrestaShop-Kompatibilität)
- **Sprint-Länge:** 1 Woche
- **Start:** [TT.MM.JJJJ]
- **Ende (geplant):** [TT.MM.JJJJ]
---
## Aufgabenliste & Prioritäten
| Nr. | Feature/Modul | Beschreibung | Priorität | Zeitrahmen | Status |
|-----|------------------------------|-----------------------------------------------------------|-----------|------------|-----------|
| 1 | **Hook-System** | PrestaShop Hook-System für Module-Kompatibilität | KRITISCH | 1 Woche | ⬜ offen |
| 2 | **Module-Base-System** | Module-Base-Class, Installation/Deinstallation | KRITISCH | 1 Woche | ⬜ offen |
| 3 | **Override-System** | Class/Template/Controller Overrides | HOCH | 1 Woche | ⬜ offen |
| 4 | **Context-System** | PrestaShop Context-API nachbauen | HOCH | 1 Woche | ⬜ offen |
| 5 | **Produktvarianten** | Attribute, Kombinationen, Lager pro Variante | HOCH | 1 Woche | ⬜ offen |
| 6 | **Produktbewertungen** | Kundenbewertungen, Moderation, Sterne, hilfreich | HOCH | 1 Woche | ⬜ offen |
| 7 | **Gutschein-/Rabatt-System** | Gutscheine, Warenkorbregeln, Rabatte | HOCH | 1 Woche | ⬜ offen |
| 8 | **Kundenkonto-Features** | Adressverwaltung, Bestellhistorie, Rücksendungen | HOCH | 1 Woche | ⬜ offen |
| 9 | **Versandmethoden & Tracking** | Versandarten, Kosten, Tracking, Versandstatus | HOCH | 1 Woche | ⬜ offen |
| 10 | **Produktvergleich** | Vergleichsliste, UI, Session-Handling | MITTEL | 0,5 Woche | ⬜ offen |
| 11 | **Cross-Selling & Zubehör** | Verwandte Produkte, Zubehör, Upselling | MITTEL | 0,5 Woche | ⬜ offen |
| 12 | **Wunschliste** | Kundenwunschlisten, Verwaltung, Teilen | MITTEL | 0,5 Woche | ⬜ offen |
| 13 | **Lagerverwaltung** | Bestandsführung, Warnungen, Lieferanten | MITTEL | 0,5 Woche | ⬜ offen |
| 14 | **Berichte & Statistiken** | Umsatz, Topseller, Kunden, Export | MITTEL | 0,5 Woche | ⬜ offen |
| 15 | **Import/Export** | CSV/XML Import/Export für Produkte, Kunden, Bestellungen | MITTEL | 0,5 Woche | ⬜ offen |
| 16 | **Benutzerverwaltung** | Admin-Rollen, Rechte, Logins, Audit-Log | HOCH | 1 Woche | ⬜ offen |
| 17 | **E-Mail-Marketing** | Automatisierte Mails, Kampagnen, Vorlagen | MITTEL | 0,5 Woche | ⬜ offen |
| 18 | **Mehrsprachigkeit (i18n)** | Übersetzungen, Sprachumschaltung, Fallbacks | HOCH | 1 Woche | ⬜ offen |
| 19 | **Template-/Theme-System** | Theme-Engine, Child-Themes, Template-Overrides | HOCH | 1 Woche | ⬜ offen |
| 20 | **Performance-Optimierung** | Caching, Minifizierung, Bildoptimierung | MITTEL | 0,5 Woche | ⬜ offen |
| 21 | **Rechtliches & DSGVO** | Cookie-Consent, Datenschutz, AGB, Widerruf | HOCH | 0,5 Woche | ⬜ offen |
| 22 | **Social Media Integration** | Teilen, Login, Pixel, OpenGraph | NIEDRIG | 0,5 Woche | ⬜ offen |
| 23 | **Affiliate-/Partner-System** | Tracking, Provisionen, Auswertungen | NIEDRIG | 0,5 Woche | ⬜ offen |
| 24 | **PrestaShop-Module-Tests** | Test mit echten PrestaShop-Modulen | HOCH | 1 Woche | ⬜ offen |
---
## PrestaShop Modul-System Komponenten
### 1. **Hook-System** (Priorität: KRITISCH)
```php
// PrestaShop Hook-Beispiele
Hook::exec('actionProductUpdate', ['id_product' => $id]);
Hook::exec('displayProductAdditionalInfo', ['product' => $product]);
Hook::exec('actionCartUpdateQuantityBefore', ['id_product' => $id, 'quantity' => $qty]);
```
**Zu implementieren:**
- ✅ Hook-Registry
- ✅ Hook-Execution
- ✅ Module-Hook-Registration
- ✅ Hook-Parameter-Handling
### 2. **Module-System** (Priorität: KRITISCH)
```php
// PrestaShop Module-Struktur
class MyModule extends Module
{
public function __construct()
{
$this->name = 'mymodule';
$this->tab = 'front_office_features';
$this->version = '1.0.0';
$this->author = 'Your Name';
$this->need_instance = 0;
}
public function install()
{
return parent::install() &&
$this->registerHook('displayProductAdditionalInfo');
}
}
```
**Zu implementieren:**
- ✅ Module-Base-Class
- ✅ Module-Installation/Deinstallation
- ✅ Module-Konfiguration
- ✅ Module-Admin-Interface
### 3. **Override-System** (Priorität: HOCH)
```php
// PrestaShop Override-Beispiele
// classes/Product.php -> modules/mymodule/override/classes/Product.php
// controllers/front/ProductController.php -> modules/mymodule/override/controllers/front/ProductController.php
```
**Zu implementieren:**
- ✅ Class-Override-Detection
- ✅ Template-Override-System
- ✅ Controller-Override-System
- ✅ Override-Priority-System
### 4. **Module-API & Services** (Priorität: HOCH)
```php
// PrestaShop Service-Beispiele
$this->context->shop
$this->context->language
$this->context->currency
Module::getInstanceByName('mymodule')
```
**Zu implementieren:**
- ✅ Context-System
- ✅ Service-Container
- ✅ Module-Discovery
- ✅ Module-Dependencies
---
## Fortschrittstracker
- ⬜ offen | 🟡 in Arbeit | ✅ erledigt
---
## Sprint-Planung
### **Sprint 1 (Woche 1): PrestaShop-Kompatibilität Basis**
- Hook-System (KRITISCH)
- Module-Base-Class (KRITISCH)
- Module-Installation/Deinstallation
### **Sprint 2 (Woche 2): PrestaShop-Kompatibilität Erweitert**
- Override-System
- Context-System
- Service-Container
### **Sprint 3 (Woche 3): Core-E-Commerce Features**
- Produktvarianten
- Produktbewertungen
- Gutschein-/Rabatt-System
### **Sprint 4 (Woche 4): Kunden- & Versand-Features**
- Kundenkonto-Features
- Versandmethoden & Tracking
- Benutzerverwaltung
### **Sprint 5 (Woche 5): Marketing & UX Features**
- Cross-Selling & Zubehör
- Produktvergleich
- Wunschliste
- E-Mail-Marketing
### **Sprint 6 (Woche 6): System-Features**
- Lagerverwaltung
- Berichte & Statistiken
- Import/Export
### **Sprint 7 (Woche 7): Internationalisierung & Performance**
- Mehrsprachigkeit (i18n)
- Template-/Theme-System
- Performance-Optimierung
### **Sprint 8 (Woche 8): Rechtliches & Tests**
- Rechtliches & DSGVO
- Social Media Integration
- Affiliate-/Partner-System
- PrestaShop-Module-Tests
---
## Bekannte PrestaShop-Module zum Testen
### **Einfache Module:**
- **blockreassurance** (Hooks: displayProductAdditionalInfo)
- **ps_mainmenu** (Hooks: displayTop, displayNav)
- **ps_shoppingcart** (Hooks: displayTop, displayNav)
### **Komplexe Module:**
- **ps_checkout** (Viele Hooks, Overrides)
- **ps_facebook** (API-Integration)
- **ps_cashondelivery** (Payment-Module)
---
## Technische Details
### **Hook-System Struktur:**
```php
// Zu implementieren in app/Core/Hook.php
class Hook
{
private static $hooks = [];
public static function register($hookName, $moduleName, $callback)
{
self::$hooks[$hookName][] = [
'module' => $moduleName,
'callback' => $callback
];
}
public static function exec($hookName, $params = [])
{
$results = [];
if (isset(self::$hooks[$hookName])) {
foreach (self::$hooks[$hookName] as $hook) {
$results[] = call_user_func($hook['callback'], $params);
}
}
return $results;
}
}
```
### **Module-Base-Class:**
```php
// Zu implementieren in app/Core/Module.php
abstract class Module
{
public $name;
public $tab;
public $version;
public $author;
public $need_instance;
abstract public function install();
abstract public function uninstall();
public function registerHook($hookName)
{
// Hook-Registration
}
public function unregisterHook($hookName)
{
// Hook-Unregistration
}
}
```
---
## Prioritäten für sofortige Implementierung
### **KRITISCH (sofort):**
1. Hook-System (Basis für alle Module)
2. Module-Base-Class (Grundlage für Module)
3. Module-Installation/Deinstallation
### **HOCH (nächste Woche):**
1. Override-System
2. Context-System
3. Service-Container
### **MITTEL (später):**
1. Module-Admin-Interface
2. Module-Dependencies
3. Performance-Optimierung
---
## Nächste Schritte
1. **Hook-System implementieren** (Sofort)
2. **Module-Base-Class erstellen** (Sofort)
3. **Einfaches Test-Modul entwickeln** (Validierung)
4. **Bekannte PrestaShop-Module testen** (Kompatibilität)
---
## Hinweise
- Die Reihenfolge kann je nach Nutzerwunsch angepasst werden.
- Nach jedem Sprint erfolgt ein Review und ggf. ein Merge/Push zu Gitea.
- Feature-Requests können jederzeit ergänzt werden.
- PrestaShop-Kompatibilität hat höchste Priorität für Module-Nutzung.
---
**Letzte Aktualisierung:** [TT.MM.JJJJ] Phase 2 Planung mit PrestaShop-Kompatibilität erstellt

340
app/Core/Hook.php Normal file
View File

@ -0,0 +1,340 @@
<?php
/**
* Copyright seit 2024 Webshop System
*
* Hook-System für PrestaShop-Modul-Kompatibilität
*
* @author Webshop System
* @license GPL v3
*/
namespace App\Core;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
class Hook
{
private static $hooks = [];
private static $hookModules = [];
private static $initialized = false;
/**
* Hook-System initialisieren
*/
public static function init()
{
if (self::$initialized) {
return;
}
self::loadHooksFromDatabase();
self::$initialized = true;
}
/**
* Hook registrieren
*/
public static function register($hookName, $moduleName, $callback, $position = 0)
{
if (!isset(self::$hooks[$hookName])) {
self::$hooks[$hookName] = [];
}
self::$hooks[$hookName][] = [
'module' => $moduleName,
'callback' => $callback,
'position' => $position
];
// Nach Position sortieren
usort(self::$hooks[$hookName], function($a, $b) {
return $a['position'] - $b['position'];
});
// Hook in Datenbank speichern
self::saveHookToDatabase($hookName, $moduleName, $position);
}
/**
* Hook ausführen
*/
public static function exec($hookName, $params = [])
{
self::init();
$results = [];
if (isset(self::$hooks[$hookName])) {
foreach (self::$hooks[$hookName] as $hook) {
try {
$result = call_user_func($hook['callback'], $params);
if ($result !== null) {
$results[] = $result;
}
} catch (\Exception $e) {
error_log("Hook Fehler in {$hook['module']}: " . $e->getMessage());
}
}
}
return $results;
}
/**
* Hook mit Rückgabewert ausführen (für display-Hooks)
*/
public static function execWithReturn($hookName, $params = [])
{
self::init();
$output = '';
if (isset(self::$hooks[$hookName])) {
foreach (self::$hooks[$hookName] as $hook) {
try {
$result = call_user_func($hook['callback'], $params);
if (is_string($result)) {
$output .= $result;
}
} catch (\Exception $e) {
error_log("Hook Fehler in {$hook['module']}: " . $e->getMessage());
}
}
}
return $output;
}
/**
* Hook entfernen
*/
public static function unregister($hookName, $moduleName)
{
if (isset(self::$hooks[$hookName])) {
foreach (self::$hooks[$hookName] as $key => $hook) {
if ($hook['module'] === $moduleName) {
unset(self::$hooks[$hookName][$key]);
}
}
}
// Hook aus Datenbank entfernen
self::removeHookFromDatabase($hookName, $moduleName);
}
/**
* Verfügbare Hooks abrufen
*/
public static function getHooks()
{
self::init();
return array_keys(self::$hooks);
}
/**
* Module für einen Hook abrufen
*/
public static function getHookModules($hookName)
{
self::init();
if (isset(self::$hooks[$hookName])) {
return array_column(self::$hooks[$hookName], 'module');
}
return [];
}
/**
* Hooks aus Datenbank laden
*/
private static function loadHooksFromDatabase()
{
try {
$conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
SELECT hook_name, module_name, position
FROM ws_hook_module
WHERE active = 1
ORDER BY position ASC
');
$stmt->execute();
$hooks = $stmt->fetchAllAssociative();
foreach ($hooks as $hook) {
$moduleName = $hook['module_name'];
$hookName = $hook['hook_name'];
$position = $hook['position'];
// Module-Klasse laden
$moduleClass = self::getModuleClass($moduleName);
if ($moduleClass && method_exists($moduleClass, 'hook' . ucfirst($hookName))) {
$callback = [$moduleClass, 'hook' . ucfirst($hookName)];
self::register($hookName, $moduleName, $callback, $position);
}
}
} catch (Exception $e) {
error_log('Hook-Datenbank Fehler: ' . $e->getMessage());
}
}
/**
* Hook in Datenbank speichern
*/
private static function saveHookToDatabase($hookName, $moduleName, $position)
{
try {
$conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
INSERT INTO ws_hook_module (hook_name, module_name, position, active, created_at)
VALUES (?, ?, ?, 1, NOW())
ON DUPLICATE KEY UPDATE position = ?, active = 1, updated_at = NOW()
');
$stmt->execute([$hookName, $moduleName, $position, $position]);
} catch (Exception $e) {
error_log('Hook-Speicher Fehler: ' . $e->getMessage());
}
}
/**
* Hook aus Datenbank entfernen
*/
private static function removeHookFromDatabase($hookName, $moduleName)
{
try {
$conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
UPDATE ws_hook_module
SET active = 0, updated_at = NOW()
WHERE hook_name = ? AND module_name = ?
');
$stmt->execute([$hookName, $moduleName]);
} catch (Exception $e) {
error_log('Hook-Entfernung Fehler: ' . $e->getMessage());
}
}
/**
* Module-Klasse laden
*/
private static function getModuleClass($moduleName)
{
$modulePath = __DIR__ . '/../../modules/' . $moduleName . '/' . $moduleName . '.php';
if (file_exists($modulePath)) {
require_once $modulePath;
$className = ucfirst($moduleName);
if (class_exists($className)) {
return new $className();
}
}
return null;
}
/**
* Hook-Statistiken abrufen
*/
public static function getHookStatistics()
{
self::init();
$stats = [];
foreach (self::$hooks as $hookName => $hookList) {
$stats[$hookName] = count($hookList);
}
return $stats;
}
/**
* Hook-Liste für Admin-Interface
*/
public static function getHookList()
{
return [
// Display Hooks (Frontend)
'displayHeader' => 'Header-Bereich',
'displayTop' => 'Oberer Bereich',
'displayNav' => 'Navigation',
'displayNav1' => 'Navigation 1',
'displayNav2' => 'Navigation 2',
'displayTopColumn' => 'Obere Spalte',
'displayLeftColumn' => 'Linke Spalte',
'displayRightColumn' => 'Rechte Spalte',
'displayFooter' => 'Footer-Bereich',
'displayFooterAfter' => 'Footer nach',
'displayHome' => 'Startseite',
'displayHomeTab' => 'Startseite Tabs',
'displayHomeTabContent' => 'Startseite Tab-Inhalt',
// Product Hooks
'displayProductListReviews' => 'Produktliste Bewertungen',
'displayProductAdditionalInfo' => 'Produkt zusätzliche Info',
'displayProductPriceBlock' => 'Produkt Preis-Block',
'displayProductButtons' => 'Produkt Buttons',
'displayProductTab' => 'Produkt Tabs',
'displayProductTabContent' => 'Produkt Tab-Inhalt',
'displayProductListFunctionalButtons' => 'Produktliste Funktions-Buttons',
// Cart Hooks
'displayShoppingCart' => 'Warenkorb',
'displayShoppingCartFooter' => 'Warenkorb Footer',
'actionCartUpdateQuantityBefore' => 'Warenkorb Menge vor Update',
'actionCartUpdateQuantityAfter' => 'Warenkorb Menge nach Update',
'actionCartListOverride' => 'Warenkorb Liste überschreiben',
// Order Hooks
'displayOrderConfirmation' => 'Bestellbestätigung',
'displayOrderDetail' => 'Bestelldetails',
'actionOrderStatusUpdate' => 'Bestellstatus Update',
'actionValidateOrder' => 'Bestellung validieren',
// Customer Hooks
'displayCustomerAccount' => 'Kundenkonto',
'displayCustomerAccountForm' => 'Kundenkonto Formular',
'actionCustomerAccountAdd' => 'Kundenkonto hinzufügen',
'actionCustomerAccountUpdate' => 'Kundenkonto Update',
// Admin Hooks
'displayAdminOrder' => 'Admin Bestellung',
'displayAdminProducts' => 'Admin Produkte',
'actionAdminProductsControllerSaveAfter' => 'Admin Produkt nach Speichern',
'actionAdminCustomersControllerSaveAfter' => 'Admin Kunde nach Speichern',
// Payment Hooks
'displayPayment' => 'Zahlungsmethoden',
'displayPaymentReturn' => 'Zahlungsrückgabe',
'actionPaymentConfirmation' => 'Zahlungsbestätigung',
// Search Hooks
'displaySearch' => 'Suche',
'actionSearch' => 'Suchaktion',
'displaySearchResults' => 'Suchergebnisse',
// Newsletter Hooks
'displayNewsletterRegistration' => 'Newsletter Registrierung',
'actionNewsletterRegistrationAfter' => 'Newsletter nach Registrierung',
// Security Hooks
'actionAuthentication' => 'Authentifizierung',
'actionCustomerLogoutAfter' => 'Kunde nach Logout',
'actionAdminLoginControllerLoginAfter' => 'Admin nach Login'
];
}
}

407
app/Core/Module.php Normal file
View File

@ -0,0 +1,407 @@
<?php
/**
* Copyright seit 2024 Webshop System
*
* Module-Base-Class für PrestaShop-Modul-Kompatibilität
*
* @author Webshop System
* @license GPL v3
*/
namespace App\Core;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
abstract class Module
{
public $name;
public $tab;
public $version;
public $author;
public $need_instance;
public $ps_versions_compliancy;
public $bootstrap;
public $displayName;
public $description;
public $confirmUninstall;
public $limited_countries;
public $module_key;
protected $context;
protected $shop;
protected $config;
public function __construct()
{
$this->context = Context::getContext();
$this->shop = new Shop();
$this->config = new Configuration();
// Standardwerte setzen
$this->need_instance = 0;
$this->bootstrap = false;
$this->ps_versions_compliancy = ['min' => '1.7.0.0', 'max' => _PS_VERSION_];
}
/**
* Modul installieren
*/
public function install()
{
try {
// Modul in Datenbank eintragen
$this->registerModule();
// Modul-Konfiguration installieren
$this->installConfiguration();
// Datenbank-Tabellen erstellen
$this->installDatabase();
// Hooks registrieren
$this->registerHooks();
return true;
} catch (\Exception $e) {
error_log("Modul-Installation Fehler: " . $e->getMessage());
return false;
}
}
/**
* Modul deinstallieren
*/
public function uninstall()
{
try {
// Hooks entfernen
$this->unregisterHooks();
// Datenbank-Tabellen entfernen
$this->uninstallDatabase();
// Modul-Konfiguration entfernen
$this->uninstallConfiguration();
// Modul aus Datenbank entfernen
$this->unregisterModule();
return true;
} catch (\Exception $e) {
error_log("Modul-Deinstallation Fehler: " . $e->getMessage());
return false;
}
}
/**
* Modul in Datenbank registrieren
*/
protected function registerModule()
{
try {
$conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
INSERT INTO ws_module (
name, display_name, description, version, author,
tab, need_instance, bootstrap, active, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW())
ON DUPLICATE KEY UPDATE
display_name = ?, description = ?, version = ?,
author = ?, tab = ?, need_instance = ?, bootstrap = ?,
updated_at = NOW()
');
$stmt->execute([
$this->name,
$this->displayName ?: $this->name,
$this->description ?: '',
$this->version ?: '1.0.0',
$this->author ?: 'Unknown',
$this->tab ?: 'administration',
$this->need_instance,
$this->bootstrap ? 1 : 0,
$this->displayName ?: $this->name,
$this->description ?: '',
$this->version ?: '1.0.0',
$this->author ?: 'Unknown',
$this->tab ?: 'administration',
$this->need_instance,
$this->bootstrap ? 1 : 0
]);
} catch (Exception $e) {
throw new \Exception('Modul-Registrierung Fehler: ' . $e->getMessage());
}
}
/**
* Modul aus Datenbank entfernen
*/
protected function unregisterModule()
{
try {
$conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
UPDATE ws_module
SET active = 0, updated_at = NOW()
WHERE name = ?
');
$stmt->execute([$this->name]);
} catch (Exception $e) {
error_log('Modul-Entfernung Fehler: ' . $e->getMessage());
}
}
/**
* Modul-Konfiguration installieren
*/
protected function installConfiguration()
{
// Standard-Konfiguration installieren
$this->config->set('MODULE_' . strtoupper($this->name) . '_ENABLED', true);
}
/**
* Modul-Konfiguration entfernen
*/
protected function uninstallConfiguration()
{
// Modul-Konfiguration entfernen
$this->config->delete('MODULE_' . strtoupper($this->name) . '_ENABLED');
}
/**
* Datenbank-Tabellen erstellen
*/
protected function installDatabase()
{
// Kann von Modulen überschrieben werden
}
/**
* Datenbank-Tabellen entfernen
*/
protected function uninstallDatabase()
{
// Kann von Modulen überschrieben werden
}
/**
* Hooks registrieren
*/
protected function registerHooks()
{
// Standard-Hooks können hier registriert werden
// Hook::register('displayHeader', $this->name, [$this, 'hookDisplayHeader']);
}
/**
* Hooks entfernen
*/
protected function unregisterHooks()
{
// Alle Hooks für dieses Modul entfernen
try {
$conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
UPDATE ws_hook_module
SET active = 0, updated_at = NOW()
WHERE module_name = ?
');
$stmt->execute([$this->name]);
} catch (Exception $e) {
error_log('Hook-Entfernung Fehler: ' . $e->getMessage());
}
}
/**
* Hook registrieren
*/
public function registerHook($hookName)
{
try {
$conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
INSERT INTO ws_hook_module (hook_name, module_name, position, active, created_at)
VALUES (?, ?, 0, 1, NOW())
ON DUPLICATE KEY UPDATE active = 1, updated_at = NOW()
');
$stmt->execute([$hookName, $this->name]);
return true;
} catch (Exception $e) {
error_log('Hook-Registrierung Fehler: ' . $e->getMessage());
return false;
}
}
/**
* Hook entfernen
*/
public function unregisterHook($hookName)
{
try {
$conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
UPDATE ws_hook_module
SET active = 0, updated_at = NOW()
WHERE hook_name = ? AND module_name = ?
');
$stmt->execute([$hookName, $this->name]);
return true;
} catch (Exception $e) {
error_log('Hook-Entfernung Fehler: ' . $e->getMessage());
return false;
}
}
/**
* Modul-Konfiguration abrufen
*/
public function getConfig($key, $default = null)
{
return $this->config->get('MODULE_' . strtoupper($this->name) . '_' . strtoupper($key), $default);
}
/**
* Modul-Konfiguration setzen
*/
public function setConfig($key, $value)
{
return $this->config->set('MODULE_' . strtoupper($this->name) . '_' . strtoupper($key), $value);
}
/**
* Modul-Konfiguration entfernen
*/
public function deleteConfig($key)
{
return $this->config->delete('MODULE_' . strtoupper($this->name) . '_' . strtoupper($key));
}
/**
* Modul-URL generieren
*/
public function getModuleUrl($controller = null, $params = [])
{
$url = '/admin/module/' . $this->name;
if ($controller) {
$url .= '/' . $controller;
}
if (!empty($params)) {
$url .= '?' . http_build_query($params);
}
return $url;
}
/**
* Modul-Pfad abrufen
*/
public function getModulePath()
{
return __DIR__ . '/../../modules/' . $this->name . '/';
}
/**
* Modul-Template-Pfad abrufen
*/
public function getTemplatePath()
{
return $this->getModulePath() . 'views/templates/';
}
/**
* Modul-Asset-Pfad abrufen
*/
public function getAssetPath()
{
return $this->getModulePath() . 'views/assets/';
}
/**
* Modul-Admin-Controller erstellen
*/
public function getContent()
{
// Kann von Modulen überschrieben werden
return '';
}
/**
* Modul-Admin-Formular verarbeiten
*/
public function postProcess()
{
// Kann von Modulen überschrieben werden
return true;
}
/**
* Modul-Status prüfen
*/
public function isEnabled()
{
return $this->getConfig('ENABLED', false);
}
/**
* Modul aktivieren
*/
public function enable()
{
return $this->setConfig('ENABLED', true);
}
/**
* Modul deaktivieren
*/
public function disable()
{
return $this->setConfig('ENABLED', false);
}
/**
* Modul-Informationen abrufen
*/
public function getModuleInfo()
{
return [
'name' => $this->name,
'display_name' => $this->displayName ?: $this->name,
'description' => $this->description ?: '',
'version' => $this->version ?: '1.0.0',
'author' => $this->author ?: 'Unknown',
'tab' => $this->tab ?: 'administration',
'need_instance' => $this->need_instance,
'bootstrap' => $this->bootstrap,
'enabled' => $this->isEnabled(),
'path' => $this->getModulePath()
];
}
}

View File

@ -0,0 +1,431 @@
<?php
/**
* Copyright seit 2024 Webshop System
*
* Module Controller für Admin-Bereich
*
* @author Webshop System
* @license GPL v3
*/
namespace App\Controllers\Admin;
use App\Core\Module;
use App\Core\Hook;
use App\Core\MultiShop;
use App\Core\Security;
use App\Core\Configuration;
class ModuleController
{
private $multiShop;
private $security;
private $config;
public function __construct()
{
$this->multiShop = new MultiShop();
$this->security = new Security();
$this->config = new Configuration();
// Session-Check
if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
header('Location: /admin/login');
exit;
}
}
/**
* Modul-Übersicht anzeigen
*/
public function index()
{
$shopId = $this->multiShop->getCurrentShopId();
// Module aus Datenbank abrufen
$modules = $this->getModules();
// Hook-Statistiken
$hookStats = Hook::getHookStatistics();
$data = [
'modules' => $modules,
'hook_stats' => $hookStats,
'shop_id' => $shopId
];
$this->render('admin/module/index.html.twig', $data);
}
/**
* Modul installieren
*/
public function install()
{
$moduleName = $_POST['module_name'] ?? '';
if (empty($moduleName)) {
$this->addError('Modul-Name ist erforderlich');
header('Location: /admin/module');
return;
}
// CSRF-Schutz
if (!$this->security->validateCSRFToken($_POST['csrf_token'] ?? '')) {
$this->addError('Sicherheitsfehler: Ungültiger Token');
header('Location: /admin/module');
return;
}
try {
$module = $this->loadModule($moduleName);
if (!$module) {
$this->addError("Modul '{$moduleName}' nicht gefunden");
header('Location: /admin/module');
return;
}
if ($module->install()) {
$this->addSuccess("Modul '{$moduleName}' erfolgreich installiert");
} else {
$this->addError("Fehler beim Installieren von '{$moduleName}'");
}
} catch (\Exception $e) {
$this->addError('Installationsfehler: ' . $e->getMessage());
}
header('Location: /admin/module');
}
/**
* Modul deinstallieren
*/
public function uninstall()
{
$moduleName = $_POST['module_name'] ?? '';
if (empty($moduleName)) {
$this->addError('Modul-Name ist erforderlich');
header('Location: /admin/module');
return;
}
// CSRF-Schutz
if (!$this->security->validateCSRFToken($_POST['csrf_token'] ?? '')) {
$this->addError('Sicherheitsfehler: Ungültiger Token');
header('Location: /admin/module');
return;
}
try {
$module = $this->loadModule($moduleName);
if (!$module) {
$this->addError("Modul '{$moduleName}' nicht gefunden");
header('Location: /admin/module');
return;
}
if ($module->uninstall()) {
$this->addSuccess("Modul '{$moduleName}' erfolgreich deinstalliert");
} else {
$this->addError("Fehler beim Deinstallieren von '{$moduleName}'");
}
} catch (\Exception $e) {
$this->addError('Deinstallationsfehler: ' . $e->getMessage());
}
header('Location: /admin/module');
}
/**
* Modul konfigurieren
*/
public function configure()
{
$moduleName = $_GET['module'] ?? '';
if (empty($moduleName)) {
$this->addError('Modul-Name ist erforderlich');
header('Location: /admin/module');
return;
}
$module = $this->loadModule($moduleName);
if (!$module) {
$this->addError("Modul '{$moduleName}' nicht gefunden");
header('Location: /admin/module');
return;
}
// POST-Verarbeitung
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->processModuleConfiguration($module);
}
$data = [
'module' => $module,
'module_info' => $module->getModuleInfo(),
'configuration' => $this->getModuleConfiguration($moduleName)
];
$this->render('admin/module/configure.html.twig', $data);
}
/**
* Modul-Konfiguration verarbeiten
*/
private function processModuleConfiguration($module)
{
// CSRF-Schutz
if (!$this->security->validateCSRFToken($_POST['csrf_token'] ?? '')) {
$this->addError('Sicherheitsfehler: Ungültiger Token');
return;
}
$moduleName = $module->name;
// Konfiguration verarbeiten
foreach ($_POST as $key => $value) {
if (strpos($key, 'config_') === 0) {
$configKey = substr($key, 7); // 'config_' entfernen
$module->setConfig($configKey, $value);
}
}
$this->addSuccess('Modul-Konfiguration erfolgreich gespeichert');
}
/**
* Module aus Datenbank abrufen
*/
private function getModules()
{
try {
$conn = \Doctrine\DBAL\DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
SELECT * FROM ws_module
ORDER BY display_name ASC
');
$stmt->execute();
return $stmt->fetchAllAssociative();
} catch (\Exception $e) {
error_log('Module-Abruf Fehler: ' . $e->getMessage());
return [];
}
}
/**
* Modul laden
*/
private function loadModule($moduleName)
{
$modulePath = __DIR__ . '/../../modules/' . $moduleName . '/' . $moduleName . '.php';
if (!file_exists($modulePath)) {
return null;
}
require_once $modulePath;
$className = ucfirst($moduleName);
if (class_exists($className)) {
return new $className();
}
return null;
}
/**
* Modul-Konfiguration abrufen
*/
private function getModuleConfiguration($moduleName)
{
try {
$conn = \Doctrine\DBAL\DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
SELECT config_key, config_value
FROM ws_module_config
WHERE module_name = ?
');
$stmt->execute([$moduleName]);
$config = [];
while ($row = $stmt->fetchAssociative()) {
$config[$row['config_key']] = $row['config_value'];
}
return $config;
} catch (\Exception $e) {
error_log('Modul-Konfiguration Fehler: ' . $e->getMessage());
return [];
}
}
/**
* Modul-Logs abrufen
*/
public function logs()
{
$moduleName = $_GET['module'] ?? '';
try {
$conn = \Doctrine\DBAL\DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$sql = 'SELECT * FROM ws_module_log';
$params = [];
if (!empty($moduleName)) {
$sql .= ' WHERE module_name = ?';
$params[] = $moduleName;
}
$sql .= ' ORDER BY created_at DESC LIMIT 100';
$stmt = $conn->prepare($sql);
$stmt->execute($params);
$logs = $stmt->fetchAllAssociative();
$data = [
'logs' => $logs,
'module_name' => $moduleName
];
$this->render('admin/module/logs.html.twig', $data);
} catch (\Exception $e) {
$this->addError('Log-Abruf Fehler: ' . $e->getMessage());
$this->render('admin/module/logs.html.twig', ['logs' => [], 'module_name' => $moduleName]);
}
}
/**
* Hook-Übersicht anzeigen
*/
public function hooks()
{
$hookList = Hook::getHookList();
$hookStats = Hook::getHookStatistics();
$data = [
'hook_list' => $hookList,
'hook_stats' => $hookStats
];
$this->render('admin/module/hooks.html.twig', $data);
}
/**
* Modul-Upload verarbeiten
*/
public function upload()
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/module');
return;
}
// CSRF-Schutz
if (!$this->security->validateCSRFToken($_POST['csrf_token'] ?? '')) {
$this->addError('Sicherheitsfehler: Ungültiger Token');
header('Location: /admin/module');
return;
}
if (!isset($_FILES['module_file']) || $_FILES['module_file']['error'] !== UPLOAD_ERR_OK) {
$this->addError('Fehler beim Datei-Upload');
header('Location: /admin/module');
return;
}
$uploadDir = __DIR__ . '/../../modules/';
$fileName = $_FILES['module_file']['name'];
$filePath = $_FILES['module_file']['tmp_name'];
// ZIP-Datei extrahieren
$zip = new \ZipArchive();
if ($zip->open($filePath) === TRUE) {
$moduleName = pathinfo($fileName, PATHINFO_FILENAME);
$extractPath = $uploadDir . $moduleName . '/';
// Verzeichnis erstellen
if (!is_dir($extractPath)) {
mkdir($extractPath, 0755, true);
}
// Dateien extrahieren
$zip->extractTo($extractPath);
$zip->close();
$this->addSuccess("Modul '{$moduleName}' erfolgreich hochgeladen");
} else {
$this->addError('Fehler beim Extrahieren der ZIP-Datei');
}
header('Location: /admin/module');
}
/**
* Template rendern
*/
private function render($template, $data = [])
{
// CSRF-Token generieren
$data['csrf_token'] = $this->security->generateCSRFToken();
// Flash-Messages
$data['success_messages'] = $_SESSION['success_messages'] ?? [];
$data['error_messages'] = $_SESSION['error_messages'] ?? [];
// Session-Messages löschen
unset($_SESSION['success_messages'], $_SESSION['error_messages']);
// Template laden
$templatePath = __DIR__ . '/../../templates/' . $template;
if (file_exists($templatePath)) {
extract($data);
include $templatePath;
} else {
throw new \Exception("Template nicht gefunden: {$template}");
}
}
/**
* Erfolgs-Message hinzufügen
*/
private function addSuccess($message)
{
if (!isset($_SESSION['success_messages'])) {
$_SESSION['success_messages'] = [];
}
$_SESSION['success_messages'][] = $message;
}
/**
* Fehler-Message hinzufügen
*/
private function addError($message)
{
if (!isset($_SESSION['error_messages'])) {
$_SESSION['error_messages'] = [];
}
$_SESSION['error_messages'][] = $message;
}
}

View File

@ -0,0 +1,148 @@
-- Module-System Schema
-- Erstellt die Tabellen für PrestaShop-kompatible Module
-- Module-Tabelle
CREATE TABLE IF NOT EXISTS `ws_module` (
`id_module` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
`display_name` varchar(255) DEFAULT NULL,
`description` text,
`version` varchar(32) DEFAULT '1.0.0',
`author` varchar(255) DEFAULT 'Unknown',
`tab` varchar(64) DEFAULT 'administration',
`need_instance` tinyint(1) DEFAULT 0,
`bootstrap` tinyint(1) DEFAULT 0,
`active` tinyint(1) DEFAULT 1,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id_module`),
UNIQUE KEY `name` (`name`),
KEY `active` (`active`),
KEY `tab` (`tab`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Hook-Module-Tabelle (Verbindung zwischen Hooks und Modulen)
CREATE TABLE IF NOT EXISTS `ws_hook_module` (
`id_hook_module` int(11) NOT NULL AUTO_INCREMENT,
`hook_name` varchar(64) NOT NULL,
`module_name` varchar(64) NOT NULL,
`position` int(11) DEFAULT 0,
`active` tinyint(1) DEFAULT 1,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id_hook_module`),
UNIQUE KEY `hook_module` (`hook_name`, `module_name`),
KEY `hook_name` (`hook_name`),
KEY `module_name` (`module_name`),
KEY `active` (`active`),
KEY `position` (`position`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Module-Konfiguration-Tabelle
CREATE TABLE IF NOT EXISTS `ws_module_config` (
`id_module_config` int(11) NOT NULL AUTO_INCREMENT,
`module_name` varchar(64) NOT NULL,
`config_key` varchar(255) NOT NULL,
`config_value` text,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id_module_config`),
UNIQUE KEY `module_config` (`module_name`, `config_key`),
KEY `module_name` (`module_name`),
KEY `config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Module-Logs-Tabelle
CREATE TABLE IF NOT EXISTS `ws_module_log` (
`id_module_log` int(11) NOT NULL AUTO_INCREMENT,
`module_name` varchar(64) NOT NULL,
`action` varchar(64) NOT NULL,
`message` text,
`level` enum('info', 'warning', 'error') DEFAULT 'info',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id_module_log`),
KEY `module_name` (`module_name`),
KEY `action` (`action`),
KEY `level` (`level`),
KEY `created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Module-Dependencies-Tabelle
CREATE TABLE IF NOT EXISTS `ws_module_dependency` (
`id_module_dependency` int(11) NOT NULL AUTO_INCREMENT,
`module_name` varchar(64) NOT NULL,
`dependency_name` varchar(64) NOT NULL,
`dependency_version` varchar(32) DEFAULT NULL,
`required` tinyint(1) DEFAULT 1,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id_module_dependency`),
KEY `module_name` (`module_name`),
KEY `dependency_name` (`dependency_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Module-Override-Tabelle
CREATE TABLE IF NOT EXISTS `ws_module_override` (
`id_module_override` int(11) NOT NULL AUTO_INCREMENT,
`module_name` varchar(64) NOT NULL,
`override_type` enum('class', 'template', 'controller') NOT NULL,
`original_path` varchar(255) NOT NULL,
`override_path` varchar(255) NOT NULL,
`active` tinyint(1) DEFAULT 1,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id_module_override`),
KEY `module_name` (`module_name`),
KEY `override_type` (`override_type`),
KEY `active` (`active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Standard-Hooks einfügen
INSERT IGNORE INTO `ws_hook_module` (`hook_name`, `module_name`, `position`) VALUES
-- Display Hooks
('displayHeader', 'system', 0),
('displayTop', 'system', 0),
('displayNav', 'system', 0),
('displayFooter', 'system', 0),
('displayHome', 'system', 0),
-- Product Hooks
('displayProductAdditionalInfo', 'system', 0),
('displayProductListReviews', 'system', 0),
('displayProductButtons', 'system', 0),
-- Cart Hooks
('displayShoppingCart', 'system', 0),
('actionCartUpdateQuantityBefore', 'system', 0),
('actionCartUpdateQuantityAfter', 'system', 0),
-- Order Hooks
('displayOrderConfirmation', 'system', 0),
('actionValidateOrder', 'system', 0),
-- Customer Hooks
('displayCustomerAccount', 'system', 0),
('actionCustomerAccountAdd', 'system', 0),
-- Admin Hooks
('displayAdminOrder', 'system', 0),
('displayAdminProducts', 'system', 0),
-- Payment Hooks
('displayPayment', 'system', 0),
('actionPaymentConfirmation', 'system', 0),
-- Search Hooks
('displaySearch', 'system', 0),
('actionSearch', 'system', 0),
-- Newsletter Hooks
('displayNewsletterRegistration', 'system', 0),
('actionNewsletterRegistrationAfter', 'system', 0),
-- Security Hooks
('actionAuthentication', 'system', 0),
('actionCustomerLogoutAfter', 'system', 0);
-- System-Modul eintragen
INSERT IGNORE INTO `ws_module` (`name`, `display_name`, `description`, `version`, `author`, `tab`, `need_instance`, `bootstrap`, `active`) VALUES
('system', 'System', 'Kern-System-Modul', '1.0.0', 'Webshop System', 'administration', 0, 0, 1);