Download the PHP package mueller-schmitz/laravel-model-integrity without Composer

On this page you can find all versions of the php package mueller-schmitz/laravel-model-integrity. 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 laravel-model-integrity

Laravel Model Integrity

Tests Static analysis Latest version

Immutable and versioned Eloquent models with a gapless, cryptographically verifiable history.

Status: before 1.0 – the API may still change in minor versions.

Scope

This package is tamper-evident, not tamper-proof:

Performance cost

All writes are appended to a single global chain. The chain head is locked with SELECT ... FOR UPDATE, which deliberately serialises recording writes across the whole application. This is the price for a gapless global sequence.

Each recorded write adds two locking reads (global head and model head), a read of the stored row, an insert and two updates. In the test suite, 8 parallel processes record roughly 250–450 versions per second on MySQL, MariaDB and PostgreSQL (GitHub Actions runners, database in a container). Measure with your own workload before using it on write-heavy tables.

The head locks are held until the surrounding transaction commits. Keep transactions that record versions short, and expect deadlocks when several transactions record versions of several models in different orders; the database aborts one of them, and the caller has to retry.

Requirements

Installation

model-integrity:install publishes the config and the migrations that are not published yet; running it again does not duplicate them. Prefer it over vendor:publish --tag=model-integrity-migrations, which copies all migrations again under new timestamps.

Upgrading from 0.3

Existing versions are unchanged. Personal attributes are encrypted from the first version recorded after you declare them; versions recorded before keep them in plain text.

Upgrading from 0.2

Run the printed statements as an administrative user, then configure an anchor disk and schedule model-integrity:anchor (see Anchors). The first anchor covers all existing versions; set MODEL_INTEGRITY_ANCHORS_SINCE to the upgrade date so that the time checks of attested proofs count those versions from then.

Upgrading from 0.1

Run the printed statements as an administrative user: with --all-tables the application user has no privileges on the new integrity_files table until then, and storing files fails.

Existing versions stay valid; the hash format is unchanged. A config file published with 0.1 keeps working: new keys (files, actor, tables.files) fall back to their defaults.

Database enforcement

Model events are not the only way to change data. Queries like Invoice::where(...)->update(...) or DB::table(...) bypass them, and so does anyone with direct database access. Two database-level measures make recorded versions append-only:

Triggers

The migrations install triggers that reject UPDATE and DELETE on integrity_versions, integrity_files, integrity_anchors and integrity_anchor_proofs (MySQL, MariaDB, PostgreSQL, SQLite). On PostgreSQL they reject TRUNCATE as well; on MySQL and MariaDB TRUNCATE fires no triggers and is prevented by privileges.

Privileges

The application's database user should only read and append versions, files and anchors, and read, add and update chain heads and subject keys. Print the matching SQL for your database:

The command only prints the statements; review and run them with an administrative user.

Triggers and privileges are tested against MySQL 8.0/8.4, MariaDB 10.11/11.4 and PostgreSQL 14/17: a user with exactly these privileges records, deletes and verifies through the package and is denied UPDATE and DELETE on versions.

Usage

Add the HasIntegrity trait to a model:

All properties are optional; defaults come from config/model-integrity.php.

What gets recorded

Action Version event
create() created
update() / save() with changes to recorded attributes updated
delete() with $integrityDeletes = 'record' deleted
restore() (soft deletes) restored
forceDelete() (soft deletes) force_deleted
recordRelation('tags') relation_synced

Reason, context and actor

Reason and context apply to the next save() or delete() only.

The actor is the authenticated user. Where nobody is authenticated, for example in queue jobs or console commands, set it explicitly:

The actor is reset after every queue job. By default only the default guard is asked; set model-integrity.actor.guards (e.g. ['web', 'admin']) to ask several guards in order.

Files

Files are stored content-addressed: under their SHA-256 hash, once per content, never overwritten and never deleted. Every stored file is recorded as a version in the global chain.

store() accepts uploaded files, file objects, local paths and streams; seekable streams are read from their start. Assigning the attribute stores nothing by itself: store the file first and assign the result (or its hash).

Personal data and crypto-shredding

An append-only history conflicts with the right to erasure (GDPR art. 17). Declare the personal attributes of a model; they are recorded encrypted with a key per data subject, and shredding that key makes them unreadable in every version, while hashes, chains and anchors stay valid because they cover the ciphertext.

Relations

sync(), attach() and detach() fire no model events. Declare the relation in $integrityRelations and record the change in the same transaction. The snapshot lists the related keys as the relation query returns them, so global scopes of the related model apply: soft-deleting a related model removes its key from the current state, and the verification reports drift until the relation is recorded again.

On MySQL and MariaDB (REPEATABLE READ), the first non-locking read of a transaction fixes what the transaction sees, whichever table it reads. Load the model outside the transaction or with lockForUpdate(), and do not read other data first: otherwise the snapshot may miss changes other processes committed in the meantime, which the state drift check would later report.

History

After schema changes

Every version stores a full snapshot. After a migration adds or removes a column, after a cast, $integrityExcept or $integrityRelations changes, the current rows no longer match their last snapshots and the verification reports StateDrift for every model. Record a new baseline:

The command records a snapshot version for every model whose last snapshot has an older $integritySchemaVersion or no longer matches the row, and a created version for rows that have none. Bump $integritySchemaVersion with the change so the history shows when the structure changed. For a single model: $invoice->recordIntegritySnapshot('schema_migrated', 'Added reference column').

Datetime columns without a timezone are interpreted in the application timezone when they are read. Do not change app.timezone after the first version was recorded, or every stored timestamp would drift. Plain date casts are stored as Y-m-d and are not affected.

Decide on the morph map before the first version is recorded: versions store the morph class, and a renamed class or alias leaves the old history under the old name (reported as Unverifiable).

Limits

The package records what goes through Eloquent model events. These bypass it and are not recorded:

Such changes are not prevented, but they are detected: the state drift check compares the current row with the last snapshot.

Updating a model whose row was deleted concurrently throws instead of recording a version for a missing row; lock rows that may be deleted (lockForUpdate()) before updating them.

Model and integrity tables must use the same database connection, otherwise both cannot be written in one transaction.

Verification

The same is available on the model: $invoice->history(), $invoice->verifyIntegrity(), $invoice->versionAt($date). A method with the same name defined on the model takes precedence over the trait.

Date strings passed to versionAt() are read in the application timezone.

Command and scheduling

The command prints the violations and exits with code 1 if there are any, so it can fail a CI job or alert from the scheduler:

What is detected

Error type Meaning
HashMismatch A version's content does not match its hash, or its hash format is unknown
BrokenChain A version is not referenced by its successor (per model or globally)
VersionGap Version numbers of a model are not consecutive
SequenceGap The global sequence has a gap: versions were removed
TruncatedChain A head (global or per model) does not match the last version: the end was cut off, the head is behind the last version, or it was removed; or an anchor attests versions beyond the end of the chain
StateDrift The current row differs from the last snapshot, was deleted or restored outside the application, or was never recorded
Unverifiable Versions exist whose model class is missing, does not use the trait, or was recorded under a former morph class
FileMismatch A file referenced by any version is unknown, missing on its disk or has another size; with checkFiles()/--files also a changed content
AnchorMismatch An anchor does not match the versions it attests, its proof, the previous anchor or the anchors head

Every model check verifies that the files referenced by a model's versions exist with their recorded size. Hashing the content reads every file, so it only runs with checkFiles() or verify --files, for example weekly in the scheduler.

When a version is replaced and re-hashed, the successor no longer references it, so the replaced version is reported as BrokenChain. Violations dispatch an IntegrityViolationDetected event with the result (and the model for checkModel()). lastValidVersion() is only set by checkModel().

Checks run in a read transaction with REPEATABLE READ, so versions recorded while a check runs do not produce false findings. Inside a caller's own transaction the caller's isolation level applies.

Limits

Anchors

The chain itself has no secret: an attacker with write access to the database can rewrite it consistently, recomputing every hash and head. An anchor prevents this from going unnoticed. It attests the versions recorded since the previous anchor in a place the database cannot change.

Each run covers the global sequence from the end of the previous anchor to the current head, so every version is anchored exactly once. Without new versions nothing is anchored. Parallel runs are serialized by a lock on the anchors head row; recording writes are not blocked. If the chain has a gap, the run fails and nothing is kept.

Anchors are linked: each one contains the digest of the previous one. If a driver fails in one run, its next anchor attests the earlier ones through this link. The command keeps an anchor as long as one driver succeeded and exits with code 1 if any driver failed.

Disk driver

The disk driver writes every statement as a file named {to_sequence}-{digest}.json; the SHA-256 hash of the file is the digest. The anchor only helps if whoever can change the database cannot change this disk: use storage on another system with its own credentials, ideally write-once (for example an S3 bucket with object lock in compliance mode). The default local disk is only suitable for trying it out.

Verification lists the files on the disk and checks each statement against the versions it covers, whether or not the database still contains that anchor. Deleting the anchor rows together with a rewritten chain is therefore detected as well. A statement left over by a failed run that matches the versions is not reported. Listing reads every statement file on each verification; on object storage that is one request per anchor.

OpenTimestamps driver

OpenTimestamps anchors the digest in Bitcoin through public calendar servers – free, without an account. Only the 32-byte digest leaves your server. A run submits it to every configured calendar and keeps the answers as one standard .ots proof; it succeeds if at least min_calendars answered.

The proof is pending at first. Within a few hours the calendars commit to a Bitcoin block; model-integrity:anchor-upgrade then fetches the completed proof and stores it as a new proof row, keeping the previous one. Only the configured calendars are asked, never a URL taken from a stored proof.

Verification evaluates the proof and checks each Bitcoin attestation against the block header from an Esplora API (esplora_url: blockstream.info by default, mempool.space, or your own electrs/mempool instance to rely on no third party). Without that check, anyone who can write the proof could invent an attestation. If the block source cannot be reached, the proof is reported as Unverifiable, never accepted unchecked.

What the time proves

A Bitcoin attestation proves that the statement existed when the block was mined. A chain rewritten later can only get fresh proofs, so verification also requires:

The time of the anchor row itself is not covered by any hash and is not relied on. Whoever rewrites old history gets only fresh proofs, which are too late for the recorded times – unless every rewritten version's created_at is moved to the time of the forgery as well. The rewritten history then claims that everything was recorded recently, which other records (documents, emails, backups) contradict; the package cannot detect that on its own. OpenTimestamps does not protect versions that are newer than the last confirmed anchor.

Versions recorded before anchoring was enabled cannot have been attested in time. Set anchors.since to the date you enabled anchoring (for example when upgrading an application that already has versions): such versions then count from that date, so their first anchor must be attested within max_delay_hours after it. A scheduler outage longer than max_delay_hours is reported, because the versions recorded during it were not attested in time.

Checking without this package

model-integrity:anchor-export writes the statement of an anchor and its proof files. The statement file's SHA-256 hash is the anchored digest, so the standard ots client verifies it against your own Bitcoin node. Recompute the Merkle root from the version hashes (see Anchor format) to tie the statement to the versions.

RFC 3161 driver

A time-stamp authority (TSA) signs that the statement's digest existed at a time. freetsa.org is free; a qualified trust service provider under eIDAS gives the time-stamp legal weight in the EU. The time-stamp is there at once, no upgrade needed. The driver needs the PHP extension openssl.

The proof is the TSA's complete response. Verification checks the signature over the time-stamp, that it has exactly one signer, that it is for the statement's digest (and, if policy is set, issued under that policy, which is also requested), and that the TSA certificate is meant for time stamping only (critical extended key usage) and leads to a certificate in ca_file, possibly through CA certificates in intermediates_file – all valid at the time of the time-stamp, so proofs stay verifiable after the TSA certificate expired. Revocation (CRL, OCSP) and the ESS signing-certificate attribute are not checked; the signer is the certificate the signature names. The time of the time-stamp is subject to the same checks as Bitcoin attestations (see What the time proves). A missing CA file or extension is reported as Unverifiable.

Add -untrusted <intermediates> if the TSA certificate is issued by an intermediate CA. openssl checks the chain at the current time; once the TSA certificate has expired, add -attime <time of the time-stamp as Unix time>.

Restoring a backup

Restoring the database to an earlier state is, from the anchors' point of view, a cut-off chain: statements on the anchor disk attest versions the database no longer has (TruncatedChain), and once new versions reuse those sequence numbers, AnchorMismatch. This is intended – the anchors show that history was lost. To continue with a passing verification:

  1. Keep the existing statement files as evidence of what was lost (with object lock they cannot be removed anyway) and document the restore.
  2. Point anchors.disk.path to a new, empty directory on the same disk. The anchors in the restored database still reference their files under the old path, so their proofs keep verifying.
  3. Run model-integrity:anchor.

Isolation on PostgreSQL

Anchor runs wait for each other on the anchors head row. With the default READ COMMITTED isolation the waiting run then continues; if the connection is configured for REPEATABLE READ or SERIALIZABLE, PostgreSQL aborts it with a serialization error instead. Use withoutOverlapping() in the scheduler, or keep the default isolation for the anchor command.

Custom drivers

A driver implements MuellerSchmitz\ModelIntegrity\Anchoring\Contracts\Anchor (submit() returns the proof to keep, verify() checks it and may return the time of the attestation) and optionally ListsStatements, UpgradesProofs and ExportsProofs. Register it in a service provider and add its name to anchors.drivers:

Anchor format

An anchor statement is the canonical JSON (see Canonical JSON) of exactly these fields; its digest is the lowercase hex SHA-256 of that string. Format 1 never changes.

Field Content
anchor_format 1
from_sequence, to_sequence the range of the global sequence, inclusive
merkle_root Merkle root of the version hashes of the range, in sequence order
prev_digest digest of the previous anchor, null for the first

The Merkle root follows RFC 6962, section 2.1, over the 32-byte version hashes: a leaf is sha256(0x00 || hash), a node sha256(0x01 || left || right), and a list of n > 1 leaves is split after the largest power of two smaller than n. Leaves are never duplicated.

Auditor export

The export is a directory an auditor can check without this package – for example for the data access of the German tax authorities (GoBD, Z3):

File Content
index.xml Description of the CSV tables in the GDPdU description standard, read by audit software such as IDEA
versions.csv Every version: chain fields, the snapshot as hashed and, unless --no-reveal, the snapshot with personal data decrypted (null where the key was shredded)
versions.jsonl The exact envelope of every version, to recompute its hash
anchors.csv, anchor_proofs.csv, proofs/ Anchors, their statements and proof files (.ots, .tsr)
inclusion_proofs.csv A Merkle inclusion proof (RFC 6962) of every exported version in its anchor
files.csv Metadata of the stored files (the files themselves are not exported)
report.json, report.html The verification result (checkAll(), with --files also the file contents), the global sequence the export covers and data that could not be exported as stored
SPEC.md The hash, anchor and inclusion proof formats, with a script to recompute every hash
SHA256SUMS Checksums of all files (sha256sum -c SHA256SUMS)

A period (--from, --to: a date covers the whole day in UTC, a date with time and offset is taken as given) or a model (--model) exports only those versions; their anchors are exported in full, and the inclusion proofs make each exported version checkable without the others. Versions newer than the last anchor have no inclusion proof.

Everything is read in one consistent view, up to the global head at the start of the export (named in the report), so versions recorded meanwhile are not half included. Tampered rows do not stop the export: they are exported as stored and listed in the report. The directory must not exist or be empty; the export is written next to it and moved into place when complete, so a failed export leaves nothing behind. The command exits with 1 if the verification found violations or data could not be exported as stored – the export is written anyway – and with 2 if it cannot be written.

The export contains personal data in plain text unless --no-reveal: it is created readable for its owner only (0700/0600). Hand it over on a protected medium and delete it when the audit is done. Set model-integrity.export.supplier (name and location of the company) for the index.xml.

GDPdU DTD. The standard requires gdpdu-01-03-2019.dtd next to index.xml. It is published by CaseWare (formerly Audicon) without a license notice, so it is not part of this package. Download it from caseware.com/de/beschreibungsstandard and set MODEL_INTEGRITY_GDPDU_DTD to its path, or let the command fetch it with --fetch-dtd (needs ext-zip). Only the unchanged published file is accepted (checked by its SHA-256). --without-dtd writes the export without it.

GDPdU limits. The CSV files are UTF-8 with ;, CRLF and a header line. The standard does not define line breaks in fields, quotes inside text or empty values: line breaks are replaced by a space, quotes are doubled, and empty fields mean no value. Times are exported as text marked as time (standard 1.6), timestamps additionally as ISO 8601 with microseconds. index.xml is validated against the DTD in CI; the import into IDEA itself has not been tested.

Procedure documentation. php artisan vendor:publish --tag=model-integrity-docs publishes a German template of the procedure documentation (Verfahrensdokumentation) for the part of your procedure this package covers.

Hash format

Every version stores the hash format it was created with (hash_format). A released format never changes; new rules always get a new format number. This section specifies format 1 so that hashes can be recomputed independently of this package, for example by an auditor.

Envelope

The hash is the lowercase hex SHA-256 of the canonical JSON encoding of this envelope, built from the stored version row:

Field Value
format 1 (integer)
sequence global sequence number (integer)
versionable_type morph class of the model (string)
versionable_id model key (string)
version version per model, starting at 1 (integer)
event created, updated, deleted, restored, relation_synced or a custom event (string)
schema_version snapshot schema version (integer)
snapshot full model state (object)
prev_hash hash of the previous version of the same model, null for version 1
global_prev_hash hash of the previous entry of the global chain, null for sequence 1
actor_type, actor_id who made the change (string or null)
reason reason for the change (string or null)
context additional context (object or null)
created_at UTC timestamp, e.g. 2026-09-28T10:05:00.123456Z (string)

No other fields are allowed. The database id is not part of the hash.

Canonical JSON

Recomputing a hash

These rules match the defaults of common JSON libraries. The stored snapshot and context columns must be canonicalized first (MySQL's JSON type reorders keys on storage); the package's Version::toEnvelope() does that. With Python, for an envelope stored in envelope.json:

Reference envelopes with their canonical strings and hashes are part of the test suite in the repository (tests/Fixtures/hash-format-1.json).

Versioning

The package follows Semantic Versioning. Before 1.0, minor versions may contain breaking changes; they are listed in the changelog.

Hash formats are independent of package versions: a released hash format never changes, so versions recorded with any release stay verifiable with every later release.

Testing

Run the suite against another database with the usual DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME and DB_PASSWORD environment variables. The trigger, privilege and concurrency tests only run on MySQL, MariaDB and PostgreSQL; the privilege tests create and drop the database user mi_restricted and need an administrative account. The suite is not meant for pest --parallel: the concurrency workers and the privilege tests share one database and one user.

License

MIT. See LICENSE.md.


All versions of laravel-model-integrity with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
ext-fileinfo Version *
illuminate/console Version ^12.0 || ^13.0
illuminate/contracts Version ^12.0 || ^13.0
illuminate/database Version ^12.0 || ^13.0
illuminate/filesystem Version ^12.0 || ^13.0
illuminate/http Version ^12.0 || ^13.0
illuminate/queue Version ^12.0 || ^13.0
illuminate/support Version ^12.0 || ^13.0
nesbot/carbon Version ^3.8
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 mueller-schmitz/laravel-model-integrity contains the following files

Loading the files please wait ...