The more you work with developers, the more you start to realize that version control, that is, keeping different versions of a codebase intact, is one of the most important things to developers. Keeping different versions of our code lets us quickly and efficiently recover from failure and track our successes. That made us wonder, why not apply that concept to content itself? Enter: Laravel Checkpoint. Checkpoint is the latest member of the Plank open-source family. It allows developers and by extension the users of their applications, to effortlessly track revisions to the content made on their site and move between those revisions.

A Content time machine

So what does that mean, exactly? The package adds 2 things to an application: “Revisions” and “Checkpoints”. A Revision, simply put, is an entry in a ledger (or rather a database table) that relates a specific change in the content to a particular date and time. Think of these “Revisions” as a changelog. Tracking all those changes alone is enough to allow you to view content at different points in time. The problem is that you may want to, instead of browsing by date, capture a collection of changes under one label, or as we call it a “Checkpoint”. 

These Checkpoints act as a way to browse the application’s content as it was at interesting points in time, simply by telling the application the name of the checkpoint you’d like to browse by. This effectively turns back the clock for that specific user and allows them to see what the state of the content was at that time. 

If you browse an application at a specific checkpoint, and browse to a specific piece of content that hasn’t had a revision, then it will show the latest version of that file since that checkpoint. For example, say you chose to look at the content as of Checkpoint 3, you would get the versions of the content as shown in the graph below.

How does it work?

When you get down to the details, we’ve taken an approach to this that is similar to how our other 2 packages, Mediable, and Metable solve their respective problems. 

Checkpoint creates 2 tables: Revisions and Checkpoints. Revisions act as a polymorphic pivot between any given class, and a Checkpoint. A Checkpoint simply acts as a moment in time or “release”. That way you can take any item and mark it as being part of a specific release. The real magic comes from the observers we define that fires during the save of a Revisionable class, or in other words, models that use the hasRevisions trait.

The hasRevisions Trait

By applying the hasRevisions trait to a specific class you enable that class to be revisioned. What happens is instead of saving the content outright, we copy the original instance of that model, and apply the changes to the copy. After that, we also create a revision for that new copy, which is stored in the revisions table. 

The observers mentioned above hook into the methods added by the trait. When a Revisionable item is saved, we call either $model->updateOrCreateRevision() or $model->saveAsRevision(). It’s worth noting, given the above-stated fact, that you almost never need to interact with any of the functions added via the trait. It does its best to get out of the way so development can proceed as normal. There are some functions added by the trait that can be overridden in your class. This will allow you to control under what circumstances you’d like a revision to be created. For example, the shouldrevision function can look something like:

  public function shouldRevision(): bool
        // Only create a revision when a model is published. 
        return $this->status == "published";

The RevisionScope

To solve the problem of “duplicate” content, we introduced a set of global scopes that handle what we call internally “temporal windowing” or just “windowing”. What this means is that given a chosen time frame, the global scopes offer a view of a  Revisionable class’ data that ensures that only 1 copy of any given item is viewable. That is to say, we find the latest revision of an item at a chosen timestamp while omitting all other copies. 

To achieve this we maintain a shared key between all revisions of a given instance and group by that key after we search for all the items before the chosen date. This windowing is applied via a set of Global scopes that are added to any query performed on a Revisionable class.

The storesRevisionMeta Trait

To handle any data that you might not want to store on the original table, we’ve also added a “meta” JSON column. This trait is used by the hasRevisions trait, so no need to use it directly! When saving a new revision, the moveMetaToRevision function is called which collects any fields marked as metadata. It then serializes those fields to a JSON object and saves said object as a part of the revision pivot, while clearing it from the previous copy of the model instance. 

The best part? When you pull a previous model instance and ask for data from the now empty column, the trait dynamically checks if the column you’re looking for is instead stored on the revision rather than the model. If so, it goes ahead and pulls the data from there.

All in all, Checkpoint was a super fun package to put together, and we hope that you get as much good use out of it as we have. We did our best to keep fellow developers in mind and produce something that is extensible and customizable.

Enjoy our Laravel package? We would love your input! We encourage anyone to report any issues or feature requests on the project GitHub page. Pull requests are also welcome.