Cell-Level Merge
Column-level merge detects when both sides changed the same column. Cell-level merge goes deeper, merging the content inside that column. Two people editing different lines of a TEXT field, or different keys of a JSON object, can merge cleanly.
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
$config = CellMergeConfig::auto();
$merge = new ThreeWayMerge($config);
$result = $merge->merge($base, $ours, $theirs);CellMergeConfig
CellMergeConfig maps columns or column types to cell merger implementations. The auto() factory provides sensible defaults:
use Merql\CellMerge\CellMergeConfig;
// Auto: TEXT/LONGTEXT columns use TextCellMerger, JSON columns 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());Merger resolution priority:
- Qualified column name (
table.column). - Unqualified column name (
column). - Column type pattern match (case-insensitive
str_contains). - Default:
OpaqueCellMerger(always reports a conflict when values differ).
TextCellMerger
Merges multi-line text using Myers diff, the same algorithm git uses for file content. Each line is treated as a unit, and changes to different lines merge cleanly.
use Merql\CellMerge\TextCellMerger;
$merger = new TextCellMerger();Example: clean 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 3Different lines were edited. The merge produces "LINE ONE\nline two\nLINE THREE" with no conflict.
Example: text conflict
Base: "line one\nline two"
Ours: "changed by us\nline two" // changed line 1
Theirs: "changed by them\nline two" // also changed line 1Both sides changed the same line to different values. The merger returns a conflict result. The merged content includes conflict markers similar to git.
How it works
TextCellMerger delegates to pitmaster's ThreeWayMerge::merge(), which computes a Myers diff between base and each side, then combines the two edit scripts. When edits overlap on the same line region, it reports a conflict.
JsonCellMerger
Merges JSON objects by comparing top-level keys independently. Each key is merged using the standard three-way rules.
use Merql\CellMerge\JsonCellMerger;
$merger = new JsonCellMerger();Example: clean 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}Different keys were changed. The merged result is:
{"name": "Bob", "role": "admin", "active": true}Example: JSON conflict
// Base
{"color": "red"}
// Ours
{"color": "blue"}
// Theirs
{"color": "green"}Both sides changed the color key to different values. This is a conflict. The merger defaults to ours and reports one conflict.
Key-level rules
| Base | Ours | Theirs | Result |
|---|---|---|---|
| Key unchanged | Key unchanged | Accept base | |
| Key unchanged | Key changed | Accept theirs | |
| Key changed | Key unchanged | Accept ours | |
| Key changed (same) | Key changed (same) | Accept (agree) | |
| Key changed (different) | Key changed (different) | Conflict (default: ours) | |
| Key exists | Key exists | Key removed | Accept removal |
| Key exists | Key removed | Key exists | Accept removal |
| Key missing | Key added | Key missing | Accept addition |
| Key missing | Key missing | Key added | Accept addition |
Nested objects are compared as opaque JSON strings, not recursively. If a nested object differs, the entire value of that key is compared, not its sub-keys.
Requirements
All three values must be valid JSON objects or arrays. If any value is not valid JSON, or is a JSON scalar (string, number, boolean), the merger falls back to a conflict.
OpaqueCellMerger
The default merger when no cell-level strategy is configured. It treats the column value as an opaque blob and always reports a conflict when both sides differ.
use Merql\CellMerge\OpaqueCellMerger;
$merger = new OpaqueCellMerger();
$result = $merger->merge($base, $ours, $theirs);
// $result->clean is always false when $ours !== $theirsCellMergeResult
All cell mergers return a CellMergeResult:
$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 cellFactory methods:
CellMergeResult::resolved($value); // clean merge
CellMergeResult::conflict($oursValue, $n); // n conflicts, defaults to oursCustom cell mergers
Implement the CellMerger interface to add your own merge strategy:
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());