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.
assertStringContainsMarkup with diff visualization).str_diff) for HTML by preserving structure (tables, lists, nested tags). More lightweight than DOM-based tools like DOMDocument for diffing.ezyang/htmlpurifier (for sanitization) and doctrine/cache (optional).caxy/htmldiff-bundle provides Laravel-like integration (e.g., Twig extensions, service container binding).HtmlDiff as a singleton with configurable defaults (e.g., config/htmldiff.php).HtmlDiff::diff($oldHtml, $newHtml) for concise usage.ShouldQueue jobs for long-running comparisons (e.g., large HTML emails).php artisan htmldiff:generate to precompute diffs for static content.longtext columns (e.g., posts.diff_html) or use Laravel Filesystem for large diffs.doctrine/cache or Laravel’s cache() helper.| Risk Area | Mitigation Strategy |
|---|---|
| Performance | Benchmark with large HTML (e.g., 10KB+). Use setCacheProvider() for repeated diffs. |
| HTML Sanitization | Disable setPurifierEnabled(false) if input is trusted (e.g., internal CMS content). |
| PHP 8+ Compatibility | Already supported (tested up to PHP 8.3). Pin v0.1.9 if stuck on PHP 7.3. |
| CSS Styling | Provide default CSS via Laravel’s mix or vite (e.g., resources/css/htmldiff.css). |
| Edge Cases | Test with: |
htmlspecialchars_decode pre-processing).<img/>). |
| License (GPL-2.0) | Ensure compliance if redistributing diffs (e.g., open-source projects). |gzencode).HtmlDiff calls in controllers.HtmlDiff to return { changes: [], stats: { additions: 0, deletions: 0 } }.<img src="data:image/png;base64,...">).setUseTableDiffing(false) to treat tables as text if structure is irrelevant.| Laravel Component | Integration Strategy |
|---|---|
| Blade Templates | @diff($oldHtml, $newHtml) helper with inline CSS. |
| Queues | HtmlDiffJob for async generation (e.g., diffing user-generated content). |
| Testing | assertHtmlDiff($expected, $actual) custom assertion in Pest. |
| APIs | Return diffs in responses (e.g., application/html with Content-Disposition: attachment). |
| Caching | Cache diffs with cache()->remember() or doctrine/cache. |
| Storage | Store diffs in: |
longtext).storage/app/diffs/).caxy/htmldiff-bundle if already using Symfony components (e.g., Twig). |Phase 1: Proof of Concept
composer require caxy/php-htmldiff.use Caxy\HtmlDiff\HtmlDiff;
$diff = new HtmlDiff($oldHtml, $newHtml);
echo $diff->build();
Phase 2: Laravel Integration
// app/Providers/HtmlDiffServiceProvider.php
public function register()
{
$this->app->singleton(HtmlDiff::class, function () {
$config = new HtmlDiffConfig();
$config->setMatchThreshold(90)->setGroupDiffs(true);
return new HtmlDiff('', '', $config);
});
}
// app/Facades/HtmlDiff.php
public static function diff(string $oldHtml, string $newHtml): string
{
return app(HtmlDiff::class)->setHtml($oldHtml, $newHtml)->build();
}
// config/htmldiff.php
return [
'match_threshold' => 85,
'group_diffs' => true,
'purifier_enabled' => env('APP_DEBUG') ? true : false,
];
Phase 3: Scaling
// app/Jobs/GenerateHtmlDiff.php
public function handle()
{
$diff = HtmlDiff::diff($this->oldHtml, $this->newHtml);
$this->user->diff_html = $diff;
$this->user->save();
}
// Cache diffs for 1 hour
$diff = cache()->remember("diff_{$postId}", 3600, function () use ($oldHtml, $newHtml) {
return HtmlDiff::diff($oldHtml, $newHtml);
});
Phase 4: Advanced Use Cases
ModelObservers) to auto-generate diffs on updated.post-update webhooks (e.g., GitHub Pages changes).// app/Console/Commands/DiffPosts.php
public function handle()
{
$posts = Post::where('updated_at', '>', now()->subDays(7))->get();
foreach ($posts as $post) {
$diff = HtmlDiff::diff($post->old_content, $post->content);
// Store or notify...
}
}
v0.1.9.DOMDocument::loadHTML()./* resources/css/htmldiff.css */
.htmldiff .ins { background: #ddffdd; }
.htmldiff .del { background: #ffdddd
How can I help you explore Laravel packages today?