png / laravel-sync-engine
Offline-first bidirectional sync engine for Laravel
Requires
- php: ^7.4|^8.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-23 17:36:14 UTC
README
Ce plugin (laravel-sync-engine) est un Moteur de Synchronisation de base de données pour Laravel.
Il sert principalement à faire le pont entre une application locale (un client) et un serveur centralisé (ou entre plusieurs applications) pour s'assurer qu'elles ont toutes les mêmes données. C'est idéal pour les applications capables de fonctionner hors connexion (offline-first) ou les architectures distribuées.
Installation
Installez le package via Composer :
composer require png/laravel-sync-engine
Publier la configuration
Afin de générer le fichier de configuration config/sync-engine.php :
php artisan vendor:publish --provider="png\SyncEngine\SyncServiceProvider"
Configuration du fichier .env
Ajoutez les clés de configuration pertinentes sur vos plateformes :
SYNC_SERVER_URL=https://votre-serveur-central.com SYNC_CLIENT_ID=client-caisse-paris SYNC_API_KEY=votre_cle_secrete_hyper_longue # Ajoutez ceci UNIQUEMENT sur le serveur central pour activer l'historique global : # SYNC_IS_SERVER=true
Fonctionnement du Moteur
1. Traçabilité des données (Syncable et SyncObserver)
Lorsque vous ajoutez le trait Syncable à l'un de vos modèles Laravel, le plugin observe chaque création, modification ou suppression. Dès qu'une donnée change, il la marque comme "sale" (is_synced = false), augmente son numéro de version, lui attribue un identifiant unique universel (uuid), et enregistre l'action dans un journal d'événements (la table sync_logs).
2. Envoi au Serveur (SyncPushJob)
Le plugin possède une tâche asynchrone (Job) qui cherche régulièrement toutes les données non synchronisées en base locales (is_synced = false). Il les regroupe en lots (la limite est paramétrable via la variable SYNC_BATCH_SIZE, 1000 par défaut) et les envoie via une requête HTTP POST vers l'URL du serveur central (configurée dans sync-engine.php). Une fois l'envoi réussi, ces données sont marquées comme "synchronisées" (is_synced = true).
3. Réception sur le Serveur (SyncController)
Le plugin expose une route API (la méthode push) conçue pour recevoir des données provenant de clients locaux. Lorsqu'il reçoit une liste de données, il vérifie l'uuid :
- Si la donnée existe déjà, elle est mise à jour avec les nouvelles informations.
- Sinon, elle est créée dans la base de données.
Le tout est géré nativement via la fonction
updateOrCreate.
4. Récupération depuis le Serveur (SyncPullJob)
À l'inverse du Push, le plugin possède également une tâche SyncPullJob pour récupérer automatiquement les dernières modifications du serveur (Bidirectionnel). Cette tâche télécharge les changements distants et les fusionne doucement dans la base locale (via l'uuid) sans relancer d'événements inutiles (saveQuietly()).
5. Tableau de Bord de Télémétrie (DashboardController)
Une petite interface visuelle est incluse, accessible via l'URL /sync-dashboard. Elle permet d'afficher des statistiques en temps réel sur la santé de la synchronisation :
- Nombre de données en attente (Pending)
- Nombre de données synchronisées (Synced)
- Cas de conflits (Conflicts)
- Cas d'erreurs de transfert (Errors)
6. Historique du Serveur Central (sync_histories)
Sur le serveur central uniquement, vous pouvez tracer toutes les opérations de requêtes entrantes de Push et de Pull effectuées par vos clients sur le réseau.
Pour activer cette fonctionnalité, déclarez explicitement votre application comme serveur dans le fichier .env :
SYNC_IS_SERVER=true
Puis exécutez vos migrations sur le serveur (php artisan migrate). Une nouvelle table d'audit sync_histories sera créée et enregistrera chaque transaction (Client, type d'opération Push/Pull, Statut, Nombre d'enregistrements insérés ou envoyés).
Note : Pour éviter tout encombrement inutile, cette table d'historique globale n'est volontairement pas créée sur les bases de données des applications clientes.
7. Gestion Manuelle des Conflits (Serveur central)
Le plugin garantit l'intégrité des données grâce à une protection stricte contre les écrasements (Server Wins). Si un client tente de synchroniser (Push) un enregistrement disposant d'une version numériquement obsolète comparativement à celle du serveur, la modification du client est interceptée, bloquée et consignée dans sync_logs avec le statut "conflict".
Vous pouvez résoudre ces conflits grâce à deux routes API pour vos interfaces d'administration :
A. Obtenir la comparaison des Conflits (GET /api/sync/conflicts)
Renvoie un relevé complet des conflits avec, pour chacun, la version actuelle sur la base du serveur (server_data) juxtaposée avec la donnée proposée et rejetée du client en cause (client_data).
B. Résoudre le Conflit manuellement (POST /api/sync/resolve-conflict)
Charge payload attendue :
{
"table": "users",
"uuid": "xxxxx-yyyy-zzzz",
"resolution": "client" // ou "server"
}
- Si
"server": Le serveur maintient sa donnée intacte, et le conflit est acquitté. (Le client téléchargera finalement la vérité du serveur lors de son prochain Pull). - Si
"client": L'administrateur décide de forcer les données du client. Le serveur intègre la modification et force une version artificiellement haute (+50) afin que cette donnée s'impose définitivement sur l'ensemble du réseau !
Comment Communiquent l'Application Locale et le Serveur ?
La communication s'effectue via Internet au moyen de requêtes HTTP/API (REST) classiques au format JSON. Il y a deux grandes étapes :
1. Le Client envoie ses données (Local vers Serveur)
L'application locale utilise une tâche en arrière-plan (SyncPushJob.php) pour rassembler les enregistrements modifiés et non synchronisés (is_synced = false). Elle construit un "colis" (payload JSON) contenant le nom de la table et une liste de lignes de données dont la quantité maximale à envoyer en une seule fois est administrable via la variable SYNC_BATCH_SIZE.
Elle envoie ensuite une requête HTTP POST vers l'URL du serveur (paramétrée via votre variable globale SYNC_SERVER_URL et la route /sync/push).
Exemple de colis JSON expédié :
{
"table": "users",
"data": [
{ "uuid": "abc-123", "name": "Pascal", "is_synced": false, "version": 2 },
{ "uuid": "def-456", "name": "Jean", "is_synced": false, "version": 1 }
]
}
2. Le Serveur reçoit et intègre les données
De l'autre côté, le serveur expose une route API (/sync/push gérée par SyncController.php).
À la réception du colis JSON mentionné plus haut :
- Il lit le nom de la
"table"(ici :users) et retrouve le Modèle Laravel correspondant dans sa configuration (sync-engine.php). - Pour chaque élément dans le tableau
"data", il utilise la méthode nativeupdateOrCreate()basée sur l'"uuid". - En conséquence : si l'UUID existe déjà sur le serveur, la donnée est mise à jour. S'il n'existe pas, la donnée est créée.
3. Le Client télécharge les nouveautés (Serveur vers Local - PULL)
Une seconde tâche en arrière-plan (SyncPullJob.php) est exécutée du côté de l'application locale. Son rôle est de récupérer les modifications effectuées par les autres noeuds ou sur le serveur.
- L'application locale demande au serveur (
GET /sync/pull) de lui envoyer toutes les données modifiées après sa dernière synchronisation. - Le serveur lui renvoie les enregistrements modifiés, en excluant ceux que ce client a lui-même envoyés.
- À la réception, le client télécharge le colis JSON, supprime l'ID physique du serveur et intègre les données localement (
updateOrCreatesur l'UUID) en utilisantsaveQuietly()pour éviter de déclencher des boucles de synchronisation infinies !
Sécurité (Clé d'API / Token)
Pour éviter que quiconque ne puisse altérer vos données en ciblant la route publique /sync/push, le Moteur de Synchronisation utilise une protection par Clé d'API (API Key).
- Configuration (
SYNC_API_KEY) : Vous devez générer une clé secrète complexe et l'inscrire dans le fichier.envdu Serveur et de tous vos Clients locaus. Exemple :SYNC_API_KEY=v1O9s3lXmZ5l... - Vérification (Middleware) : Lorsque le client (
SyncPushJob) expédie ses données vers le serveur, il ajoute cette clé secrète dans son en-tête privé (X-Sync-Key). Le serveur possède un garde-barrière (MiddlewareVerifySyncApiKey) qui vérifiera cette clé avant d'accepter la synchronisation. Si la clé est mauvaise, le serveur refuse tout accès(Erreur 401 Unauthorized).
Cohabitation avec des Clés Primaires Auto-Incrémentées (id)
Il n'est absolument pas nécessaire d'abandonner vos clés primaires traditionnelles id existantes. Votre code local et vos relations classiques ($article->user_id) continuent de fonctionner tels quels.
Le plugin gère intelligemment les bases de données distribuées grâce à la séparation id / uuid :
- L'
id(Auto-Incrémenté) : Il reste totalement Local. Chaque base (Client ou Serveur) garde son propre système d'ID physiques sans jamais les écraser (afin d'éviter les collisions dans le cas où plusieurs clients créeraient l'ID #5 en même temps). - L'
uuid(Universel) : C'est le lien Global généré dynamiquement par le traitSyncable. Il permet au Serveur de retrouver de façon absolue un enregistrement distant. Le serveur "bloque" nativement (unset($row['id'])) l'injection d'IDs d'un client distant et se base uniquement sur l'uuidpour mettre à jour ou créer la donnée avec son propre auto-incrément.
🛠️ Configuration : Vous pouvez informer le plugin du système de clé primaire que votre base de données utilise via la configuration
uses_auto_increment(config/sync-engine.php) :
- Si
uses_auto_increment = true(Défaut) : le plugin ignore et coupe votreidauto-incrément local lors des communications entre serveurs pour éviter tout conflit.- Si
uses_auto_increment = false: le plugin devine que votre application utilise uniquement les UUID en tant que clé primaire absolue et laisse transiter la clé telle quelle.⚙️ Paramétrage du statut initial de synchronisation (
default_is_synced) :
Lorsque vous utilisez la macro$table->syncable();, vous pouvez configurer l'état par défaut de la colonne de traçabilitéis_synced.
- Ajoutez
SYNC_DEFAULT_IS_SYNCED=false(Défaut) dans votre.envpour que toute nouvelle migration ou donnée massive considère les enregistrements comme non-synchronisés (et force le client à les expédier).- Passez à
truesi votre logique de départ considère les données comme déjà ancrées.🎯 Exception par Table : Vous pouvez facilement surcharger cette valeur globale lors de l'exécution d'une migration pour une table spécifique en passant l'argument
trueoufalseà la macro :$table->syncable(true); // Ignore le .env et force à true !
Mise en place sur une application existante :
Afin que cela fonctionne, vous devez simplement ajouter les colonnes nécessaires dans vos migrations de tables existantes sans altérer la colonne id.
Pour vous simplifier la vie, le plugin inclut une macro Laravel native. Vous n'avez qu'à appeler $table->syncable(); !
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use png\SyncEngine\SyncEngine; return new class extends Migration { public function up() { // 1. On ajoute les nouvelles colonnes via la macro Schema::table('votre_table', function (Blueprint $table) { $table->syncable(); }); // 2. (Très Important) On génère un UUID pour les anciennes données existantes // Clé simple (par défaut 'id') : SyncEngine::populateExistingData('votre_table'); // Ou clé composite : // SyncEngine::populateExistingData('votre_table', ['store_id', 'numero']); } public function down() { Schema::table('votre_table', function (Blueprint $table) { // Méthode inverse très pratique pour le rollback $table->dropSyncable(); }); } };
Activer la Synchronisation sur un Modèle
Pour rendre un modèle de votre application 100% synchronisable, voici les 2 étapes obligatoires :
1. Ajouter le Trait "Syncable" au Modèle
Ouvrez la classe de votre Modèle (ex: Customer.php) et instanciez le trait du plugin :
namespace App\Models; use Illuminate\Database\Eloquent\Model; use png\SyncEngine\Traits\Syncable; class Customer extends Model { use Syncable; // Active l'observation magique des événements Eloquent protected $fillable = ['nom', 'email']; }
2. Enregistrer la table dans la configuration
Ouvrez le fichier config/sync-engine.php et associez le nom SQL de la table avec son Modèle Laravel équivalent :
'tables' => [ 'customers' => \App\Models\Customer::class, ]
(Cette configuration vitale permet aux requêtes Pull/Push réseau de savoir quel modèle instancier lorsqu'elles reçoivent un colis visant la table SQL 'customers').
Automatisation en Arrière-Plan (CRON)
Le plugin est fourni avec une commande universelle permettant de tout synchroniser automatiquement. Vous n'avez pas besoin de programmer vos modèles un par un !
Sur vos serveurs locaux (clients), ouvrez le fichier app/Console/Kernel.php et ajoutez cette ligne dans la méthode schedule :
protected function schedule(Schedule $schedule) { // Lance un cycle de synchronisation global pour TOUS les modèles configurés $schedule->command('sync:run')->everyMinute(); }
(Vous pouvez évidemment remplacer ->everyMinute(); par ->everyFiveMinutes(); ou ->hourly(); ou même lire la fréquence depuis votre .env avec ->cron(env('SYNC_CRON'))).
Assurez-vous bien entendu que le CRON de Linux est activé sur vos machines clientes (les serveurs centraux, eux, n'ont besoin d'aucun Cron !). En environnement de développement (Sail/Docker), laissez tourner dans un terminal :
php artisan schedule:work
En résumé, Laravel Sync Engine est un outil qui permet à votre application de collecter localement toutes les alertes de modifications de la base de données, pour ensuite les synchroniser de manière silencieuse en réseau avec un nœud distant garantissant une équivalence parfaite sur tous vos environnements.
📋 Récapitulatif : Serveur Central vs Application Locale
Voici le guide rapide pour configurer chaque environnement :
1. Serveur Central (Cloud / Hub)
- Rôle : Reçoit les données (
Push), sert de référence pour les conflits et fournit les mises à jour (Pull). - Variables d'environnement (
.env) :SYNC_IS_SERVER=true SYNC_API_KEY=votre_cle_secrete_partagee
- Base de données :
- Exécuter les migrations (
php artisan migrate). La table d'historique globalsync_historiessera automatiquement créée grâce àSYNC_IS_SERVER=true. - Vos tables synchronisées doivent comporter la macro
$table->syncable();.
- Exécuter les migrations (
- Planificateur (Cron) :
- ❌ Aucun cron requis pour la synchronisation (le serveur agit de manière passive aux requêtes API).
2. Application Locale (Client / Offline-first)
- Rôle : Enregistre les modifications hors-ligne ou locales, pousse les données vers le serveur et récupère les nouveautés.
- Variables d'environnement (
.env) :SYNC_SERVER_URL=https://votre-serveur-central.com SYNC_CLIENT_ID=client-caisse-01 # Identifiant unique de ce client SYNC_API_KEY=votre_cle_secrete_partagee SYNC_BATCH_SIZE=1000 # Optionnel (défaut: 1000)
- Base de données :
- Vos tables synchronisées doivent comporter la macro
$table->syncable();. - La table
sync_historiesn'est pas créée localement.
- Vos tables synchronisées doivent comporter la macro
- Planificateur (Cron) :
- ⚠️ Requis : Ajouter la commande dans
app/Console/Kernel.php(ouroutes/console.phppour Laravel 11+) :$schedule->command('sync:run')->everyMinute();
- S'assurer que le planificateur Laravel tourne (
crontabouphp artisan schedule:work).
- ⚠️ Requis : Ajouter la commande dans