$objectPaths * @param array $context */ public function purgeArtworkObjectPaths(array $objectPaths, array $context = []): bool { $urls = array_values(array_unique(array_filter(array_map( fn (mixed $path): ?string => is_string($path) && trim($path) !== '' ? $this->cdnUrlForObjectPath($path) : null, $objectPaths, )))); return $this->purgeUrls($urls, $context); } /** * @param array $variants * @param array $context */ public function purgeArtworkHashVariants(string $hash, string $extension = 'webp', array $variants = ['xs', 'sm', 'md', 'lg', 'xl', 'sq'], array $context = []): bool { $urls = array_values(array_unique(array_filter(array_map( fn (string $variant): ?string => ThumbnailService::fromHash($hash, $extension, $variant), $variants, )))); return $this->purgeUrls($urls, $context + ['hash' => $hash]); } /** * @param array $urls * @param array $context */ private function purgeUrls(array $urls, array $context = []): bool { if ($urls === []) { return false; } if ($this->hasCloudflareCredentials()) { return $this->purgeViaCloudflare($urls, $context); } $legacyPurgeUrl = trim((string) config('cdn.purge_url', '')); if ($legacyPurgeUrl !== '') { return $this->purgeViaLegacyWebhook($legacyPurgeUrl, $urls, $context); } Log::debug('CDN purge skipped - no Cloudflare or legacy purge configuration is available', $context + [ 'url_count' => count($urls), ]); return false; } private function purgeViaCloudflare(array $urls, array $context): bool { $purgeUrl = sprintf( 'https://api.cloudflare.com/client/v4/zones/%s/purge_cache', trim((string) config('cdn.cloudflare.zone_id')), ); try { $response = Http::timeout(10) ->acceptJson() ->withToken(trim((string) config('cdn.cloudflare.api_token'))) ->post($purgeUrl, ['files' => $urls]); if ($response->successful()) { return true; } Log::warning('Cloudflare artwork CDN purge failed', $context + [ 'status' => $response->status(), 'body' => $response->body(), 'url_count' => count($urls), ]); } catch (\Throwable $e) { Log::warning('Cloudflare artwork CDN purge threw an exception', $context + [ 'error' => $e->getMessage(), 'url_count' => count($urls), ]); } return false; } private function purgeViaLegacyWebhook(string $purgeUrl, array $urls, array $context): bool { $paths = array_values(array_unique(array_filter(array_map(function (string $url): ?string { $path = parse_url($url, PHP_URL_PATH); return is_string($path) && $path !== '' ? $path : null; }, $urls)))); if ($paths === []) { return false; } try { $response = Http::timeout(10)->acceptJson()->post($purgeUrl, ['paths' => $paths]); if ($response->successful()) { return true; } Log::warning('Legacy artwork CDN purge failed', $context + [ 'status' => $response->status(), 'body' => $response->body(), 'path_count' => count($paths), ]); } catch (\Throwable $e) { Log::warning('Legacy artwork CDN purge threw an exception', $context + [ 'error' => $e->getMessage(), 'path_count' => count($paths), ]); } return false; } private function hasCloudflareCredentials(): bool { return trim((string) config('cdn.cloudflare.zone_id', '')) !== '' && trim((string) config('cdn.cloudflare.api_token', '')) !== ''; } private function cdnUrlForObjectPath(string $objectPath): string { return rtrim((string) config('cdn.files_url', 'https://cdn.skinbase.org'), '/') . '/' . ltrim($objectPath, '/'); } }