helios-ag/fm-tinymce-bundle
Symfony bundle that integrates the TinyMCE WYSIWYG editor into forms and templates, with configurable toolbars, asset handling, and optional file-manager support. It simplifies adding rich-text editing to Symfony admin interfaces and content forms.
This is still a Symfony bundle, not a Laravel package — 3.0 does not change that. composer require helios-ag/fm-tinymce-bundle will pull the code into vendor/, but there is no service provider, no auto-discovery, and nothing will boot. It registers FM\TinyMCEBundle\FMTinyMCEBundle for Symfony's kernel and ships Twig templates. Treat it as reference material and a design blueprint, not a dependency. What you actually port is two things:
DependencyInjection/Configuration.php (what options exist and their defaults).Resources/views/ (what the emitted tinymce.init({...}) call looks like).Read those two files first — they are the entire contract. Everything else in the repo is Symfony wiring you will not reuse. Read them at the 3.0 tag specifically: one of the 3.0 changes is a bump of the bundled TinyMCE assets ("Tinymce up"), so the emitted init script and default option names may differ from the 2.0-era examples you'll find in blog posts.
A minimal Laravel translation, in the order you'd build it:
npm i tinymce
php artisan vendor:publish --tag=... # or just create config/tinymce.php by hand
// config/tinymce.php — mirror the bundle's option names so docs/README examples stay transferable
return [
'base_url' => '/vendor/tinymce', // self-hosted; copied out of node_modules at build time
'suffix' => '.min',
'config' => [
'menubar' => false,
'plugins' => 'advlist autolink link image lists charmap preview code',
'toolbar' => 'undo redo | bold italic | bullist numlist | link image | code',
'height' => 400,
'valid_elements' => '*[*]',
],
];
First use case: one body textarea in an admin CRUD form. Render the textarea normally, then emit the init call with a selector scoped to a wrapper class — same shape as the bundle's tinymce_init() Twig function, just a Blade component:
{{-- resources/views/components/tinymce.blade.php --}}
@props(['name', 'value' => ''])
<div class="tinymce-wrap" data-tinymce='@json(config("tinymce.config"))'>
<textarea name="{{ $name }}" id="{{ $name }}">{{ $value }}</textarea>
</div>
// resources/js/tinymce.js — the equivalent of the bundle's emitted inline script
import tinymce from 'tinymce/tinymce';
import 'tinymce/themes/silver';
import 'tinymce/icons/default';
import 'tinymce/models/dom';
import 'tinymce/plugins/advlist'; /* ...one import per plugin in your toolbar, or nothing loads */
const boot = () => document.querySelectorAll('.tinymce-wrap').forEach(el => {
const ta = el.querySelector('textarea');
if (tinymce.get(ta.id)) return;
tinymce.init({ ...JSON.parse(el.dataset.tinymce), target: ta });
});
document.addEventListener('DOMContentLoaded', boot);
Config → provider. Use mergeConfigFrom + publishes so per-environment overrides work the way Symfony's fm_tinymce: tree did:
$this->mergeConfigFrom(__DIR__.'/../config/tinymce.php', 'tinymce');
$this->publishes([__DIR__.'/../config/tinymce.php' => config_path('tinymce.php')], 'tinymce');
Per-form overrides. The bundle's value was letting a controller/form class override theme, toolbar, plugins per instance rather than globally. Reproduce that with a component prop that shallow-merges into the global config:
<x-tinymce name="summary" :options="['toolbar' => 'bold italic', 'height' => 200]" />
Merge server-side (array_replace_recursive(config('tinymce.config'), $options)) and @json the result into the data- attribute. Never build the options array in JS from string concatenation.
Asset delivery. Don't import the whole editor through Vite — skins, icons, and plugins are loaded lazily by URL and Vite won't know about them. Copy the runtime directory into public/vendor/tinymce at build time (vite-plugin-static-copy or a postinstall cp) and set base_url to that path. This is exactly the role of the bundle's base_url option, and it's the single most common source of 404-on-skin failures.
Multiple editors per page. Always init via selector/target on a scoped wrapper, never a global textarea selector — the bundle's mode: textareas convenience selector is the pattern that breaks first when a page grows a search box.
Livewire / Inertia / Turbo. This is where day-to-day friction lives. TinyMCE mutates the DOM outside your framework's knowledge.
wire:ignore so morphs don't destroy it, and sync content manually on change:
tinymce.init({ target: ta, setup(ed) {
ed.on('change', () => ed.save()); // writes back to the textarea
ed.on('blur', () => Livewire.find(ta.closest('[wire\\:id]').getAttribute('wire:id'))
.set(ta.getAttribute('wire:model'), ed.getContent()));
}});
Without wire:ignore you get "editor already exists" on every re-render.onMounted, tinymce.remove() in onUnmounted. Keep the options object in a plain JS module, not reactive state.livewire:navigated / Turbo turbo:load: re-run the boot function; the old listener won't fire on soft navigation.Image/file uploads. The bundle punts on this; you'll wire TinyMCE's images_upload_handler to a Laravel route. Return { location: '...' }, include X-CSRF-TOKEN from a meta tag, and validate MIME server-side — the handler trusts whatever the browser sends.
Persistence and rendering. Store the HTML in a TEXT/LONGTEXT column. Sanitize on save with mews/purifier or HTMLPurifier (TinyMCE is an editor, not a sanitizer), then render with {!! !!}. Validate against the stripped length, not the raw string — max:2000 on raw HTML will reject a 900-character post full of markup.
composer require any of the TinyMCE-wrapper Laravel packages, or just use tinymce from npm — the bundle's only real value here is its option naming and its init template.matthiasnoback/symfony-*-test to ^5.1/^6.0 and Codacy badges). All of that is Symfony-side or CI-side. Nothing in the release notes adds runtime features you'd port, and nothing removes an option name — but the editor major under Resources/public/ may have moved, so check which TinyMCE version 3.0 ships before copying defaults from it.//tinymce.cachefly.net/4.0/tinymce.min.js; that CDN and that major version are long gone. The config key also moved between releases (config: in 2.x/3.x-era tags vs. tinymce_config: later). Always diff Configuration.php against the tag you're reading — 3.0's doc rewrite is the most current, but the README has been wrong before.plugins but you didn't import its module, TinyMCE logs a console warning and simply drops the button. Symptom: toolbar shows a gap. Check the console, not the config.base_url must end with / and must contain skins/ui/oxide/, icons/default/, themes/silver/, models/dom/, and plugins/<name>/. Any one of them missing produces an unstyled editor or a blank iframe with a 404 you'll only see in the Network tab.style-src 'unsafe-inline' and, if using the Cloud CDN, script-src https://cdn.tiny.cloud. Add frame-src 'self' blob: too.valid_elements. Custom components, <iframe> embeds, or data-* attributes vanish on save. Extend via extended_valid_elements rather than loosening to *[*] blindly.if (tinymce.get(id)) return; or tinymce.remove('#'+id) before re-init. The error is thrown, not logged, and kills the rest of your boot loop.$browser->script("tinymce.get('body').setContent('<p>hi</p>')") before submitting. Assert on the stored HTML, not on the editor.@FMTinyMCE/... templates to change the emitted script. The Laravel equivalent is publishing the Blade view (php artisan vendor:publish --tag=views) — keep the init script in one overridable partial rather than inlining it in three admin forms.How can I help you explore Laravel packages today?