m6web/draftjs-bundle
m6web/draftjs-bundle is a Symfony bundle for server-side rendering of Draft.js content. It converts Draft.js raw JSON into HTML, letting rich text created with the React editor be displayed in Twig templates without relying on client-side JavaScript.
What it is: a Symfony bundle from M6Web that takes the raw JSON produced by a Draft.js editor (blocks + entityMap + inlineStyleRanges + entityRanges) and converts it to HTML server-side. You store editor JSON in your database and render it from Twig or PHP without shipping a JS renderer to every page.
Read this first: the package is archived and marked DEPRECATED (last release Feb 2017). Before writing any code, decide which of two situations you are in:
draft-js-export-html / draft-convert, or store HTML produced by a sanitizer you control.Minimal adoption steps (legacy project):
composer require m6web/draftjs-bundle — expect to have to pin old Symfony (~2.7|~3.0) and PHP (>=5.5) constraints; check composer.json in the repo, not just Packagist.new M6Web\Bundle\DraftjsBundle\M6WebDraftjsBundle() in AppKernel.php, or in config/bundles.php for Symfony 4+ flex apps.bin/console cache:clear.DependencyInjection/Configuration.php. That file is the whole public API surface — it lists the block types, inline styles and entity types you can map to HTML tags/classes. Read it before reading any other file.Twig/Extension/DraftjsExtension.php or similar). It tells you the exact filter name to use — the naming has changed historically, so grep getFilters() rather than guessing between draftjs, draftjs_html and draftjs_render.First use case — an article body:
// entity
$article->setContent($request->request->get('content')); // raw JSON string from the editor
{# template — filter name per step 5 #}
<div class="article-body">
{{ article.content|draftjs }}
</div>
The output is an HTML string you must mark |raw, or pipe through the converter service in PHP and pass the string to the view.
Store raw JSON, render on demand. Persist the editor's ContentState.toRaw() output in a JSON/TEXT column. Never store the bundle's HTML output as the source of truth — you will want to change markup, add classes, or migrate renderers later, and re-rendering from JSON is trivial while re-parsing HTML is not.
PHP service instead of Twig when you need the result elsewhere. Inject the bundle's converter/Draftjs service into a controller, an API serializer, or a console exporter. Typical integration points:
content_html alongside content.Cache aggressively. Conversion is a pure function of the JSON plus the config. Key a cache entry on sha1($rawContent . $configVersion) and invalidate on config change. Article pages that render the same body on every request should not re-parse the JSON.
Extend via custom decorators, not by editing vendor code. The bundle is built around a decorator concept: one decorator per block type / inline style / entity type, each responsible for producing the tags for its node. The day-to-day extension path is:
m6web_draftjs config tree under the relevant style/block/entity key, or as a tagged service (m6web_draftjs.decorator or equivalent tag — confirm the tag name in the bundle's Resources/config/services.yml).This is how you handle header-two → <h2 class="...">, custom block types from a Draft.js plugin, or atomic blocks wrapping embedded videos.
Handle the entity map explicitly. Links and images live in entityMap and are referenced by index from entityRanges. Decide early what your policy is for an entity key that doesn't exist (dangling reference after partial edits), an entity type you don't support (render text, drop the node?), and IMMUTABLE entities with missing data.href.
Validate the payload on write, not on read. Reject content that doesn't match the Draft.js raw schema at the controller/FormType level. The bundle was written assuming well-formed input from a known client; a malformed blocks array or a block with a null text will surface as a runtime error on a public page.
Sanitize the output. The converter emits whatever the entity data says. Never |raw untrusted content directly:
{{ article.content|draftjs|raw }} {# only after HTMLPurifier or equivalent #}
Configure an HTMLPurifier pass (or html-sanitizer) that allows only the tags/attributes your decorators produce, and whitelist http/https/mailto in href/src.
It will not run on modern PHP. The codebase predates PHP 7.2 typed properties, void return types, and Symfony 4's container changes. Expect deprecation notices at minimum and fatal errors from removed functions at worst. Budget a morning for a php -l sweep and a Symfony version bump, or accept that you are freezing the app's runtime alongside the bundle.
Unknown block types fail silently or noisily. A block whose type has no registered decorator usually renders nothing — a whole section of an article disappears with no error. Add a catch-all decorator that emits a <p> (or logs) so content loss is visible.
unstyled is not "empty". Draft.js's default block type is literally the string unstyled; a paragraph of plain text uses it. If your config only maps header-one, blockquote, etc., every normal paragraph is dropped. Map unstyled first and verify with a fixture before touching anything else.
Nested lists don't exist in the raw format. Draft.js encodes ordered/unordered lists as flat blocks with depth in the block data (set by plugins) or as consecutive unordered-list-item blocks. Producing valid <ul><li> nesting requires your own depth-tracking decorator; the bundle will not invent it.
Offsets are UTF-16 code units, not bytes and not characters. inlineStyleRanges/entityRanges offsets assume JS string indexing. Emoji and other astral-plane characters will shift every inline style and entity after them by one position, producing mis-placed <strong> tags or a link that wraps the wrong words. If your content contains emoji, test with "Hello 😀 world" before going live.
Soft newlines. Shift+Enter inside a block produces \n inside block.text with no style range. If your decorators don't convert \n to <br>, paragraphs collapse onto one line.
Config is compiled. After changing any decorator mapping, run cache:clear in every environment, including prod. A stale var/cache with the old decorator map is the most common "my change did nothing" report.
Debugging recipe. Dump the raw JSON for the failing article, shrink it to two or three blocks, and feed that fixture through the converter in a unit test (or bin/console with a scratch command). Ninety percent of issues are: missing block-type decorator, dangling entity key, or UTF-16 offset drift.
Extension points beyond decorators are thin. There's no events system worth using. If you need different behaviour for the same content type, the pragmatic move is to wrap the converter in your own service (alias/decorates the bundle's converter) that pre-processes the raw JSON — normalising block types or splitting unsupported nodes — before handing it to the bundle.
Have an exit plan. Because the JSON format is stable and well documented, migrating off the bundle is mechanical: write a DraftJsonToHtml class using draft-convert's server-side equivalent in your language of choice, or a small recursive walker of your own, then swap the service alias. Keep the raw JSON in the database and the migration stays a one-day job whenever you decide to leave — which, given the package's status, you eventually will.
How can I help you explore Laravel packages today?