rhapsody-project/backbone-js-bundle
Rhapsody Project's Symfony bundle for integrating Backbone.js into PHP applications. It packages the Backbone front-end library for easier asset management and configuration, helping teams add structured JavaScript MVC to Symfony projects with less manual setup.
This release is a frontend asset bundle, not a PHP package: Backbone 1.2 pinned to Underscore 1.6. It ships no service provider, no config file, and no Eloquent/HTTP layer — everything you get is backbone.js + underscore.js (and their minified builds) plus whatever version pinning the package applies. Treat it as "vendored JS with a stable, known-good pairing" rather than a Laravel integration.
Where to look first:
composer.json / src/ — confirm whether a provider exists at all. If there is one, its only job is usually publishes() for the asset directory.public/vendor/<package>/ (via php artisan vendor:publish --tag=...) or resources/js/vendor/.package.json / bower.json in the package root — this is the authoritative statement of the Backbone 1.2 ↔ Underscore 1.6 pairing. Both files are the source of truth for which globals you'll get.Minimal wiring (Blade + @stack, no build step):
{{-- resources/views/layouts/app.blade.php --}}
<script src="{{ asset('vendor/jquery/jquery.min.js') }}"></script>
<script src="{{ asset('vendor/backbone/underscore-min.js') }}"></script>
<script src="{{ asset('vendor/backbone/backbone-min.js') }}"></script>
@stack('scripts')
Order matters — see Gotchas.
First use case: a single CRUD widget (list + create + delete) against a Laravel resource controller that returns JSON. You get the most value from this bundle when the server stays in charge of routing, authorization, and validation, and Backbone owns one interactive island on an otherwise server-rendered page. Do not attempt to make it own the whole app shell in a Laravel codebase on day one.
Laravel-aware base model. Put the Laravel conventions in one place instead of repeating them per model:
App.Model = Backbone.Model.extend({
parse: function (resp) {
// unwrap { data: {...} } envelopes from API resources
return resp && resp.data !== undefined ? resp.data : resp;
},
urlRoot: function () { return '/api/' + this.resource; }
});
Collection + paginator envelope. Laravel's ResourceCollection returns { data: [...], links: {...}, meta: {...} }. Either collection.reset(resp.data) in the success callback, or override parse to stash meta on the collection for pager UI. Don't let the envelope leak into View code.
Single Backbone.sync override is the highest-leverage integration point. One override gets you CSRF, method spoofing, and consistent error shape:
Backbone.sync = (function (sync) {
return function (method, model, options) {
options = options || {};
options.headers = _.extend({}, options.headers, {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
});
// Laravel does not honour X-HTTP-Method-Override
if (options.emulateHTTP) {
var data = options.attrs || model.toJSON(options);
data._method = method;
method = 'POST';
options.data = JSON.stringify(data);
options.contentType = 'application/json';
delete options.emulateHTTP;
}
return sync.call(this, method, model, options);
};
})(Backbone.sync);
Templates. Keep Underscore templates in real .html files (resources/js/templates/items.html) and load them into window.JST['items'] at boot, rather than inlining them in Blade. If you must inline, wrap in @verbatim ... @endverbatim so Blade doesn't eat {{ }}. This single convention prevents most template-related confusion.
Hydrate from server state. Render the first page with Blade, then hand the data to JS:
<script id="bootstrap" type="application/json">@json($items)</script>
var seed = JSON.parse(document.getElementById('bootstrap').textContent);
var collection = new App.Items(seed.data);
View lifecycle. Use listenTo on models/collections (not model.on), and call view.remove() — which runs stopListening — from the parent view. In a Laravel app where you replace islands on navigation, leaked listeners on a shared collection are the #1 source of "the handler fires twice" bugs.
CSRF and 419s. The meta tag is regenerated per page, so token staleness only bites long-lived pages. Add an error handler on Backbone.sync that reloads on 419, otherwise users see silent failures.
422 → form errors. xhr.status === 422 returns { message, errors: { field: [...] } }. Feed errors straight into your Blade-rendered field wrappers; this keeps server-side validation as the single source of truth.
Backbone.$ must be set explicitly. Backbone 1.2 removed automatic jQuery/Zepto/ender detection; if Backbone.$ is undefined at View construction time you get a throw about a missing DOM library. Load jQuery before Backbone, and if you're on Vite/ESM, assign globals at the entry point (window.$ = window.jQuery = $; import Backbone from 'backbone';).X-HTTP-Method-Override. Backbone's emulateHTTP sends a header; Laravel only honours a _method field. Any PUT/PATCH/DELETE that silently becomes a POST and hits a store route is this bug. See the sync override above.{{ name }} is Blade syntax. Either @verbatim, or set _.templateSettings.interpolate = /\{\{(.+?)\}\}/g and use @{{ }} — pick one and document it, because two conventions in one repo is worse than either._.isNil, _.assign aliasing, and the lodash-4 removal of _.contains. Swapping in lodash 4 will break Backbone internals. If you need lodash, you need a Backbone upgrade, not a substitution.<%= %> interpolates unescaped; <%- %> escapes. In Blade-heavy teams the habit of "Blade escapes for me" carries over and doesn't apply here.Backbone.sync's $.ajax and every view's DOM work. Also watch for a second jQuery loaded by a theme/plugin — the classic "events fire but nothing renders" symptom.Backbone.sync (log method, model, options) and wrap Backbone.history.start. Those two points reveal nearly every "nothing happens" issue — bad URL, method spoofing, or routing conflicts.Backbone.history vs. Laravel routes. Push-state routing will intercept links Laravel expects to handle. Scope the history to a prefix (Backbone.history.start({ pushState: true, root: '/app/' })) or stay on hashbang and leave the router to Laravel.Backbone.sync (transport), Backbone.Model.prototype.parse (envelope), Backbone.ajax (swap the transport entirely for fetch/axios), _.templateSettings (delimiters), Backbone.Model/Collection.extend (base classes). Everything else is best left alone at this version.Backbone.ajax → fetch, or replace the island with Alpine/Vue) before the surrounding Blade views calcify around it.How can I help you explore Laravel packages today?