sebastian/diff
sebastian/diff is a standalone PHP diff library, factored out of PHPUnit. It generates unified, strict unified, or diff-only output, parses unified diffs into object graphs, and supports custom output builders. Install via Composer as a project or development dependency.
Build vs. buy on diffing — buy. Any feature that answers "what changed between these two versions of a thing?" (config files, API payloads, templates, CMS content, translations, policy documents, database seed data) is a solved problem. sebastian/diff is the component PHPUnit itself uses, so it is effectively validated against millions of CI runs. The realistic alternative is not "build a better diff," it is "spend 2–4 engineer-weeks writing a correct Myers diff + hunk renderer + patch-compatible headers, then own it forever." The BSD-3-Clause license removes legal friction for redistribution or embedding in a commercial product.
Developer experience and time-to-diagnosis in test/CI tooling. Wrapping assertion failures and contract-test mismatches in StrictUnifiedDiffOutputBuilder (or DiffOnlyOutputBuilder when only changed lines matter) turns "expected != actual" into a readable hunk. This is a measurable support-load and on-call lever, not a cosmetic one: it shortens the loop between a red build and a fix. The DiffOnlyOutputBuilder variant is also the right call when diff output lands in logs or chat notifications where context lines are noise.
Audit trails and change review for regulated or high-trust data. The Parser turns a unified diff — from Git, from a change-log table, from an approval workflow — into a typed object graph (Diff → Chunk → Line with line types and ranges). That unlocks programmatic review: flag changes to a whitelisted set of sensitive fields, count deletions in a protected file, enforce "no changes to billing/* without a second approver," or render a per-field redline in an admin UI. This is a compliance and risk-mitigation story (SOC 2 change management, GDPR data-modification evidence, financial record integrity) as much as a feature story.
Replacing bespoke diff code and shell-outs. Teams accumulate a custom differ, a diff -u exec with escaping hazards, and a JS library on the frontend — three formats, three bug surfaces. Consolidating on one parser/renderer pair means one set of edge cases (trailing newlines, CRLF, no-newline-at-EOF markers) handled in one place. This is pure technical-debt retirement with a clean migration narrative.
Roadmap sequencing.
StrictUnifiedDiffOutputBuilder; low risk, immediate DX win, no schema changes.git apply, diff-only for logs).Parser-based change-intelligence — approval gating, redline UI, compliance reporting — where the value is highest but the product design work is heavier.DiffOutputBuilderInterface if a product surface needs Markdown, HTML, or a JSON patch shape; this is the package's designed extension point and avoids forking.Explicit non-goals to record in the decision log. Not a real-time collaborative-editing primitive (batch-oriented, no incremental/OT model), not a binary or media differ, not a merge tool. Scoping these out up front prevents the library being blamed later for problems it never claimed.
Adopt when:
UnifiedDiffOutputBuilder and the LongestCommonSubsequenceCalculator interfaces in favour of StrictUnifiedDiffOutputBuilder and Myers' linear-space algorithm. If you are writing new code today, target the 9.x API and avoid the long-deprecated $lcs parameter.patch or git apply — or you need to consume unified diffs produced elsewhere. Both directions ship in one dependency.Look elsewhere or build something thin when:
Parser on a canonicalized serialization, but an AST/JSON-aware differ is a different product.composer update.Migration watch-items for the PM: the 8.x line contains a 8.2.0 change that was reverted in 8.2.1, plus a 8.3.0 behavioural change where UnifiedDiffOutputBuilder returns an empty string when there is no difference (previously it returned a header). If any downstream code branches on "empty output means no diff," audit it — that is exactly the kind of silent contract change that produces a confusing bug six months later.
To executives, in one breath: We are standardising text comparison on the diff engine that PHPUnit already ships with — a mature, BSD-licensed, actively maintained component — instead of maintaining our own. It removes custom code we currently own, gives engineers readable change output in build failures and admin tooling, and gives us a foundation for automated change review on sensitive data. The cost is one Composer dependency and a contained migration; the alternative is continuing to pay maintenance on three inconsistent implementations.
To engineering leadership: this is a buy, not build decision with a proven pedigree and a small, well-defined API surface (Differ + an output builder for rendering; Parser for structured consumption; DiffOutputBuilderInterface as the extension seam). It is already transitively present via PHPUnit, so we are consolidating rather than adding. The main engineering asks are: pin to the 9.x API and skip the deprecated $lcs parameter, and budget a real upgrade task for each major version — the project drops PHP versions and removes classes at majors, so a lockfile-and-forget posture will eventually break us.
To security, legal, and compliance: BSD-3-Clause, permissive, safe to embed and redistribute. Text-only processing with no network, filesystem, or execution surface. The Parser's structured output is what lets us make change review evidence-producing: we can assert programmatically that protected fields were not modified, that deletions in a sensitive directory were approved, and that a deployed diff matches the reviewed diff — turning an audit narrative into a query.
Honest framing of the trade-off, because stakeholders remember being surprised more than they remember being reassured: the library is text-only, batch-oriented, and not a real-time editing primitive; it is a single-maintainer ecosystem project with an outsized footprint; and it ships breaking majors with some frequency. None of those are reasons to decline — they are reasons to adopt deliberately, pin versions, and write down the non-goals before someone asks for live collaborative editing on top of it.
How can I help you explore Laravel packages today?