Boot guards and lints¶
The first time a temporal model is used, the package validates its configuration. That validation has two channels:
- Guards are hard checks. A guard failure means the model is mis-wired in a way that would corrupt data, so it throws
TemporalConfigurationExceptionand stops boot. - Lints are advisory. A lint means "this is probably a mistake, but the package will still work" — it logs a warning and fires an event, and boot continues.
Both run once per model class, then cache. They are controlled by guards.enabled (see Configuration).
Guards¶
Guards enforce the invariants the writer relies on. The shipped set:
| Guard | Rejects |
|---|---|
BootGuardSoftDeletes |
combining Bitemporal with SoftDeletes — use retract() / forceDeleteHistory() instead |
BootGuardRelationType |
a temporalEntityRelation() that isn't BelongsTo or MorphTo (Pivot models are exempt) |
BootGuardNewEloquentBuilder |
a model whose newEloquentBuilder() isn't a BitemporalBuilder |
BootGuardNewCollection |
a model whose newCollection() isn't a BitemporalCollection |
BootGuardDimensions |
a temporalDimensions() that isn't an array of column-name strings |
BootGuardPrimaryKey |
a primary key that collides with a temporal column or a declared dimension |
BootGuardConnection |
a model whose temporalEntityRelation() lives on a different connection than the model — the writer locks and joins both in one transaction |
BootGuardColumnsExist |
a temporal column (at its configured, possibly overridden, name) that is missing from the table — degrades to a pass when the table can't be introspected |
A guard failure is not recoverable at runtime — it always indicates a configuration bug to fix before shipping.
Application guards¶
A handful of guards validate the process-wide wiring rather than one model. They run once per boot (gated by guards.enabled), after the lens-lifecycle listeners are registered, and collect their failures into a single TemporalConfigurationException:
| App guard | Rejects |
|---|---|
AppGuardLockStrategy |
a writes.lock_strategy that isn't one of parent_row, advisory, custom |
AppGuardLockerBinding |
a non-custom strategy with no bound WriteLocker |
AppGuardAsOfLifecycle |
the queue JobProcessing lens-reset listener not being registered |
Lints¶
Lints catch the subtler mistakes that compile and run but quietly do the wrong thing:
| Lint | Warns when |
|---|---|
BootLintCompactionExcludesDomainColumn |
writes.compaction_excluded_columns lists a domain column — compaction would silently merge segments that differ only on that column, erasing real history |
BootLintMutableDatetimeCast |
a temporal column is declared with a mutable datetime/date cast — the trait applies immutable_datetime automatically, and a mutable cast is usually a copy-paste error |
BootLintAdvisoryLockUnavailable |
writes.lock_strategy is advisory but the model's connection (e.g. SQLite) has no advisory locks, so writes silently fall back to parent_row |
Each raised lint is logged at warning level and dispatched as a TemporalBootLintRaised event:
use Vusys\Bitemporal\Events\TemporalBootLintRaised;
Event::listen(function (TemporalBootLintRaised $e) {
// $e->model — class-string of the temporal model
// $e->lint — class-string of the lint that fired
// $e->message — the advisory text
});
Suppressing a lint¶
If a lint is a false positive for a given model — you genuinely do want a domain column excluded from compaction — silence that specific lint with the $suppressedBootLints property. List the lint class strings:
use Vusys\Bitemporal\Boot\Lints\BootLintCompactionExcludesDomainColumn;
class ProductPrice extends Model
{
use Bitemporal;
protected array $suppressedBootLints = [
BootLintCompactionExcludesDomainColumn::class,
];
}
Suppression is per-model and per-lint; everything not listed still runs. There is no equivalent for guards — a guard failure must be fixed, not suppressed.
Warming guards ahead of time¶
Guards and lints run lazily, on first model use. To surface problems at deploy time instead of on the first request that happens to touch each model, warm them explicitly.
From the command line — pass one or more model classes (see bitemporal:warm-guards):
Or programmatically, via the TemporalLens facade, when you want the diagnostics as an object rather than an exit code:
use Vusys\Bitemporal\Facades\TemporalLens;
$report = TemporalLens::warmGuards([ProductPrice::class]);
$report->failedGuards; // Collection: model => failure message
$report->raisedLints; // Collection: model => [lint => message]
$report->summary(); // "0 model(s) failed guards, 0 lint(s) raised."
warmGuards() never throws — it collects everything into a BootDiagnosticsReport for you to inspect. Use warmGuardsOrFail() instead when you want a hard failure on any guard breach (lints still never throw):
That is the call to put in a deploy smoke-test or a health check.
TemporalLens::withoutBootGuards(fn () => …)exists to disable guards and lints inside a closure. It is a testing affordance — there is no production use case for skipping the guards.
Next: Commands.