Merge
Three-way merge and diff. Pluggable differ + merger + optional validator.
composer require wp-php-toolkit/merge
Content synchronization needs more than "last write wins." A Markdown file changes in Git while the same post changes in WordPress. A generated config changes through both a CLI tool and a UI. In those cases you need a common ancestor, two edited versions, and a way to explain conflicts to a human.
The Merge component provides the diff and three-way merge primitives used by those workflows. The default examples are line-oriented because that is the most familiar shape, but the strategy is intentionally pluggable: choose the differ, choose the merger, and optionally validate the merged result before accepting it.
Use the merge result to auto-accept independent edits and to show structured conflicts when a person must decide.
Diff two strings line by line
Feed two strings to LineDiffer and inspect the operations. Every get_changes() entry is a [op, text] pair.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Merge\Diff\Diff;
use WordPress\Merge\Diff\LineDiffer;
$diff = ( new LineDiffer() )->diff(
"alpha\nbeta\ngamma\n",
"alpha\nBETA\ngamma\ndelta\n"
);
$labels = array( Diff::DIFF_EQUAL => '=', Diff::DIFF_DELETE => '-', Diff::DIFF_INSERT => '+' );
foreach ( $diff->get_changes() as $change ) {
echo $labels[ $change[0] ] . ' ' . rtrim( $change[1] ) . "\n";
}
Render a unified patch
format_as_git_patch() produces output that mirrors git diff, including hunk headers — handy for emails, CI annotations, or a "what changed?" panel.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Merge\Diff\LineDiffer;
$old = "title: Hello\nauthor: Alice\nstatus: draft\n";
$new = "title: Hello, world\nauthor: Alice\nstatus: published\ntags: greeting\n";
$diff = ( new LineDiffer() )->diff( $old, $new );
echo $diff->format_as_git_patch( array(
'a_source' => 'a/post.yml',
'b_source' => 'b/post.yml',
) );
Three-way merge with no conflicts
The classic case: each branch changes a different region. Pass the common ancestor plus both edits to MergeStrategy::merge() and read the merged result.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Merge\Diff\LineDiffer;
use WordPress\Merge\Merge\LineMerger;
use WordPress\Merge\MergeStrategy;
$strategy = new MergeStrategy( new LineDiffer(), new LineMerger() );
$result = $strategy->merge(
"intro\nbody\noutro\n",
"intro updated\nbody\noutro\n",
"intro\nbody\noutro\nappendix\n"
);
echo $result->has_conflicts() ? "conflicts!\n" : "clean merge:\n";
echo $result->get_merged_content();
Inspect and surface conflicts
When both sides edit the same region, the merger produces a MergeConflict. The merged content carries Git-style markers, but the structured get_conflicts() output is what you want for a UI that lets the user pick a side.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Merge\Diff\LineDiffer;
use WordPress\Merge\Merge\LineMerger;
use WordPress\Merge\MergeStrategy;
$strategy = new MergeStrategy( new LineDiffer(), new LineMerger() );
$result = $strategy->merge(
"line 1\nline 2\n",
"line 1\nline 2 from Alice\n",
"line 1\nline 2 from Bob\n"
);
if ( $result->has_conflicts() ) {
foreach ( $result->get_conflicts() as $c ) {
echo "ours: " . trim( $c->ours ) . "\n";
echo "theirs: " . trim( $c->theirs ) . "\n";
}
}
echo "\n--- merged content with markers ---\n";
echo $result->get_merged_content();
Sync a Markdown folder against an edited DB copy
A real-world scenario: posts live both in a Git-tracked Markdown folder and in WordPress, and someone edits each. Three-way-merge each post against its common ancestor.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Merge\Diff\LineDiffer;
use WordPress\Merge\Merge\LineMerger;
use WordPress\Merge\MergeStrategy;
$strategy = new MergeStrategy( new LineDiffer(), new LineMerger() );
$posts = array(
'hello.md' => array(
'base' => "# Hello\nDraft body.\n",
'disk' => "# Hello\nDraft body, expanded on disk.\n",
'db' => "# Hello\nDraft body.\nNew section from the editor.\n",
),
'about.md' => array(
'base' => "# About\nWho we are.\n",
'disk' => "# About\nWho *they* are.\n",
'db' => "# About\nWho we really are.\n",
),
);
foreach ( $posts as $name => $sides ) {
$result = $strategy->merge( $sides['base'], $sides['disk'], $sides['db'] );
echo "=== {$name} ===\n";
echo $result->has_conflicts() ? "(conflict — needs review)\n" : "(auto-merged)\n";
echo $result->get_merged_content() . "\n";
}