laravel-bitemporal maintained by vusys
Laravel Bitemporal Models
Safe effective-dated and bitemporal relations for Laravel.
vusys/laravel-bitemporal provides first-class support for effective-dated and bitemporal Eloquent data — modelling facts that change over time, where the application needs to answer:
- What was true on this business date?
- What did the system believe was true on this business date at the time?
- When did this value become effective, and when was a correction recorded?
The value is not a handful of query scopes. It is a custom temporal relation with safe write operations, range splitting, correction handling, point-in-time eager loading, and overlap prevention — so you can correct the past without destroying the previous state of knowledge or creating overlapping historical facts.
$product->prices()->correct(attributes: ['amount' => 12.00], validFrom: '2026-02-01');
Installation
composer require vusys/laravel-bitemporal
PHP 8.4+, Laravel 11 / 12 / 13, and PostgreSQL, MySQL/MariaDB, or SQLite. The service provider is auto-discovered. See Installation for the optional config publish.
Quick example
A product has many price versions. The entity model exposes the timeline; the temporal model carries the period columns.
use Vusys\Bitemporal\Concerns\HasBitemporalRelations;
use Vusys\Bitemporal\Relations\BitemporalMany;
class Product extends Model
{
use HasBitemporalRelations;
public function prices(): BitemporalMany
{
return $this->bitemporalMany(ProductPrice::class);
}
}
use Vusys\Bitemporal\Bitemporal;
class ProductPrice extends Model
{
use Bitemporal;
protected string $temporalEntity = Product::class;
}
Period-column casts are applied automatically — you don't declare them. The migration uses the package's Blueprint macros:
Schema::create('product_prices', function (Blueprint $table) {
$table->id();
$table->bitemporalForeignFor(Product::class);
$table->decimal('amount', 10, 2);
$table->bitemporalPeriods();
$table->timestamps();
$table->preventBitemporalOverlaps(['product_id']);
});
Read at a point in time:
$price = $product->prices()
->validAt($invoice->issued_at) // true on this business date
->knownAt($invoice->created_at) // as we believed it then
->sole();
Write safely:
$product->prices()->changeEffectiveFrom(['amount' => 12.00], validFrom: '2026-06-01');
$product->prices()->correct(['amount' => 12.00], validFrom: '2026-02-01', validTo: '2026-03-01');
$product->prices()->retract(validFrom: '2026-02-01', validTo: '2026-03-01');
Documentation
📚 Full documentation: vusys.github.io/laravel-bitemporal
Also readable directly in docs/:
- Concepts · Installation · Defining models
- Reading · Writing · Dimensions
- As-of lens · Events · Configuration · Testing
- Temporal pivots · Diffs and timelines · Boot guards and lints · Commands · Exception catalogue
Requirements
- PHP 8.4+
- Laravel 11 / 12 / 13
- PostgreSQL, MySQL/MariaDB, or SQLite
Status
The package is built from a detailed internal specification and its feature set is complete and green across SQLite, MySQL 8.4, MariaDB, and PostgreSQL 16. The read side, the core write side (change / correct / retract / end / supersede / hard-delete), dimensions, the as-of lens, polymorphic entities, backfill, optimistic concurrency, lock strategies, and the migration macros are implemented — along with temporal pivots (BitemporalBelongsToMany), idempotency keys, the first-party audit-log subscriber, diff and timeline helpers, the testing helpers and factories, boot guards and advisory lints, and the full generator/command set (make:bitemporal-*, bitemporal:audit-*, bitemporal:diff-timelines, bitemporal:warm-guards, bitemporal:prune-idempotency-keys).
Enhancements and further hardening are tracked in the issue tracker under the roadmap label.
Development
composer install
composer ci # phpstan (L9, no baseline) + pint + rector + phpunit
composer test
composer infection # mutation testing
License
MIT. See LICENSE.