Jonathan Scheiber

Set up CKEditor 5 with Symfony, Asset Mapper and Stimulus

Read time: 3 minutes 23 comments
Set up CKEditor 5 with Symfony, Asset Mapper and Stimulus

Setting up CKEditor 5 in Symfony, whether using Asset Mapper or Webpack Encore, is not straightforward.

In this article, we will see how to achieve this with Symfony, Asset Mapper, and a small, lazily loaded Stimulus controller, so we won't load CKEditor on pages that don't need it.

Prerequisites for this article:

Adding CKEditor Resources

First, let's install the CKEditor JS/CSS files using Asset Mapper:

bin/console importmap:require ckeditor5
bin/console importmap:require ckeditor5/dist/ckeditor5.min.css
# If you need translations, e.g. here French translations
bin/console importmap:require ckeditor5/translations/fr.js

CKEditor Configuration

Let's move on to the JS file that will allow us to configure CKEditor—license key, translations, and loaded plugins...

In this example, we import the basic plugins and configure the associated toolbar, while loading the translations in French.

// assets/ckeditor5.js
import {
    Autoformat,
    Bold,
    ClassicEditor,
    Essentials,
    Italic,
    Link,
    Paragraph,
} from 'ckeditor5';
// If you need to import translations, here French translations
import coreTranslations from 'ckeditor5/translations/fr.js';
import 'ckeditor5/dist/ckeditor5.min.css';

export default class EnhancedEditor extends ClassicEditor {}

EnhancedEditor.builtinPlugins = [Autoformat, Bold, Essentials, Italic, Link, Paragraph];

EnhancedEditor.defaultConfig = {
    licenseKey: 'GPL',
    toolbar: [
        'bold',
        'italic',
        '|',
        'link',
        '|',
        'undo',
        'redo',
    ],
    // You can remove the following part if you don't need other translations
    translations: [coreTranslations],
};

Stimulus controller

Here is the Stimulus controller, which allows us to load resources on demand (the controller itself being loaded lazily), and to instantiate the editors if they appear dynamically on the page.

// assets/controllers/ckeditor5_controller.js
import { Controller } from '@hotwired/stimulus';
import EnhancedEditor from '../ckeditor5.js';

/* stimulusFetch: 'lazy' */
export default class extends Controller {
    connect() {
        this.editor = EnhancedEditor.create(this.element)
            .then(editor => (this.editor = editor))
            .catch(error => console.error(error));
    }

    disconnect() {
        this.editor.destroy().catch(error => console.error(error));
    }
}

Dedicated Form Type

To make things easier, we'll create a dedicated form type that will allow us to easily load CKEditor into a form.

<?php

declare(strict_types=1);

namespace App\Form\Type;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;

/**
 * @extends AbstractType<string>
 */
final class CKEditor5Type extends AbstractType
{
    public function __construct(
        #[Autowire(service: 'stimulus.helper')]
        private readonly StimulusHelper $stimulusHelper,
    ) {}

    public function finishView(FormView $view, FormInterface $form, array $options): void
    {
        parent::finishView($view, $form, $options);

        $attr = $this->stimulusHelper->createStimulusAttributes();
        $attr->addController('ckeditor5');

        $view->vars['attr'] = $attr->toArray();
    }

    public function getParent(): string
    {
        return TextareaType::class;
    }
}

Finally, we can use it!

Yes, we'll finally be able to use it however we want in our forms! Here's how:

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder->add('content', CKEditor5Type::class);
}

Et voilà! :) If you have any questions, comments, suggestions... Feel free to post a comment below :)

Comments

Posted by Stof on 21/01/2025 at 15:25.

To avoid leaking resources, your Stimulus component should also handle the case where it is remove dynamically from the page, not just when it is added. For that, you need to store the editor instance (returned asynchronously from ClassicEditor.create()) and call its destroy() method in disconnect

Posted by jmsche on 22/01/2025 at 13:26.

Thank you for your feedback Stof, I fixed the Stimulus controller :)

Posted by Hervé Le Crosnier on 15/03/2025 at 16:57.

Merci beaucoup, c'est très clair, efficace.
Compte-tenu de l'usage massif de CKeditor dans les projets symfony, cela devrait figurer sur leur site...

Avez-vous d'autres éclairage sur l'usage de plugins sur CKeditor avec symfony et les assetmap ?

Posted by jmsche on 15/03/2025 at 18:05.

Bonjour Hervé, de quels éclairages auriez-vous besoin ? Un plugin en particulier ?

Posted by Hervé Le Crosnier on 15/03/2025 at 18:13.

Non, quelque chose de plus général : comment créer un plugin dans cette configuration (je savais le faire en CK4 sans asset mapper). En fait, ce n'est pas tellement ce qu'on met dans le plugin, mais où met-on le plugin.
Et aussi, où met-on les diverses configurations pour les intégrer au niveau du FormType de tel ou tel objet.
En fait, C'est une question sur l'architecture : où met-on les choses et comment les appelle-t-on.
Le genre de chose qui doit être tellement évident pour les spécialistes que c'est justement ce qu'on ne trouve pas... et aussi ce que votre post a très bien réussi : nom des fichiers, positionnement et liens.
Merci encore
Hervé

Posted by Yolserve on 27/07/2025 at 09:21.

Qu'en est-il de l'intégration avec EasyAdmin

Posted by jmsche on 27/07/2025 at 09:43.

Bonjour Yolserve,

Il suffit d'utiliser le nouveau form type créé avec EasyAdmin, et de configurer EasyAdmin pour qu'il charge un asset où les contrôleurs Stimulus sont chargés.

Posted by plesk on 27/07/2025 at 18:23.

Bonjour, comment ajouter plusieurs langues, par exemple fr, en, de, it, etc.? Merci.

Posted by plesk on 27/07/2025 at 20:56.

J'ai trouver une solution qui marche correctement.

// assets/controllers/ckeditor5_controller.js

import { Controller } from '@hotwired/stimulus';
import EnhancedEditor from '../ckeditor5.js';

/* stimulusFetch: 'lazy' */
export default class extends Controller {
    connect() {
        const locale = document.querySelector('html').getAttribute('lang') || 'en';
        this.editor = EnhancedEditor.create(this.element, {
            language: locale
        })
            .then(editor => (this.editor = editor))
            .catch(error => console.error(error));
    }

    disconnect() {
        if (this.editor) {
            this.editor.destroy().catch(error => console.error(error));
        }
    }
}
// assets/ckeditor5.js 
import {
    Autoformat,
    Bold,
    ClassicEditor,
    Essentials,
    Italic,
    Link,
    Paragraph,
} from 'ckeditor5';

import 'ckeditor5/dist/ckeditor5.min.css';

import translationsFr from 'ckeditor5/translations/fr.js';
import translationsEn from 'ckeditor5/translations/en.js';
import translationsDe from 'ckeditor5/translations/de.js';
import translationsIt from 'ckeditor5/translations/it.js';

export default class EnhancedEditor extends ClassicEditor { }

EnhancedEditor.builtinPlugins = [Autoformat, Bold, Essentials, Italic, Link, Paragraph];

EnhancedEditor.defaultConfig = {
    licenseKey: 'GPL',
    toolbar: [
        'bold',
        'italic',
        '|',
        'link',
        '|',
        'undo',
        'redo',
    ],

    translations: [translationsFr, translationsEn, translationsDe, translationsIt],
};
Posted by Damien on 21/08/2025 at 23:04.

Bonjour,
Perso j'ai beau suivre le tuto et faire l'appel dans EasyAdmin via la fonction configureAssets et le setFormType, rien à faire pour faire fonctionner CKEditor.
C'est fou que ça soit toujours aussi compliqué en 2025.
Est-ce possible d'avoir un exemple EasyAdmin svp ?

Posted by jmsche on 28/08/2025 at 21:12.

Bonjour Damien,

Avec EasyAdmin, tu as bien ajouté l'entrée qui charge les contrôleurs Stimulus ?

Sinon, ça ne peut pas fonctionner.

N'hésite pas à envoyer un lien vers un reproducer. :)

Posted by Peter Harbig on 31/08/2025 at 16:35.

Dear Damien,
many thanks for this very helpful solution.

Peter

Posted by Damien on 02/09/2025 at 09:36.

Bonjour jmsche.
Je pense que oui, j'ai configuré cela via la fonction "configureAssets".
Sinon c'est que je ne vois pas de quoi l'on parle, je vais recheck ça pour être sûr de ne rien louper du coup mais si tu as un exemple, je ne dis pas non :)
Pour le reproducer, je vais le préparer si ça ne veut toujours pas.

Posted by Damien on 04/09/2025 at 22:04.

Bonsoir, J'ai réessayé aujourd'hui pendant des heures, tentait via override du layout, configureAssets et autre mais rien à faire. Je vais refaire encore des tentatives demain et sinon bah, je ne sais pas :( Merci en tout cas pour vos retours.

Posted by jmsche on 08/09/2025 at 20:34.

Bonsoir Damien,

Je viens de faire un test rapide basé sur la démo d'EasyAdmin et ça a l'air de fonctionner.

Le repository Git : https://github.com/jmsche/easyadmin-ckeditor5

Tu peux voir le résultat sur la page "Form Field Reference" après avoir installé et démarré le projet (j'ai mis le champ au début de la page).

N'hésite pas à me contacter sur Slack si besoin, ou même juste à me dire si ça t'a aidé :)

Posted by Damien on 10/09/2025 at 22:43.

Bonsoir,
Merci beaucoup pour le repo.
La seule différence que j'ai trouvé mais qui fait que ça fonctionne (alors qu'il me semblait l'avoir testé), c'est que le configureAssets est dans le dashboard controller au lieu du controller de l'objet.
En tout cas merci beaucoup ça fonctionne à présent !!
Bonne soirée

Posted by rachid on 11/09/2025 at 22:53.

Bonjour,
je viens de lancer un projet Symfony 7. Avec une seule entité Article et admin/article/new. J'ai tout de suite suivi toutes vos étapes mais la barre ckeditor ne s'affiche pas. Pourquoi??? merci pour votre aide.

Posted by jmsche on 11/09/2025 at 23:07.

Bonjour Rachid, sans reproducer, code ou message d'erreur ça va être compliqué de répondre...

Posted by Damien on 12/09/2025 at 09:13.

@Rachid, le repo qui a été préparé pour moi est top et encore merci à @jmsche N'hésite pas à le parcourir et le tester sur ton poste.

Posted by rachid on 13/09/2025 at 18:09.

Bonjour @Damien et merci. Bonjour @jmsche. Je précise que je suis débutant autodidacte. Comment utiliser le Form Field Reference? La barre CKEditor5 ne s'affiche pas dans le content. Comment faire pour qu'elle le soit? Merci.

Posted by jmsche on 14/09/2025 at 11:44.

Bonjour Rachid,

Je t'invite à jeter un œil au dernier commit sur le repository.

Tu n'as pas spécialement besoin du FormFieldReference, il faut juste déclarer le contrôleur Stimulus CKEditor via un data attribute sur le form type (via une option, ou via un form type custom comme je l'ai fait ici), et faire en sorte que le contrôleur Stimulus soit chargé sur la page (donc le fichier assets/bootstrap.js doit être chargé).

Posted by rachid on 14/09/2025 at 15:49.

Bonjour et merci pour la réponse. Mais franchement si ça vous embête pas, j'ai besoin d'être guidé pas à pas. Déjà, je ne sais pas où placer ce code :

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder->add('content', CKEditor5Type::class);
}

Ensuite, pourrait tu me préciser comment déclarer le contrôleur Stimulus CKEditor via un data attribute sur le form type via : Quel form type? Quel data attribute?
Est-ce que je dois ajouter mon propre crud Article pour pouvoir accéder à admin/ article/new afin d'y placer la varre ckeditor? Merci

Posted by jmsche on 14/09/2025 at 15:57.

Je t'invite à lire un peu la documentation d'EasyAdmin sur les Fields, tout y est documenté : https://symfony.com/bundles/EasyAdminBundle/current/fields.html

Si tu as vraiment besoin d'aide par après, tu peux me contacter sur le Slack Symfony.

Add a comment

This field accepts content in Markdown format.