hoangphamdev / laravel-bitemporal
Laravel package for bitemporal Eloquent models
Package info
github.com/hoangpham5694/laravel-bitemporal
pkg:composer/hoangphamdev/laravel-bitemporal
Requires
- php: ^8.0
- illuminate/database: ^9.0
- illuminate/support: ^9.0
Requires (Dev)
- orchestra/testbench: ^7.0
- phpunit/phpunit: ^9.5
- squizlabs/php_codesniffer: *
This package is auto-updated.
Last update: 2026-08-05 03:02:56 UTC
README
Laravel package for working with bitemporal records in Eloquent models.
The package provides:
- a
bitemporal()migration macro - a reusable
HasBitemporaltrait - a custom Eloquent builder for current and historical queries
- helper methods for versioned updates and deletes
Requirements
- PHP 8.0+
- Laravel 9+
Installation
composer require hoangphamdev/laravel-bitemporal
If you are developing locally with a path repository, make sure the package is registered in your application's composer.json.
The package uses Laravel package discovery, so the service provider is loaded automatically.
Database Columns
Use the bitemporal() macro in your migrations:
use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->timestamps(); $table->bitemporal(); });
This macro adds these columns:
record_uuidoperated_atvalid_fromvalid_totransaction_fromtransaction_to
Defaults created by the migration macro:
record_uuidis nullableoperated_atuses the current timestampvalid_fromuses the current timestampvalid_todefaults to9999-12-31 23:59:59transaction_fromuses the current timestamptransaction_todefaults to9999-12-31 23:59:59
Column Purpose
record_uuidgroups all versions of the same logical recordoperated_atstores when the change was madevalid_fromandvalid_todefine business validitytransaction_fromandtransaction_todefine system/transaction validity
Rollback helper:
Schema::table('posts', function (Blueprint $table) { $table->dropBitemporal(); });
Model Usage
Add the trait to any Eloquent model:
use Illuminate\Database\Eloquent\Model; use HoangPhamDev\Bitemporal\Traits\HasBitemporal; class Post extends Model { use HasBitemporal; }
What the Trait Adds
- a global scope that returns only the current snapshot
- helper scopes for current,
asOf(), and unscoped access - a custom Eloquent builder with bitemporal-aware query helpers
bitemporalDelete($validAt = null)for versioned deletesbitemporalUpdate(array $data = [], $validAt = null)for versioned updates- automatic
record_uuidgeneration on create when the field is empty - automatic
operated_atinitialization on create when the field is empty
Static Helpers
The trait also exposes:
Post::getBitemporalColumns(); Post::getInfinityDatetime();
Querying
Current Snapshot
By default, models using HasBitemporal are automatically scoped to the current snapshot.
$posts = Post::all();
You can also call the explicit scope:
$posts = Post::current()->get();
The custom builder also exposes:
$posts = Post::query()->current()->get();
Query As Of a Specific Time
$posts = Post::asOf('2025-01-01 00:00:00')->get();
asOf() filters by valid time and still requires transaction_to to be the infinity datetime.
If a model is retrieved with asOf(), that valid time is stored on the instance and later used by bitemporalDelete() when no explicit valid time is provided.
Access Historical Rows
The custom builder exposes history() as a convenience clone of the current builder state, useful when you want to branch a query without mutating the original builder.
$query = Post::withoutBitemporal()->history();
Disable Bitemporal Filtering
To query all rows without the automatic current-snapshot scope:
$posts = Post::withoutBitemporal()->get();
Or directly:
$posts = Post::withoutGlobalScope('bitemporal_current')->get();
Find Helpers
The custom builder keeps find-style methods bitemporal-aware:
Post::find(1); Post::findMany([1, 2]); Post::findOrFail(1); Post::findOrNew(1); Post::findOr(1, fn () => null); Post::findSole(1);
touch() is also wrapped to run on the matched models inside a transaction.
Versioned Update
bitemporalUpdate() closes the current version and creates a new version with updated data.
$post = Post::find(1); $post->bitemporalUpdate([ 'title' => 'Published title', ]);
You can pass an explicit valid time:
$post->bitemporalUpdate([ 'title' => 'Published title', ], '2025-01-08 00:00:00');
Behavior:
- the current version is closed by setting
transaction_to - matching rows for the same
record_uuidand later valid windows are also closed - a new cloned record is inserted
- the new record gets:
record_uuidcopied from the originaloperated_at = now()valid_from = validAtvalid_to = infinity datetimetransaction_from = now()transaction_to = infinity datetime
If the model does not exist or does not have a record_uuid, the method returns false.
Versioned Delete
bitemporalDelete($validAt = null) versions the record instead of physically removing it.
$post = Post::find(1); $post->bitemporalDelete();
You can also pass a valid time:
$post->bitemporalDelete('2025-01-08 00:00:00');
Behavior:
- the trait resolves the record's
record_uuid - the current version is closed by setting
transaction_to - a cloned record is created with a truncated
valid_to - the clone gets:
valid_to = validAttransaction_from = now()transaction_to = infinity datetime
If the model was loaded with asOf(), the stored valid time is used automatically when you call bitemporalDelete() without arguments.
If the model does not exist, bitemporalDelete() returns false.
Physical Delete
If you want to physically remove rows without bitemporal versioning, use normal Eloquent delete methods:
$post = Post::find(1); $post->delete();
Bulk deletes also remain physical deletes:
Post::withoutBitemporal()->whereIn('id', [1, 2])->delete(); Post::destroy([1, 2]); Post::withoutBitemporal()->whereIn('id', [1, 2])->toBase()->delete();
These bypass the versioning logic entirely.
Infinity Datetime Constant
Use the shared constant when you need the package-wide infinity value:
use HoangPhamDev\Bitemporal\Support\BitemporalDefaults; $value = BitemporalDefaults::INFINITY_DATETIME;
Example Model
use HoangPhamDev\Bitemporal\Traits\HasBitemporal; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasBitemporal; protected $fillable = [ 'title', 'valid_from', 'valid_to', 'transaction_from', 'transaction_to', ]; protected $casts = [ 'operated_at' => 'datetime', 'valid_from' => 'datetime', 'valid_to' => 'datetime', 'transaction_from' => 'datetime', 'transaction_to' => 'datetime', ]; }
Testing
Run the package test suite:
vendor/bin/phpunit -c phpunit.xml.dist
License
MIT