evgeniy-it/widget-bundle
Widget Bundle brings MODX-style shortcodes to your CMS, parsing nested [[widget? &option=value]] tags through a Twig filter. Includes simple template-based widgets, Doctrine repository widgets, array settings, and nesting.
Minimal steps
composer require evgeniy-it/widget-bundle. Expect to hit platform conflicts — the last release is from January 2017, so its composer.json will almost certainly pin Symfony 2.x/3.x and PHP 5.x/7.0. You will likely need --ignore-platform-reqs or a fork with loosened constraints before anything autoloads.AppKernel::registerBundles(). On Symfony 4+/Flex there is no AppKernel, so you must manually add it in config/bundles.php and verify the bundle's service definitions still load (they are probably XML/YAML under Resources/config, not the modern services: layout).widget, so if {{ 'x'|widget }} throws "Unknown filter", the bundle did not boot — that is your first diagnostic.Where to look first
WidgetBundle.php) and its DI extension, to see which services/tags are registered.widget filter — this is the entry point for every render.[[name? &opt=val]] into a widget class, plus the interface a widget must implement.First use case
Take a CMS body field containing:
{{ '[[simple? &content=`Hello world`]]'|widget }}
and confirm you get Hello world. Then move to a custom-template widget and a nested widget before touching anything custom. That single call proves install, DI, Twig wiring, parsing, and rendering all at once.
Render at output, not at save. Store the shortcode string in the DB untouched and run it through |widget when composing the response. This keeps content editable and lets widget behaviour change without a data migration. Add |raw after it ({{ page.body|widget|raw }}) or Twig will escape the HTML the widget produced.
Use the filter as a view-layer boundary. Widgets are essentially parameterised template includes. The clean pattern is: content editors write [[widget_name? &setting=…]], developers own the widget templates and the defaults. Treat widget names as part of your public content contract.
Custom widgets. Implement the bundle's widget interface/abstract class, define the template resolution, and register the class as a service with whatever tag the bundle expects (check the compiler pass). Keep each widget narrow — one widget that "renders a product card" is better than one that "renders anything".
Data passing. Flat options (&setting=value) arrive as strings. Structured options use the documented array/assoc-array notation (`&settingArray=`[1,2,3], &settingAssocArray={'item1':'value1'}``) — note these are JS-style single-quoted literals, not JSON. Cast and validate inside the widget; never trust the editor to send the right shape.
Nested composition. Because nesting is supported, build small widgets and compose:
[[layout? &slot=`[[card? &title=`Hi` &body=`[[simple? &content=`x`]]`]]`]]
This is the real power of the package and the closest analogue to MODX snippets. Keep nesting shallow (2–3 levels) so the string stays readable and debuggable.
Caching. Parsing + rendering on every request is cheap for a couple of widgets but not for a page of twenty. Cache the rendered HTML per widget call keyed on widget name + normalised options, invalidate on template change. Do not cache when widget output depends on the current user — either exclude those widgets from cache or key on user/role.
Laravel integration. There is no Laravel package here. Realistic options, in order of cost:
Given 0 stars and 0 dependents, treat it as a reference implementation to study, not a dependency to adopt.
The syntax is unforgiving and failures are quiet. Widget name must end in ?; every option starts with &; every value must be wrapped in backticks; the whole thing is bounded by [[ and ]]. A missing backtick typically means the widget renders with an empty setting or doesn't match at all — no exception. When something is wrong, isolate the exact shortcode into a dd/dump of the raw string first, then test it in isolation through |widget.
Backticks collide with nesting. The parser uses backticks as the value delimiter, so nested widgets work only because the whole inner widget sits inside one backtick-quoted value. The moment an inner option value itself needs a backtick, or content contains one, parsing breaks. Sanitise or reject backticks in editor content, or escape them deliberately.
Everything is a string. Integers, booleans, and arrays all arrive as strings. &enabled=false`` is truthy in PHP. Cast explicitly, and convert the {'k':'v'} / [1,2,3] forms to PHP arrays yourself — json_decode will fail because of the single quotes.
Security: shortcodes are template execution. A widget that renders &content through Twig is a server-side template injection vector if content is user-submitted. Only trusted editors should be able to author shortcodes; otherwise run a strict allowlist of widget names and strip unknown ones before rendering.
Template resolution and path traversal. The README allows the template to be given as a path or an inline string. If that path comes from content, an editor can point at any template on disk. Constrain templates to a configured directory and prefer named templates over raw paths.
Recursion. Nested widgets plus a self-referencing widget equals infinite recursion or a stack blowout. Add a depth counter in the parser and a max_depth config, and refuse a widget that includes itself.
2017-era Symfony baggage. Expect AppKernel registration, Resources/config/services.yml, and no autowiring or autoconfiguration. On modern Symfony you'll be rewriting the service definitions. Check the bundle's PHP constraint before spending time — if it's ~5.6|~7.0, you cannot run it on PHP 8 unmodified.
Escaping in Twig. Forgetting |raw gives you a page of visible HTML tags and looks like a rendering bug when it's an autoescape issue. Conversely, don't add |raw to output that includes editor-supplied HTML without sanitising.
Debugging checklist, in order: (1) does {{ '[[simple? &content=x]]'|widget }} work at all; (2) is the filter registered; (3) is the widget name registered in the bundle's registry; (4) does the template exist and resolve; (5) validate the exact bracket/backtick/ampersand grammar character by character.
Extension point to lean on: the widget registry. Registering a new widget name + template is the intended, low-risk way to add functionality — avoid subclassing the parser or the Twig extension, since you'll be maintaining a fork of a five-year-stale package and want your diff as small as possible.
How can I help you explore Laravel packages today?