jgizinski/laravel-firebase-sync
Install and wire up (15 minutes, once per app).
composer require jgizinski/laravel-firebase-sync
The package targets Laravel 5.x, so on 5.5+ you may still need to register it by hand in config/app.php:
'providers' => [
Mpociot\Firebase\FirebaseServiceProvider::class,
],
Publish the config and fill it from .env:
php artisan vendor:publish --provider="Mpociot\Firebase\FirebaseServiceProvider"
FIREBASE_URL=https://your-app.firebaseio.com
FIREBASE_TOKEN=your-database-secret-or-token
Then opt a model in with the trait and an explicit allow-list of fields:
use Mpociot\Firebase\SyncsWithFirebase;
class Post extends Model
{
use SyncsWithFirebase;
protected $syncable = ['id', 'title', 'body', 'published_at'];
}
From that point every save() and delete() writes the projection to the Realtime Database at a path derived from the table and key (typically /posts/{id}).
Where to look first. Read src/SyncsWithFirebase.php before anything else. That trait is the package — it hooks the saved/deleted model events, builds the payload from $syncable, and calls the small Firebase REST client. The config array and that one client class are the only other moving parts; there is no queue, no migration, no schema.
First real use case. Pick a public, read-only projection: a "live feed" or presence node that a browser/mobile client subscribes to with firebase.database().ref('posts').on('value', ...). Keep the first model narrow (no PII, no relationships) so you can verify the round trip with curl https://your-app.firebaseio.com/posts.json?auth=$TOKEN before trusting it with anything important.
Scoped paths for multi-tenant or nested data. Don't let every model land at the table root. Override the path builder in the model (the trait exposes getFirebasePath() / a reference helper — check the exact signature in the trait source) so the document lands where the client expects:
public function getFirebasePath()
{
return "accounts/{$this->account_id}/posts/{$this->getKey()}";
}
Treat Firebase as a projection, not a replica. $syncable is your contract with the client app. Sync only what the client reads, and prefer derived/display fields (a status label, an avatar URL, an unread count) over raw columns. Anything sensitive stays out of the array — the Realtime Database rules are the only thing standing between a synced field and the public internet.
Decouple the write from the request. The client is a synchronous HTTP call, so a Firebase outage or slow response slows down (or fails) your save(). The robust pattern is to skip the automatic event sync and push from a queued job or a listener:
// App\Listeners\SyncPostToFirebase
public function handle(PostSaved $event)
{
SyncPostJob::dispatch($event->post);
}
If you keep the trait's automatic behaviour, at minimum wrap the model write in a transaction and sync after commit, so a rolled-back transaction doesn't leave a phantom document in Firebase:
DB::transaction(function () use ($post) {
$post->save();
// trait fires here — still inside the transaction
});
Bulk and query-builder writes are invisible. Model::insert(), Post::where(...)->update(), and raw SQL never fire model events, so nothing reaches Firebase. Either loop over models, or dispatch an explicit sync job for each affected key after the bulk operation.
Denormalize deliberately. If a client needs a post and its author's name, either write the author's name into the payload at sync time (accepting staleness and a re-sync on author rename) or let the client join by ID against a separately synced /users node. Don't try to sync Eloquent relations automatically — the package won't walk them for you.
Use it as a broadcasting alternative. For "something changed, tell every client" flows, this package can replace a Pusher/Echo setup: write the state to the Realtime Database and let Firebase's own client SDK handle fan-out, reconnection, and offline caching. It's a good fit for dashboards, notification badges, and lightweight presence; a poor fit for high-frequency event streams.
Testing. Mock the client (or the underlying Guzzle handler) and assert the URL path and JSON body your trait produces. If the client is resolved from the container, bind a fake in your test case so the suite never touches the network.
jgizinski/...) and the GitHub repo (mpociot/...) don't line up. Confirm which name actually resolves in your composer.json and pin the commit you audited — a fork can drift from the code you reviewed.?auth= with a legacy token may 401 or 403 depending on your project and rules. Expect to move to a service-account/OAuth flow or a narrowly-scoped custom token, and log the raw HTTP status/response body in development — 401/403 means auth, 404 means wrong URL (regional *.firebaseio.com host), 400 usually means a malformed path.., $, #, [, ], or /. Composite or raw string IDs will silently corrupt the tree; sanitize or hash them before they reach the path.SoftDeletes model fires deleted on soft delete, so the trait will remove the Firebase document even though the row is still queryable. If clients should see archived records, override the delete hook to write a deleted: true flag instead.FIREBASE_URL/FIREBASE_TOKEN, run php artisan config:cache again or your .env edit won't take effect.$syncable) for shaping, swap the Firebase client binding for your own HTTP layer, or drop the trait entirely and call the client from explicit listeners — the last option gives you the cleanest transaction/queue semantics at the cost of touching more code.$syncable contract, and test them with the Firebase simulator before shipping.How can I help you explore Laravel packages today?