Gegen ein echtes Firebase-Projekt zu entwickeln ist völlig in Ordnung — bis ihr zu zweit seid.
Dann hört es auf, in Ordnung zu sein, und zwar in sehr vorhersehbarer Reihenfolge. Jemandes Testlauf löscht eine Collection, die eine andere Person gerade nutzt. Auth-User stapeln sich aus hundert Registrierungstests. Jemand zeigt lokal "nur für einen Nachmittag" aufs Staging-Projekt und schreibt echte Daten hinein. Und keiner deiner Funktionstests kann irgendetwas leeren, weil "irgendetwas" geteilt ist.
Die Lösung ist die Firebase Emulator Suite, als weiterer Container in derselben Compose-Datei wie alles andere — sie startet und stoppt also mit dem Rest der Umgebung, und niemand muss an sie denken.

Den Emulator in docker-compose aufnehmen
Du kannst den Emulator über die Firebase-CLI laufen lassen, aber dann brauchen alle Node und die CLI auf ihrer Maschine — was die Regel bricht, dass nichts auf dem Host installiert wird. Ein Container-Image erledigt dieselbe Aufgabe, ohne dass etwas installiert werden muss:
services:
firebase:
image: spine3/firebase-emulator
ports:
- "4000:4000" # Emulator-UI
- "8080:8080" # Firestore
- "9099:9099" # Auth
environment:
- GCP_PROJECT=my-project-id
- ENABLE_UI=true
container_name: myproject_firebase_dev
php:
build:
context: ./env/dev/Php
links:
- "db:database"
- "firebase:firebase"
Der Wert von GCP_PROJECT muss kein Projekt sein, das existiert. Der Emulator braucht nur eine Projekt-ID, unter der er Daten einordnet, und deine echte zu nehmen hält die Konfiguration identisch zur Produktion, was ein Unterschied weniger ist, über den du nachdenken musst.
Öffne localhost, und du bekommst die Emulator-UI — jedes Dokument, jeden Auth-User und ein Request-Log. Allein dieses Log ist die Einrichtung wert: Du kannst genau sehen, was dein Backend geschickt hat, und das zeigt dir die echte Firebase-Konsole nicht.
Das SDK darauf zeigen lassen
Das ist der Teil, der Leute überrascht: Du änderst deinen Anwendungscode nicht. Die Google-SDKs — auch die für PHP — schauen nach einer Reihe von Umgebungsvariablen und leiten sich selbst auf den Emulator um, wenn diese gesetzt sind.
# .env.dev.local
FIRESTORE_EMULATOR_HOST=firebase:8080
FIREBASE_AUTH_EMULATOR_HOST=firebase:9099
Achte auf den Hostnamen: firebase, der Compose-Servicename, nicht localhost. Innerhalb des PHP-Containers ist localhost dieser Container. Das erwischt jeden genau einmal, und das Symptom ist ein Verbindungs-Timeout, das aussieht, als liefe der Emulator nicht.
Deine Host-Maschine ist der umgekehrte Fall — aus dem Browser oder einem REST-Client auf deinem Laptop ist es localhost:8080, weil das der veröffentlichte Port ist. Derselbe Emulator, zwei verschiedene Adressen, je nachdem, auf welcher Seite der Containerwand du stehst.
Zugangsdaten, oder deren Abwesenheit
Der Emulator prüft keine Zugangsdaten. Das ist der Punkt — es heißt, dass niemand ein echtes Service-Account-JSON auf dem Laptop braucht, um an dem Projekt zu arbeiten.
Mit dem kreait-Firebase-Bundle für Symfony sieht die Konfiguration so aus:
kreait_firebase:
projects:
app:
credentials: '%kernel.project_dir%/config/firebase.json'
In der Entwicklung kann diese Datei ein Dummy mit der richtigen Struktur und einem Fake-Key sein — der Emulator prüft sie nicht. In Produktion ist es der echte Service Account, zur Laufzeit gemountet, nie committet, nie in eine Image-Schicht gebacken.
Dieser Unterschied ist der ganze Sicherheitsgewinn dieses Setups. Vor dem Emulator brauchte jeder Entwickler Zugangsdaten zu einem echten Google-Cloud-Projekt. Danach brauchen sie nichts, und die einzige echte Zugangsberechtigung liegt auf dem Server.
Was das für Tests freischaltet
Der Emulator macht Firebase zu etwas, das deine Testsuite besitzen kann.
protected function setUp(): void
{
parent::setUp();
// alles löschen — das ist ein Emulator, es gibt nichts zu verlieren
$this->http->request('DELETE', sprintf(
'http://firebase:8080/emulator/v1/projects/%s/databases/(default)/documents',
$_ENV['GCP_PROJECT']
));
}
Ein HTTP-Aufruf, und die Datenbank ist leer. Gegen ein echtes Projekt kannst du das nicht — nicht weil es technisch unmöglich wäre, sondern weil irgendwann jemand das mit der falschen geladenen Umgebung ausführt, und dann stellst du Produktion aus einem Backup wieder her.
Dasselbe für Auth: Der Emulator bietet einen Endpunkt, um alle Accounts zu löschen, ein Test kann also einen Nutzer anlegen, den Ablauf durchspielen und nichts zurücklassen.
Eine Testsuite, die ihren eigenen Zustand nicht zurücksetzen kann, ist keine Testsuite. Sie ist eine Sammlung von Skripten, die in einer bestimmten Reihenfolge funktionieren, einmal.
Wo der Emulator dir nicht hilft
Sei bei den Grenzen klar, denn dort kann das gewonnene Vertrauen schiefgehen.
Push-Benachrichtigungen funktionieren nicht. Cloud Messaging wird nicht emuliert — es gibt kein Gerät, an das zugestellt werden könnte. Das zu testen braucht weiterhin ein echtes Projekt und ein echtes Token, worum es in dem Artikel zu Push-Nachrichten geht.
Security Rules verhalten sich, Daten nicht. Der Emulator wertet Firestore-Rules tatsächlich aus, was wirklich nützlich ist. Was er dir nicht sagen kann, ist, wie sich deine Abfragen gegen eine Collection mit 200.000 Dokumenten und ohne Index verhalten. Jede Emulator-Abfrage ist schnell, weil jede Emulator-Collection winzig ist.
Es ist keine Persistenzschicht. Standardmäßig verliert der Emulator beim Neustart alles. Du kannst einen Datensatz exportieren und importieren, und für einen gemeinsamen Seed lohnt sich das — aber behandle ihn nie als einen Ort, an dem Daten leben.
Der eigentliche Grund, das zu machen
Nicht Geschwindigkeit, obwohl es schneller ist. Der Grund ist, dass es die Entwicklung zerstörungssicher macht.
Jede riskante Sache, die du gegen ein geteiltes Projekt vermeidest — eine Collection löschen, zehntausend Dokumente erzeugen, jeden Nutzer löschen, die Migration ausführen, bei der du dir nicht sicher bist — wird kostenlos. Du hörst auf, mit deiner Entwicklungsumgebung vorsichtig zu sein, und genau so sollte sich eine Entwicklungsumgebung anfühlen.