plank / laravel-checkpoint
A package for keeping a history of your models' revisions and accessing your data as it was at an older date.
Installs: 9 577
Dependents: 0
Suggesters: 0
Security: 0
Stars: 8
Watchers: 12
Forks: 2
Open Issues: 5
Requires
- php: ^7.1.3|^8.0
- ext-json: *
- illuminate/support: 5.8.*|^6.0|^7.0|^8.0|^9.0
Requires (Dev)
- doctrine/dbal: ^2.6|^3.3
- laravel/legacy-factories: ^1.0.4
- orchestra/testbench: ^4.8 || ^5.2 || ^6.0 || ^7.2
- phpunit/phpunit: ^9.5
- dev-master
- v2.1.0
- v2.0.1-alpha
- 2.0.0
- v2.0.0-alpha
- v1.1.0
- v1.0.0
- v0.2-beta
- v0.0.11-alpha
- v0.0.10-alpha
- v0.0.9-alpha
- v0.0.8-alpha
- v0.0.7-alpha
- v0.0.6-alpha
- v0.0.5-alpha
- v0.0.4-alpha
- v0.0.3-alpha
- v0.0.2-alpha
- v0.0.1-alpha
- dev-add-support-for-laravel-9
- dev-move-local-scopes-to-custom-query-builders
- dev-checkpoint-store-as-singleton
- dev-fix-newest-id-attribute
- dev-models-from-container
- dev-add-timelines
- dev-fix-newest-scopes
- dev-general-revisioning-improvements
- dev-checkpoint-query-improvements
- dev-update-readme
- dev-pivot-trait-feature
This package is auto-updated.
Last update: 2024-10-22 03:40:44 UTC
README
Table of Contents
- Laravel Checkpoint
Why Use This Package
Do you need to store the state of how your models change over time? Do you need a way to query and view the state of your models at different points in time? If the answer is yes, then this package is for you!
Installation
You can install the package via composer:
composer require plank/laravel-checkpoint
Concepts
Timelines
A Timeline
is a way to have completely separate views of your content. A Timeline
allows you to filter the Revision
s of your models based on the Timeline
it belongs to.
Table: timelines
Checkpoints
A Checkpoint
is a point in time which is of interest. A Checkpoint
allows you to filter the Revision
s
of your models based on the Checkpoint
's checkpoint_date
.
Table: checkpoints
Revisions
A Revision
references a record of a Model
in a particular state at a particular point in time. When this
package is enabled, and you use the HasRevisions
trait on a Model, the concept of an instance of a Model in
Laravel changes. Since we want to store Revision
s of a Model, and have them searchable in their different
states, the notion that an Entity (instance of a Model) is associated with exactly one id, is no longer correct. Each
Revision
of a Model has its own unique id in the table, even though it represents the same Entity.
The same entity is linked via the original_revisionable_id
field.
Table: revisions
Usage
Revisioning Models
To have a model be revisioned, all you need to do is have it use the HasRevisions
trait.
What gets Revisioned?
This package handles revisioning by creating a new row for a Model in the database every time it changes state in a
meaningful way. When a new Revision
is created, the package will also recursively duplicate all Models related
via child relationships, and will create new many-to-many relationships in pivot tables.
Start Revisioning Command
If you have an existing project with Models already populated in the database, the php artisan checkpoint:start
command will begin revisioning all of the Models which are using the HasRevsions
trait.
Query Scopes
The way this package achieves it's goal is by adding scopes (and one global scope) to query models that have revisions.
Active Checkpoint
By setting the active checkpoint Checkpoint::setActive($checkpoint)
, all queries for revisioned models will be
scoped to that $checkpoint
. Also, when there is an active checkpoint set, any new revisions that get created will be associated with that $checkpoint
.
at($moment)
/** * @param $moment Checkpoint|Carbon|string */ at($moment = null)
This is the default global query scope added to all queries on a Model with Revision
s.
This query scope will limit the query to return the Model whose Revision
has the max primary key, where
the Revision
was created at or before the given moment.
The moment can either be an instance of a Checkpoint
using its checkpoint_date
field, a string representation of a date or a Carbon
instance.
since($moment)
/** * @param $moment Checkpoint|Carbon|string */ since($moment = null)
This query scope will limit the query to return the Model whose Revision
has the max primary key, where
the Revision
was created after the given moment.
The moment can either be an instance of a Checkpoint
using its checkpoint_date
field, a string
representation of a date or a Carbon
instance.
temporal($upper, $lower)
/** * @param $upper Checkpoint|Carbon|string * @param $upper Checkpoint|Carbon|string */ temporal($until = null, $since = null)
This query scope will limit the query to return the Model whose Revision
has the max primary key created at
or before $until
. This method can also limit the query to the Model whose revision has the max primary key
created after $since
.
Each argument operates independently of each other and $until
and $since
can
either be an instance of a Checkpoint
using its checkpoint_date
field, a string representation of a
date or a Carbon
instance.
withoutRevisions()
withoutRevisions()
This query scope is used to query the models without taking revisioning into consideration.
Dynamic Relationships
Inspired by https://reinink.ca/articles/dynamic-relationships-in-laravel-using-subqueries, this package supplies a few dynamic relationships as a convenience for navigating through a model's revision history. The following scopes will run subqueries to get the additional columns and eagerload the corresponding relations, saving you the hassle of caching them on each of the tables for your revisionable models. As a fallback when these scopes are not applied, we use get mutators to run queries and fetch the same columns, making sure the relations are always available but at the expense of running a bit more queries. NOTE: when applying these scopes, you will have extra columns in your models attributes, any update or insert operations will not work.
withNewestAt($until, $since)
/** * @param $until Checkpoint|Carbon|string * @param $since Checkpoint|Carbon|string */ withNewestAt($until = null, $since = null)
This scope will retrieve the id of the newest model given the until / since constraints. Stored in the newest_id
attribute, this allows you to use ->newest()
relation as a quick way to navigate to that model. Defaults to the
newest model in the revision history.
withNewest()
This scope is a shortcut of withNewestAt
with the default parameters. Uses the same attribute, mutator and relation.
withInitial()
This scope will retrieve the id of the initial model from its revision history. Stored in the initial_id attribute,
this allows you to use ->initial()
relation as a quick way to navigate to that first item in the revision history.
withPrevious()
This scope will retrieve the id of the previous model from its revision history. Stored in the previous_id attribute,
this allows you to use ->previous()
relation as a quick way to navigate to that previous item in the revision history.
withNext()
This scope will retrieve the id of the next model from its revision history. Stored in the next_id attribute,
this allows you to use ->next()
relation as a quick way to navigate to that next item in the revision history.
Revision Metadata & Uniqueness
As a workaround to some package compatibility issues, this package offers a convenient way to store the values of some
columns as metadata
on the revisions
table. The primary use-case for this feature is to deal with columns or
indexes which force some sort of uniqueness constraint on the Model's table.
For example, imagine a Room
model we wish to revision and it has a code
field which needs to be unique.
Since multiple instances of the same Room
need to exist as revisions, there would be duplicated codes
. By
specifying the code
field in theprotected $revisionMeta;
of the Room
Model, this package will
manage this field by storing it as metadata on the Revision
. The package achieve's this by overriding the
getAttributeValue($value)
method on the model, to retrieve the value of code
from the Revision
. When
saving a new Revision
of the Room
the code
will automatically be saved on the metadata
field of
the revision and set as null on the Room
.
Ignored Fields
When updating the fields of a Model, some fields may not warrant creating a new Revision
of the Model. You can
prevent a new Revision
from being created when specific fields are updated by setting the protected $ignored
array on the model being revisioned.
Should Revision
If you have more complex cases where you may not want to create a new Revision
when updating a Model, you can
override the public function shouldRevision()
on the Model being revisioned. When this method returns a truthy
value, a new Revision
will be created when updating, and when it returns a falsy value it will not.
Excluded Columns
When creating a new Revision
of a Model there may be some fields which do not make sense to have their values
copied over. In those cases you can add those values to the protected $excluded
array on the Model you
are revisioning. Some operations like deleting / restoring / revisioning children require a full copy and will ignore
this option.
Excluded Relations
When creating a new Revision
of a Model there may be relations which do not make sense to duplicate. In those
cases you can add the names of the relations to the protected $excludedRelations
array on the Model you are
revisioning. Excluding all relations to the Checkpoint
s and other related Revision
s are handled by the
package.
Testing
composer test
Changelog
Please see CHANGELOG for more information what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security related issues, please email massimo@plankdesign.com instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.