Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Froala Editor Bundle Laravel Package

leapt/froala-editor-bundle

Symfony bundle integrating the Froala WYSIWYG HTML editor. It provides a form type plus Twig and asset integration, making it simple to add configurable rich text fields to Symfony forms and templates with minimal boilerplate. Useful for admin panels and content management screens that need a ready-...

View on GitHub
Deep Wiki
Context7

Getting Started

The bundle is a thin Symfony glue layer: a form type that renders a <textarea> plus the JS that upgrades it, and a config tree for default Froala options. That's the whole surface area — which is good news, because there's very little to learn.

Install and register

composer require leapt/froala-editor-bundle
// config/bundles.php
return [
    // ...
    Leapt\FroalaEditorBundle\LeaptFroalaEditorBundle::class => ['all' => true],
];

If you're on Symfony 8 you need v1.8.0 or later — that's the release that widened the constraint. v1.8.0 is maintenance-only (CI, PHPStan, PHP CS Fixer, PHPUnit 11, PHP 8.5 test matrix, Symfony 8 allowance); no form-type options, config keys, or Twig blocks changed, so upgrading from 1.7.x is a composer update and a smoke test of one admin page.

Look here first, not at the README

bin/console config:dump-reference leapt_froala_editor

This is the single highest-value command. The bundle is old enough that the README has drifted from the code in places; the dumped config tree is the ground truth for option names, defaults, and asset-inclusion flags. Do the same for the form type:

bin/console debug:form Leapt\FroalaEditorBundle\Form\Type\FroalaEditorType

That tells you exactly which options the type accepts (attr, toolbar, height, language, whatever the current release exposes) instead of guessing from blog posts written for Froala 2.

Minimal config

# config/packages/leapt_froala_editor.yaml
leapt_froala_editor:
    language: en
    height: 400
    toolbar: [bold, italic, underline, |, formatOL, formatUL, |, insertLink, insertImage, |, html]

First use case: a CMS body field

// src/Entity/Page.php
#[ORM\Column(type: Types::TEXT)]
private string $body = '';
// src/Form/PageType.php
$builder
    ->add('title', TextType::class)
    ->add('body', FroalaEditorType::class, ['label' => 'Content']);
{# templates/admin/page/edit.html.twig #}
{{ form_start(form) }}
    {{ form_row(form.title) }}
    {{ form_row(form.body) }}
    <button type="submit">Save</button>
{{ form_end(form) }}

That's it — submit, persist, and render with {{ page.body|raw }} on the front end. The first real decision you have to make is not in the bundle at all: sanitize before you persist, not before you render. See the patterns below.

Implementation Patterns

Treat the form type as the only public API

Everything else in the bundle (asset inclusion, config) is convenience. Wrap the form type in your own alias on day one:

// src/Form/Type/RichTextType.php
final class RichTextType extends AbstractType
{
    public function getParent(): string
    {
        return FroalaEditorType::class;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'attr' => ['class' => 'js-richtext'],
            'required' => false,
        ]);
    }
}

Now ->add('body', RichTextType::class) is the only thing in your codebase that references the bundle. Swapping to Trix, TinyMCE, or a custom Stimulus editor later is a one-file change instead of a grep across 40 form types. Given the package's footprint (small star count, single maintainer), this insurance is cheap and worth taking — and it's also what makes the Symfony-major upgrade path a non-event (see the last gotcha).

Central defaults, surgical overrides

Put the toolbar, height, and language in the bundle config so every editor is consistent, then override only where the content model genuinely differs — a short "meta description" field shouldn't get the same 40-button toolbar as the article body.

$builder->add('metaDescription', RichTextType::class, [
    'toolbar' => [['bold', 'italic'], ['undo', 'redo']],
    'height' => 120,
]);

If the bundle version you're on doesn't forward per-field options, the fallback is attr with a data-* payload read by your own initializer script. Verify with debug:form before reaching for that.

Own the asset pipeline

Most versions of this bundle offer a CDN/asset-inclusion flag. For anything beyond a prototype, turn it off and load Froala through your normal pipeline (AssetMapper, Webpack Encore, or a pinned vendored build):

  • You control the Froala major version, which is what determines plugin availability.
  • You satisfy CSP — a CDN <script> from cdn.jsdelivr.net is usually blocked in a corporate or hardened setup.
  • You get deterministic builds; a CDN version bump shouldn't silently change your editor's HTML output.

Self-hosting is also the only way to keep the editor working offline and to avoid a third-party request on every admin page load.

Upload endpoints are yours to build

The bundle renders the editor; it does not provide a file-upload controller. Froala posts a multipart file field to whatever URL you configure and expects {"link": "https://..."} back.

#[Route('/admin/editor/upload', name: 'admin_editor_upload', methods: ['POST'])]
public function upload(Request $request, FileStorage $storage): JsonResponse
{
    $file = $request->files->get('file');

    if (!$file || !in_array($file->getMimeType(), ['image/jpeg', 'image/png', 'image/webp'], true)) {
        return new JsonResponse(['error' => 'Unsupported file type'], 422);
    }

    if ($file->getSize() > 5 * 1024 * 1024) {
        return new JsonResponse(['error' => 'File too large'], 422);
    }

    $path = $storage->store($file, 'editor');

    return new JsonResponse(['link' => $storage->publicUrl($path)]);
}

Wire it in config (imageUploadURL, fileUploadURL, videoUploadURL) and — critically — put the route behind your admin firewall. Froala's uploader does not send a Symfony CSRF token by default; if you need CSRF, add your token to the request via a fileUploadParams-style option and validate it in the controller. Otherwise, at minimum, require an authenticated session.

Sanitize on write, in one place

Froala's client-side HTML is attacker-controlled the moment a lower-privileged user or a compromised session touches the form. Do not rely on the editor's own cleanup.

// src/EventListener/HtmlSanitizerListener.php
final class HtmlSanitizerListener
{
    public function prePersist(Page $page): void
    {
        $page->setBody($this->purifier->purify($page->getBody()));
    }
}

Use HTMLPurifier (ezyang/htmlpurifier) with an explicit allowlist that matches what you actually render — p, h2–h4, ul, ol, li, a[href|title], strong, em, blockquote, figure, img[src|alt|width|height], table if you allow tables. Register it as a Doctrine lifecycle listener or a EntityListener so every write path is covered, including fixtures and imports.

Store the sanitized output. Render with |raw. If you ever have a code path that bypasses the listener, sanitize again at render for defence in depth.

Derive plain-text companions

Every rich-text field eventually needs an excerpt, a search index, a meta description, or an email plain-text part:

$excerpt = mb_substr(trim(preg_replace('/\s+/', ' ', strip_tags($page->getBody()))), 0, 160);

Store it in its own column at save time rather than computing it on every list-page render, and use it for Length validation — validating the raw HTML against a 160-character limit will reject perfectly good content because of markup.

Testing

  • Form type test: assert the rendered widget contains the initialization hook and the correct data attributes.
  • Functional test: $client->submitForm('Save', ['page[body]' => '<p>Hi</p><script>alert(1)</script>']) and assert the persisted row has no <script> — this is the test that actually protects you.
  • Upload test: post a real file, assert 200 + link key; post a .php, assert 422.

Because the bundle is thin, most of your tests will be integration tests through your own form and controller, which is the right level anyway. (v1.8.0 moved the bundle's own suite to PHPUnit 11; that's internal to the package and only matters if you plan to run its tests locally or send a PR.)

Gotchas and Tips

Froala is commercial software. Version 4 shows a licensing notice and gates features without a valid licenseKey. Budget for this before you build a workflow around it — it is the most common reason teams swap the editor out mid-project.

VARCHAR(255) will bite you. Rich text HTML is large: a few paragraphs with formatting easily exceeds 255 characters. Use TEXT (64KB) as the floor and LONGTEXT/MEDIUMTEXT for article bodies. This mistake usually surfaces as a truncated or rejected save in production, not in dev.

Froala re-serializes your HTML. Even opening and saving a record with no edits will change attribute order, normalise self-closing tags, and strip things it doesn't recognise. If you audit content changes, diff the sanitized-and-normalized output, not the raw strings, or you'll get noise on every save.

Multiple editors on a page need lifecycle management. If you use Turbo, Hotwire, LiveComponents, or any SPA-ish navigation, the editor instance survives the DOM swap and you get duplicated toolbars or an editor bound to a detached node. Destroy on disconnect:

document.addEventListener('turbo:before-cache', () => {
  document.querySelectorAll('.js-richtext').forEach((el) => {
    if (el._froalaEditor) { el._froalaEditor.destroy(); }
  });
});

Version mismatch between the bundle's Froala and your JS. If you self-host Froala and the bundle also injects a script tag, you get two copies, plugin registration failures, and cryptic Cannot read properties of undefined errors. Pick one source of truth and disable the other via the config flag you found with config:dump-reference.

Debugging checklist. Editor doesn't appear → view source, confirm the init script is present and the textarea has the right id; 404s on plugins/*.js → asset path mismatch; toolbar buttons missing → plugin not loaded, not a config problem; upload fails silently → check the network tab for the JSON error body, Froala swallows HTTP error text.

CSP will break the inline init. Most bundle versions render an inline <script> to instantiate the editor. If you have script-src without 'unsafe-inline', add a nonce or move initialization into your own entrypoint that targets .js-richtext elements.

Don't leak the editor into composer.json of a Laravel app. This is a Symfony bundle — service definitions, config tree, form type. In Laravel you'd use Froala's Laravel package or plain JS plus a Blade component; the concepts above (sanitize on write, own the upload endpoint, pin the asset version) all transfer, the code does not.

The maintenance signal, updated. v1.8.0 is a genuinely reassuring release: the maintainer has added GitHub Actions, PHPStan, and PHP CS Fixer, moved the suite to PHPUnit 11, and tests against PHP 8.5. More importantly it declares Symfony 8 compatibility, which is the signal you actually care about — the project is tracking Symfony majors rather than letting the constraint rot, and the release is a pure tooling/constraint bump with no API churn. It's still one maintainer and a small user base, so keep the wrapper type from the patterns section: that's what makes the next Symfony major (9) a one-file change rather than a migration. But "abandoned package" is no longer the right prior — pin ^1.8 and check the repo's composer.json constraint each time you bump Symfony.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels