prolix/tinymce-bundle
Prolix TinymceBundle integrates the TinyMCE WYSIWYG editor into Symfony applications, with bundled assets and straightforward configuration for adding rich-text editing to forms and templates. It helps teams quickly enable a customizable editor without manually wiring frontend dependencies.
v3.0.5 is one commit: symfony/framework-bundle constraint bumped to 7.*. That changes which Symfony generation this targets — and finally makes the "release date looks like metadata churn" suspicion from the previous assessment wrong in a good way — but it changes nothing about the headline fact: this is still a Symfony bundle, not a Laravel package. No Prolix\TinymceBundle\TinymceServiceProvider, no extra.laravel, no package discovery. A version bump does not create a service provider.
Two paths, again.
Path A — Symfony 7 (now the stated target, but verify before trusting it).
// config/bundles.php (AppKernel is long gone; Flex uses bundles.php)
return [
// ...
Prolix\TinymceBundle\ProlixTinymceBundle::class => ['all' => true],
];
# config/packages/prolix_tinymce.yaml
prolix_tinymce:
base_url: /bundles/prolixtinymce/tinymce
theme: oxide # 'modern' was removed in TinyMCE 6
plugins: "lists link image table code"
toolbar: "undo redo | bold italic | bullist numlist | link image"
php bin/console assets:install public --symlink
# NOT assetic:dump — AsseticBundle is abandoned and has no Symfony 7 support
{{ tinymce_init() }}
{{ form(form) }}
A constraint bump is not a port. The PR edited composer.json; it did not touch PHP. Before you build on v3.0.5, read the source for things Symfony 7 / Twig 3 removed: \Twig_Extension (must extend Twig\Extension\AbstractExtension), missing return types on load() / getAlias() / getConfiguration(), Symfony\Component\DependencyInjection\Extension\Extension signatures under PHP 8.2+, and any $container->getDefinition() calls that assume Symfony 2 internals. If the extension class still says Twig_Extension, the bundle will install and then fatal at container compile.
Path B — you're a Laravel developer. Unchanged: don't require it, lift ~150 lines.
DependencyInjection/Configuration.php — the config tree (theme, plugins, toolbar, language, base_url, content_css, theme_options). Genuinely reusable.Twig/Extension/* — the array→tinymce.init({...}) transform.Resources/views/* — the <script> emission you'll replace with a Blade component.composer require tinymce/tinymce # or dump the dist into public/vendor/tinymce
# publish your own config/tinymce.php
{{-- resources/views/components/richtext.blade.php --}}
@props(['name' => 'body', 'value' => ''])
<textarea name="{{ $name }}" id="{{ $name }}">{{ $value }}</textarea>
<script src="{{ asset('vendor/tinymce/tinymce.min.js') }}"></script>
<script>
tinymce.init({!! json_encode(array_merge(
config('tinymce.options'),
['selector' => '#'.$name, 'height' => 400]
), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG) !!});
</script>
First real use case: one "body" textarea on an admin CRUD form — render, save HTML to a text column, round-trip via {!! $post->body !!} — before generalizing to a component.
Centralize the option array, override per call. Same design the bundle uses: one config block, one reader. In Laravel that's config/tinymce.php plus a shallow merge in the component. Never scatter tinymce.init through Blade.
Two init strategies — pick one:
{{-- (a) selector-based: each component owns its init --}}
{{-- (b) global init in the layout, opt-in via .richtext class --}}
(a) is safer with 1–2 editors; (b) wins at 10+. Don't mix on one page — double init on the same element no-ops or throws Editor ID already exists.
SPA / Livewire. TinyMCE replaces the textarea with an iframe; DOM morphing orphans it.
function bootEditors(root = document) {
root.querySelectorAll('textarea.richtext').forEach(el => {
if (tinymce.get(el.id)) tinymce.get(el.id).remove();
tinymce.init({ selector: '#' + el.id, /* ... */ });
});
}
document.addEventListener('livewire:navigated', () => bootEditors());
Livewire.hook('morph.updated', ({ el }) => bootEditors(el));
Force-sync before serialization: tinymce.triggerSave() on submit, or read tinymce.get('body').getContent() and $wire.set(...).
Server-side sanitization is still your job. The bundle (and the new release) does zero sanitizing — it wires up an editor. Run content through mews/purifier on validation:
$validated['body'] = clean($validated['body'], 'rich');
Asset strategy: self-host. Drop tinymce.min.js, themes/, plugins/, skins/, models/, icons/ into public/vendor/tinymce/. The base_url config exists because the editor lazy-loads plugins relative to its own script URL — get it wrong and you 404 only when someone clicks "image".
If you do maintain a Symfony 7 install: register the bundle in config/bundles.php, put config in config/packages/prolix_tinymce.yaml, use when@prod for fingerprinted asset paths, and prefer PHP/services.php DI definitions over the bundle's legacy Resources/config/services.xml when overriding anything.
Testing. Don't type into the iframe; drive the API.
// Laravel Dusk
$browser->script("tinymce.get('body').setContent('<p>Hello</p>');");
$browser->click('@save');
Migration path. Form field names and stored HTML are portable. Rewrite: Twig function → Blade component, config.yml → config/tinymce.php, assets:install → public/vendor. Half a day.
Don't composer require this into Laravel — but the reason changed. On Symfony 2 the constraint conflicted hard with illuminate/*. With framework-bundle: 7.*, Laravel 11 (which already runs on Symfony 7 components) may resolve it cleanly; Laravel 10 and below (Symfony 6 components) will still conflict. Either way you gain nothing: no service provider, no extra.laravel, no discovery, just a dead Symfony kernel sitting in vendor/. Copy the three files instead.
v3.0.5 is a patch number that may be a breaking change. The note says the constraint was set to 7.*. If it previously allowed ^5|^6|^7, this silently drops Symfony 5/6 from a 3.0.4 → 3.0.5 bump — semver-hostile. Read the composer.json diff before composer update. On Symfony 6, pin 3.0.4. On Symfony 7, remember framework-bundle 7.* means PHP ≥ 8.2 and no Assetic, no AppKernel, no templating helper.
Assetic is dead. The old php app/console assetic:dump step is invalid on any modern Symfony; symfony/assetic-bundle is abandoned. Use bin/console assets:install public plus Webpack Encore or Vite. If the bundle's docs still mention Assetic, they're stale — treat the README as Symfony-2-era prose with a Symfony-7 constraint stapled on.
Still a one-maintainer, near-zero-star project. The new contributor and the real code change are genuine positives — this isn't abandoned — but it is still a ~150-line glue bundle with no test suite you can see and no downstream dependents. Treat it as a reference implementation, not a security-maintained dependency. You are the maintainer of your fork.
tinymce is not defined. The init ran before tinymce.min.js executed — usually the init is in a component rendered above the <script src>. Put the script in <head> (or defer + DOMContentLoaded), init at the end of <body>.
404s on plugins/skins appear late. Failed to load plugin: link from url ... means base_url is wrong (trailing slash, wrong subdirectory) or you never copied the dist. The base editor loads fine; only the button fails. Click every toolbar button during QA, not just "does the editor appear."
JSON into a <script> context. Any option value containing </script> (a content_style, a valid_elements string) terminates the block and breaks the page in a way that looks unrelated. Use JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES, or emit a data-options attribute and JSON.parse it.
tinyMCE.settings is mutable global state. Setting defaults once and calling tinymce.init() bare leaks config between editors. Pass a fresh object literal per init.
Strict CSP will bite you. TinyMCE injects inline styles and the bundle emits an inline <script>; you need a nonce or style-src 'unsafe-inline'. This is the #1 reason teams abandon "just drop in TinyMCE."
Reading content back. document.getElementById('body').value is empty until tinymce.triggerSave(). Validation that "passes" on an empty body and saves a blank post is the classic bug — add one top-level submit listener, not one per editor.
content_css and body_class. Editors look wrong out of the box because the iframe has no site CSS. Point content_css at a build tuned for the editor's narrow viewport, or authors format text that looks nothing like the published result.
Debug order when no editor appears: (1) Network — did tinymce.min.js 200? (2) Console — tinymce is not defined (ordering) vs. Editor ID already exists (double init) vs. Failed to load plugin (base_url). (3) tinymce.get('body') — non-null but blank UI means a CSS/z-index/height conflict with your admin theme.
How can I help you explore Laravel packages today?