PHPackages                             alengo/sulu-bunny-cdn-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Caching](/categories/caching)
4. /
5. alengo/sulu-bunny-cdn-bundle

ActiveSymfony-bundle[Caching](/categories/caching)

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.

0.1(1mo ago)066MITPHPPHP ^8.2

Since Jul 9Pushed 1mo agoCompare

[ Source](https://github.com/alengodev/SuluBunnyCdnBundle)[ Packagist](https://packagist.org/packages/alengo/sulu-bunny-cdn-bundle)[ Docs](https://github.com/alengodev/SuluBunnyCdnBundle)[ RSS](/packages/alengo-sulu-bunny-cdn-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (9)Versions (2)Used By (0)

AlengoBunnyCdnBundle
====================

[](#alengobunnycdnbundle)

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
------------

[](#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-Tags` response header (FOS TagListener / Sulu) into bunny.net's `CDN-Tag` header (`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 a `MultiplexerClient` that forwards every invalidation to the local Symfony HttpCache kernel client *and* bunny.net.
- Prepends `fos_http_cache.cache_manager.custom_proxy_client`, so Sulu's `CacheManager` purges bunny.net on publish — no project wiring needed.

### The bunny.net single-tag pitfall

[](#the-bunnynet-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
---------------

[](#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)
---------------------------------------------

[](#2-create-the-pull-zone-bunnynet-dashboard)

**CDN → Add Pull Zone**

SettingValueNamee.g. `example` (becomes `example.b-cdn.net`)Origin TypeOrigin URLOrigin 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 ZonesFor 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
------------------------------------

[](#3-caching-settings-of-the-pull-zone)

**Caching → General:**

- **Cache Expiration Time**: `Respect Origin Cache-Control`. Sulu sends `s-maxage` according 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) — keeps `utm_*`, `gclid` &amp; co. from fragmenting the edge cache.
- **Strip Response Cookies**: **DISABLE!** bunny.net strips `Set-Cookie`from 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's `private`/`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!)
--------------------------

[](#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` → header `Cookie` → 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 &amp; origin lockdown
--------------------------------------

[](#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`:

    ```

        RequestHeader set Host "www.example.com"

    ```
- **Origin lockdown**: edge rule `Add Request Header` (acts on requests to the origin only) → `X-Origin-Verify: `, condition `Request URL`matches `*`; the `.htaccess` rejects requests without it:

    ```
    RewriteCond %{HTTP:X-Origin-Verify} !^$
    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
------

[](#6-dns)

Point the public domain as **CNAME** to `.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
---------

[](#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": "*  *"}' -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 &amp; limits
--------------------------

[](#8-operations--limits)

- **Publish** invalidates both layers automatically (tag-based).
- **Full flush**: `bin/console fos:httpcache:clear` or the dashboard's "Purge Cache". Note: Symfony's `cache:clear` does NOT purge HTTP caches.
- **401 "Unauthorized" from caching proxy** on publish/console commands: `BUNNY_API_KEY` is 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-Tag` header 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.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance90

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

47d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/4a3bea97975bdeddcfa5b76c222740e703e4ff5f9bbf5f6d5d75e8827e0a48ac?d=identicon)[alengodev](/maintainers/alengodev)

---

Top Contributors

[![hual7](https://avatars.githubusercontent.com/u/89628357?v=4)](https://github.com/hual7 "hual7 (2 commits)")

---

Tags

symfonycdnbunnysulufoshttp-cachecache invalidation

###  Code Quality

Static AnalysisRector

### Embed Badge

![Health badge](/badges/alengo-sulu-bunny-cdn-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/alengo-sulu-bunny-cdn-bundle/health.svg)](https://phpackages.com/packages/alengo-sulu-bunny-cdn-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k18.3M433](/packages/easycorp-easyadmin-bundle)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M236](/packages/sulu-sulu)[symfony/framework-bundle

Provides a tight integration between Symfony components and the Symfony full-stack framework

3.6k257.3M12.5k](/packages/symfony-framework-bundle)[symfony/security-bundle

Provides a tight integration of the Security component into the Symfony full-stack framework

2.5k190.0M2.6k](/packages/symfony-security-bundle)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

86337.5k](/packages/flow-php-flow)[contao/core-bundle

Contao Open Source CMS

1301.7M3.1k](/packages/contao-core-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
