Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Time Traveller Laravel Package

conceptbyte/time-traveller

ConceptByte Time Traveller lets you freeze, shift, and travel through time in Laravel apps and tests. Mock now(), set custom timestamps, and verify time-dependent logic without waiting. Works with Carbon and PHP's DateTime, integrates with PHPUnit and Pest, and makes testing expirations, schedules,...

View on GitHub
Deep Wiki
Context7

Getting Started

This package predates Laravel 5.5 auto-discovery, so expect manual wiring. Verify the exact namespace against composer.json's PSR-4 block before copying anything below.

composer require conceptbyte/time-traveller
  1. Register the provider in config/app.php under providers:

    ConceptByte\TimeTraveller\TimeTravellerServiceProvider::class,
    

    Add the facade to aliases only if you intend to use it.

  2. Publish and run the migration that creates the revisions table:

    php artisan vendor:publish --provider="ConceptByte\TimeTraveller\TimeTravellerServiceProvider"
    php artisan migrate
    

    Check the published migration for the table name before writing any queries against it — you'll need it for pruning and reporting.

  3. Opt a model in by adding the trait:

    use ConceptByte\TimeTraveller\Traits\TimeTraveller;
    
    class Page extends Model
    {
        use TimeTraveller;
    }
    

First use case: an admin "history" panel for CMS content. Save a Page, change the title twice, then list its revisions and restore the earlier one. Do this on a throwaway model first — the trait hooks into Eloquent's save lifecycle, and you want to see exactly which events produce a revision before you attach it to User.

Implementation Patterns

Be selective about which models get the trait. Versioning is cheap to add and expensive to remove. Attach it to content models (pages, products, settings), not to high-write tables like sessions, counters, or log rows.

Decide between automatic and explicit snapshots. If the trait records on every updated event, a model saved five times in one request produces five revisions. For noisy models, either:

  • Record manually at meaningful checkpoints ($model->saveRevision() — confirm the method name in the trait), or
  • Temporarily disable the behaviour around bulk operations.
DB::transaction(function () use ($page, $data) {
    $page->fill($data)->save();          // creates a revision
    $page->refresh();
});

Reverting is a write, not a rollback. Treat revertTo/travelTo (whichever the version exposes) as "apply the old attributes as a new save" — wrap it in a transaction, and confirm whether the revert itself produces a revision. If it does, you get a clean audit trail; if it doesn't, your history has a hole.

DB::transaction(function () use ($page, $revision) {
    $page->revertTo($revision);
    $page->save();
});

Enrich revisions with who/why. The package stores model state; it won't know the acting user. If the revisions table has spare columns (or you add them in your own migration), write user_id and a short reason at save time via a model observer, so the history panel reads "Alice restored v3 — typo fix" rather than a bare timestamp.

Surface history read-only. Build a GET /pages/{page}/revisions endpoint returning id, created_at, and a diffed attribute set, and a separate POST .../revisions/{revision}/restore for the mutation. Keeping these separate means the restore action is auditable and easy to gate behind a policy.

Pair with soft deletes deliberately. Soft-deleted records still exist, so their revisions remain reachable. Restoring a soft-deleted model from a revision may or may not clear deleted_at — check before promising users an "undelete" feature.

Test the round trip. A factory creates a model, two updates create two revisions, revert returns the original attributes:

$page = factory(Page::class)->create(['title' => 'A']);
$page->update(['title' => 'B']);
$page->update(['title' => 'C']);

$this->assertSame(2, $page->revisions()->count());
$page->revertTo($page->revisions()->oldest('created_at')->first());
$this->assertSame('A', $page->fresh()->title);

Gotchas and Tips

This package is archived and its last release was May 2016 — Laravel 5.x era. It will not work on Laravel 6+ without patching: no support for the newer event dispatcher signatures, HasAttributes internals, or attribute casting expectations. Before writing a single line of integration code, run composer require in a throwaway branch and confirm it boots on your actual framework version. If it doesn't, stop — the alternatives (owen-it/laravel-auditing, spatie/laravel-activitylog, venturecraft/revisionable) are maintained and cover the same audit/restore ground.

No auto-discovery means two silent failure modes: a missing provider (trait methods missing, or the model saves with no revision) and a missing facade alias (Class 'TimeTraveller' not found). Both are config omissions, not bugs.

Model events fire on revert. Restoring triggers saving/saved/updating/updated, so anything listening — search index syncers, slug generators, observers that write their own audit rows — will run again with old data. In Laravel 5.5+ use saveQuietly(); earlier, wrap with Model::withoutEvents(fn () => ...) or Event::fake() in tests.

Snapshots capture one model's attributes, not its relationships. If a Page has blocks or meta in a separate table, restoring the page will not restore them. Model this explicitly: either version the child records too, or store the relation payload in a JSON column on the versioned model.

Sensitive columns land in the revisions table. Passwords, remember tokens, API keys — anything on the model — get copied. Check whether the trait honours $hidden (many don't); if it doesn't, add a $revisionExcluded-style guard or strip fields before saving.

Storage grows without bound. Every save is a full row of JSON/text. Write a scheduled command that prunes revisions older than N days or keeps only the last M per model — and make sure you're not pruning the only copy of a record that was later broken.

Timestamps are the whole API surface for "travel to". Querying by date means timezone consistency matters: whereDate on a UTC-stored created_at with an application in Europe/Berlin will silently return the wrong revision near midnight. Convert the boundary explicitly rather than relying on the default.

Hydrating old attributes bypasses mass assignment. If a revert fails with a MassAssignmentException, the trait is likely using fill(); you'll need forceFill() or setRawAttributes() in a custom revert method. Override the trait method in your model — that's the cleanest extension point, alongside swapping in your own revision model via config if the package supports it.

Vendor it if you must ship it. Given the archive status, if the package is load-bearing, copy the trait into app/ and own it. A 34-star, eight-year-old dependency with no security releases is a liability in composer.json, and the actual logic — snapshot attributes, write a row, restore them — is a few dozen lines you can maintain yourself.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle