fsi/ckeditor-bundle
The FSI CKEditor Bundle integrates the CKEditor WYSIWYG editor into Symfony applications, providing a configurable form type, toolbar and plugin options, asset/configuration handling, and simple embedding for rich-text content editing.
Install the bundle through Composer and register it. It's a Symfony bundle wrapper around CKEditor 4, so the bulk of "getting started" is wiring config and assets — not writing editor code.
composer require fsi/ckeditor-bundle
Traditional Symfony (3.x / 4.0–4.2) registration:
// app/AppKernel.php
public function registerBundles()
{
return [
// ...
new Ivory\CKEditorBundle\IvoryCKEditorBundle(),
];
}
With Flex, the bundle is auto-discovered; put configuration in config/packages/ivory_ck_editor.yaml.
Minimal configuration — one named config you'll reuse everywhere:
# config/packages/ivory_ck_editor.yaml
ivory_ck_editor:
default_config: my_config
render_assets: true
configs:
my_config:
toolbar:
- [Bold, Italic, '|', Link, Unlink]
- [NumberedList, BulletedList, '|', Undo, Redo]
uiColor: '#f4f4f4'
height: 300
Make sure the CKEditor library is available where the bundle expects it (typically public/bundles/ivoryckeditor/ in Symfony 4+, web/bundles/... in Symfony 3), then run bin/console assets:install.
First use case — a form field. This is 95% of real usage:
use Ivory\CKEditorBundle\Form\Type\CKEditorType;
$builder->add('body', CKEditorType::class, [
'config_name' => 'my_config',
]);
Render the assets once in your base layout (usually before </body>):
{{ ckeditor_assets() }}
Then let the form type do the rest: {{ form_row(form.body) }} automatically emits the textarea plus the inline replacement script.
If you're not using Symfony Forms, the Twig helpers cover the rest:
{# textarea + replace script #}
{{ ckeditor_widget('my_config') }}
{# or replace existing elements by selector #}
{{ ckeditor_replace('my_config', {'selector': '.rich-text'}) }}
First thing to check when it doesn't work: whether the CKEditor core JS is actually reachable at the configured base_path/js_path, and whether ckeditor_assets() is rendered exactly once.
Config-driven toolbar variations. Rather than stuffing toolbar arrays into every form type call, define named configs in YAML (basic, full, comment) and reference them:
ivory_ck_editor:
configs:
basic:
toolbar: [[Bold, Italic, Link]]
full:
toolbar:
- [Bold, Italic, Underline, '|', NumberedList, BulletedList]
- [Link, Unlink, Image, Table, '|', Source]
allowedContent: true
That keeps the toolbar consistent across admin and frontend, and makes it trivial to swap in tests.
Per-form overrides when you need one-off behavior — the config option merges over the named config:
$builder->add('footer', CKEditorType::class, [
'config_name' => 'basic',
'config' => ['height' => 120, 'toolbar' => [['Bold', 'Italic']]],
]);
Plugin registration goes through a plugins section, then extraPlugins on the config that wants them:
ivory_ck_editor:
plugins:
wordcount:
path: bundles/ivoryckeditor/plugins/wordcount/
filename: plugin.js
configs:
full:
extraPlugins: 'wordcount'
File uploads / filebrowser — the bundle does not ship a file browser. The common pattern is to enable the filebrowser* options on the config (which just sets CKEditor's URL properties) and point them at your own controller:
configs:
full:
filebrowserUploadUrl: '/admin/ckeditor/upload'
filebrowserBrowseUrl: '/admin/ckeditor/browse'
Your controller is responsible for validating MIME types, size, and returning a CKEditor-compatible JSON response.
Twig-only pages (static content editors, snippets, email templates) are often cleaner than dragging Symfony Forms in:
<form method="post">
<textarea name="content">{{ content }}</textarea>
{{ ckeditor_replace('full', {'selector': 'textarea[name=content]'}) }}
<button type="submit">Save</button>
</form>
Re-rendering after AJAX is a classic gotcha. When you inject a form fragment via AJAX, the inline CKEDITOR.replace() script won't re-run. The usual pattern is to return the rendered HTML and explicitly call CKEDITOR.replace in the success handler using config exported from the bundle, or use ckeditor_replace() inside the returned fragment.
Testing. Because CKEditorType is just a TextareaType subclass with attributes, functional tests work normally — the rendered output is a <textarea> with data-* attributes. Assert on the textarea value rather than on CKEditor UI.
This is effectively unmaintained. Last release is 2018, it wraps CKEditor 4 (which is EOL), and it targets old Symfony versions. On Symfony 5+, expect dependency conflicts (symfony/form version constraints). Treat it as a stopgap: plan a migration to CKEditor 5 with a dedicated Symfony integration, or to a maintained alternative. If you must use it, pin versions aggressively and check the composer constraints before upgrading Symfony.
Sanitize on the server. CKEditor output is raw HTML. Escaping with |raw in Twig without sanitizing is an XSS hole — especially with allowedContent: true. Run content through HTMLPurifier (or a whitelist sanitizer) on save, not just on render.
Render assets exactly once. Calling ckeditor_assets() in multiple layouts (base + child) produces duplicate CKEditor loads; symptoms include CKEDITOR is not defined, toolbar rendering twice, or silent failures. Guard it with a block or render only in the base layout.
Asset paths break after upgrades. base_path / js_path / css_path defaults assume the old web/bundles/ layout. In Symfony 4+ you need public/bundles/ivoryckeditor/, and in Symfony 5+ with asset versioning/CDN, the paths must be absolute or go through the asset mapper. This is the number-one cause of "blank editor" issues.
Config merge semantics are shallow-ish. Arrays merge, scalars override. If you set toolbar in both the named config and the form option, the form option wins entirely — not element-by-element. Debug with bin/console config:dump-reference ivory_ck_editor.
required and empty content. The form field is required by default; an empty CKEditor produces <p><br></p> in some configurations, which passes validation as non-empty. Either normalize empty editor HTML server-side or use a NotBlank constraint aware of the placeholder.
File uploads are yours to secure. The filebrowser* config options are just URL hooks — no upload handling exists. Don't blindly trust CKEditorFuncNum or the type parameter; validate against an allowlist and never write to a public directory without sanitizing the filename.
Debugging workflow. When the editor doesn't initialize: (1) view source — is the inline CKEDITOR.replace(...) script emitted? (2) check the browser console for a 404 on ckeditor.js; (3) confirm the textarea id matches the script's selector; (4) verify your configs key matches config_name — a typo silently falls back to the default config, which is why "my toolbar changes do nothing."
Extension point for custom form types. If you need a project-wide default (e.g. always add wordcount), create a custom CKEditorType subclass or a form type extension and register it — far cleaner than repeating config arrays across every form.
Watch for Sonata/EasyAdmin conflicts. Both admin bundles ship their own CKEditor integrations; registering this bundle alongside them often causes two copies of CKEditor on the same page and duplicated assets. Pick one integration per project.
How can I help you explore Laravel packages today?