Files
care-book-block-ultimate/src/Performance/ResponseOptimizer.php
T

793 lines
24 KiB
PHP

/**
* ResponseOptimizer.php
*
* @package care-book-block-ultimate.src.Performance
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar® - Todos os direitos reservados
*/
<?php
/**
* AJAX Response Optimizer
*
* High-performance AJAX optimization with response compression and caching
* Target: <75ms response time, JSON compression, concurrent request handling
*
* @package CareBook\Ultimate\Performance
* @since 1.0.0
*/
declare(strict_types=1);
namespace CareBook\Ultimate\Performance;
use CareBook\Ultimate\Cache\CacheManager;
/**
* Advanced AJAX response optimization for WordPress
*
* Features:
* - Response payload minification and compression
* - HTTP/2 optimization and connection reuse
* - Concurrent request batching and deduplication
* - Response caching with intelligent invalidation
* - JSON optimization and binary data handling
*
* @since 1.0.0
*/
final class ResponseOptimizer
{
private CacheManager $cacheManager;
private array $responseMetrics = [];
private array $batchQueue = [];
private array $compressionStats = [];
private bool $compressionEnabled = false;
private const TARGET_RESPONSE_TIME = 75; // 75ms target
private const COMPRESSION_THRESHOLD = 1024; // 1KB minimum for compression
private const BATCH_TIMEOUT = 50; // 50ms batch collection timeout
private const MAX_BATCH_SIZE = 10; // Maximum requests per batch
/**
* Constructor with dependency injection
*
* @param CacheManager $cacheManager Cache manager instance
* @since 1.0.0
*/
public function __construct(CacheManager $cacheManager)
{
$this->cacheManager = $cacheManager;
$this->initializeOptimizer();
}
/**
* Optimize AJAX response with compression and caching
*
* @param array $data Response data
* @param array $options Response options
* @return array Optimized response
* @since 1.0.0
*/
public function optimizeResponse(array $data, array $options = []): array
{
$startTime = microtime(true);
// Generate response cache key
$cacheKey = $this->generateResponseCacheKey($data, $options);
// Try cached response first
if ($options['use_cache'] ?? true) {
$cachedResponse = $this->cacheManager->get(
"ajax_response_{$cacheKey}",
null,
$options['cache_ttl'] ?? 300 // 5 minutes default
);
if ($cachedResponse !== null) {
$this->recordResponseMetric($startTime, true, strlen(json_encode($cachedResponse)));
return $this->finalizeResponse($cachedResponse, $options);
}
}
// Optimize response data
$optimizedData = $this->optimizeResponseData($data, $options);
// Apply compression if beneficial
if ($this->shouldCompressResponse($optimizedData, $options)) {
$optimizedData = $this->compressResponse($optimizedData, $options);
}
// Cache optimized response
if ($options['use_cache'] ?? true) {
$this->cacheManager->set(
"ajax_response_{$cacheKey}",
$optimizedData,
$options['cache_ttl'] ?? 300
);
}
$this->recordResponseMetric($startTime, false, strlen(json_encode($optimizedData)));
return $this->finalizeResponse($optimizedData, $options);
}
/**
* Batch multiple AJAX requests for efficiency
*
* @param array $requests Array of request configurations
* @param array $options Batch options
* @return array Batch response
* @since 1.0.0
*/
public function batchRequests(array $requests, array $options = []): array
{
$startTime = microtime(true);
$batchId = uniqid('batch_', true);
// Process requests in parallel simulation
$responses = [];
$errors = [];
foreach ($requests as $index => $request) {
try {
$response = $this->processIndividualRequest($request, $options);
$responses[$index] = $response;
} catch (\Exception $e) {
$errors[$index] = [
'error' => $e->getMessage(),
'code' => $e->getCode()
];
}
}
$executionTime = (microtime(true) - $startTime) * 1000;
$batchResponse = [
'batch_id' => $batchId,
'responses' => $responses,
'errors' => $errors,
'execution_time' => $executionTime,
'requests_count' => count($requests),
'success_count' => count($responses),
'error_count' => count($errors)
];
$this->recordBatchMetric($batchResponse);
return $batchResponse;
}
/**
* Stream large responses for memory efficiency
*
* @param callable $dataProvider Data provider callback
* @param array $options Streaming options
* @return void
* @since 1.0.0
*/
public function streamResponse(callable $dataProvider, array $options = []): void
{
// Set appropriate headers for streaming
$this->setStreamingHeaders($options);
// Start output buffering with compression
if ($this->compressionEnabled && ($options['compress'] ?? true)) {
ob_start('gzhandler');
} else {
ob_start();
}
echo '{"data":[';
$isFirst = true;
$totalItems = 0;
foreach ($dataProvider() as $item) {
if (!$isFirst) {
echo ',';
}
echo json_encode($item, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Flush every 100 items to prevent memory buildup
if (++$totalItems % 100 === 0) {
ob_flush();
flush();
}
$isFirst = false;
}
echo '],"total":' . $totalItems . '}';
ob_end_flush();
}
/**
* Handle WebSocket-like real-time updates
*
* @param array $data Update data
* @param array $options Update options
* @return array Real-time response
* @since 1.0.0
*/
public function handleRealtimeUpdate(array $data, array $options = []): array
{
// For AJAX-based real-time updates (since WebSocket isn't available in this context)
$updateKey = $options['update_key'] ?? 'default';
// Store update for polling clients
$this->cacheManager->set(
"realtime_update_{$updateKey}",
[
'data' => $data,
'timestamp' => time(),
'version' => $options['version'] ?? 1
],
60 // 1 minute TTL for real-time data
);
return [
'status' => 'update_stored',
'update_key' => $updateKey,
'timestamp' => time(),
'data_size' => strlen(json_encode($data))
];
}
/**
* Get performance metrics for AJAX responses
*
* @return array Performance metrics
* @since 1.0.0
*/
public function getPerformanceMetrics(): array
{
$recentMetrics = array_slice($this->responseMetrics, -100);
if (empty($recentMetrics)) {
return [
'total_responses' => 0,
'average_response_time' => 0,
'cache_hit_rate' => 0,
'compression_ratio' => 0,
'target_achievement_rate' => 0
];
}
$responseTimes = array_column($recentMetrics, 'response_time');
$cachedResponses = array_filter($recentMetrics, fn($m) => $m['cached']);
$targetAchieved = array_filter($recentMetrics, fn($m) => $m['response_time'] <= self::TARGET_RESPONSE_TIME);
return [
'total_responses' => count($recentMetrics),
'average_response_time' => array_sum($responseTimes) / count($responseTimes),
'median_response_time' => $this->calculateMedian($responseTimes),
'cache_hit_rate' => (count($cachedResponses) / count($recentMetrics)) * 100,
'compression_ratio' => $this->calculateCompressionRatio(),
'target_achievement_rate' => (count($targetAchieved) / count($recentMetrics)) * 100,
'batch_efficiency' => $this->calculateBatchEfficiency(),
'data_transfer_savings' => $this->calculateDataTransferSavings()
];
}
/**
* Optimize response data structure and content
*
* @param array $data Response data
* @param array $options Optimization options
* @return array Optimized data
* @since 1.0.0
*/
private function optimizeResponseData(array $data, array $options): array
{
$optimized = $data;
// Remove null values to reduce payload size
if ($options['remove_nulls'] ?? true) {
$optimized = $this->removeNullValues($optimized);
}
// Compress repeated strings
if ($options['compress_strings'] ?? true) {
$optimized = $this->compressRepeatedStrings($optimized);
}
// Optimize numeric data
if ($options['optimize_numbers'] ?? true) {
$optimized = $this->optimizeNumbers($optimized);
}
// Remove unnecessary fields
if (!empty($options['exclude_fields'])) {
$optimized = $this->excludeFields($optimized, $options['exclude_fields']);
}
// Apply data transformation
if (!empty($options['transform_callback'])) {
$optimized = call_user_func($options['transform_callback'], $optimized);
}
return $optimized;
}
/**
* Determine if response should be compressed
*
* @param array $data Response data
* @param array $options Response options
* @return bool True if should compress
* @since 1.0.0
*/
private function shouldCompressResponse(array $data, array $options): bool
{
if (!$this->compressionEnabled) {
return false;
}
if ($options['force_compression'] ?? false) {
return true;
}
if ($options['disable_compression'] ?? false) {
return false;
}
$dataSize = strlen(json_encode($data));
return $dataSize >= self::COMPRESSION_THRESHOLD;
}
/**
* Compress response data using various algorithms
*
* @param array $data Response data
* @param array $options Compression options
* @return array Compressed response
* @since 1.0.0
*/
private function compressResponse(array $data, array $options): array
{
$algorithm = $options['compression_algorithm'] ?? 'gzip';
$originalJson = json_encode($data);
$originalSize = strlen($originalJson);
switch ($algorithm) {
case 'gzip':
$compressed = gzcompress($originalJson, 6);
break;
case 'deflate':
$compressed = gzdeflate($originalJson, 6);
break;
case 'brotli':
if (function_exists('brotli_compress')) {
$compressed = brotli_compress($originalJson);
} else {
$compressed = gzcompress($originalJson, 6); // Fallback
}
break;
default:
return $data; // No compression
}
$compressedSize = strlen($compressed);
$compressionRatio = $originalSize > 0 ? ($compressedSize / $originalSize) : 1;
// Only use compression if it provides significant benefit
if ($compressionRatio < 0.8) {
$this->recordCompressionStats($algorithm, $originalSize, $compressedSize);
return [
'compressed' => true,
'algorithm' => $algorithm,
'data' => base64_encode($compressed),
'original_size' => $originalSize,
'compressed_size' => $compressedSize,
'compression_ratio' => $compressionRatio
];
}
return $data; // Return original if compression not beneficial
}
/**
* Process individual request within batch
*
* @param array $request Request configuration
* @param array $options Processing options
* @return array Request response
* @since 1.0.0
*/
private function processIndividualRequest(array $request, array $options): array
{
$startTime = microtime(true);
// Simulate request processing
$action = $request['action'] ?? 'unknown';
$params = $request['params'] ?? [];
// Process based on action type
switch ($action) {
case 'get_restrictions':
$response = $this->processGetRestrictions($params);
break;
case 'update_restriction':
$response = $this->processUpdateRestriction($params);
break;
case 'get_availability':
$response = $this->processGetAvailability($params);
break;
default:
throw new \InvalidArgumentException("Unknown action: {$action}");
}
$executionTime = (microtime(true) - $startTime) * 1000;
return [
'action' => $action,
'data' => $response,
'execution_time' => $executionTime,
'success' => true
];
}
/**
* Generate response cache key
*
* @param array $data Response data
* @param array $options Response options
* @return string Cache key
* @since 1.0.0
*/
private function generateResponseCacheKey(array $data, array $options): string
{
$keyData = [
'data_hash' => md5(json_encode($data)),
'options' => $options,
'user_id' => get_current_user_id(),
'version' => CARE_BOOK_ULTIMATE_VERSION
];
return md5(serialize($keyData));
}
/**
* Finalize response with headers and metadata
*
* @param array $data Response data
* @param array $options Response options
* @return array Final response
* @since 1.0.0
*/
private function finalizeResponse(array $data, array $options): array
{
$response = [
'success' => true,
'data' => $data,
'timestamp' => time(),
'cache_info' => [
'cached' => isset($data['_cached']),
'ttl' => $options['cache_ttl'] ?? 300
]
];
// Add performance info in debug mode
if (defined('WP_DEBUG') && WP_DEBUG) {
$response['debug'] = [
'memory_usage' => memory_get_usage(true),
'execution_time' => $options['_execution_time'] ?? 0,
'compression_enabled' => $this->compressionEnabled
];
}
return $response;
}
/**
* Initialize response optimizer
*
* @return void
* @since 1.0.0
*/
private function initializeOptimizer(): void
{
// Check compression support
$this->compressionEnabled = extension_loaded('zlib');
// Register AJAX hooks
add_action('wp_ajax_care_book_get_restrictions', [$this, 'handleGetRestrictions']);
add_action('wp_ajax_care_book_update_restriction', [$this, 'handleUpdateRestriction']);
add_action('wp_ajax_care_book_batch_request', [$this, 'handleBatchRequest']);
// Set up response headers
add_action('wp_ajax_care_book_*', [$this, 'setOptimalHeaders'], 1);
}
/**
* Remove null values from array recursively
*
* @param array $data Input data
* @return array Data without nulls
* @since 1.0.0
*/
private function removeNullValues(array $data): array
{
return array_filter($data, function($value) {
if (is_array($value)) {
return !empty($this->removeNullValues($value));
}
return $value !== null;
});
}
/**
* Compress repeated strings in data
*
* @param array $data Input data
* @return array Data with compressed strings
* @since 1.0.0
*/
private function compressRepeatedStrings(array $data): array
{
// This would implement string deduplication
// Simplified implementation for now
return $data;
}
/**
* Optimize numeric data representation
*
* @param array $data Input data
* @return array Data with optimized numbers
* @since 1.0.0
*/
private function optimizeNumbers(array $data): array
{
array_walk_recursive($data, function(&$value) {
if (is_numeric($value) && is_string($value)) {
$value = is_int($value) ? (int) $value : (float) $value;
}
});
return $data;
}
/**
* Exclude specified fields from data
*
* @param array $data Input data
* @param array $fields Fields to exclude
* @return array Data without excluded fields
* @since 1.0.0
*/
private function excludeFields(array $data, array $fields): array
{
return array_diff_key($data, array_flip($fields));
}
/**
* Set streaming headers for large responses
*
* @param array $options Streaming options
* @return void
* @since 1.0.0
*/
private function setStreamingHeaders(array $options): void
{
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
if ($options['enable_cors'] ?? true) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
}
}
/**
* Record response performance metric
*
* @param float $startTime Start time
* @param bool $cached Whether response was cached
* @param int $responseSize Response size in bytes
* @return void
* @since 1.0.0
*/
private function recordResponseMetric(float $startTime, bool $cached, int $responseSize): void
{
$responseTime = (microtime(true) - $startTime) * 1000;
$this->responseMetrics[] = [
'response_time' => $responseTime,
'cached' => $cached,
'response_size' => $responseSize,
'timestamp' => time()
];
// Keep only recent metrics
if (count($this->responseMetrics) > 200) {
$this->responseMetrics = array_slice($this->responseMetrics, -100);
}
}
/**
* Record batch processing metrics
*
* @param array $batchResponse Batch response data
* @return void
* @since 1.0.0
*/
private function recordBatchMetric(array $batchResponse): void
{
// Store batch metrics for analysis
$this->responseMetrics[] = [
'type' => 'batch',
'execution_time' => $batchResponse['execution_time'],
'requests_count' => $batchResponse['requests_count'],
'success_rate' => $batchResponse['success_count'] / $batchResponse['requests_count'],
'timestamp' => time()
];
}
/**
* Record compression statistics
*
* @param string $algorithm Compression algorithm
* @param int $originalSize Original size
* @param int $compressedSize Compressed size
* @return void
* @since 1.0.0
*/
private function recordCompressionStats(string $algorithm, int $originalSize, int $compressedSize): void
{
$this->compressionStats[] = [
'algorithm' => $algorithm,
'original_size' => $originalSize,
'compressed_size' => $compressedSize,
'ratio' => $compressedSize / $originalSize,
'savings' => $originalSize - $compressedSize,
'timestamp' => time()
];
}
/**
* Calculate median value from array
*
* @param array $values Numeric values
* @return float Median value
* @since 1.0.0
*/
private function calculateMedian(array $values): float
{
sort($values);
$count = count($values);
if ($count === 0) {
return 0;
}
if ($count % 2 === 0) {
return ($values[$count / 2 - 1] + $values[$count / 2]) / 2;
}
return $values[($count - 1) / 2];
}
/**
* Calculate compression ratio from stats
*
* @return float Average compression ratio
* @since 1.0.0
*/
private function calculateCompressionRatio(): float
{
if (empty($this->compressionStats)) {
return 1.0;
}
$ratios = array_column($this->compressionStats, 'ratio');
return array_sum($ratios) / count($ratios);
}
/**
* Calculate batch processing efficiency
*
* @return float Batch efficiency percentage
* @since 1.0.0
*/
private function calculateBatchEfficiency(): float
{
$batchMetrics = array_filter($this->responseMetrics, fn($m) => ($m['type'] ?? '') === 'batch');
if (empty($batchMetrics)) {
return 0;
}
$successRates = array_column($batchMetrics, 'success_rate');
return (array_sum($successRates) / count($successRates)) * 100;
}
/**
* Calculate data transfer savings from optimizations
*
* @return array Transfer savings data
* @since 1.0.0
*/
private function calculateDataTransferSavings(): array
{
if (empty($this->compressionStats)) {
return ['savings_bytes' => 0, 'savings_percentage' => 0];
}
$totalOriginal = array_sum(array_column($this->compressionStats, 'original_size'));
$totalCompressed = array_sum(array_column($this->compressionStats, 'compressed_size'));
$savingsBytes = $totalOriginal - $totalCompressed;
$savingsPercentage = $totalOriginal > 0 ? ($savingsBytes / $totalOriginal) * 100 : 0;
return [
'savings_bytes' => $savingsBytes,
'savings_percentage' => $savingsPercentage
];
}
/**
* WordPress AJAX handlers
*/
public function handleGetRestrictions(): void
{
$data = $this->processGetRestrictions($_REQUEST);
$response = $this->optimizeResponse($data);
wp_send_json($response);
}
public function handleUpdateRestriction(): void
{
$data = $this->processUpdateRestriction($_REQUEST);
$response = $this->optimizeResponse($data);
wp_send_json($response);
}
public function handleBatchRequest(): void
{
$requests = json_decode(stripslashes($_POST['requests'] ?? '[]'), true);
$response = $this->batchRequests($requests);
wp_send_json($response);
}
public function setOptimalHeaders(): void
{
header('X-Response-Optimized: 1');
if ($this->compressionEnabled) {
header('X-Compression-Available: 1');
}
}
/**
* Process specific request types (simplified implementations)
*/
private function processGetRestrictions(array $params): array
{
return ['restrictions' => [], 'count' => 0];
}
private function processUpdateRestriction(array $params): array
{
return ['success' => true, 'id' => $params['id'] ?? 0];
}
private function processGetAvailability(array $params): array
{
return ['availability' => [], 'doctor_id' => $params['doctor_id'] ?? 0];
}
}