greywolfs/fine-diff-bundle
A Symfony bundle for fine-grained diffing, helping you detect and present precise changes between values or entity states. Integrates into Symfony applications to support change tracking, audit trails, and readable comparisons of before/after data.
First, calibrate expectations: this is a Symfony bundle, not a Laravel package. It is a thin wrapper around Raymond Hill's FineDiff text-diff library, last tagged in October 2017, with no dependents. There is no service provider, no facade, no config/fine_diff.php, and no Laravel auto-discovery. You are effectively adopting an unmaintained Symfony DI extension and its bundled engine.
Minimal path in a Laravel app:
composer require greywolfs/fine-diff-bundle
Expect the install to fight you. The 2017 release will constrain symfony/* to ^2.8 || ^3.0 and probably PHP ^5.5 || ^7.0. If Composer resolves it at all against a modern Laravel skeleton, verify what actually landed:
composer show greywolfs/fine-diff-bundle
ls vendor/greywolfs/fine-diff-bundle
Where to look first, in this order:
vendor/greywolfs/fine-diff-bundle/DependencyInjection/ — the extension class tells you the config keys and the service ID it registers. The service ID is the only thing you actually need.vendor/greywolfs/fine-diff-bundle/Resources/config/services.* — the concrete class behind the service, plus constructor args (usually a default granularity).FineDiff class itself — getDiffOpcodes(), renderDiffFromStringToText(), renderDiffToHTMLFromOpcode(), and the $characterGranularity / $wordGranularity / $lineGranularity statics. That API, not the bundle, is what you'll spend your time with.First use case — show what changed between two revisions of a model field:
// app/Providers/AppServiceProvider.php, register()
$this->app->singleton('fine-diff', function () {
$class = \Greywolfs\FineDiffBundle\...\Diff::class; // confirm in vendor/
return new $class(\FineDiff::$wordGranularity);
});
$html = app('fine-diff')->render($post->revision->body, $post->body);
return view('posts.diff', ['html' => $html]);
If that feels like more ceremony than value, the honest alternative is sebastian/diff (maintained, no framework coupling, and already in most Laravel dependency trees) or jfcherng/php-diff for a richer renderer. Treat this bundle as a candidate you evaluate, not a default you adopt.
Wrap the service ID in a typed binding. Don't scatter app('greywolfs_fine_diff...') calls. Bind an interface once and let Laravel inject it:
$this->app->singleton(DiffRenderer::class, fn () => new FineDiffRenderer(
granularity: \FineDiff::$wordGranularity
));
Choose granularity per call site, not globally. The bundle's config gives you one default; most real apps need several:
$wordGranularity — prose, commit messages, comment edits. Best signal-to-noise for humans.$lineGranularity — code, config files, structured text.$characterGranularity — short strings (titles, slugs, names) where word-level produces one giant replace block.sentence / paragraph — long documents where you want a summary-level "what changed" view.Expose granularity as a parameter on your wrapper rather than reconfiguring the container.
Store opcodes, not just rendered HTML. FineDiff's opcode string (c5i3d2c10...) is compact, deterministic, and lets you re-render at a different granularity or with a different template later without the original inputs:
$opcodes = $differ->getOpcodes($old, $new); // persist this
$html = $differ->renderFromOpcodes($opcodes); // render on read
Put it in a text column on your revisions table. This is the single highest-value pattern with any FineDiff wrapper — it decouples "detect the change" from "display the change."
Keep rendering and diffing separate. Compute the diff when the edit is saved (cheap, bounded, in the request), render it lazily. For large documents, push the opcode computation to a queued job and store the result; a diff view should never compute on page load.
Render with explicit escaping. FineDiff's HTML renderer emits <ins>/<del> markup but does not escape the source text. Escape first, diff second — or post-process. In Blade:
{!! $diffHtml !!} {{-- only if you've escaped the inputs yourself --}}
Prefer a whitelist pass (strip_tags($html, '<ins><del><br>')) over trusting the renderer.
Style with plain CSS, not a component library. The output is ins/del elements. A dozen lines of CSS gets you a GitHub-style view; don't reach for a diffing JS library to render server-side HTML.
Test the semantics, not the markup. Assert on opcode structure or on presence/absence of ins/del around known tokens. Snapshot-testing the full HTML will break on any engine or escaping change and tells you nothing.
symfony/http-kernel and a compiled container. Nothing in it knows about Laravel. You cannot "enable" it; you instantiate the class yourself or bind it manually. Reading DependencyInjection/*Extension.php is the fastest way to learn the intended defaults.symfony/console, symfony/yaml, or psr/*. Don't fight it with --ignore-platform-reqs in anything you intend to deploy — that's how you get a runtime fatal in production instead of an install-time error.app/Support/ if you keep it at all — that's a legitimate "extension point."c copy, d delete, i insert + length). Never hand-parse it in application code; treat it as an opaque blob. Never compare opcodes across library versions.var_dump($differ->getOpcodes($a, $b)) — and check whether the problem is diff detection or HTML rendering. Nearly always it's the former, caused by an unnormalized input (trailing whitespace, CRLF vs LF, HTML entities vs raw characters). Normalize line endings before diffing.& fragments and unreadable output. Diff raw text, escape the segments.DiffRenderer interface with FineDiff as one implementation and sebastian/diff as another. That single abstraction makes migrating off this bundle a one-line change later, which, given the maintenance profile, you should assume you'll want.How can I help you explore Laravel packages today?