Newwebshop/app/Core/Cache.php

1124 lines
28 KiB
PHP

<?php
/**
* Copyright seit 2024 Webshop System
*
* Cache-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 Cache
{
private static $instance = null;
private $drivers = [];
private $defaultDriver = 'file';
private $enabled = true;
private $statistics = [];
private function __construct()
{
$this->initializeDrivers();
}
/**
* Singleton-Instanz abrufen
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Cache-Drivers initialisieren
*/
private function initializeDrivers()
{
// File-Cache Driver
$this->drivers['file'] = new FileCacheDriver();
// Redis-Cache Driver (falls verfügbar)
if (extension_loaded('redis')) {
$this->drivers['redis'] = new RedisCacheDriver();
}
// Memcached-Cache Driver (falls verfügbar)
if (extension_loaded('memcached')) {
$this->drivers['memcached'] = new MemcachedCacheDriver();
}
// Database-Cache Driver
$this->drivers['database'] = new DatabaseCacheDriver();
}
/**
* Cache-Wert abrufen
*/
public function get($key, $default = null, $driver = null)
{
if (!$this->enabled) {
return $default;
}
$driver = $driver ?: $this->defaultDriver;
if (!isset($this->drivers[$driver])) {
return $default;
}
$startTime = microtime(true);
try {
$value = $this->drivers[$driver]->get($key);
$endTime = microtime(true);
$this->updateStatistics($driver, 'get', $endTime - $startTime, $value !== null);
return $value !== null ? $value : $default;
} catch (\Exception $e) {
$this->logCacheError($driver, 'get', $key, $e);
return $default;
}
}
/**
* Cache-Wert setzen
*/
public function set($key, $value, $ttl = 3600, $driver = null)
{
if (!$this->enabled) {
return false;
}
$driver = $driver ?: $this->defaultDriver;
if (!isset($this->drivers[$driver])) {
return false;
}
$startTime = microtime(true);
try {
$result = $this->drivers[$driver]->set($key, $value, $ttl);
$endTime = microtime(true);
$this->updateStatistics($driver, 'set', $endTime - $startTime, $result);
return $result;
} catch (\Exception $e) {
$this->logCacheError($driver, 'set', $key, $e);
return false;
}
}
/**
* Cache-Wert löschen
*/
public function delete($key, $driver = null)
{
if (!$this->enabled) {
return false;
}
$driver = $driver ?: $this->defaultDriver;
if (!isset($this->drivers[$driver])) {
return false;
}
$startTime = microtime(true);
try {
$result = $this->drivers[$driver]->delete($key);
$endTime = microtime(true);
$this->updateStatistics($driver, 'delete', $endTime - $startTime, $result);
return $result;
} catch (\Exception $e) {
$this->logCacheError($driver, 'delete', $key, $e);
return false;
}
}
/**
* Cache-Wert prüfen
*/
public function has($key, $driver = null)
{
if (!$this->enabled) {
return false;
}
$driver = $driver ?: $this->defaultDriver;
if (!isset($this->drivers[$driver])) {
return false;
}
try {
return $this->drivers[$driver]->has($key);
} catch (\Exception $e) {
$this->logCacheError($driver, 'has', $key, $e);
return false;
}
}
/**
* Cache leeren
*/
public function clear($driver = null)
{
if (!$this->enabled) {
return false;
}
$driver = $driver ?: $this->defaultDriver;
if (!isset($this->drivers[$driver])) {
return false;
}
try {
return $this->drivers[$driver]->clear();
} catch (\Exception $e) {
$this->logCacheError($driver, 'clear', '', $e);
return false;
}
}
/**
* Cache-Tags verwenden
*/
public function getByTag($tag, $driver = null)
{
if (!$this->enabled) {
return [];
}
$driver = $driver ?: $this->defaultDriver;
if (!isset($this->drivers[$driver])) {
return [];
}
try {
return $this->drivers[$driver]->getByTag($tag);
} catch (\Exception $e) {
$this->logCacheError($driver, 'getByTag', $tag, $e);
return [];
}
}
/**
* Cache-Tags löschen
*/
public function deleteByTag($tag, $driver = null)
{
if (!$this->enabled) {
return false;
}
$driver = $driver ?: $this->defaultDriver;
if (!isset($this->drivers[$driver])) {
return false;
}
try {
return $this->drivers[$driver]->deleteByTag($tag);
} catch (\Exception $e) {
$this->logCacheError($driver, 'deleteByTag', $tag, $e);
return false;
}
}
/**
* Cache-Wert mit Tags setzen
*/
public function setWithTags($key, $value, $tags = [], $ttl = 3600, $driver = null)
{
if (!$this->enabled) {
return false;
}
$driver = $driver ?: $this->defaultDriver;
if (!isset($this->drivers[$driver])) {
return false;
}
try {
return $this->drivers[$driver]->setWithTags($key, $value, $tags, $ttl);
} catch (\Exception $e) {
$this->logCacheError($driver, 'setWithTags', $key, $e);
return false;
}
}
/**
* Cache-Warmup
*/
public function warmup($keys = [])
{
if (!$this->enabled) {
return false;
}
$results = [];
foreach ($this->drivers as $driverName => $driver) {
try {
$results[$driverName] = $driver->warmup($keys);
} catch (\Exception $e) {
$this->logCacheError($driverName, 'warmup', '', $e);
$results[$driverName] = false;
}
}
return $results;
}
/**
* Cache-Statistiken abrufen
*/
public function getStatistics($driver = null)
{
if ($driver) {
return isset($this->statistics[$driver]) ? $this->statistics[$driver] : [];
}
return $this->statistics;
}
/**
* Cache-Status abrufen
*/
public function getStatus($driver = null)
{
if ($driver) {
return isset($this->drivers[$driver]) ? $this->drivers[$driver]->getStatus() : false;
}
$status = [];
foreach ($this->drivers as $driverName => $driver) {
$status[$driverName] = $driver->getStatus();
}
return $status;
}
/**
* Cache aktivieren/deaktivieren
*/
public function setEnabled($enabled)
{
$this->enabled = $enabled;
return $this;
}
/**
* Cache-Status prüfen
*/
public function isEnabled()
{
return $this->enabled;
}
/**
* Standard-Driver setzen
*/
public function setDefaultDriver($driver)
{
if (isset($this->drivers[$driver])) {
$this->defaultDriver = $driver;
}
return $this;
}
/**
* Verfügbare Drivers abrufen
*/
public function getAvailableDrivers()
{
return array_keys($this->drivers);
}
/**
* Cache zurücksetzen
*/
public function reset()
{
foreach ($this->drivers as $driver) {
$driver->clear();
}
$this->statistics = [];
return $this;
}
/**
* Statistiken aktualisieren
*/
private function updateStatistics($driver, $operation, $time, $success)
{
if (!isset($this->statistics[$driver])) {
$this->statistics[$driver] = [
'operations' => [],
'total_operations' => 0,
'total_time' => 0,
'avg_time' => 0,
'hits' => 0,
'misses' => 0
];
}
$stats = &$this->statistics[$driver];
if (!isset($stats['operations'][$operation])) {
$stats['operations'][$operation] = [
'count' => 0,
'total_time' => 0,
'avg_time' => 0,
'success_count' => 0
];
}
$stats['total_operations']++;
$stats['total_time'] += $time;
$stats['avg_time'] = $stats['total_time'] / $stats['total_operations'];
$stats['operations'][$operation]['count']++;
$stats['operations'][$operation]['total_time'] += $time;
$stats['operations'][$operation]['avg_time'] = $stats['operations'][$operation]['total_time'] / $stats['operations'][$operation]['count'];
if ($success) {
$stats['operations'][$operation]['success_count']++;
if ($operation === 'get') {
$stats['hits']++;
}
} else {
if ($operation === 'get') {
$stats['misses']++;
}
}
}
/**
* Cache-Fehler loggen
*/
private function logCacheError($driver, $operation, $key, $exception)
{
$errorMessage = sprintf(
'Cache-Fehler: Driver=%s, Operation=%s, Key=%s, Fehler=%s',
$driver,
$operation,
$key,
$exception->getMessage()
);
error_log($errorMessage);
// Fehler in Datenbank loggen
$this->logCacheErrorToDatabase($driver, $operation, $key, $exception);
}
/**
* Cache-Fehler in Datenbank loggen
*/
private function logCacheErrorToDatabase($driver, $operation, $key, $exception)
{
try {
$conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$stmt = $conn->prepare('
INSERT INTO ws_cache_errors (
driver_name, operation, cache_key, error_message,
error_trace, created_at
) VALUES (?, ?, ?, ?, ?, NOW())
');
$stmt->execute([
$driver,
$operation,
$key,
$exception->getMessage(),
$exception->getTraceAsString()
]);
} catch (Exception $e) {
error_log('Cache-Error-Log Fehler: ' . $e->getMessage());
}
}
}
/**
* File-Cache Driver
*/
class FileCacheDriver
{
private $cacheDir;
public function __construct()
{
$this->cacheDir = __DIR__ . '/../../../cache/file/';
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0755, true);
}
}
public function get($key)
{
$filename = $this->getFilename($key);
if (!file_exists($filename)) {
return null;
}
$data = unserialize(file_get_contents($filename));
if ($data['expires'] > 0 && time() > $data['expires']) {
unlink($filename);
return null;
}
return $data['value'];
}
public function set($key, $value, $ttl = 3600)
{
$filename = $this->getFilename($key);
$data = [
'value' => $value,
'expires' => $ttl > 0 ? time() + $ttl : 0,
'created' => time()
];
return file_put_contents($filename, serialize($data)) !== false;
}
public function delete($key)
{
$filename = $this->getFilename($key);
if (file_exists($filename)) {
return unlink($filename);
}
return true;
}
public function has($key)
{
$filename = $this->getFilename($key);
if (!file_exists($filename)) {
return false;
}
$data = unserialize(file_get_contents($filename));
if ($data['expires'] > 0 && time() > $data['expires']) {
unlink($filename);
return false;
}
return true;
}
public function clear()
{
$files = glob($this->cacheDir . '*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
return true;
}
public function getByTag($tag)
{
$files = glob($this->cacheDir . '*');
$results = [];
foreach ($files as $file) {
if (is_file($file)) {
$data = unserialize(file_get_contents($file));
if (isset($data['tags']) && in_array($tag, $data['tags'])) {
$key = basename($file, '.cache');
$results[$key] = $data['value'];
}
}
}
return $results;
}
public function deleteByTag($tag)
{
$files = glob($this->cacheDir . '*');
$deleted = 0;
foreach ($files as $file) {
if (is_file($file)) {
$data = unserialize(file_get_contents($file));
if (isset($data['tags']) && in_array($tag, $data['tags'])) {
unlink($file);
$deleted++;
}
}
}
return $deleted > 0;
}
public function setWithTags($key, $value, $tags = [], $ttl = 3600)
{
$filename = $this->getFilename($key);
$data = [
'value' => $value,
'tags' => $tags,
'expires' => $ttl > 0 ? time() + $ttl : 0,
'created' => time()
];
return file_put_contents($filename, serialize($data)) !== false;
}
public function warmup($keys = [])
{
// File-Cache benötigt kein Warmup
return true;
}
public function getStatus()
{
return [
'driver' => 'file',
'enabled' => is_dir($this->cacheDir),
'directory' => $this->cacheDir,
'files_count' => count(glob($this->cacheDir . '*')),
'size' => $this->getDirectorySize($this->cacheDir)
];
}
private function getFilename($key)
{
return $this->cacheDir . md5($key) . '.cache';
}
private function getDirectorySize($dir)
{
$size = 0;
$files = glob($dir . '*');
foreach ($files as $file) {
if (is_file($file)) {
$size += filesize($file);
}
}
return $size;
}
}
/**
* Redis-Cache Driver
*/
class RedisCacheDriver
{
private $redis;
public function __construct()
{
$this->redis = new \Redis();
try {
$this->redis->connect(
getenv('REDIS_HOST') ?: 'localhost',
getenv('REDIS_PORT') ?: 6379
);
if (getenv('REDIS_PASSWORD')) {
$this->redis->auth(getenv('REDIS_PASSWORD'));
}
} catch (\Exception $e) {
error_log('Redis-Verbindung fehlgeschlagen: ' . $e->getMessage());
}
}
public function get($key)
{
try {
$value = $this->redis->get($key);
return $value !== false ? unserialize($value) : null;
} catch (\Exception $e) {
return null;
}
}
public function set($key, $value, $ttl = 3600)
{
try {
$serialized = serialize($value);
return $ttl > 0 ? $this->redis->setex($key, $ttl, $serialized) : $this->redis->set($key, $serialized);
} catch (\Exception $e) {
return false;
}
}
public function delete($key)
{
try {
return $this->redis->del($key) > 0;
} catch (\Exception $e) {
return false;
}
}
public function has($key)
{
try {
return $this->redis->exists($key);
} catch (\Exception $e) {
return false;
}
}
public function clear()
{
try {
return $this->redis->flushDB();
} catch (\Exception $e) {
return false;
}
}
public function getByTag($tag)
{
try {
$keys = $this->redis->keys('tag:' . $tag . ':*');
$results = [];
foreach ($keys as $key) {
$cacheKey = str_replace('tag:' . $tag . ':', '', $key);
$value = $this->get($cacheKey);
if ($value !== null) {
$results[$cacheKey] = $value;
}
}
return $results;
} catch (\Exception $e) {
return [];
}
}
public function deleteByTag($tag)
{
try {
$keys = $this->redis->keys('tag:' . $tag . ':*');
foreach ($keys as $key) {
$cacheKey = str_replace('tag:' . $tag . ':', '', $key);
$this->delete($cacheKey);
$this->redis->del($key);
}
return true;
} catch (\Exception $e) {
return false;
}
}
public function setWithTags($key, $value, $tags = [], $ttl = 3600)
{
try {
$result = $this->set($key, $value, $ttl);
if ($result) {
foreach ($tags as $tag) {
$this->redis->set('tag:' . $tag . ':' . $key, 1, $ttl);
}
}
return $result;
} catch (\Exception $e) {
return false;
}
}
public function warmup($keys = [])
{
// Redis benötigt kein Warmup
return true;
}
public function getStatus()
{
try {
$info = $this->redis->info();
return [
'driver' => 'redis',
'enabled' => $this->redis->ping() === '+PONG',
'version' => $info['redis_version'] ?? 'unknown',
'memory_used' => $info['used_memory_human'] ?? 'unknown',
'connected_clients' => $info['connected_clients'] ?? 0
];
} catch (\Exception $e) {
return [
'driver' => 'redis',
'enabled' => false,
'error' => $e->getMessage()
];
}
}
}
/**
* Memcached-Cache Driver
*/
class MemcachedCacheDriver
{
private $memcached;
public function __construct()
{
$this->memcached = new \Memcached();
try {
$this->memcached->addServer(
getenv('MEMCACHED_HOST') ?: 'localhost',
getenv('MEMCACHED_PORT') ?: 11211
);
} catch (\Exception $e) {
error_log('Memcached-Verbindung fehlgeschlagen: ' . $e->getMessage());
}
}
public function get($key)
{
try {
$value = $this->memcached->get($key);
return $value !== false ? $value : null;
} catch (\Exception $e) {
return null;
}
}
public function set($key, $value, $ttl = 3600)
{
try {
return $this->memcached->set($key, $value, $ttl);
} catch (\Exception $e) {
return false;
}
}
public function delete($key)
{
try {
return $this->memcached->delete($key);
} catch (\Exception $e) {
return false;
}
}
public function has($key)
{
try {
return $this->memcached->get($key) !== false;
} catch (\Exception $e) {
return false;
}
}
public function clear()
{
try {
return $this->memcached->flush();
} catch (\Exception $e) {
return false;
}
}
public function getByTag($tag)
{
// Memcached unterstützt keine Tags nativ
return [];
}
public function deleteByTag($tag)
{
// Memcached unterstützt keine Tags nativ
return false;
}
public function setWithTags($key, $value, $tags = [], $ttl = 3600)
{
// Memcached unterstützt keine Tags nativ
return $this->set($key, $value, $ttl);
}
public function warmup($keys = [])
{
// Memcached benötigt kein Warmup
return true;
}
public function getStatus()
{
try {
$stats = $this->memcached->getStats();
return [
'driver' => 'memcached',
'enabled' => !empty($stats),
'servers' => count($stats),
'memory_used' => $stats['memory'] ?? 'unknown'
];
} catch (\Exception $e) {
return [
'driver' => 'memcached',
'enabled' => false,
'error' => $e->getMessage()
];
}
}
}
/**
* Database-Cache Driver
*/
class DatabaseCacheDriver
{
private $conn;
public function __construct()
{
try {
$this->conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
} catch (Exception $e) {
error_log('Database-Cache-Verbindung fehlgeschlagen: ' . $e->getMessage());
}
}
public function get($key)
{
try {
$stmt = $this->conn->prepare('
SELECT cache_value, expires_at
FROM ws_cache
WHERE cache_key = ? AND (expires_at IS NULL OR expires_at > NOW())
');
$stmt->execute([$key]);
$result = $stmt->fetchAssociative();
if ($result) {
return unserialize($result['cache_value']);
}
return null;
} catch (Exception $e) {
return null;
}
}
public function set($key, $value, $ttl = 3600)
{
try {
$expiresAt = $ttl > 0 ? date('Y-m-d H:i:s', time() + $ttl) : null;
$stmt = $this->conn->prepare('
INSERT INTO ws_cache (cache_key, cache_value, expires_at, created_at)
VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
cache_value = ?, expires_at = ?, updated_at = NOW()
');
$serialized = serialize($value);
$stmt->execute([$key, $serialized, $expiresAt, $serialized, $expiresAt]);
return true;
} catch (Exception $e) {
return false;
}
}
public function delete($key)
{
try {
$stmt = $this->conn->prepare('DELETE FROM ws_cache WHERE cache_key = ?');
$stmt->execute([$key]);
return $stmt->rowCount() > 0;
} catch (Exception $e) {
return false;
}
}
public function has($key)
{
try {
$stmt = $this->conn->prepare('
SELECT 1 FROM ws_cache
WHERE cache_key = ? AND (expires_at IS NULL OR expires_at > NOW())
');
$stmt->execute([$key]);
return $stmt->rowCount() > 0;
} catch (Exception $e) {
return false;
}
}
public function clear()
{
try {
$stmt = $this->conn->prepare('DELETE FROM ws_cache');
$stmt->execute();
return true;
} catch (Exception $e) {
return false;
}
}
public function getByTag($tag)
{
try {
$stmt = $this->conn->prepare('
SELECT c.cache_key, c.cache_value
FROM ws_cache c
JOIN ws_cache_tags ct ON c.cache_key = ct.cache_key
WHERE ct.tag_name = ? AND (c.expires_at IS NULL OR c.expires_at > NOW())
');
$stmt->execute([$tag]);
$results = [];
while ($row = $stmt->fetchAssociative()) {
$results[$row['cache_key']] = unserialize($row['cache_value']);
}
return $results;
} catch (Exception $e) {
return [];
}
}
public function deleteByTag($tag)
{
try {
$stmt = $this->conn->prepare('
DELETE c FROM ws_cache c
JOIN ws_cache_tags ct ON c.cache_key = ct.cache_key
WHERE ct.tag_name = ?
');
$stmt->execute([$tag]);
return $stmt->rowCount() > 0;
} catch (Exception $e) {
return false;
}
}
public function setWithTags($key, $value, $tags = [], $ttl = 3600)
{
try {
$this->conn->beginTransaction();
// Cache-Wert setzen
$result = $this->set($key, $value, $ttl);
if ($result) {
// Tags setzen
foreach ($tags as $tag) {
$stmt = $this->conn->prepare('
INSERT INTO ws_cache_tags (cache_key, tag_name, created_at)
VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE updated_at = NOW()
');
$stmt->execute([$key, $tag]);
}
}
$this->conn->commit();
return $result;
} catch (Exception $e) {
$this->conn->rollBack();
return false;
}
}
public function warmup($keys = [])
{
// Database-Cache benötigt kein Warmup
return true;
}
public function getStatus()
{
try {
$stmt = $this->conn->prepare('
SELECT
COUNT(*) as total_entries,
SUM(LENGTH(cache_value)) as total_size,
COUNT(CASE WHEN expires_at IS NOT NULL AND expires_at <= NOW() THEN 1 END) as expired_entries
FROM ws_cache
');
$stmt->execute();
$stats = $stmt->fetchAssociative();
return [
'driver' => 'database',
'enabled' => true,
'total_entries' => $stats['total_entries'] ?? 0,
'total_size' => $stats['total_size'] ?? 0,
'expired_entries' => $stats['expired_entries'] ?? 0
];
} catch (Exception $e) {
return [
'driver' => 'database',
'enabled' => false,
'error' => $e->getMessage()
];
}
}
}