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

Modal Bundle Laravel Package

rares/modal-bundle

rares/modal-bundle is a Symfony bundle for adding reusable modal dialogs to PHP applications. It provides Twig helpers and controller integration for rendering and managing modals, helping teams standardize dialog UI without bespoke JavaScript.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package — worth stating up front, because if you're coming from Laravel the mental model differs: there is no service provider auto-discovery (in older Symfony), no facade, and assets are published through assets:install rather than vendor:publish. The concepts map roughly as: Bundle ≈ Package, Twig helper ≈ Blade directive, bundle config ≈ config/*.php.

Before you install, do a 5-minute due-diligence pass. Zero stars and zero dependents on a Bitbucket-hosted bundle means you are the maintainer the moment you composer require it. Open the repo and read, in this order:

  1. README.md — the actual public API and any version constraints.
  2. composer.json — the Symfony version constraint. If it caps at ^3.4 or ^4.0, it will not install on Symfony 6/7 without a fork or a replace/alias hack.
  3. Resources/config/routing.* — the route names you must import.
  4. Resources/public/js/ — what the front-end actually expects (data-* attributes, jQuery, Bootstrap version).
  5. Resources/views/ — the modal shell template you may want to override.

Installation (assuming a private/undisclosed Bitbucket repo):

composer config repositories.modalbundle vcs https://bitbucket.org/raresbundles/modalbundle.git
composer require rares/modal-bundle:dev-master

If Bitbucket prompts for credentials, add an access token or an SSH URL ([email protected]:...) in the repositories entry; anonymous HTTPS clones of private repos fail with a confusing could not be found error from Composer.

Register it. Symfony 4+ (config/bundles.php):

return [
    // ...
    Rares\ModalBundle\RaresModalBundle::class => ['all' => true],
];

Symfony 3 (app/AppKernel.php): new Rares\ModalBundle\RaresModalBundle(), in registerBundles().

Import routes (config/routes.yaml) — verify the exact route file name in the repo:

rares_modal:
    resource: "@RaresModalBundle/Resources/config/routing.xml"
    prefix: /

Publish assets so the JS/CSS lands in public/:

php bin/console assets:install --symlink
# or, if you use Encore:
# import '../../vendor/rares/modal-bundle/Resources/public/js/modal.js';

First use case — a link that opens a modal whose body is rendered server-side:

<a href="{{ path('rares_modal_open', { template: 'modal/delete_user.html.twig' }) }}"
   class="btn btn-danger"
   data-modal="open">
    Delete user
</a>

Confirm the exact attribute/route names from the README — this bundle's whole surface area is likely one Twig helper plus one controller, so a mismatch here is the difference between "working in 2 minutes" and "nothing happens".

Implementation Patterns

Pattern 1 — Server-rendered modal content (the sweet spot)

The value of this bundle is that modal bodies are normal Twig templates rendered by a normal Symfony controller. Keep them dumb:

templates/modal/delete_user.html.twig   ← form + body only, no <html>, no layout
templates/modal/_shell.html.twig        ← optional override of the modal chrome

Rules that keep this clean:

  • Modal templates extend nothing (no base.html.twig). The controller returns a fragment.
  • Put the .modal-header / .modal-body / .modal-footer markup either in the bundle's shell or in your template, but never both.
  • Pass data through query params or a small DTO hydrated from the request, not through session flash.

Pattern 2 — Form in a modal (the one you'll actually write)

#[Route('/modal/user/{id}/edit', name: 'app_modal_user_edit', methods: ['GET', 'POST'])]
public function edit(User $user, Request $request): Response
{
    $form = $this->createForm(UserType::class, $user);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $this->em->flush();

        if ($request->isXmlHttpRequest()) {
            // Tell the JS layer to close and reload/redirect
            return $this->json(['ok' => true, 'redirect' => $this->generateUrl('app_user_index')]);
        }
        return $this->redirectToRoute('app_user_index');
    }

    return $this->render('modal/user_edit.html.twig', ['form' => $form->createView()]);
}

The controller must be dual-mode: full-page POST (no-JS fallback, or someone hitting it directly) and XHR POST (validation errors re-rendered into the same modal). Detect with $request->isXmlHttpRequest() or a ?_modal=1 marker — don't branch on Accept headers alone; Symfony's form component doesn't care but your JS does.

Pattern 3 — Progressive enhancement wrapper

Use a real <a href>/<form action> as the trigger and let the bundle's JS intercept it. This gives you:

  • Working behaviour with JS disabled (full page load).
  • Crawlable/right-clickable links.
  • A single source of truth for the target URL.
<a href="{{ path('app_modal_user_edit', {id: user.id}) }}"
   data-modal
   class="btn btn-sm btn-outline-primary">Edit</a>

Pattern 4 — Stacking modals / chained flows

Rarely supported out of the box. If you need "open modal from modal" (e.g. pick an address, then edit it), do not stack. Instead swap the modal body via a second fetch into the same .modal-content. Keeps aria-hidden, focus trapping, and body scroll-lock correct.

Pattern 5 — Centralize the JS contract

Wrap the bundle's behaviour in one small Stimulus controller (or a single jQuery module) rather than sprinkling data-modal attributes everywhere. Then, if the bundle dies, you swap one file:

// assets/controllers/modal_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    open(e) {
        e.preventDefault();
        fetch(this.element.href, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
            .then(r => r.text())
            .then(html => {
                document.querySelector('#app-modal .modal-body').innerHTML = html;
                bootstrap.Modal.getOrCreateInstance('#app-modal').show();
            });
    }
}

This is ~15 lines. Keep that number in mind.

Integration tips

  • CSRF: forms inside modals get a token rendered at page load. If a modal stays open for hours behind a cached page, the token expires — refresh the fragment on open rather than caching it client-side.
  • Flash messages: a redirect issued from inside a modal will render into the modal body if you follow it in-place. Always redirect via window.location from JS on success, not by following the 302 into the dialog.
  • Turbo/Turbolinks: if present, intercepting clicks at the document level will conflict. Disable Turbo on modal triggers with data-turbo="false".
  • Asset versioning: when you upgrade the bundle, run assets:install again and bump your Encore version hash, or browsers will hold the old JS for hours.

Gotchas and Tips

The big one: this is unmaintained-by-design

Zero stars, zero dependents, no releases visible, Bitbucket (not Packagist-indexed). Practical consequences:

Risk Symptom Mitigation
Bitbucket repo deleted/made private composer install fails on fresh CI Mirror into your own VCS or composer.json "repositories": [{"type": "path"}] with a vendored copy
No Symfony 5/6/7 support Constraint conflict at install Fork, bump composer.json, drop @expectedDeprecation noise
No tagged versions dev-master floats Pin a commit hash: "rares/modal-bundle": "dev-master#abc1234"
No security process A jQuery-era XSS in the JS Audit Resources/public/js before shipping

Honest cost/benefit: the observable feature surface of a "simple bundle that opens Bootstrap modals" is 30–80 lines of JS plus one controller action. Unless the README reveals non-trivial machinery (nested-modal management, focus restoration, accessibility plumbing), writing it in-house is cheaper than owning an orphan dependency. Do this evaluation before you've built five forms against its API.

Version-mismatch traps

  • Bootstrap 3 vs 4 vs 5. The bundle was almost certainly written for BS3/BS4. BS5 renamed events (show.bs.modal → show.bs.modal is same, but dropped jQuery and changed .modal('show') to bootstrap.Modal.getOrCreateInstance(...)). If Bootstrap 5 is loaded and the bundle calls the jQuery plugin, you get TypeError: $(...).modal is not a function — silent in production if you build with minification and swallow errors.
  • jQuery dependency. Many small bundles require jQuery. If your app doesn't ship it, the bundle's JS throws before wiring listeners. Check Resources/public/js for $( at the top of the file.
  • Symfony <5 service definitions using @service or %parameter% strings instead of service('...')/param('...') — installs on 5.x with deprecations, breaks on 6.x.

Debugging checklist

When "the modal doesn't open":

  1. Network tab — is the fragment URL 404 (route not imported) or 500 (Twig template path wrong)? Both look identical on screen.
  2. Console — jQuery/Bootstrap load order. The bundle's JS must load after Bootstrap.
  3. DOM — did the fetched HTML include a second .modal wrapper, producing a modal inside a modal? Inspect the injected .modal-body.
  4. Event lifecycle — attach diagnostics: document.addEventListener('shown.bs.modal', e => console.log(e.target)).
  5. Cache — php bin/console cache:clear after changing config/routes.yaml; route import changes are the classic "I edited it and nothing happened".

Accessibility pitfalls (worth flagging to your team)

  • Focus must return to the trigger on close. Hand-rolled modal implementations almost always miss this.
  • aria-labelledby requires the modal template to define a heading with a stable id — templated fragments often don't.
  • Body scroll-lock leaks when you close via innerHTML swap instead of the Bootstrap API. Always close through bootstrap.Modal.getInstance(el).hide().

Extension points, if you keep it

  • Template override via bundle inheritance: create templates/bundles/RaresModalBundle/... matching the path in Resources/views/. Symfony resolves your copy first — no fork needed for cosmetic changes.
  • Service decoration: decorates: rares_modal.something in services.yaml if the bundle registers a service you need to intercept (check Resources/config/services.xml for the id).
  • Event hooks: if it dispatches an event on open, subscribe to it rather than patching the JS — this survives Composer updates.
  • Fork strategy: if you must patch, fork to your org's GitLab/GitHub, tag 1.0.0, and "replace": {"rares/modal-bundle": "1.0.0"} so nothing else tries to pull the Bitbucket original.

When to bail

Bail out and write your own if any of these are true:

  • Your Symfony version is 6.x or 7.x (constraint almost certainly fails).
  • You're on Bootstrap 5 (jQuery-style API mismatch).
  • You need more than one modal behaviourally distinct (form, confirm, picker) — you'll be writing the controllers anyway.
  • Your accessibility requirements are non-trivial (WCAG 2.2 focus management).

The realistic outcome is: vendor the JS as a starting point, keep the two controller actions, and move on — but only after the 5-minute README read confirms the bundle is more than a data-toggle shim.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console