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.
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:
README.md — the actual public API and any version constraints.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.Resources/config/routing.* — the route names you must import.Resources/public/js/ — what the front-end actually expects (data-* attributes, jQuery, Bootstrap version).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".
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:
base.html.twig). The controller returns a fragment..modal-header / .modal-body / .modal-footer markup either in the bundle's shell or in your template, but never both.#[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.
Use a real <a href>/<form action> as the trigger and let the bundle's JS intercept it. This gives you:
<a href="{{ path('app_modal_user_edit', {id: user.id}) }}"
data-modal
class="btn btn-sm btn-outline-primary">Edit</a>
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.
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.
redirect via window.location from JS on success, not by following the 302 into the dialog.data-turbo="false".assets:install again and bump your Encore version hash, or browsers will hold the old JS for hours.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.
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.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.<5 service definitions using @service or %parameter% strings instead of service('...')/param('...') — installs on 5.x with deprecations, breaks on 6.x.When "the modal doesn't open":
.modal wrapper, producing a modal inside a modal? Inspect the injected .modal-body.document.addEventListener('shown.bs.modal', e => console.log(e.target)).php bin/console cache:clear after changing config/routes.yaml; route import changes are the classic "I edited it and nothing happened".aria-labelledby requires the modal template to define a heading with a stable id — templated fragments often don't.innerHTML swap instead of the Bootstrap API. Always close through bootstrap.Modal.getInstance(el).hide().templates/bundles/RaresModalBundle/... matching the path in Resources/views/. Symfony resolves your copy first — no fork needed for cosmetic changes.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).1.0.0, and "replace": {"rares/modal-bundle": "1.0.0"} so nothing else tries to pull the Bitbucket original.Bail out and write your own if any of these are true:
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.
How can I help you explore Laravel packages today?