Merge auf Zellenebene
Beim Merge auf Spaltenebene wird erkannt, wenn beide Seiten dieselbe Spalte geändert haben. Das Merge auf Zellenebene geht tiefer und führt die Inhalte innerhalb dieser Spalte zusammen. Zwei Personen können unterschiedliche Zeilen eines TEXT-Felds oder unterschiedliche Schlüssel desselben JSON-Objekts konfliktfrei bearbeiten.
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
$config = CellMergeConfig::auto();
$merge = new ThreeWayMerge($config);
$result = $merge->merge($base, $ours, $theirs);
Merge auf Zellenebene aktivieren
Das Merge auf Zellenebene ist optional. Es wird nur in einem ThreeWayMerge ausgeführt, das mit einem CellMergeConfig erstellt wurde. Ohne dieses Element führt jede Spalte, die beide Seiten auf unterschiedliche Werte geändert haben, zu einem update_update-Konflikt.
Die Fassade Merql nimmt keine Konfiguration entgegen: Merql::merge(), Merql::patch() und Merql::plan() erstellen new ThreeWayMerge() ohne eine solche, und der Befehl merge von CLI läuft über Merql::merge(). Sie führen immer nur auf Spaltenebene zusammen (#7).
Um Snapshots, die mit der Fassade oder CLI erstellt wurden, mit einem Merge auf Zellenebene zusammenzuführen, lade sie und rufe selbst ThreeWayMerge auf. Das Ergebnis ist ein gewöhnliches MergeResult. Die Fassade kann es also weiterhin anwenden, und MergePlanBuilder wandelt es in einen Merge-Plan um:
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Merql;
use Merql\Plan\MergePlanBuilder;
use Merql\Snapshot\SnapshotStore;
$base = SnapshotStore::load('base');
$ours = SnapshotStore::load('ours');
$theirs = SnapshotStore::load('theirs');
$result = (new ThreeWayMerge(CellMergeConfig::auto()))->merge($base, $ours, $theirs);
$plan = (new MergePlanBuilder())->build('plan-1', $base, $ours, $theirs, $result);
Zellen-Merger werden nur für Spalten ausgeführt, die beide Seiten geändert haben. Hat nur eine Seite eine Spalte geändert, wird ihr Wert unverändert übernommen.
CellMergeConfig
CellMergeConfig ordnet Spalten oder Spaltentypen passende Zellen-Merger-Implementierungen zu. Die Factory auto() stellt sinnvolle Standardwerte bereit:
use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\JsonCellMerger;
use Merql\CellMerge\TextCellMerger;
// Auto: column types containing "text" (TEXT, MEDIUMTEXT, LONGTEXT, ...) use
// TextCellMerger, column types containing "json" use JsonCellMerger.
$config = CellMergeConfig::auto();
// Custom: assign mergers to specific columns.
$config = (new CellMergeConfig())
->forColumn('posts.content', new TextCellMerger())
->forColumn('settings', new JsonCellMerger())
->forType('json', new JsonCellMerger());
Priorität bei der Auswahl des Mergers:
- Vollständiger Spaltenname (
table.column). - Spaltenname ohne Qualifizierer (
column). - Abgleich mit Spaltentyp (ohne Beachtung der Groß- und Kleinschreibung:
str_contains). - Standard:
OpaqueCellMerger(meldet bei unterschiedlichen Werten immer einen Konflikt).
TextCellMerger
Führt mehrzeiligen Text mit dem Myers-Diff zusammen, demselben Algorithmus, den git für Datei-Inhalte verwendet. Jede Zeile gilt als Einheit; Änderungen an unterschiedlichen Zeilen lassen sich konfliktfrei zusammenführen.
use Merql\CellMerge\TextCellMerger;
$merger = new TextCellMerger();
Beispiel: konfliktfreies Text-Merge
Base: "line one\nline two\nline three"
Ours: "LINE ONE\nline two\nline three" // changed line 1
Theirs: "line one\nline two\nLINE THREE" // changed line 3
Es wurden unterschiedliche Zeilen bearbeitet. Das Merge-Ergebnis ist "LINE ONE\nline two\nLINE THREE" ohne Konflikt.
Beispiel: Textkonflikt
Base: "line one\nline two"
Ours: "changed by us\nline two" // changed line 1
Theirs: "changed by them\nline two" // also changed line 1
Beide Seiten haben dieselbe Zeile auf unterschiedliche Werte geändert. Der Merger gibt ein Konfliktergebnis zurück. Der zusammengeführte Inhalt enthält konfliktähnliche Markierungen wie bei git.
Funktionsweise
TextCellMerger delegiert die Aufgabe an ThreeWayMerge::merge() von pitmaster. Dieser berechnet einen Myers-Diff zwischen Base und jeder Seite und führt anschließend die beiden Bearbeitungsskripte zusammen. Überschneiden sich Änderungen im selben Zeilenbereich, wird ein Konflikt gemeldet.
JsonCellMerger
Führt JSON-Objekte zusammen, indem die Schlüssel der obersten Ebene einzeln verglichen werden. Für jeden Schlüssel gelten die üblichen Drei-Wege-Regeln.
use Merql\CellMerge\JsonCellMerger;
$merger = new JsonCellMerger();
Beispiel: konfliktfreies JSON-Merge
// Base
{"name": "Alice", "role": "editor", "active": true}
// Ours: changed "role"
{"name": "Alice", "role": "admin", "active": true}
// Theirs: changed "name"
{"name": "Bob", "role": "editor", "active": true}
Die Schlüssel wurden unterschiedlich geändert. Das Merge-Ergebnis lautet:
{"name": "Bob", "role": "admin", "active": true}
Beispiel: JSON-Konflikt
// Base
{"color": "red"}
// Ours
{"color": "blue"}
// Theirs
{"color": "green"}
Beide Seiten haben den Schlüssel color auf unterschiedliche Werte geändert. Das ist ein Konflikt. Standardmäßig übernimmt der Merger Ours und meldet einen Konflikt.
Regeln auf Schlüsselebene
| Basis | Ours | Theirs | Ergebnis |
|---|---|---|---|
| Schlüssel unverändert | Schlüssel unverändert | Basis übernehmen | |
| Schlüssel unverändert | Schlüssel geändert | Theirs übernehmen | |
| Schlüssel geändert | Schlüssel unverändert | Ours übernehmen | |
| Schlüssel geändert (gleich) | Schlüssel geändert (gleich) | Übernehmen (übereinstimmend) | |
| Schlüssel geändert (unterschiedlich) | Schlüssel geändert (unterschiedlich) | Konflikt (Standard: Ours) | |
| Schlüssel vorhanden | Schlüssel vorhanden | Schlüssel entfernt | Entfernung übernehmen |
| Schlüssel vorhanden | Schlüssel entfernt | Schlüssel vorhanden | Entfernung übernehmen |
| Schlüssel fehlt | Schlüssel hinzugefügt | Schlüssel fehlt | Hinzufügung übernehmen |
| Schlüssel fehlt | Schlüssel fehlt | Schlüssel hinzugefügt | Hinzufügung übernehmen |
Verschachtelte Objekte werden als undurchsichtige JSON-Zeichenfolgen verglichen, nicht rekursiv. Unterscheidet sich ein verschachteltes Objekt, wird der gesamte Wert dieses Schlüssels verglichen, nicht seine Unterschlüssel.
Voraussetzungen
Alle drei Werte müssen gültige JSON-Objekte oder Arrays sein. Ist ein Wert kein gültiges JSON oder ein skalarer JSON-Wert (String, Zahl, Boolean), fällt der Merger auf einen Konflikt zurück.
OpaqueCellMerger
Der Standard-Merger, wenn keine Strategie für die Zellenebene konfiguriert ist. Er behandelt den Spaltenwert als undurchsichtigen Block und meldet immer einen Konflikt, wenn sich die Werte beider Seiten unterscheiden.
use Merql\CellMerge\OpaqueCellMerger;
$merger = new OpaqueCellMerger();
$result = $merger->merge($base, $ours, $theirs);
// $result->clean is always false when $ours !== $theirs
CellMergeResult
Alle Zellen-Merger geben ein CellMergeResult zurück:
$result = $merger->merge($base, $ours, $theirs);
$result->clean; // bool: true if merge resolved cleanly
$result->value; // mixed: the merged value (or ours on conflict)
$result->conflicts; // int: number of conflicts within the cell
Factory-Methoden:
CellMergeResult::resolved($value); // clean merge
CellMergeResult::conflict($oursValue, $n); // n conflicts, defaults to ours
Benutzerdefinierte Zellen-Merger
Implementiere die Schnittstelle CellMerger, um eine eigene Merge-Strategie hinzuzufügen:
use Merql\CellMerge\CellMerger;
use Merql\CellMerge\CellMergeResult;
class YamlCellMerger implements CellMerger
{
public function merge(mixed $base, mixed $ours, mixed $theirs): CellMergeResult
{
// Parse YAML, merge keys, return result.
$merged = $this->mergeYaml($base, $ours, $theirs);
if ($merged['clean']) {
return CellMergeResult::resolved($merged['value']);
}
return CellMergeResult::conflict($ours);
}
}
$config = (new CellMergeConfig())
->forColumn('config_data', new YamlCellMerger());