462 lines
13 KiB
PHP
462 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* Copyright seit 2024 Webshop System
|
|
*
|
|
* Performance-Core-Klasse für das Webshop-System
|
|
*
|
|
* @author Webshop System
|
|
* @license GPL v3
|
|
*/
|
|
|
|
namespace App\Core;
|
|
|
|
class Performance
|
|
{
|
|
private $startTime;
|
|
private $memoryStart;
|
|
private $queries = [];
|
|
private $cache;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->startTime = microtime(true);
|
|
$this->memoryStart = memory_get_usage();
|
|
$this->cache = new Cache();
|
|
}
|
|
|
|
/**
|
|
* Performance-Monitoring starten
|
|
*/
|
|
public function startMonitoring()
|
|
{
|
|
// Query-Logging aktivieren
|
|
if (getenv('WS_DEBUG') === 'true') {
|
|
$this->enableQueryLogging();
|
|
}
|
|
|
|
// Gzip-Kompression aktivieren
|
|
$this->enableGzip();
|
|
|
|
// Browser-Caching aktivieren
|
|
$this->setBrowserCache();
|
|
}
|
|
|
|
/**
|
|
* Performance-Statistiken abrufen
|
|
*/
|
|
public function getStats()
|
|
{
|
|
$endTime = microtime(true);
|
|
$memoryEnd = memory_get_usage();
|
|
$peakMemory = memory_get_peak_usage();
|
|
|
|
return [
|
|
'execution_time' => round(($endTime - $this->startTime) * 1000, 2), // ms
|
|
'memory_usage' => $this->formatBytes($memoryEnd - $this->memoryStart),
|
|
'peak_memory' => $this->formatBytes($peakMemory),
|
|
'queries_count' => count($this->queries),
|
|
'cache_hits' => $this->cache->getStats()['hits'] ?? 0,
|
|
'cache_misses' => $this->cache->getStats()['misses'] ?? 0
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Query-Logging aktivieren
|
|
*/
|
|
private function enableQueryLogging()
|
|
{
|
|
// Query-Logger registrieren
|
|
register_shutdown_function([$this, 'logQueries']);
|
|
}
|
|
|
|
/**
|
|
* Gzip-Kompression aktivieren
|
|
*/
|
|
private function enableGzip()
|
|
{
|
|
if (extension_loaded('zlib') && !ini_get('zlib.output_compression')) {
|
|
ini_set('zlib.output_compression', 1);
|
|
ini_set('zlib.output_compression_level', 6);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Browser-Caching setzen
|
|
*/
|
|
private function setBrowserCache()
|
|
{
|
|
$cacheTime = 3600; // 1 Stunde
|
|
|
|
header('Cache-Control: public, max-age=' . $cacheTime);
|
|
header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + $cacheTime));
|
|
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T'));
|
|
}
|
|
|
|
/**
|
|
* Query hinzufügen
|
|
*/
|
|
public function addQuery($sql, $params = [], $executionTime = 0)
|
|
{
|
|
$this->queries[] = [
|
|
'sql' => $sql,
|
|
'params' => $params,
|
|
'execution_time' => $executionTime,
|
|
'timestamp' => microtime(true)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Queries loggen
|
|
*/
|
|
public function logQueries()
|
|
{
|
|
if (empty($this->queries)) {
|
|
return;
|
|
}
|
|
|
|
$logFile = __DIR__ . '/../../logs/queries.log';
|
|
$logDir = dirname($logFile);
|
|
|
|
if (!is_dir($logDir)) {
|
|
mkdir($logDir, 0755, true);
|
|
}
|
|
|
|
$log = "=== Query Log " . date('Y-m-d H:i:s') . " ===\n";
|
|
$log .= "Total Queries: " . count($this->queries) . "\n";
|
|
$log .= "Total Time: " . array_sum(array_column($this->queries, 'execution_time')) . "ms\n\n";
|
|
|
|
foreach ($this->queries as $i => $query) {
|
|
$log .= "Query " . ($i + 1) . ":\n";
|
|
$log .= "SQL: " . $query['sql'] . "\n";
|
|
$log .= "Params: " . json_encode($query['params']) . "\n";
|
|
$log .= "Time: " . $query['execution_time'] . "ms\n\n";
|
|
}
|
|
|
|
file_put_contents($logFile, $log, FILE_APPEND | LOCK_EX);
|
|
}
|
|
|
|
/**
|
|
* Bild-Optimierung
|
|
*/
|
|
public function optimizeImage($sourcePath, $destinationPath, $options = [])
|
|
{
|
|
$defaultOptions = [
|
|
'quality' => 85,
|
|
'max_width' => 1200,
|
|
'max_height' => 1200,
|
|
'format' => 'jpeg'
|
|
];
|
|
|
|
$options = array_merge($defaultOptions, $options);
|
|
|
|
if (!file_exists($sourcePath)) {
|
|
return false;
|
|
}
|
|
|
|
$imageInfo = getimagesize($sourcePath);
|
|
if (!$imageInfo) {
|
|
return false;
|
|
}
|
|
|
|
list($width, $height, $type) = $imageInfo;
|
|
|
|
// Neue Dimensionen berechnen
|
|
$ratio = min($options['max_width'] / $width, $options['max_height'] / $height);
|
|
$newWidth = round($width * $ratio);
|
|
$newHeight = round($height * $ratio);
|
|
|
|
// Bild laden
|
|
switch ($type) {
|
|
case IMAGETYPE_JPEG:
|
|
$source = imagecreatefromjpeg($sourcePath);
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
$source = imagecreatefrompng($sourcePath);
|
|
break;
|
|
case IMAGETYPE_GIF:
|
|
$source = imagecreatefromgif($sourcePath);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
// Neues Bild erstellen
|
|
$destination = imagecreatetruecolor($newWidth, $newHeight);
|
|
|
|
// Transparenz für PNG beibehalten
|
|
if ($type === IMAGETYPE_PNG) {
|
|
imagealphablending($destination, false);
|
|
imagesavealpha($destination, true);
|
|
}
|
|
|
|
// Bild skalieren
|
|
imagecopyresampled($destination, $source, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
|
|
|
|
// Verzeichnis erstellen
|
|
$dir = dirname($destinationPath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
|
|
// Bild speichern
|
|
$result = false;
|
|
switch ($options['format']) {
|
|
case 'jpeg':
|
|
$result = imagejpeg($destination, $destinationPath, $options['quality']);
|
|
break;
|
|
case 'png':
|
|
$result = imagepng($destination, $destinationPath, round($options['quality'] / 10));
|
|
break;
|
|
case 'gif':
|
|
$result = imagegif($destination, $destinationPath);
|
|
break;
|
|
}
|
|
|
|
imagedestroy($source);
|
|
imagedestroy($destination);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* CSS/JS-Minifizierung
|
|
*/
|
|
public function minify($content, $type = 'css')
|
|
{
|
|
if ($type === 'css') {
|
|
return $this->minifyCSS($content);
|
|
} elseif ($type === 'js') {
|
|
return $this->minifyJS($content);
|
|
}
|
|
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* CSS minifizieren
|
|
*/
|
|
private function minifyCSS($css)
|
|
{
|
|
// Kommentare entfernen
|
|
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
|
|
|
|
// Whitespace entfernen
|
|
$css = preg_replace('/\s+/', ' ', $css);
|
|
$css = str_replace(['; ', ' {', '{ ', ' }', '} ', ': '], [';', '{', '{', '}', '}', ':'], $css);
|
|
|
|
// Leerzeichen um Operatoren entfernen
|
|
$css = preg_replace('/\s*([{}:;,>~+^$])\s*/', '$1', $css);
|
|
|
|
return trim($css);
|
|
}
|
|
|
|
/**
|
|
* JavaScript minifizieren
|
|
*/
|
|
private function minifyJS($js)
|
|
{
|
|
// Kommentare entfernen
|
|
$js = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $js);
|
|
$js = preg_replace('/\/\/.*$/m', '', $js);
|
|
|
|
// Whitespace entfernen
|
|
$js = preg_replace('/\s+/', ' ', $js);
|
|
$js = preg_replace('/\s*([{}:;,()])\s*/', '$1', $js);
|
|
|
|
return trim($js);
|
|
}
|
|
|
|
/**
|
|
* Asset-Versioning
|
|
*/
|
|
public function getAssetVersion($file)
|
|
{
|
|
$filePath = __DIR__ . '/../../public/' . $file;
|
|
|
|
if (file_exists($filePath)) {
|
|
return filemtime($filePath);
|
|
}
|
|
|
|
return time();
|
|
}
|
|
|
|
/**
|
|
* Lazy Loading für Bilder
|
|
*/
|
|
public function addLazyLoading($html)
|
|
{
|
|
// data-src zu src konvertieren für Lazy Loading
|
|
$html = preg_replace('/<img([^>]*?)src=(["\'])([^"\']+)\2([^>]*?)>/i',
|
|
'<img$1src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-src="$3"$4>', $html);
|
|
|
|
// Lazy Loading JavaScript hinzufügen
|
|
$lazyScript = '
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
var lazyImages = [].slice.call(document.querySelectorAll("img[data-src]"));
|
|
|
|
if ("IntersectionObserver" in window) {
|
|
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
|
|
entries.forEach(function(entry) {
|
|
if (entry.isIntersecting) {
|
|
let lazyImage = entry.target;
|
|
lazyImage.src = lazyImage.dataset.src;
|
|
lazyImage.classList.remove("lazy");
|
|
lazyImageObserver.unobserve(lazyImage);
|
|
}
|
|
});
|
|
});
|
|
|
|
lazyImages.forEach(function(lazyImage) {
|
|
lazyImageObserver.observe(lazyImage);
|
|
});
|
|
}
|
|
});
|
|
</script>';
|
|
|
|
return $html . $lazyScript;
|
|
}
|
|
|
|
/**
|
|
* CDN-URL generieren
|
|
*/
|
|
public function getCdnUrl($path)
|
|
{
|
|
$cdnUrl = getenv('CDN_URL');
|
|
|
|
if ($cdnUrl) {
|
|
return rtrim($cdnUrl, '/') . '/' . ltrim($path, '/');
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Critical CSS extrahieren
|
|
*/
|
|
public function extractCriticalCSS($html, $css)
|
|
{
|
|
// Einfache Critical CSS Extraktion
|
|
$criticalSelectors = [
|
|
'body', 'html', '.container', '.navbar', '.header', '.footer',
|
|
'.btn', '.btn-primary', '.form-control', '.card', '.alert'
|
|
];
|
|
|
|
$criticalCSS = '';
|
|
$lines = explode("\n", $css);
|
|
|
|
foreach ($lines as $line) {
|
|
foreach ($criticalSelectors as $selector) {
|
|
if (strpos($line, $selector) !== false) {
|
|
$criticalCSS .= $line . "\n";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->minifyCSS($criticalCSS);
|
|
}
|
|
|
|
/**
|
|
* Service Worker generieren
|
|
*/
|
|
public function generateServiceWorker()
|
|
{
|
|
$cacheName = 'webshop-v1';
|
|
$assets = [
|
|
'/css/bootstrap.min.css',
|
|
'/js/bootstrap.bundle.min.js',
|
|
'/img/logo.png'
|
|
];
|
|
|
|
$sw = "
|
|
const CACHE_NAME = '$cacheName';
|
|
const urlsToCache = " . json_encode($assets) . ";
|
|
|
|
self.addEventListener('install', function(event) {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME)
|
|
.then(function(cache) {
|
|
return cache.addAll(urlsToCache);
|
|
})
|
|
);
|
|
});
|
|
|
|
self.addEventListener('fetch', function(event) {
|
|
event.respondWith(
|
|
caches.match(event.request)
|
|
.then(function(response) {
|
|
if (response) {
|
|
return response;
|
|
}
|
|
return fetch(event.request);
|
|
}
|
|
)
|
|
);
|
|
});
|
|
";
|
|
|
|
return $sw;
|
|
}
|
|
|
|
/**
|
|
* Bytes formatieren
|
|
*/
|
|
private function formatBytes($bytes, $precision = 2)
|
|
{
|
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
|
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
|
$bytes /= 1024;
|
|
}
|
|
|
|
return round($bytes, $precision) . ' ' . $units[$i];
|
|
}
|
|
|
|
/**
|
|
* Performance-Header setzen
|
|
*/
|
|
public function setPerformanceHeaders()
|
|
{
|
|
// Security Headers
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
|
|
// Performance Headers
|
|
header('Connection: keep-alive');
|
|
header('Keep-Alive: timeout=5, max=1000');
|
|
}
|
|
|
|
/**
|
|
* Datenbank-Optimierung
|
|
*/
|
|
public function optimizeDatabase()
|
|
{
|
|
try {
|
|
$connectionParams = [
|
|
'dbname' => getenv('DB_DATABASE') ?: 'freeshop',
|
|
'user' => getenv('DB_USERNAME') ?: 'freeshop_user',
|
|
'password' => getenv('DB_PASSWORD') ?: 'freeshop_password',
|
|
'host' => getenv('DB_HOST') ?: 'db',
|
|
'driver' => 'pdo_mysql',
|
|
'port' => getenv('DB_PORT') ?: 3306,
|
|
'charset' => 'utf8mb4',
|
|
];
|
|
|
|
$conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams);
|
|
|
|
// Tabellen optimieren
|
|
$tables = ['ws_product', 'ws_category', 'ws_order', 'ws_customer'];
|
|
|
|
foreach ($tables as $table) {
|
|
$conn->executeStatement("OPTIMIZE TABLE $table");
|
|
}
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|