ibexa/fieldtype-richtext
The official Ibexa RichText field type for Ibexa DXP stores rich formatted text as structured XML, enabling clean rendering and editing of content. Part of Ibexa DXP; install via the platform.
This is not a Laravel package — it is a Symfony bundle that ships the ezrichtext (RichText) Field Type for Ibexa DXP. Don't try to composer require it into a Laravel app; it depends on ibexa/core, the Field Type registry, and the repository layer. Install it the supported way (as part of an Ibexa DXP install) and let Composer pull it in.
First steps:
composer show ibexa/fieldtype-richtext and check config/bundles.php for IbexaFieldTypeRichTextBundle. If you have Ibexa DXP, you almost never add it manually — you add it to a content type.Ibexa\FieldTypeRichText\FieldType\RichText\Type::IDENTIFIER (historically ezrichtext; the README refers to it as ibexa_richtext). Everything — config keys, Twig rendering, migrations — hangs off that identifier.Ibexa\FieldTypeRichText\FieldType\RichText\Value holds a DOMDocument in $value->xml. That is the single most important fact about this package: the stored payload is structured XML (a DocBook-ish dialect), not HTML and not Markdown.body), edit it in the back office, then render it in Twig:{{ ibexa_render_field(content, 'body') }}
{# or, legacy naming #}
{{ ez_render_field(content, 'body') }}
If that renders, the plumbing works. Then move on to reading/writing the field from PHP.
Rendering in templates. Prefer the render helper over dumping the XML. If you need the raw document (e.g. to iterate over embedded content references), drop down to field.value.xml:
{% set doc = content.getFieldValue('body').xml %}
{% for embed in doc.getElementsByTagName('ezembed') %}
{{ ez_render_field(embed.getAttribute('data-content-id')|ibexa_internal_id, 'embed') }}
{% endfor %}
Creating/updating a field programmatically. Build the XML, then let the converter normalise it — don't hand-assemble nodes and hope validation passes:
use Ibexa\FieldTypeRichText\FieldType\RichText\Value;
$doc = new \DOMDocument();
$doc->loadXML('<section xmlns="http://docbook.org/ns/docbook"><para>Hello</para></section>');
$value = new Value();
$value->xml = $doc;
$content->setFieldValue('body', $value);
$contentService->publishVersion($content->getVersionInfo());
Converting HTML ↔ XML. The bundle exposes converter interfaces (Ibexa\Contracts\FieldTypeRichText\RichText\Converter) and an aggregate converter configured under ibexa.field_type.richtext.converter. Use the input converter when importing legacy HTML into a RichText field, and the output converter when you need HTML for a non-Twig consumer (API response, PDF, search index export). Inject the named converter service rather than new-ing anything.
Rendering outside the back office. If you expose content via a JSON API, don't return $value->xml->saveXML() and let the consumer figure it out. Convert to XHTML server-side with the output converter and cache the result keyed on contentId + versionNo + siteaccess. Conversion walks the whole DOM and resolves embeds; doing it per request at scale will hurt.
Migrations. In ibexa:migrations YAML, a richtext field value is raw XML:
- type: content
mode: create
fields:
- identifier: body
fieldtype: ezrichtext
value: "<section xmlns=\"http://docbook.org/ns/docbook\"><para>Hi</para></section>"
Custom tags. These are the extension point for "a thing inside the text" (embed block, product card, ad slot). Implement the custom tag interface (name + converter + template) and register it with the ibexa.field_type.richtext.custom_tag tag, then declare it in the siteaccess config so the editor toolbar offers it. The tag name must match between the config, the XML element, and the converter — mismatches produce silently empty output rather than an error.
Pin a recent patch. Two security/correctness fixes matter: XSS in custom tags (fixed in 4.6.25 / 5.0.3) and inline custom tags rendering empty when the inline string is "0" (fixed in 5.0.8, note the PHP falsy-string trap). Also setBooleanValue had a bad cast corrected in 5.0.8. If you're on an older tag, upgrade.
The RNG schema is a wall you will hit. Every stored document is validated against ezpublish.rng. Add a custom element without extending the schema and publishing fails with a validation error that reads like a malformed-XML complaint. Recent versions added an option to extend ezpublish.rng — use it, and extend it in the same change as the custom tag, not later.
Don't edit the XML in the database. The back office re-saves through the converter chain; anything you inject that isn't schema-valid will be rejected on the next edit, and inline markup like <strong> written as HTML will be dropped or mangled. Always go through Value + converter.
Empty vs. missing. $value->xml is a DOMDocument; an empty field is an empty document, not null. getFieldValue() on a field that doesn't exist returns null — guard both cases before calling saveXML().
Twig auto-escaping. Converted output is HTML, so it needs |raw if you're printing a converter result (the render helper already handles this). Printing xml.saveXML() directly into a template is an XSS hole and also leaks internal embed IDs.
Config quirks. Toolbar buttons, tag presets, custom tag declarations and style definitions are all per-siteaccess under ibexa.system.<scope>.fieldtypes.ezrichtext. Put shared defaults in default, override per siteaccess. Note the recent "fallback configuration when failing init" fix — an invalid toolbar config used to blow up the editor rather than degrade, so validate after editing YAML.
Front-end build. The editor plugins ship TypeScript/JS that must be compiled through Webpack Encore. If the toolbar renders but plugins 404, your Encore config is stale — re-run the asset build and clear the back-office cache. TS config support was only added in 5.0.8, which tells you how fragile this area has been.
Front-end event subtleties. If custom JS calls updateInput, the optional shouldFireInputEvent attribute controls whether an input event fires; fire it indiscriminately and you'll clear validation error messages the user needed to see.
Debugging strategy. Validation errors come from Ibexa\Contracts\FieldTypeRichText\RichText\Validator — catch and log the specific RNG line number, not just "invalid XML". For rendering bugs, dump $value->xml->saveXML() and diff against a known-good document produced by the back office; nine times out of ten the difference is a missing namespace or a data- attribute the converter expects.
How can I help you explore Laravel packages today?