rmzamora/block-bundle
rmzamora/block-bundle is a Laravel/PHP package for organizing reusable content blocks in your applications. It provides a simple way to manage, compose, and render blocks, helping keep templates clean and content flexible.
Read this first: the package is a fork, not a product. rmzamora/block-bundle is a verbatim fork of sonata-project/block-bundle (same Sonata\BlockBundle namespace) pushed to keep an old Symfony/Sonata version matrix installable. Zero stars, zero dependents, archived. Before writing a line of code, answer one question: does the project already pull this in transitively (typically via sonata-project/page-bundle or an old sonata-project/admin-bundle)? If yes, you inherit it and the guidance below applies. If no, composer require sonata-project/block-bundle instead — the API is identical and it still gets security fixes.
Assuming you're on the fork:
composer.json and require the branch/commit your project is pinned to, alongside sonata-project/core-bundle (hard dependency for the older versions) and whichever cache/menu packages that Sonata generation expects.new Sonata\BlockBundle\SonataBlockBundle() in AppKernel/bundles.php, plus dependencies first.sonata_block:
default_contexts: [cms]
blocks:
sonata.block.service.text:
contexts: [cms]
{{ sonata_block_render({'type': 'sonata.block.service.text'}, {'content': '<p>Hello</p>'}) }}
The first array is the block identity (type + settings that participate in the cache key); the second is runtime settings merged on top. From PHP: $container->get('sonata.block.renderer')->render('sonata.block.service.text', ['content' => '…']).bin/console debug:container --tag=sonata.block lists every registered type. If your type isn't there, nothing else will work.Reading order for the source (you will need it — docs for this generation are thin): Resources/config/block.xml (service/tag wiring) → Block/BlockServiceManager.php (type → service registry) → Block/BlockContextManager.php (settings merge, defaults, cache key generation) → Block/BlockRenderer.php (exception handling, response shape) → Twig/Extension/BlockExtension.php (sonata_block_render, output escaping) → Block/Service/* for the built-in catalog (text, container, template, rss, menu…).
1. Custom block — the pattern you'll write weekly. Extend AbstractBlockService, implement three methods, tag the service, add a config entry:
class PromoBlockService extends AbstractBlockService
{
public function getName(): string { return 'acme.block.promo'; }
public function setDefaultSettings(OptionsResolverInterface $resolver): void
{
$resolver->setDefaults([
'template' => 'AcmeBundle:Block:promo.html.twig',
'title' => null,
'ttl' => 3600,
]);
}
public function execute(BlockContextInterface $context, Response $response = null): Response
{
return $this->renderResponse($context->getTemplate(), [
'block' => $context,
'title' => $context->getSetting('title'),
], $response);
}
}
<service id="acme.block.promo" class="Acme\Block\PromoBlockService">
<tag name="sonata.block" type="acme.block.promo"/>
<argument type="service" id="twig"/>
</service>
The type attribute, getName(), and the sonata_block.blocks config key must be the same string. setDefaultSettings() is your entire validation layer — anything an editor can put into a block goes through the OptionsResolver, so use setAllowedTypes/setAllowedValues there rather than validating in execute().
2. Three rendering surfaces — pick deliberately.
sonata_block_render_event('sidebar.left') in the template + a BlockEvent listener in your app — this is the underused one. It lets a bundle or feature module inject a block into a slot it doesn't own, keeping templates free of cross-module imports. Use it for plugin-style extension in your own codebase.3. Contexts are a contract, not decoration. Declare meaningful names (cms, sidebar, email) in sonata_block.contexts and restrict each block with contexts:. The context list is what SonataAdmin's block picker filters on and what default_contexts seeds for ad-hoc rendering — it's admin UX and a safety rail against putting a 600px hero into a 60px email slot. Blocks rendered without a matching context still render; the restriction is advisory, so don't treat it as access control.
4. Caching is opt-in per block and is the main performance lever. Set a cache: service per block in config and a ttl in the settings defaults. When output varies by anything other than settings, override:
public function getCacheKeys(BlockInterface $block): array
{
return ['locale' => $this->requestStack->getCurrentRequest()->getLocale()];
}
or pass extra_cache_keys through the render call's options array. Anything user-scoped (cart, "my orders", greeting-with-name) needs this or ttl: 0 — see the gotcha below.
5. Containers are your repeater. sonata.block.service.container lets an editor nest arbitrary child blocks; children are serialized as JSON in the code setting and a layout manager renders the wrapper (stacked/band styles ship with the bundle). For CMS-style pages this is the standard way to build multi-column regions — compose container blocks rather than writing a custom "row" block, and override the container template if you need different markup.
6. Static vs. dynamic split. Twig-declared blocks are versioned with your templates; DB-backed blocks (via SonataAdmin's block admin or your own entity) are edited by content people and need cache TTLs. Keep the two catalogs mentally separate — the most common support ticket in a Sonata site is "someone edited the block but the page didn't change," which is always the TTL.
7. Test at the renderer, not the service. Instantiate BlockRenderer through the container in a KernelTestCase and assert on rendered HTML plus the resolved BlockContext. That single test covers settings merging, template resolution, and cache-key shape — the three things that actually break.
OptionsResolverInterface signature here was removed in 3.0; Twig_Markup is gone in Twig 2+). Pin it, don't build new features on it, and put "migrate to sonata-project/block-bundle" on the backlog. The migration is mostly a composer change plus service-id renames — do it in an isolated PR with your block-render test as the safety net.Cannot redeclare class at autoload time. Use composer replace/conflict to enforce exactly one.getCacheKeys(), extra_cache_keys, or ttl: 0.execute().execute() returning null is legal — it means "skip this block," not an error. Keep the $response parameter for BC and forward it to renderResponse(); that's what makes a block ESI/sub-request friendly.BlockRenderer catches and renders block_exception.html.twig — in prod you get an empty region, not a stack trace. Debug from var/logs, not the page source. debug:container --tag=sonata.block and debug:twig are your first two commands; there's no block-specific debug command in this generation.SonataBlockBundle:Block:block_*.html.twig; override under app/Resources/SonataBlockBundle/views/ on Symfony 3, templates/bundles/SonataBlockBundle/ on 4+ (which this fork won't run on anyway).code setting to editors, validate it before save or you'll get an empty region with no log entry.type mismatch fails loudly, config mismatch fails quietly. A missing service tag throws BlockNotFoundException; forgetting the sonata_block.blocks entry just means no cache/context config is applied and you'll chase phantom staleness.sonata.block.manager, sonata.block.context_manager, and sonata.block.renderer are all container aliases — decorate them instead of editing vendor/, otherwise the next composer update silently reverts your fix (and on an archived fork, "next update" may be never, which is worse: the fix is invisible to your team).How can I help you explore Laravel packages today?