PHP Toolkit

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";
}

See also