feat: extract CDN providers from app/Plug/Cdn
Bunny CDN (Purge, Stats), CdnManager, domain contracts (HasStats, Purgeable) with Core\Plug\Cdn namespace alignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2de9e3dcb6
commit
fb78c05c32
6 changed files with 500 additions and 0 deletions
17
composer.json
Normal file
17
composer.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "core/php-plug-cdn",
|
||||
"description": "CDN provider integrations for the Plug framework",
|
||||
"type": "library",
|
||||
"license": "EUPL-1.2",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"core/php": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Core\\Plug\\Cdn\\": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
166
src/Bunny/Purge.php
Normal file
166
src/Bunny/Purge.php
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Cdn\Bunny;
|
||||
|
||||
use Core\Plug\Cdn\Contract\Purgeable;
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Bunny CDN cache purging operations.
|
||||
*
|
||||
* Supports:
|
||||
* - Single URL purge
|
||||
* - Bulk URL purge
|
||||
* - Full cache purge
|
||||
* - Tag-based purge (for workspace isolation)
|
||||
*/
|
||||
class Purge implements Purgeable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use UsesHttp;
|
||||
|
||||
protected string $apiKey;
|
||||
|
||||
protected string $pullZoneId;
|
||||
|
||||
protected string $baseUrl = 'https://api.bunny.net';
|
||||
|
||||
public function __construct(?string $apiKey = null, ?string $pullZoneId = null)
|
||||
{
|
||||
$this->apiKey = $apiKey ?? config('cdn.bunny.api_key') ?? '';
|
||||
$this->pullZoneId = $pullZoneId ?? config('cdn.bunny.pull_zone_id') ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is configured.
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! empty($this->apiKey) && ! empty($this->pullZoneId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge a single URL from cache.
|
||||
*/
|
||||
public function url(string $url): Response
|
||||
{
|
||||
return $this->urls([$url]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge multiple URLs from cache.
|
||||
*
|
||||
* @param array<string> $urls
|
||||
*/
|
||||
public function urls(array $urls): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny CDN not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
$failed = [];
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$response = $this->http()
|
||||
->withHeaders(['AccessKey' => $this->apiKey])
|
||||
->post("{$this->baseUrl}/purge", ['url' => $url]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
$failed[] = $url;
|
||||
Log::error('Bunny CDN: Purge failed', [
|
||||
'url' => $url,
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($failed)) {
|
||||
return $this->error('Some URLs failed to purge', [
|
||||
'failed' => $failed,
|
||||
'total' => count($urls),
|
||||
'failed_count' => count($failed),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->ok([
|
||||
'purged' => count($urls),
|
||||
'urls' => $urls,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny CDN: Purge exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge entire pull zone cache.
|
||||
*/
|
||||
public function all(): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny CDN not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->http()
|
||||
->withHeaders(['AccessKey' => $this->apiKey])
|
||||
->post("{$this->baseUrl}/pullzone/{$this->pullZoneId}/purgeCache");
|
||||
|
||||
return $this->fromHttp($response, fn () => ['purged' => 'all']);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny CDN: PurgeAll exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge by cache tag.
|
||||
*
|
||||
* Useful for workspace isolation - tag content with workspace ID.
|
||||
*/
|
||||
public function tag(string $tag): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny CDN not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->http()
|
||||
->withHeaders(['AccessKey' => $this->apiKey])
|
||||
->post("{$this->baseUrl}/pullzone/{$this->pullZoneId}/purgeCache", [
|
||||
'CacheTag' => $tag,
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, fn () => [
|
||||
'purged' => 'tag',
|
||||
'tag' => $tag,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny CDN: PurgeByTag exception', [
|
||||
'tag' => $tag,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge all cached content for a workspace.
|
||||
*
|
||||
* Convenience method using tag-based purging.
|
||||
*/
|
||||
public function workspace(string $workspaceUuid): Response
|
||||
{
|
||||
return $this->tag("workspace-{$workspaceUuid}");
|
||||
}
|
||||
}
|
||||
109
src/Bunny/Stats.php
Normal file
109
src/Bunny/Stats.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Cdn\Bunny;
|
||||
|
||||
use Core\Plug\Cdn\Contract\HasStats;
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Bunny CDN statistics and bandwidth reporting.
|
||||
*
|
||||
* Provides:
|
||||
* - Traffic statistics
|
||||
* - Bandwidth usage
|
||||
* - Cache hit rates
|
||||
* - Origin traffic
|
||||
*/
|
||||
class Stats implements HasStats
|
||||
{
|
||||
use BuildsResponse;
|
||||
use UsesHttp;
|
||||
|
||||
protected string $apiKey;
|
||||
|
||||
protected string $pullZoneId;
|
||||
|
||||
protected string $baseUrl = 'https://api.bunny.net';
|
||||
|
||||
public function __construct(?string $apiKey = null, ?string $pullZoneId = null)
|
||||
{
|
||||
$this->apiKey = $apiKey ?? config('cdn.bunny.api_key') ?? '';
|
||||
$this->pullZoneId = $pullZoneId ?? config('cdn.bunny.pull_zone_id') ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is configured.
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! empty($this->apiKey) && ! empty($this->pullZoneId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CDN statistics for date range.
|
||||
*
|
||||
* @param string|null $from Start date (ISO 8601)
|
||||
* @param string|null $to End date (ISO 8601)
|
||||
*/
|
||||
public function stats(?string $from = null, ?string $to = null): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny CDN not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
$params = ['pullZone' => $this->pullZoneId];
|
||||
|
||||
if ($from) {
|
||||
$params['dateFrom'] = $from;
|
||||
}
|
||||
if ($to) {
|
||||
$params['dateTo'] = $to;
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['AccessKey' => $this->apiKey])
|
||||
->get("{$this->baseUrl}/statistics", $params);
|
||||
|
||||
return $this->fromHttp($response, fn (array $data) => [
|
||||
'total_bandwidth' => $data['TotalBandwidthUsed'] ?? 0,
|
||||
'cache_hit_rate' => $data['CacheHitRate'] ?? 0,
|
||||
'origin_traffic' => $data['TotalOriginTraffic'] ?? 0,
|
||||
'requests_served' => $data['TotalRequestsServed'] ?? 0,
|
||||
'error_4xx' => $data['Error4xxCount'] ?? 0,
|
||||
'error_5xx' => $data['Error5xxCount'] ?? 0,
|
||||
'raw' => $data,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny CDN: Stats exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bandwidth usage for date range.
|
||||
*
|
||||
* @param string|null $from Start date (ISO 8601)
|
||||
* @param string|null $to End date (ISO 8601)
|
||||
*/
|
||||
public function bandwidth(?string $from = null, ?string $to = null): Response
|
||||
{
|
||||
$statsResponse = $this->stats($from, $to);
|
||||
|
||||
if ($statsResponse->hasError()) {
|
||||
return $statsResponse;
|
||||
}
|
||||
|
||||
return $this->ok([
|
||||
'total_bandwidth' => $statsResponse->get('total_bandwidth', 0),
|
||||
'cached_bandwidth' => $statsResponse->get('cache_hit_rate', 0),
|
||||
'origin_bandwidth' => $statsResponse->get('origin_traffic', 0),
|
||||
]);
|
||||
}
|
||||
}
|
||||
144
src/CdnManager.php
Normal file
144
src/CdnManager.php
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Cdn;
|
||||
|
||||
use Core\Plug\Cdn\Contract\HasStats;
|
||||
use Core\Plug\Cdn\Contract\Purgeable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* CDN Manager - Factory for CDN provider operations.
|
||||
*
|
||||
* Resolves CDN providers from config and provides type-safe access to operations.
|
||||
*
|
||||
* @example
|
||||
* $cdn = app(CdnManager::class);
|
||||
*
|
||||
* // Get operations
|
||||
* $purger = $cdn->purge(); // Returns Purgeable for default driver
|
||||
* $stats = $cdn->stats(); // Returns HasStats for default driver
|
||||
*
|
||||
* // Specific driver
|
||||
* $purger = $cdn->driver('bunny')->purge();
|
||||
*/
|
||||
class CdnManager
|
||||
{
|
||||
protected string $defaultDriver;
|
||||
|
||||
/**
|
||||
* @var array<string, array{purge: class-string<Purgeable>, stats: class-string<HasStats>}>
|
||||
*/
|
||||
protected array $drivers = [
|
||||
'bunny' => [
|
||||
'purge' => Bunny\Purge::class,
|
||||
'stats' => Bunny\Stats::class,
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->defaultDriver = config('cdn.driver', 'bunny');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default driver name.
|
||||
*/
|
||||
public function getDefaultDriver(): string
|
||||
{
|
||||
return $this->defaultDriver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default driver.
|
||||
*/
|
||||
public function setDefaultDriver(string $driver): self
|
||||
{
|
||||
$this->defaultDriver = $driver;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a driver instance.
|
||||
*/
|
||||
public function driver(?string $driver = null): self
|
||||
{
|
||||
if ($driver !== null) {
|
||||
$this->defaultDriver = $driver;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the purge operation for the current driver.
|
||||
*/
|
||||
public function purge(): Purgeable
|
||||
{
|
||||
return $this->resolve('purge');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stats operation for the current driver.
|
||||
*/
|
||||
public function stats(): HasStats
|
||||
{
|
||||
return $this->resolve('stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a driver is registered.
|
||||
*/
|
||||
public function hasDriver(string $driver): bool
|
||||
{
|
||||
return isset($this->drivers[$driver]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered driver names.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function drivers(): array
|
||||
{
|
||||
return array_keys($this->drivers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom driver.
|
||||
*
|
||||
* @param array{purge?: class-string<Purgeable>, stats?: class-string<HasStats>} $operations
|
||||
*/
|
||||
public function extend(string $driver, array $operations): self
|
||||
{
|
||||
$this->drivers[$driver] = array_merge($this->drivers[$driver] ?? [], $operations);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an operation class for the current driver.
|
||||
*
|
||||
* @template T
|
||||
*
|
||||
* @return T
|
||||
*/
|
||||
protected function resolve(string $operation): object
|
||||
{
|
||||
if (! isset($this->drivers[$this->defaultDriver])) {
|
||||
throw new InvalidArgumentException("CDN driver [{$this->defaultDriver}] not registered.");
|
||||
}
|
||||
|
||||
if (! isset($this->drivers[$this->defaultDriver][$operation])) {
|
||||
throw new InvalidArgumentException(
|
||||
"Operation [{$operation}] not available for CDN driver [{$this->defaultDriver}]."
|
||||
);
|
||||
}
|
||||
|
||||
$class = $this->drivers[$this->defaultDriver][$operation];
|
||||
|
||||
return new $class;
|
||||
}
|
||||
}
|
||||
29
src/Contract/HasStats.php
Normal file
29
src/Contract/HasStats.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Cdn\Contract;
|
||||
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* CDN statistics and bandwidth reporting.
|
||||
*/
|
||||
interface HasStats
|
||||
{
|
||||
/**
|
||||
* Get CDN statistics for date range.
|
||||
*
|
||||
* @param string|null $from Start date (ISO 8601)
|
||||
* @param string|null $to End date (ISO 8601)
|
||||
*/
|
||||
public function stats(?string $from = null, ?string $to = null): Response;
|
||||
|
||||
/**
|
||||
* Get bandwidth usage for date range.
|
||||
*
|
||||
* @param string|null $from Start date (ISO 8601)
|
||||
* @param string|null $to End date (ISO 8601)
|
||||
*/
|
||||
public function bandwidth(?string $from = null, ?string $to = null): Response;
|
||||
}
|
||||
35
src/Contract/Purgeable.php
Normal file
35
src/Contract/Purgeable.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Cdn\Contract;
|
||||
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* CDN cache purging operations.
|
||||
*/
|
||||
interface Purgeable
|
||||
{
|
||||
/**
|
||||
* Purge a single URL from cache.
|
||||
*/
|
||||
public function url(string $url): Response;
|
||||
|
||||
/**
|
||||
* Purge multiple URLs from cache.
|
||||
*
|
||||
* @param array<string> $urls
|
||||
*/
|
||||
public function urls(array $urls): Response;
|
||||
|
||||
/**
|
||||
* Purge entire cache.
|
||||
*/
|
||||
public function all(): Response;
|
||||
|
||||
/**
|
||||
* Purge by cache tag.
|
||||
*/
|
||||
public function tag(string $tag): Response;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue