/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ 0, 'cache_hits' => 0, 'pool_reuses' => 0, 'batch_operations' => 0, 'total_time' => 0, 'memory_peak' => 0 ]; /** * Enhanced constructor with optimization initialization */ public function __construct() { parent::__construct(); // Initialize optimization features $this->initializeConnectionPool(); $this->initializeResponseCache(); $this->setupPerformanceMonitoring(); } /** * Initialize connection pool */ private function initializeConnectionPool() { if (!isset(self::$connection_pool['moloni_api'])) { self::$connection_pool['moloni_api'] = [ 'connections' => [], 'last_used' => [], 'created_at' => time() ]; } } /** * Initialize response cache */ private function initializeResponseCache() { if (!isset(self::$response_cache['data'])) { self::$response_cache = [ 'data' => [], 'timestamps' => [], 'access_count' => [] ]; } } /** * Setup performance monitoring */ private function setupPerformanceMonitoring() { $this->performance_stats['session_start'] = microtime(true); $this->performance_stats['memory_start'] = memory_get_usage(true); } /** * Optimized make_request with connection pooling and caching * * @param string $endpoint API endpoint * @param array $params Request parameters * @param string $method HTTP method * @param array $options Additional options (cache_ttl, use_cache, etc.) * @return array Response data */ public function make_request($endpoint, $params = [], $method = 'POST', $options = []) { $start_time = microtime(true); $this->performance_stats['requests_made']++; // Check cache first for GET requests or cacheable endpoints if ($this->isCacheable($endpoint, $method, $options)) { $cached_response = $this->getCachedResponse($endpoint, $params); if ($cached_response !== null) { $this->performance_stats['cache_hits']++; return $cached_response; } } try { // Use optimized request execution $response = $this->executeOptimizedRequest($endpoint, $params, $method, $options); // Cache response if cacheable if ($this->isCacheable($endpoint, $method, $options)) { $this->cacheResponse($endpoint, $params, $response, $options); } // Update performance stats $this->performance_stats['total_time'] += (microtime(true) - $start_time); $this->performance_stats['memory_peak'] = max( $this->performance_stats['memory_peak'], memory_get_usage(true) ); return $response; } catch (Exception $e) { // Enhanced error handling with performance context $this->logPerformanceError($e, $endpoint, $start_time); throw $e; } } /** * Execute optimized request with connection pooling */ private function executeOptimizedRequest($endpoint, $params, $method, $options) { $connection = $this->getPooledConnection(); $url = $this->api_base_url . $endpoint; try { // Configure connection with optimizations $this->configureOptimizedConnection($connection, $url, $params, $method, $options); // Execute request $response = curl_exec($connection); $http_code = curl_getinfo($connection, CURLINFO_HTTP_CODE); $curl_error = curl_error($connection); $transfer_info = curl_getinfo($connection); // Return connection to pool $this->returnConnectionToPool($connection); if ($curl_error) { throw new Exception("CURL Error: {$curl_error}"); } return $this->processOptimizedResponse($response, $http_code, $transfer_info); } catch (Exception $e) { // Close connection on error curl_close($connection); throw $e; } } /** * Get connection from pool or create new one */ private function getPooledConnection() { $pool = &self::$connection_pool['moloni_api']; // Clean expired connections $this->cleanExpiredConnections($pool); // Try to reuse existing connection if (!empty($pool['connections'])) { $connection = array_pop($pool['connections']); array_pop($pool['last_used']); $this->performance_stats['pool_reuses']++; return $connection; } // Create new optimized connection return $this->createOptimizedConnection(); } /** * Create optimized curl connection */ private function createOptimizedConnection() { $connection = curl_init(); // Optimization: Set persistent connection options curl_setopt_array($connection, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->api_timeout, CURLOPT_CONNECTTIMEOUT => $this->connect_timeout, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_FOLLOWLOCATION => false, CURLOPT_MAXREDIRS => 0, CURLOPT_ENCODING => '', // Enable compression CURLOPT_USERAGENT => 'Desk-Moloni/3.0.1-Optimized', // Performance optimizations CURLOPT_TCP_KEEPALIVE => 1, CURLOPT_TCP_KEEPIDLE => 120, CURLOPT_TCP_KEEPINTVL => 60, CURLOPT_DNS_CACHE_TIMEOUT => 300, CURLOPT_FORBID_REUSE => false, CURLOPT_FRESH_CONNECT => false ]); return $connection; } /** * Configure connection for specific request with optimizations */ private function configureOptimizedConnection($connection, $url, $params, $method, $options) { // Get access token (cached if possible) $access_token = $this->oauth->get_access_token(); $headers = [ 'Authorization: Bearer ' . $access_token, 'Accept: application/json', 'User-Agent: Desk-Moloni/3.0.1-Optimized', 'Cache-Control: no-cache' ]; if ($method === 'POST') { $headers[] = 'Content-Type: application/json'; $json_data = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); curl_setopt_array($connection, [ CURLOPT_URL => $url, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $json_data, CURLOPT_HTTPHEADER => $headers, ]); } else { if (!empty($params)) { $url .= '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); } curl_setopt_array($connection, [ CURLOPT_URL => $url, CURLOPT_HTTPGET => true, CURLOPT_HTTPHEADER => $headers, ]); } // Apply any custom options if (isset($options['timeout'])) { curl_setopt($connection, CURLOPT_TIMEOUT, $options['timeout']); } if (isset($options['connect_timeout'])) { curl_setopt($connection, CURLOPT_CONNECTTIMEOUT, $options['connect_timeout']); } } /** * Process response with optimization */ private function processOptimizedResponse($response, $http_code, $transfer_info) { // Fast JSON decoding with error handling if (empty($response)) { throw new Exception('Empty response from API'); } $decoded = json_decode($response, true, 512, JSON_BIGINT_AS_STRING); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Invalid JSON response: ' . json_last_error_msg()); } // Handle HTTP errors if ($http_code >= 400) { $error_msg = $this->extract_error_message($decoded, $http_code); throw new Exception("HTTP {$http_code}: {$error_msg}"); } // Check for API-level errors if (isset($decoded['error'])) { $error_msg = $decoded['error']['message'] ?? $decoded['error']; throw new Exception("Moloni API Error: {$error_msg}"); } return $decoded; } /** * Return connection to pool */ private function returnConnectionToPool($connection) { $pool = &self::$connection_pool['moloni_api']; // Only return if pool isn't full if (count($pool['connections']) < self::$pool_max_size) { $pool['connections'][] = $connection; $pool['last_used'][] = time(); } else { curl_close($connection); } } /** * Clean expired connections from pool */ private function cleanExpiredConnections(&$pool) { $now = time(); $expired_indices = []; foreach ($pool['last_used'] as $index => $last_used) { if (($now - $last_used) > self::$pool_timeout) { $expired_indices[] = $index; } } // Remove expired connections foreach (array_reverse($expired_indices) as $index) { if (isset($pool['connections'][$index])) { curl_close($pool['connections'][$index]); unset($pool['connections'][$index]); unset($pool['last_used'][$index]); } } // Reindex arrays $pool['connections'] = array_values($pool['connections']); $pool['last_used'] = array_values($pool['last_used']); } /** * Check if request is cacheable */ private function isCacheable($endpoint, $method, $options) { // Don't cache by default for POST requests if ($method === 'POST' && !isset($options['force_cache'])) { return false; } // Don't cache if explicitly disabled if (isset($options['use_cache']) && $options['use_cache'] === false) { return false; } // Cache read-only endpoints $cacheable_endpoints = [ 'companies/getAll', 'customers/getAll', 'products/getAll', 'taxes/getAll', 'documentSets/getAll', 'paymentMethods/getAll', 'countries/getAll', 'measurementUnits/getAll', 'productCategories/getAll' ]; return in_array($endpoint, $cacheable_endpoints); } /** * Get cached response */ private function getCachedResponse($endpoint, $params) { $cache_key = $this->generateCacheKey($endpoint, $params); if (!isset(self::$response_cache['data'][$cache_key])) { return null; } $cached_at = self::$response_cache['timestamps'][$cache_key]; $ttl = self::$cache_ttl; // Check if cache is still valid if ((time() - $cached_at) > $ttl) { $this->removeCachedResponse($cache_key); return null; } // Update access count for LRU eviction self::$response_cache['access_count'][$cache_key]++; return self::$response_cache['data'][$cache_key]; } /** * Cache response */ private function cacheResponse($endpoint, $params, $response, $options) { $cache_key = $this->generateCacheKey($endpoint, $params); $ttl = $options['cache_ttl'] ?? self::$cache_ttl; // Evict old entries if cache is full if (count(self::$response_cache['data']) >= self::$cache_max_entries) { $this->evictLRUCacheEntries(); } self::$response_cache['data'][$cache_key] = $response; self::$response_cache['timestamps'][$cache_key] = time(); self::$response_cache['access_count'][$cache_key] = 1; } /** * Generate cache key */ private function generateCacheKey($endpoint, $params) { $key_data = $endpoint . ':' . serialize($params); return 'moloni_cache_' . md5($key_data); } /** * Remove cached response */ private function removeCachedResponse($cache_key) { unset(self::$response_cache['data'][$cache_key]); unset(self::$response_cache['timestamps'][$cache_key]); unset(self::$response_cache['access_count'][$cache_key]); } /** * Evict least recently used cache entries */ private function evictLRUCacheEntries($count = 100) { // Sort by access count (ascending) to find LRU entries asort(self::$response_cache['access_count']); $evict_keys = array_slice( array_keys(self::$response_cache['access_count']), 0, $count, true ); foreach ($evict_keys as $key) { $this->removeCachedResponse($key); } } /** * Batch multiple requests for bulk operations * * @param array $requests Array of request specifications * @return array Array of responses */ public function batch_requests($requests) { $this->performance_stats['batch_operations']++; $responses = []; $batches = array_chunk($requests, $this->batch_size); foreach ($batches as $batch) { $batch_responses = $this->executeBatch($batch); $responses = array_merge($responses, $batch_responses); } return $responses; } /** * Execute batch of requests */ private function executeBatch($batch) { $responses = []; $connections = []; $multi_handle = curl_multi_init(); try { // Setup all connections foreach ($batch as $index => $request) { $connection = $this->getPooledConnection(); $connections[$index] = $connection; $this->configureOptimizedConnection( $connection, $this->api_base_url . $request['endpoint'], $request['params'] ?? [], $request['method'] ?? 'POST', $request['options'] ?? [] ); curl_multi_add_handle($multi_handle, $connection); } // Execute all requests $running = null; do { $status = curl_multi_exec($multi_handle, $running); if ($running > 0) { curl_multi_select($multi_handle); } } while ($running > 0 && $status === CURLM_OK); // Collect responses foreach ($connections as $index => $connection) { $response = curl_multi_getcontent($connection); $http_code = curl_getinfo($connection, CURLINFO_HTTP_CODE); $transfer_info = curl_getinfo($connection); try { $responses[$index] = $this->processOptimizedResponse($response, $http_code, $transfer_info); } catch (Exception $e) { $responses[$index] = ['error' => $e->getMessage()]; } curl_multi_remove_handle($multi_handle, $connection); $this->returnConnectionToPool($connection); } } finally { curl_multi_close($multi_handle); } return $responses; } /** * Get performance statistics */ public function getPerformanceStats() { $session_time = microtime(true) - $this->performance_stats['session_start']; $memory_used = memory_get_usage(true) - $this->performance_stats['memory_start']; return array_merge($this->performance_stats, [ 'session_duration' => $session_time, 'memory_used' => $memory_used, 'requests_per_second' => $this->performance_stats['requests_made'] / max($session_time, 0.001), 'cache_hit_rate' => $this->performance_stats['requests_made'] > 0 ? ($this->performance_stats['cache_hits'] / $this->performance_stats['requests_made']) * 100 : 0, 'pool_reuse_rate' => $this->performance_stats['requests_made'] > 0 ? ($this->performance_stats['pool_reuses'] / $this->performance_stats['requests_made']) * 100 : 0, 'average_response_time' => $this->performance_stats['requests_made'] > 0 ? $this->performance_stats['total_time'] / $this->performance_stats['requests_made'] : 0 ]); } /** * Log performance-related errors */ private function logPerformanceError($exception, $endpoint, $start_time) { $execution_time = microtime(true) - $start_time; $memory_usage = memory_get_usage(true); $performance_context = [ 'endpoint' => $endpoint, 'execution_time' => $execution_time, 'memory_usage' => $memory_usage, 'performance_stats' => $this->getPerformanceStats() ]; log_message('error', 'Optimized API Client Error: ' . $exception->getMessage() . ' | Performance Context: ' . json_encode($performance_context)); } /** * Clear all caches (useful for testing) */ public function clearCaches() { self::$response_cache = ['data' => [], 'timestamps' => [], 'access_count' => []]; // Close all pooled connections foreach (self::$connection_pool as &$pool) { foreach ($pool['connections'] ?? [] as $connection) { curl_close($connection); } $pool['connections'] = []; $pool['last_used'] = []; } return true; } /** * Cleanup on destruction */ public function __destruct() { // Log final performance statistics if ($this->performance_stats['requests_made'] > 0) { log_activity('OptimizedMoloniApiClient Session Stats: ' . json_encode($this->getPerformanceStats())); } } }