alengo/sulu-bunny-cdn-bundle
Composer 安装命令:
composer require alengo/sulu-bunny-cdn-bundle
包简介
bunny.net CDN integration for Sulu/Symfony: tag-based edge cache invalidation (CDN-Tag header + wildcard purge) wired into FOS HttpCache.
README 文档
README
bunny.net CDN integration for Sulu/Symfony websites: tag-based edge cache invalidation wired into FOS HttpCache. On every Sulu publish, both the local HttpCache kernel and the bunny.net pull zone are purged — content updates go live worldwide in seconds instead of waiting for the TTL.
Architecture
Browser ──> bunny.net pull zone ──> local HttpCache kernel (origin) ──> Symfony/Sulu
(edge cache, CDN-Tag) (e.g. Sulu's Psr6Store, X-Cache-Tags)
Two cache layers, both invalidated on every publish:
- Mirrors the
X-Cache-Tagsresponse header (FOS TagListener / Sulu) into bunny.net'sCDN-Tagheader (BunnyCdnTagSubscriber), hashing each tag to 12 chars (xxh128) to fit bunny.net's hard 1024-byte header limit (~78 tags per response instead of ~27 raw UUIDs). - Registers a
BunnyProxyClient(FOS proxy client: tags, URLs, full clear) plus aMultiplexerClientthat forwards every invalidation to the local Symfony HttpCache kernel client and bunny.net. - Prepends
fos_http_cache.cache_manager.custom_proxy_client, so Sulu'sCacheManagerpurges bunny.net on publish — no project wiring needed.
The bunny.net single-tag pitfall
bunny.net stores the whole CDN-Tag header value as ONE tag — it does not
split at commas or any other separator, and its purge API answers 204 even
when nothing matches. Exact-match tag purges are therefore silent no-ops.
This bundle uses the working pattern (same as the Drupal bunny_cdn module):
the header is written as a space-separated list with : … : guards
(CDN-Tag: : a1b2c3d4e5f6 0f9e8d7c6b5a :), and purges use a wildcard pattern
({"CacheTag": "* a1b2c3d4e5f6 *"}) that matches the space-bounded tag inside
the stored value.
1. Installation
composer require alengo/sulu-bunny-cdn-bundle
// config/bundles.php Alengo\SuluBunnyCdnBundle\AlengoBunnyCdnBundle::class => ['all' => true],
# config/packages/alengo_bunny_cdn.yaml alengo_bunny_cdn: enabled: false when@prod: &bunny alengo_bunny_cdn: enabled: true when@stage: *bunny
The credentials default to env placeholders — set them in .env.local
(never commit them):
BUNNY_API_KEY=… # ACCOUNT API key (Account → API Key), NOT a pull-zone key BUNNY_PULL_ZONE_ID=123456 # numeric id, visible in the dashboard URL of the zone BUNNY_SITE_BASE_URL=https://www.example.com
The env vars must be set wherever the bundle is enabled — FOS flushes the invalidation queue on
console.terminate, so the proxy client is instantiated by every console command.
All options:
alengo_bunny_cdn: enabled: true api_key: '%env(BUNNY_API_KEY)%' pull_zone_id: '%env(BUNNY_PULL_ZONE_ID)%' base_url: '%env(BUNNY_SITE_BASE_URL)%' api_endpoint: 'https://api.bunny.net' # override for tests/mocks additional_proxy_clients: # purged alongside bunny.net - fos_http_cache.proxy_client.symfony
Requires the FOS symfony proxy client (or adjust additional_proxy_clients):
# config/packages/fos_http_cache.yaml fos_http_cache: proxy_client: symfony: use_kernel_dispatcher: true
2. Create the pull zone (bunny.net dashboard)
CDN → Add Pull Zone
| Setting | Value |
|---|---|
| Name | e.g. example (becomes example.b-cdn.net) |
| Origin Type | Origin URL |
| Origin URL | https://origin.example.com — the host that reaches the web server WITHOUT the CDN. Must be https:// — with http:// the origin's force-SSL redirect (301 to the origin host!) gets cached and served to visitors. |
| Pricing Zones | For GDPR-sensitive sites: enable Europe only |
Under Hostnames:
- add the public domain (e.g.
www.example.com) as custom hostname - enable Free SSL (Let's Encrypt) and Force SSL
3. Caching settings of the pull zone
Caching → General:
- Cache Expiration Time:
Respect Origin Cache-Control. Sulu sendss-maxageaccording to the template cache lifetime; with working tag purges the TTL can be raised later without losing freshness. - Query String Sort: enable.
- Query String Vary: "Vary by Query String Parameters" with a whitelist
of the parameters that actually matter (e.g.
q,page, form params) — keepsutm_*,gclid& co. from fragmenting the edge cache. - Strip Response Cookies: DISABLE! bunny.net strips
Set-Cookiefrom ALL responses — including uncached POSTs, not just cache hits. The admin login never receives its session cookie and every subsequent request fails with 401. Cached personalized responses are prevented by the session-bypass edge rule (§4) and the origin'sprivate/no-cache. - Perma-Cache: keep disabled — tag purging does not work with it.
- Stale Cache: "While Origin Offline" is a sensible resilience option; keep "While Updating" OFF, otherwise the first visitor after a purge is served the old version once.
4. Edge rules (mandatory!)
bunny.net does not include cookies in the cache key. Personalized or logged-in requests must therefore BYPASS the edge cache (the origin kernel cache handles them correctly). Create these rules (Pull Zone → Edge Rules → Add Edge Rule):
Rule "SessionBypass" — logged-in users:
- Action:
Override Cache Time→0 - Condition:
Request Header→ headerCookie→ pattern*SULUSESSID*(adjust to your session cookie name)
Rule "AdminNoCache" — never cache the admin:
- Action:
Override Cache Time→0 - Condition:
Request URL→ matches →*/admin* - Needed even though the origin sends
private: bare redirects (e.g. Apache's/admin→/admin/) carry no Cache-Control and DO get cached.
Additional bypass rules for any personalization cookies your site uses
(same action, conditions Request Header: Cookie with *cookie_name*
patterns, "Match Any").
⚠ Cookie condition pitfall: do NOT use the Cookie Value condition type
with pattern * — it also matches requests WITHOUT the cookie, the rule then
applies to ALL requests and the whole zone becomes BYPASS (nothing is
cached anymore). Use Request Header: Cookie with a substring pattern — a
missing header never matches a substring.
Edge rules always need at least one condition. For rules that should
apply to every request, use Request URL → matches → *.
CDN-Tag stays visible to browsers: response headers starting with
CDN- cannot be set/cleared via edge rules ("Header names starting with
CDN- are not allowed"). The header only contains 12-char hashes and is
useful for debugging — leave it.
5. Host handling & origin lockdown
If the origin vhost only serves the origin hostname (and answers the public
host with 421 Misdirected Request):
-
Forward Host Header: keep DISABLED (default) — bunny.net sends the host from the Origin URL. Forwarding the public host does NOT work (421).
-
So the app still generates public URLs, present the public host to the app in the origin's
.htaccess:<IfModule mod_headers.c> RequestHeader set Host "www.example.com" </IfModule>
-
Origin lockdown: edge rule
Add Request Header(acts on requests to the origin only) →X-Origin-Verify: <secret>, conditionRequest URLmatches*; the.htaccessrejects requests without it:RewriteCond %{HTTP:X-Origin-Verify} !^<secret>$ RewriteRule ^ - [F]
Do not commit the secret. Save the edge rule BEFORE arming the
.htaccess, or you lock bunny.net out. Rotating = change both places.
6. DNS
Point the public domain as CNAME to <zone>.b-cdn.net (only after SSL
for the hostname is provisioned). The origin must stay directly reachable
under its own hostname (A record, own vhost with TLS).
7. Verify
# Edge cache status (2nd request should be a HIT), CDN-Tag visible curl -sI https://www.example.com/ | grep -iE 'cdn-cache|cache-control|cdn-tag' # With a session cookie the edge must NEVER answer from cache (BYPASS) curl -sI -H 'Cookie: SULUSESSID=test' https://www.example.com/ | grep -i cdn-cache # Admin: BYPASS curl -sI https://www.example.com/admin | grep -i cdn-cache # Origin lockdown: direct origin hit → 403 curl -sI https://origin.example.com/ | head -1 # Manual tag purge: take a 12-char hash from the CDN-Tag header — MUST be # purged as a wildcard pattern (single-tag storage, see above): curl -s -X POST "https://api.bunny.net/pullzone/$BUNNY_PULL_ZONE_ID/purgeCache" \ -H "AccessKey: $BUNNY_API_KEY" -H 'Content-Type: application/json' \ -d '{"CacheTag": "* <tag-hash> *"}' -w '%{http_code}\n'
cdn-cache legend: MISS/EXPIRED/HIT = normal caching, BYPASS = an
override-cache-time-0 rule matched. MISS after a purge = entry was gone;
EXPIRED = regular TTL refetch.
⚠ Verifying purges: a 204 from the API proves nothing (bunny.net
returns 204 even for non-matching tags), and a "successful purge" observed
after the entry's TTL has expired is just the regular refetch. Always check
cdn-cachedat WITHIN the TTL of the entry.
Publish test: change + publish a page in the admin → the next request to the live URL must return the new content within seconds.
8. Operations & limits
- Publish invalidates both layers automatically (tag-based).
- Full flush:
bin/console fos:httpcache:clearor the dashboard's "Purge Cache". Note: Symfony'scache:cleardoes NOT purge HTTP caches. - 401 "Unauthorized" from caching proxy on publish/console commands:
BUNNY_API_KEYis missing/wrong — it must be the ACCOUNT API key, not a pull-zone key. Test:curl -H "AccessKey: $KEY" https://api.bunny.net/pullzone. invalidateDomain()(BAN/regex) is not supported by bunny.net and is a no-op.- The
CDN-Tagheader is limited to 1024 bytes (~78 hashed tags); overflow tags are dropped cleanly — affected pages refresh via the regular TTL. - Wildcard purges are rate-limited by bunny.net (burst ~20, ~0.5/s refill); mass publishes may drop some purges — affected pages heal via the TTL.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 0
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-09