contenir / contenir-cdn
Framework-free CDN purging for Contenir: the purger interface, cache-tag naming, URL batching, debouncing and a Cloudflare API client that signs in with a token.
Requires
- php: ~8.3.0 || ~8.4.0 || ~8.5.0
- psr/clock: ^1.0
- psr/http-client: ^1.0.3
- psr/http-factory: ^1.1
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^3.0.2
Requires (Dev)
- contenir/contenir-qa-tools: 0.1.x-dev
- infection/infection: ^0.34.1
- nyholm/psr7: ^1.8.2
- phpunit/phpunit: ^11.5.42
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-21 03:53:15 UTC
README
Framework-free CDN purging for Contenir sites:
PurgerInterface, which a CMS calls to drop pages from whatever caches the site in front of it, andNullPurgerfor a site with none;- cache-tag naming (
CacheTag), so a purge can name the pages showing a record instead of their URLs; - batching (
Batch), at most 100 URLs or tags to a request; - debouncing (
DebouncingPurger), which merges purges asked for within a few seconds into one request; - checking an editor's list of URLs against the site's hosts (
UrlList); - a Cloudflare API client that signs in with an API token, a purger built on it that logs every request, and a connection test.
It has no framework dependency. It sends HTTP through any PSR-18 client and logs through PSR-3. The CMS uses it
through the Cloudflare add-on (contenir-extension/cloudflare),
and a second CDN would be a second add-on over the same interface.
Requirements
- PHP 8.3, 8.4 or 8.5
- A PSR-18 HTTP client and PSR-17 factories, such as Guzzle 7 or
symfony/http-clientwithnyholm/psr7
Purging through Cloudflare
use Contenir\Cdn\Cloudflare\Client; use Contenir\Cdn\Cloudflare\CloudflarePurger; use Contenir\Cdn\Cloudflare\Credentials; use Contenir\Cdn\Cloudflare\ZoneId; $credentials = Credentials::choose($token, $email, $globalKey); // null: Cloudflare isn't set up $client = new Client($httpClient, $requestFactory, $streamFactory, $credentials); $purger = new CloudflarePurger($client, ZoneId::parse($zoneId), $logger, trigger: 'content'); $purger->purgeEverything(); // best effort: logs a failure, never throws $report = $purger->purgeUrls(['https://www.example.com/news/']); // and says how it went
Credentials
Credentials::choose() takes an API token when there is one. Without one, it falls back to the account's email and
Global API Key, which is deprecated: the key can do anything the account can, where a token can be limited to
purging one zone. Make a token in Cloudflare under My Profile > API Tokens, with these permissions for the site's
zone:
| Permission | Needed for |
|---|---|
| Zone · Cache Purge · Purge | purging |
| Zone · Zone · Read | the connection test, which reads the zone's plan |
Neither secret is ever shown: var_dump() and print_r() name only how the client signs in, the constructors mark
them #[SensitiveParameter], and every message the client or purger writes has them taken out.
Requests
Client makes the five requests purging needs, and returns Cloudflare's answer as an ApiResponse: whether it
succeeded, its errors, its result, and its request id (the cf-ray header), which Cloudflare's support asks for. A
request Cloudflare refuses is returned like any other; only one that gets no answer at all throws a
TransportException.
| Method | Request |
|---|---|
verify() |
GET /user/tokens/verify, or GET /user with the Global API Key |
zone($zone) |
GET /zones/{zone} |
purgeEverything($zone) |
POST /zones/{zone}/purge_cache with purge_everything |
purgeUrls($zone, $urls) |
the same with files: 1 to 100 URLs |
purgeTags($zone, $tags) |
the same with tags: 1 to 100 tags |
Since April 2025 every Cloudflare plan can purge everything, by URL, by hostname, by cache tag and by prefix
(Zone::purgeMethods()). Plans differ in how many purges a minute they take, and Enterprise takes 500 URLs or tags to
a request rather than 100. This library always sends at most 100.
The log
CloudflarePurger logs one entry for every request it makes, at info when Cloudflare purged and error when it
refused or couldn't be reached. Each entry's context holds:
| Key | Holds |
|---|---|
trigger |
what started the purge, as the purger was given it: content, asset or person, say |
scope |
everything or urls |
count |
how many URLs; 0 for everything |
targets |
the URLs |
zone |
the zone ID |
result |
purged, rejected or failed |
request_id |
Cloudflare's id for the request, when it gave one |
status |
the HTTP status, when Cloudflare answered |
error |
Cloudflare's errors, or why it couldn't be reached |
The URLs are public addresses. Nothing about the site's visitors is logged.
Testing the connection
$connection = (new ConnectionCheck($client))->run($zone); $connection->succeeded(); // the credentials work and can read the zone $connection->zone?->plan; // "Pro Plan" $connection->problem; // what to fix, in words an editor can act on $connection->deprecatedKey; // signed in with the Global API Key
The test purges nothing, so it can't prove the token may purge. The first purge does, and the log says if it may not.
Debouncing
$purger = new DebouncingPurger($cloudflare, $clock, seconds: 5);
The first purge waits. Those asked for in the next five seconds join it, and the next one after that sends them all
as one request. A purge of everything takes in the URLs waiting with it. Whatever still waits is sent by flush(),
which runs when the purger is destroyed, at the latest as the request ends.
Cache tags
CacheTag::site(); // c-site CacheTag::section('library.news.articles'); // c-sec-library.news.articles CacheTag::record('library.news.articles', 12); // c-rec-library.news.articles-12
A public page carries c-site, c-sec-<section> for each section whose records it shows, listings included, and
c-rec-<section>-<id> for each record it shows. A save then purges its record's tag and its section's, which covers
the listing pages, and the CMS needs no URLs. Names are lower-cased, and anything but letters, digits, dots and
underscores becomes an underscore, so a hyphen only ever separates the parts.
Sending the tags is a site adapter's job. Those adapters (contenir/contenir-cdn-laminas-mvc and contenir/contenir-cdn-mezzio) come
with version 2 of the Cloudflare add-on.
An editor's list of URLs
$list = UrlList::fromText($typed, ['example.com', '*.example.com']); $list->urls; // the URLs to purge $list->refused; // each line refused, and why
A CDN caches each URL by its full address, so each line must be an absolute http or https URL on one of the site's
hosts. A host written *.example.com stands for every subdomain of example.com. Blank lines are skipped and repeats
kept once.
Licence
BSD-3-Clause. See LICENSE.md.