Download the PHP package heyosseus/difflock without Composer

On this page you can find all versions of the php package heyosseus/difflock. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package difflock

Difflock

Latest Version Total Downloads Tests License

Diff, analyze, and protect your Laravel database schema.

Difflock reads your migrations and your database, tells you what is about to change, how badly it could go, and stops the changes that should not run unattended.


Contents

  1. Overview
  2. Why Difflock?
  3. Installation
  4. Quick start
  5. Schema diff
  6. Migration linting
  7. Risk levels
  8. Migration protection
  9. CI
  10. JSON output
  11. Configuration
  12. Custom rules
  13. Programmatic API
  14. Supported databases
  15. Limitations
  16. Architecture
  17. Contributing
  18. License

Overview

Difflock has three jobs, and it keeps them separate. The diff engine does not know how findings are rendered; the rules do not know Artisan exists; the guard consumes analysis rather than repeating it. Architecture tests enforce each of those boundaries.

Command Answers
php artisan difflock All of it, in one screen
php artisan difflock:diff Has the schema drifted from the recorded baseline?
php artisan difflock:lint What will the pending migrations do, and how risky is it?
php artisan difflock:check Both, with an exit code CI can act on
php artisan difflock:migrate Migrate — but only if it is safe to

Every command has real help text. php artisan help difflock:lint is worth reading once.

Why Difflock?

Code review catches the migration that is wrong. It rarely catches the migration that is correct and dangerous, because that one looks fine:

Each of those passes review, passes CI against an empty database, and behaves differently against production. Difflock reads them the way a careful reviewer would, with the live schema and the table sizes in front of it.

It is deliberately conservative about what it claims. It will tell you an index build reads every row; it will not tell you it takes a lock, because that depends on an engine and a version it cannot see from a migration file. Everywhere the honest answer is "it depends", Difflock says so and tells you what it depends on.

It never writes to the database it inspects. Introspection and size metadata are reads. The only command that writes anything is difflock:migrate, and all it does is hand over to Laravel's own migrate once it has decided the migrations are safe.

Installation

Laravel discovers the package automatically. Publish the config if you want to tune it:

Requirements

Laravel 11 is deliberately not supported. Every one of its releases is now covered by a security advisory, so Composer's default policy refuses to install any of them — claiming support for a major nobody can install would be a promise the package cannot keep.

No Doctrine DBAL. Laravel 11 moved schema introspection into the framework, so Difflock uses that and carries no driver-specific SQL of its own beyond one cheap metadata query for table sizes.

Quick start

Adding it to a codebase that already exists

Point Difflock at a mature project and it will find every risky migration ever written — about code that already shipped. On a real 170-migration application it reported 199 findings, 124 of them high. Nobody acts on 199 findings, and a build that is red on day one gets switched off by the end of the week.

So accept the backlog first:

Commit database/difflock/accepted.json. Every report still counts what is in it (199 previously accepted findings not shown), so the backlog stays visible instead of quietly becoming permanent — and a genuinely new DROP TABLE still turns the build red immediately. Delete a line from the file to bring a finding back.

Findings are matched on what they are about — rule, migration, table, subject — never on line numbers or wording, so reformatting a migration doesn't resurrect its findings.

Schema diff

difflock:diff compares two schemas that were both actually observed.

+ gained, - lost, ~ altered — and a ~ shows what it was above what it becomes. The markers carry the meaning, so the output reads identically under --no-ansi, in a CI log, or pasted into a pull request.

It detects tables, columns, indexes and foreign keys added, removed and changed — including nullability, defaults, lengths, precision, uniqueness and referential actions.

To compare two connections instead of a baseline:

Why a recorded baseline

Drift means "the database no longer matches what we agreed on". Difflock makes that a claim you can check by comparing against a snapshot somebody deliberately recorded and committed, rather than against a schema reconstructed from migration source — which, for reasons in Limitations, cannot be made reliable.

The baseline is a versioned JSON file. A baseline that exists and cannot be read is an error, not an empty schema: exit code 2, never a green tick.

What you are committing

The baseline is the one file Difflock asks you to put in git, so it is worth knowing what goes in it. Structure only — table, column and index names, types, nullability, defaults, comments and foreign keys. Never a row of data, never a credential.

For a private repository that is close to no new exposure: your database/migrations directory already describes the same structure. Two things deserve a thought anyway.

Three controls, in order of bluntness:

  1. Or do not commit it at all: .gitignore the file and record the baseline in CI from a freshly-migrated database. You keep "did this branch change the schema" and lose "did production drift from what we agreed" — a real trade, not a free win.

Turning snapshot.defaults off costs one thing and nothing else: a default changing stops counting as drift. It produces no false differences, because the comparator only compares fields both sides reported, and it does not affect the rules, which read the live database rather than this file.

Migration linting

Real output from a 170-migration production application: 251 findings on one screen. The summary's length does not grow with the number of findings — -v expands every one, --rule= takes them a rule at a time.

Only pending migrations are analysed by default. A migration that has already run cannot be made safer by a finding, and a build that fails over a drop committed two years ago is a build nobody keeps green.

When nothing is pending — which is the normal state of a machine that is up to date — it audits every migration instead of printing nothing, and says that is what it did.

Built-in rules

Rule Detects Risk
drop-table Schema::drop(), dropIfExists(), dropAllTables() Critical
drop-column dropColumn(), dropTimestamps(), dropSoftDeletes(), dropConstrainedForeignId(), … Critical
rename-column renameColumn(), Schema::rename() High
change-column ->change() — type, length, nullability, precision, defaults Computed
add-not-null-column A NOT NULL column with no default added to a table with rows Low → High
add-index index(), unique(), fullText(), … on an existing table Low → High
drop-index dropIndex(), dropUnique(), dropPrimary() Low → High
foreign-key Added, dropped, and cascading constraints Low → High
large-table Any alter on a table above the configured size Medium

Two of them earn their place immediately.

add-not-null-column is the migration that passes review, passes CI against an empty database, and fails in production — a NOT NULL column with no default has nothing to put in the rows already there, and most engines refuse the statement. Difflock scales it by the actual row count: high when the table is known to hold rows, low when it is known to be empty, medium when the count is unknown, and it says which.

foreign-key flags cascadeOnDelete(). Four keystrokes that turn $user->delete() into a delete of every order, invoice and line item, inside the database, with no model events, no observers and no soft deletes. The migration that introduces it is the last moment anybody looks at it on purpose.

change-column computes its risk

->change() covers everything from widening a varchar, which costs nothing, to turning a nullable text column into a NOT NULL integer, which can fail partway through a deploy. Difflock compares the declaration against the column as it exists now and reports the worst thing it finds:

Change Risk
Nullable → NOT NULL, table has rows High
Length reduced, table has rows High
Type family changed (text → integer), table has rows High
Default dropped Medium
Live column could not be read Medium
Length increased, NOT NULL → nullable, nothing changed Low

Type comparison is by family — text, integer, decimal, datetime, json, uuid — not by name, so string() against character varying(255) is correctly not a change, and integer() against varchar(50) correctly is.

Risk levels

Levels are deterministic. Every rule documents the conditions under which it returns each one, and the same migration against the same database always produces the same level. Nothing is scored, weighted or inferred — a level is the name of a branch a rule took.

Every finding also carries two facts rather than opinions:

reversible does not mean the data comes back. A dropped column's down() recreates the column and not one row of what was in it. Rules that destroy data set destructive and say so, whatever down() looks like.

Migration protection

Analyses the pending migrations. If nothing reaches the block level it hands over to Laravel's own migrate, unchanged. If something does:

Nothing touched the database.

--allow-risky is deliberately not spelled --force. Bypassing Difflock and skipping Laravel's production confirmation are different decisions and should not share a flag.

What Difflock will not do

CI

Exit code Meaning
0 Nothing at or above the threshold
1 Findings above the threshold, or the schema has drifted
2 Configuration or runtime error

Treat 2 as a failure. It means the check did not run — a disabled package, an unparseable --fail-on, an unreadable baseline — which is different from running and finding nothing.

GitHub Actions

GitLab CI

Difflock runs with no database attached. Every rule that reads only the source — the drop, the rename, the cascade — still fires; the size-dependent ones report that the count was unknown, and the report says at the top that it ran blind. It never quietly grades on a curve.

JSON output

Every command supports --format=json.

Notes on the shape, which is documented and stable:

Configuration

config/difflock.php, in full:

enabled => false makes the commands refuse to run rather than report a clean result. A check that goes green because it never looked is worse than no check.

Ignores are matched against findings after the rules have run, so the ignore list can only ever remove findings — a mistake in it cannot make a rule report something it would not otherwise have reported.

Custom rules

A rule implements one interface and knows nothing about Artisan, rendering, or the database:

Register it either way:

Rules are resolved through the container, so they may take constructor dependencies. Registration order does not matter. Rules are keyed by identifier with the last one winning, so registering a rule that answers to drop-column replaces the built-in of that name.

The context gives a rule everything it is allowed to know:

null from rows() means unknown, never zero. A rule that confused the two would call a migration against an eight-million-row table safe.

Testing a rule needs no database:

AI agents

An agent writing a migration cannot see what Difflock can see. It does not know the table has eight million rows, that two indexes are built on the column it is about to drop, or that the schema drifted last Tuesday. So it writes the migration that passes review and takes production down — the same failure as always, generated faster.

Difflock ships an MCP server that closes the loop.

Four tools, in the order a careful developer would use them:

Tool Answers
difflock_table_context What does this table look like — rows, columns, indexes, foreign keys?
difflock_lint_migration What is wrong with this migration — including one not written yet?
difflock_schema_drift Has this database already diverged from the baseline?
difflock_rules What does this rule actually check, in this project?

Check the draft, not the file

difflock_lint_migration takes source as well as path. An agent can validate the migration it is holding — against real row counts and real indexes — fix it, and write once. Checking after writing means every intermediate mistake lands in the repository first.

Why -d display_errors=stderr

On this transport STDOUT carries the protocol and nothing else. A single PHP deprecation notice printed during bootstrap lands ahead of the handshake, the client cannot parse it, and Difflock's tools appear not to exist — with nothing in the error to suggest why. I hit exactly this on a live application whose config/database.php used PDO::MYSQL_ATTR_SSL_CA on PHP 8.5.

The flag redirects PHP's diagnostics to STDERR, where MCP clients collect server logs, so you still see them. Difflock also seals STDOUT around every request itself, so a dd() left in a model cannot corrupt the stream either.

It is a standalone stdio server, not a Boost plugin. Boost publishes no documented API for third-party tool registration, and writing against an undocumented internal is how a package breaks on someone else's patch release. This works with Boost and with everything else.

A skill for coding agents

skills/difflock/SKILL.md teaches an agent the workflow — check the table, write the migration, lint it, fix, then show the user — and the things it must not do, such as silencing a finding to make a check pass. Copy it into .claude/skills/.

difflock:explain

A Markdown briefing on one migration: what it touches, the live state of every table involved, every finding, and what the analysis could not see.

Nothing in it is generated. This does not ask a language model whether your migration is safe — that would be the unfalsifiable guessing this package exists to argue against. Difflock supplies the facts; you or your agent supply the judgement. No API key, no network call, no model provider in a package whose whole argument is that it only says what it can check.

Programmatic API

The facade is a convenience, never a requirement. Nothing in the package depends on it, and every contract is injectable:

Supported databases

Driver Schema Row counts Table bytes
MySQL / MariaDB Yes Estimated (information_schema) Yes
PostgreSQL Yes Estimated (pg_class.reltuples) Yes
SQLite Yes Exact (COUNT(*)) No

Row counts come from database metadata, not from scanning tables. Difflock is meant to be safe to point at production; a tool that reads every row to find out how big a table is has become the problem it was installed to prevent.

Where a driver will not answer, Difflock reports unknown and the rules become more cautious, not less. PostgreSQL's reltuples = -1 on a never-analysed table is unknown, not zero.

Limitations

Read this section. It is why the rest of the output can be trusted.

Laravel migrations are arbitrary executable PHP. Difflock reads them statically — it never loads or runs a migration class, because a linter that boots the code it is linting is a linter that can be made to drop your tables. Static analysis cannot resolve everything:

Difflock does not guess at any of these. It reports what it could not read:

Difflock does not reconstruct an expected schema from migrations. For the reasons above that reconstruction cannot be made reliable, and a diff built on a guess is worse than no diff. Drift is measured against a schema that was actually observed and deliberately recorded.

Difflock cannot tell you whether a statement locks. Whether an index build or a column rewrite takes a lock, and for how long, depends on the engine, its version, its configuration and sometimes the row contents. Difflock says an index build reads every row, scales its concern by table size, and stops there. Language like may and depending on the database engine and version is deliberate.

Difflock has no view of your query workload. It cannot tell you whether dropping an index will make anything slower. It tells you the difference between dropping an index and dropping a constraint, which it can know.

SQLite reports less than the others. Laravel's SQLite grammar emits varchar for string('email', 320), so lengths and precisions are genuinely unavailable there, and SQLite records no constraint names. Difflock reports null rather than inventing a value, and the comparison layer treats null as not comparable — so no diff ever claims a length changed on a driver that never knew it.

What Difflock does not claim. Not "100% safe migrations". Not "zero downtime guaranteed". Not "perfect migration analysis". It is a careful second reader with the schema and the row counts in front of it, and it says so where it is guessing.

Architecture

The boundaries are enforced by architecture tests, not just intended:

What semver covers

Treated as public API from 1.0: the contracts, the value objects (DatabaseSchema, Table, Column, Index, ForeignKey, the diff objects, MigrationFinding, MigrationReport), RiskLevel, the facade, the configuration keys, and the --format=json documents. Breaking any of them needs a major version.

Room left for later

The architecture supports, without being built for it today: HTML reports, a difflock:report command, and a separate difflock/filament package for a dashboard. Filament is deliberately not a dependency of this package and will not become one.

Contributing

That runs Rector, Pint, PHPStan at max level, 100% type coverage, and the test suite with a 90% line-coverage floor. See CONTRIBUTING.md.

License

MIT. See LICENSE.md.


All versions of difflock with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
illuminate/console Version ^12.0 || ^13.0
illuminate/contracts Version ^12.0 || ^13.0
illuminate/database Version ^12.0 || ^13.0
illuminate/support Version ^12.0 || ^13.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package heyosseus/difflock contains the following files

Loading the files please wait ...