2 · Tenir un miroir
Le cas le plus courant : garder chez soi une copie à jour du portefeuille d'une agence. Le premier passage charge tout ; les suivants ne rapportent que ce qui a changé — et les suppressions, qu'une simple liste ne peut pas montrer.
La copie locale
28
biens
22
locataires
16
proprietaires
56
occupations
Point de reprise :
2026-10-30T14:07:02+01:00 — l'heure du serveur au début du dernier passage réussi.
Journal des synchronisations
| Quand | Mode | Reçues | Supprimées | Appels | Durée | Résultat |
|---|---|---|---|---|---|---|
| 30/10 14:07:02 | rien de neuf | 0 | 0 | 1 | 278 ms | réussie |
| 30/10 13:07:02 | rien de neuf | 0 | 0 | 1 | 253 ms | réussie |
| 30/10 12:07:01 | rien de neuf | 0 | 0 | 1 | 212 ms | réussie |
| 30/10 11:07:01 | rien de neuf | 0 | 0 | 1 | 216 ms | réussie |
| 30/10 10:07:01 | rien de neuf | 0 | 0 | 1 | 230 ms | réussie |
| 30/10 09:07:01 | rien de neuf | 0 | 0 | 1 | 209 ms | réussie |
| 30/10 08:07:01 | rien de neuf | 0 | 0 | 1 | 198 ms | réussie |
| 30/10 07:07:01 | rien de neuf | 0 | 0 | 1 | 232 ms | réussie |
| 30/10 06:07:01 | rien de neuf | 0 | 0 | 1 | 221 ms | réussie |
| 30/10 05:07:01 | rien de neuf | 0 | 0 | 1 | 244 ms | réussie |
Derniers biens modifiés
| Identifiant | Adresse | Modifié |
|---|---|---|
bien_6acf9ffb617f69.38387722 |
14 Rue François Toulouse | 14/10 17:30 |
bien_d26_01 |
14 rue de la Fusterie Perpignan | 14/10 15:50 |
bien_d26_02 |
14 rue de la Fusterie Perpignan | 14/10 15:50 |
bien_d26_03 |
27 avenue du Général Leclerc Perpignan | 14/10 15:50 |
bien_d26_04 |
27 avenue du Général Leclerc Perpignan | 14/10 15:50 |
bien_d26_05 |
27 avenue du Général Leclerc Perpignan | 14/10 15:50 |
bien_d26_06 |
6 impasse des Micocouliers Canet-en-Roussillon | 14/10 15:50 |
bien_d26_07 |
Résidence Les Mouettes, 45 boulevard de la Mer Saint-Cyprien | 14/10 15:50 |
Derniers locataires modifiés
| Identifiant | Nom | Modifié |
|---|---|---|
loc_d26_01 |
Lucas Lucas MARTINEZ | 14/10 15:50 |
loc_d26_02 |
Emma Emma VIDAL | 14/10 15:50 |
loc_d26_03 |
Thomas Thomas GARRIGUE | 14/10 15:50 |
loc_d26_04 |
Chloé Chloé BONNET | 14/10 15:50 |
loc_d26_05 |
Inès Inès BENYAHIA | 14/10 15:50 |
loc_d26_06 |
Maxime Maxime SOLER | 14/10 15:50 |
loc_d26_07 |
Manon Manon PUIG | 14/10 15:50 |
loc_d26_08 |
Julien Julien DELCROS | 14/10 15:50 |
Le code de la synchronisation — src/Miroir.php
/**
* Une synchronisation complète. Rend un bilan, et l'inscrit au journal.
*/
public function synchroniser(ClientOkleon $api): array
{
$t0 = microtime(true);
$depuis = $this->etat('point_de_reprise'); // null la toute première fois
$bilan = ['mode' => $depuis === null ? 'initiale' : 'incrementale',
'recus' => 0, 'supprimes' => 0, 'ignorees' => [], 'erreur' => null];
try {
// ── 0 et 2. Y a-t-il du NOUVEAU ? — et l'heure du SERVEUR ─────────────
// Un seul appel répond aux deux questions. L'heure est prise AVANT de lire
// quoi que ce soit : ce qui changera PENDANT notre lecture aura une date
// postérieure et sera repris au prochain passage. On relira peut-être deux
// fois une fiche — sans danger, l'enregistrement local écrase. On n'en perd
// aucune.
// ⚠️ Jamais l'heure de notre machine : deux horloges qui divergent d'une
// minute font sauter une minute de changements, en silence.
$tete = $api->get('/changements', ['limit' => 1] + ($depuis ? ['since' => $depuis] : []));
$nouveauPoint = (string)$tete['server_time'];
// ⭐ Rien depuis notre point de reprise : on s'arrête là. C'est le cas de
// la très grande majorité des passages. Mesuré sur cette démo le 22/09 :
// sans ce raccourci, 7 appels par minute pour rien — la MOITIÉ du quota
// quotidien de la clé, consommée à ne rien faire. Avec : un seul.
// (Le flux couvre toutes les collections que la clé peut lire, suppressions
// comprises : s'il est vide, aucune des nôtres n'a bougé.)
if ($depuis !== null && empty($tete['data'])) {
$this->poserEtat('point_de_reprise', $nouveauPoint);
$bilan['mode'] = 'rien';
return $this->clore($bilan, $api, $t0);
}
// ── 1. Ce que cette clé a le droit de lire sur CET espace ─────────────
$espace = $api->get('/espace');
$lisibles = array_values(array_intersect(self::COLLECTIONS, $espace['collections'] ?? []));
$bilan['ignorees'] = array_values(array_diff(self::COLLECTIONS, $lisibles));
// Tout ou rien : si la synchronisation échoue en route, la copie locale
// reste exactement dans l'état du passage précédent.
$this->db->beginTransaction();
// ── 3a. Créations et modifications : les listes, filtrées ─────────────
foreach ($lisibles as $c) {
$filtre = $depuis ? ['updated_since' => $depuis] : [];
foreach ($api->toutesLesPages('/' . $c, $filtre) as $fiche) {
$this->enregistrer($c, $fiche);
$bilan['recus']++;
}
}
// ── 3b. Suppressions : SEUL le flux s'en souvient ─────────────────────
// Au premier passage il n'y a rien à supprimer : la copie est vide.
if ($depuis !== null) {
foreach ($api->toutesLesPages('/changements', ['since' => $depuis]) as $ch) {
if (($ch['action'] ?? '') === 'supprime' && in_array($ch['collection'] ?? '', $lisibles, true)) {
$bilan['supprimes'] += $this->supprimer((string)$ch['collection'], (string)$ch['id']);
}
}
}
// ── 4. Le point de reprise n'avance QU'UNE FOIS TOUT APPLIQUÉ ─────────
$this->poserEtat('point_de_reprise', $nouveauPoint);
$this->db->commit();
} catch (Throwable $e) {
if ($this->db->inTransaction()) $this->db->rollBack();
$bilan['erreur'] = $e instanceof ErreurOkleon ? $e->codeApi . ' — ' . $e->getMessage() : $e->getMessage();
}
return $this->clore($bilan, $api, $t0);
}