fvchs/sonata-ajax-block-bundle
fvchs/sonata-ajax-block-bundle extends Sonata Admin with AJAX-rendered blocks, letting dashboard widgets and content panels load asynchronously without full page reloads. A lightweight way to keep admin interfaces modular and responsive.
At its core this bundle is a thin layer on top of sonata-project/block-bundle: instead of inlining a block's HTML during the main request, the page ships a placeholder and the browser fetches the rendered block from a dedicated endpoint. That is the whole mental model — everything else is Sonata's normal block lifecycle (BlockServiceInterface → BlockContext → Twig template) plus one extra route and a bit of JavaScript.
Minimal path to first render:
composer require fvchs/sonata-ajax-block-bundle
// config/bundles.php
return [
// ...
Fvchs\SonataAjaxBlockBundle\FvchsSonataAjaxBlockBundle::class => ['all' => true],
];
Then, before writing any application code, read three things in the repo — they tell you 90% of what you need:
composer.json — which sonata-project/block-bundle major it supports. A 1-star bundle is usually pinned to an older major (3.x/4.x). This decides whether execute() still takes a Response argument and whether AbstractBlockService is even the right base class.src/Resources/config/ — the service definitions and, critically, the sonata.block tagged service(s). The type string you pass to sonata_block_render is defined here (commonly something like sonata.block.service.ajax).Resources/public/js (or assets/) — the request contract (query params vs. body, HTML vs. JSON) and the DOM selector the JS looks for.Your first use case should be the one this pattern actually earns: a block on a cacheable page shell whose content is user- or time-sensitive — "recently viewed", "your cart", "latest comments". Render the page from HTTP cache, let the block hydrate over XHR.
If the bundle ships routing, import it explicitly:
# config/routes.yaml
sonata_ajax_block:
resource: "@SonataAjaxBlockBundle/Resources/config/routing.xml"
prefix: /_ajax
…and remember bin/console assets:install --symlink (or wire the JS through Encore) — Symfony does not serve Resources/public automatically.
Pattern A — reuse an existing block service. The bundle's most useful trick is usually that any Sonata block type can be rendered asynchronously, with the endpoint forwarding to the same BlockServiceInterface. That means your custom services keep working untouched:
{# illustrative — confirm the opt-in flag name in setDefaultSettings #}
{{ sonata_block_render({ type: 'acme.block.recent_activity' }, {
ajax: true,
container: 'sidebar-widgets',
limit: 5
}) }}
Everything under the second argument is serialized to the endpoint and rebuilt into a BlockContext on the other side, so keep settings scalar and small.
Pattern B — dedicated AJAX block type. For payloads that should never be inlined, register your own service tagged sonata.block with a distinct type and have the JS request only that. This is the version you can safely allow-list.
Sign your settings. The endpoint is, by construction, "render an arbitrary block type with arbitrary settings". If the bundle doesn't already sign or allow-list the payload, add that immediately — HMAC the serialized settings with kernel.secret and reject anything that doesn't verify:
$signature = hash_hmac('sha256', $serializedSettings, $this->secret);
Progressive enhancement. Always render a server-side fallback inside the placeholder (<noscript>, a static skeleton, or the full block for crawlers via a user-agent/cache strategy). Do not ship a page whose main content only exists after XHR.
Layering caches. There are two caches in play and they are independent: Sonata's block cache ('cache' => ['ttl' => 3600] in setDefaultSettings) and your HTTP cache. The XHR response is not covered by your page cache — set Cache-Control on the endpoint deliberately, and never let user-specific data into a shared-cached response (use Vary: Cookie or make it uncacheable).
Re-init after swap. When the fetched HTML replaces the placeholder, any JS plugin bound to markup (tooltips, sliders, char counters) must be re-initialised. Fire a custom event after insertion rather than duplicating init logic in the bundle's callback.
Sonata admin integration. Block edit forms (and SonataPageBundle's inline editing) assume synchronous rendering. Keep "ajax" off the default settings so admins see real content in preview, and only enable it per-render in the frontend template.
Testing. A functional test with KernelBrowser against the endpoint beats any unit test here — assert status, content type, that an unverified settings payload 400s, and that the response is stable for a fixed context.
execute() signature drift. sonata-project/block-bundle changed the method signature across majors. If the bundle targets an older version, you'll hit Declaration of ... must be compatible with .... Check composer.json first; do not "fix" it by bumping the constraint blind.type values let a caller render blocks you never intended to expose (RSS fetchers, admin-only blocks, blocks that leak data). Allow-list types and require the signature check described above. Also rate-limit it — it's cheap for you to serve and cheap for someone to hammer.encore_entry_link_tags. If you go through Encore, the bundle's JS may live in Resources/public and need a small wrapper entry to be importable.bin/console debug:container --tag=sonata.block confirms the type is registered; Sonata's block profiler panel (if enabled) shows the context the endpoint actually rebuilt — that's where mismatched settings reveal themselves.{{ render_esi(...) }}) already solves the same problem with HTTP caching, no JS, and no new attack surface. Reach for this bundle when you need to keep the HTML shell stable for a CDN and the block is genuinely request-scoped.How can I help you explore Laravel packages today?