caxy/php-htmldiff
Compare two HTML snippets/files and generate a marked-up diff highlighting insertions, deletions, and changes. Easy Composer install, simple API (HtmlDiff->build()), configurable behavior and CSS-friendly output; includes a live demo and Symfony bundle option.
Installation:
composer require caxy/php-htmldiff
Add to composer.json if using Laravel’s require-dev for testing:
"require-dev": {
"caxy/php-htmldiff": "^0.1.14"
}
First Use Case: Compare two HTML strings (e.g., from a database or API response) and render differences in a Blade view:
use Caxy\HtmlDiff\HtmlDiff;
$oldHtml = '<p>Hello <strong>World</strong></p>';
$newHtml = '<p>Hello <strong>PHP</strong></p>';
$htmlDiff = new HtmlDiff($oldHtml, $newHtml);
$diffHtml = $htmlDiff->build();
Output the result in a Blade template:
<div class="htmldiff">{{ $diffHtml }}</div>
Styling:
Include the demo CSS from the package’s demo/codes.css or use a minimal custom style:
.htmldiff ins { background: #ddffdd; }
.htmldiff del { background: #ffdddd; }
Comparing Dynamic Content:
old_version and new_version fields (e.g., CMS content, user profiles) and diff them:
$oldContent = $post->old_content;
$newContent = $post->new_content;
$diff = (new HtmlDiff($oldContent, $newContent))->build();
$oldResponse = response()->json(['html' => $oldHtml]);
$newResponse = response()->json(['html' => $newHtml]);
Reusable Configuration:
$config = (new HtmlDiffConfig())
->setMatchThreshold(90)
->setGroupDiffs(true);
$diff = HtmlDiff::create($oldHtml, $newHtml, $config)->build();
Caching Diffs:
diff_cache table):
$cacheKey = md5($oldHtml . $newHtml);
if (cache()->has($cacheKey)) {
$diff = cache()->get($cacheKey);
} else {
$diff = (new HtmlDiff($oldHtml, $newHtml))->build();
cache()->put($cacheKey, $diff, now()->addHours(1));
}
Integration with Laravel Services:
public function rules() {
return [
'content' => 'required|htmldiff:' . $this->oldContent,
];
}
Create a custom validation rule:
use Caxy\HtmlDiff\HtmlDiff;
class HtmlDiffRule extends AbstractRule {
public function passes($attribute, $value) {
$diff = (new HtmlDiff($this->oldContent, $value))->build();
return strpos($diff, '<del') === false; // No deletions allowed
}
}
Table/List-Specific Diffs:
$config = (new HtmlDiffConfig())->setUseTableDiffing(true);
$diff = HtmlDiff::create($oldTableHtml, $newTableHtml, $config)->build();
Diffing in Artisan Commands:
public function handle() {
$posts = Post::where('needs_review', true)->get();
foreach ($posts as $post) {
$diff = (new HtmlDiff($post->old_content, $post->new_content))->build();
$this->info("Changes: " . substr($diff, 0, 50));
}
}
Real-Time Diffing (Livewire/Alpine):
document.getElementById('content').addEventListener('input', function() {
fetch('/diff', {
method: 'POST',
body: JSON.stringify({
oldHtml: oldContent,
newHtml: this.value
})
}).then(response => response.text())
.then(diff => document.getElementById('diff').innerHTML = diff);
});
Route::post('/diff', function (Request $request) {
return (new HtmlDiff($request->oldHtml, $request->newHtml))->build();
});
Diffing Markdown/Converted HTML:
spatie/laravel-markdown to convert Markdown to HTML before diffing:
$oldMarkdown = "# Old Title";
$newMarkdown = "# New Title";
$oldHtml = Markdown::parse($oldMarkdown);
$newHtml = Markdown::parse($newMarkdown);
$diff = (new HtmlDiff($oldHtml, $newHtml))->build();
HTML Purifier Dependencies:
ezyang/htmlpurifier for sanitization. If disabled (setPurifierEnabled(false)), unsanitized input may break diffing:
$config = (new HtmlDiffConfig())->setPurifierEnabled(false);
// Risk: Malformed HTML may cause errors or incorrect diffs.
Performance with Large HTML:
setMatchThreshold(70) to balance accuracy/speed:
$config = (new HtmlDiffConfig())->setMatchThreshold(70);
Newlines Handling:
<pre>) may lose formatting if setKeepNewLines(false) (default). Enable for code blocks:
$config = (new HtmlDiffConfig())->setKeepNewLines(true);
Self-Closing Tags:
<img/> or <br/> may cause issues. Ensure proper HTML structure before diffing.Unicode/Encoding:
$config = (new HtmlDiffConfig())->setEncoding('ISO-8859-1');
Inspect Raw Diff Output:
$diff = (new HtmlDiff($oldHtml, $newHtml))->build();
Log::debug('Diff HTML:', ['diff' => $diff]);
Test with Minimal HTML:
$diff = (new HtmlDiff('<p>Hello</p>', '<p>Hi</p>'))->build();
Check for Malformed HTML:
htmlspecialchars() to validate input before diffing:
$oldHtml = htmlspecialchars_decode($oldHtml);
$newHtml = htmlspecialchars_decode($newHtml);
Cache Provider Issues:
$config = (new HtmlDiffConfig())->setCacheProvider($cacheProvider);
Custom Diff Styling:
.htmldiff ins { border-bottom: 2px solid green; }
.htmldiff del { border-bottom: 2px solid red; }
Post-Processing Diffs:
$diff = (new HtmlDiff($oldHtml, $newHtml))->build();
$diff = str_replace(
'<ins>',
'<ins title="Added in this version">',
$diff
);
Integrate with Laravel Notifications:
Notification::send($user, new HtmlDiffNotification($diffHtml));
Custom notification:
class HtmlDiffNotification extends Notification {
public function toMail($notifiable) {
return (new MailMessage)
->subject('HTML Changes')
->line('The following changes were detected:')
->line($this->diffHtml);
}
}
Extend for Custom Tags:
setIsolatedDiffTags() is deprecated, you can manually wrap tags before diffing:
$oldHtml = preg_replace('/<script>(
How can I help you explore Laravel packages today?