Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Fastly domain purges invalidate the entire configured service. Use one domain pe

### Cache routing

`Cache\Adapter\Proxy` routes the application domain to one adapter, configured network domains to another, and fans custom domains out to every custom adapter.
`Cache\Adapter\Proxy` routes domain and path purges: it sends the application domain to one adapter, configured network domains to another, and fans custom domains out to every custom adapter.

```php
use Utopia\Cdn\Cache\Adapter\Proxy;
Expand All @@ -94,6 +94,17 @@ $cache = new Cache(new Proxy(
));
```

Cache keys and tags are scoped to a Fastly service or Cloudflare zone. Consequently, `Proxy` does not route `purgeKeys()` calls. The consuming application must select the adapter using its own routing context before constructing `Cache`:

```php
$adapter = $cdnAdapterResolver->resolve($rule);

$cache = new Cache($adapter);
$cache->purgeKeys([
'domain-' . \strtolower($domain),
]);
```

## Certificates

The current certificate provider support is focused on CDN-managed certificates through Fastly TLS subscriptions.
Expand Down
29 changes: 3 additions & 26 deletions src/Cdn/Cache/Adapter/Proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,9 @@ public function purgeKeys(array $keys): void
return;
}

$purged = false;
foreach ($this->all() as $adapter) {
try {
$adapter->purgeKeys($keys);
$purged = true;
} catch (UnsupportedOperation) {
continue;
}
}

if (!$purged) {
throw new UnsupportedOperation('Cache key purging is not supported by any configured adapter.');
}
throw new UnsupportedOperation(
'Cache key purging cannot be routed by domain. Select the service or zone adapter before purging keys.'
);
}

/** @return array<int, Adapter> */
Expand All @@ -85,17 +75,4 @@ private function select(string $domain): array

return $this->customDomainAdapters;
}

/** @return array<int, Adapter> */
private function all(): array
{
$adapters = [$this->appDomainAdapter, $this->networkAdapter, ...$this->customDomainAdapters];
$unique = [];

foreach ($adapters as $adapter) {
$unique[\spl_object_id($adapter)] = $adapter;
}

return \array_values($unique);
}
}
39 changes: 19 additions & 20 deletions tests/Cdn/Cache/Adapter/ProxyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class ProxyTest extends TestCase
{
public function testRoutesAndFansOut(): void
public function testRoutesDomainsAndPaths(): void
{
$calls = new \ArrayObject();
$app = $this->adapter('app', $calls);
Expand All @@ -22,9 +22,8 @@ public function testRoutesAndFansOut(): void
$proxy->purgeDomain('app.example.com');
$proxy->purgePaths('network.example.com', ['/a']);
$proxy->purgeDomain('customer.example.com');
$proxy->purgeKeys(['key']);

$this->assertSame(['app:domain', 'network:paths', 'custom-a:domain', 'custom-b:domain', 'app:keys', 'network:keys', 'custom-a:keys', 'custom-b:keys'], $calls->getArrayCopy());
$this->assertSame(['app:domain', 'network:paths', 'custom-a:domain', 'custom-b:domain'], $calls->getArrayCopy());
}

public function testRejectsMissingCustomAdapters(): void
Expand All @@ -35,33 +34,36 @@ public function testRejectsMissingCustomAdapters(): void
$proxy->purgeDomain('custom.example.com');
}

public function testKeyPurgeSkipsUnsupportedAdapters(): void
public function testRejectsKeyPurgeWithoutServiceOrZoneSelection(): void
{
$calls = new \ArrayObject();
$unsupported = $this->adapter('cloudflare', $calls, false);
$fastly = $this->adapter('fastly', $calls);
$proxy = new Proxy('app.example.com', $unsupported, $fastly, [$unsupported, $fastly]);
$app = $this->adapter('app', $calls);
$network = $this->adapter('network', $calls);
$custom = $this->adapter('custom', $calls);
$proxy = new Proxy('app.example.com', $app, $network, [$custom]);

$this->expectException(UnsupportedOperation::class);
$this->expectExceptionMessage('Select the service or zone adapter');
$proxy->purgeKeys(['key']);

$this->assertSame(['fastly:keys'], $calls->getArrayCopy());
}

public function testKeyPurgeFailsWhenEveryAdapterIsUnsupported(): void
public function testEmptyKeyPurgeIsANoOp(): void
{
$unsupported = $this->adapter('cloudflare', new \ArrayObject(), false);
$proxy = new Proxy('app.example.com', $unsupported, $unsupported, [$unsupported]);
$calls = new \ArrayObject();
$adapter = $this->adapter('adapter', $calls);
$proxy = new Proxy('app.example.com', $adapter, $adapter, [$adapter]);

$this->expectException(UnsupportedOperation::class);
$proxy->purgeKeys(['key']);
$proxy->purgeKeys([]);

$this->assertSame([], $calls->getArrayCopy());
}

/** @param \ArrayObject<int, mixed> $calls */
private function adapter(string $name, \ArrayObject $calls, bool $supportsKeys = true): Adapter
private function adapter(string $name, \ArrayObject $calls): Adapter
{
return new class ($name, $calls, $supportsKeys) implements Adapter {
return new class ($name, $calls) implements Adapter {
/** @param \ArrayObject<int, mixed> $calls */
public function __construct(private string $name, private \ArrayObject $calls, private bool $supportsKeys)
public function __construct(private string $name, private \ArrayObject $calls)
{
}
public function purgePaths(string $domain, array $paths): void
Expand All @@ -74,9 +76,6 @@ public function purgeDomain(string $domain): void
}
public function purgeKeys(array $keys): void
{
if (!$this->supportsKeys) {
throw new UnsupportedOperation('Unsupported.');
}
$this->calls->append($this->name . ':keys');
}
};
Expand Down
Loading