PHPackages                             silverstripe/versioned-snapshots - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. silverstripe/versioned-snapshots

ActiveSilverstripe-vendormodule

silverstripe/versioned-snapshots
================================

SilverStripe Versioned Snapshots

2.1.0(1y ago)1317.5k↓25%11[14 issues](https://github.com/silverstripe/silverstripe-versioned-snapshots/issues)[3 PRs](https://github.com/silverstripe/silverstripe-versioned-snapshots/pulls)1BSD-3-ClausePHPPHP ^8.3CI failing

Since Sep 9Pushed 9mo ago9 watchersCompare

[ Source](https://github.com/silverstripe/silverstripe-versioned-snapshots)[ Packagist](https://packagist.org/packages/silverstripe/versioned-snapshots)[ Docs](http://silverstripe.org)[ RSS](/packages/silverstripe-versioned-snapshots/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (5)Dependencies (6)Versions (17)Used By (1)

SilverStripe Versioned Snapshots
--------------------------------

[](#silverstripe-versioned-snapshots)

[![Build Status](https://github.com/silverstripe/silverstripe-versioned-snapshots/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-versioned-snapshots/actions/workflows/ci.yml)[![Latest Stable Version](https://camo.githubusercontent.com/420ed4e44ef5e64daf8d4bf82b3070983bc741949cc0856d8029cd4cfa199b6a/687474703a2f2f706f7365722e707567782e6f72672f73696c7665727374726970652f76657273696f6e65642d736e617073686f74732f76)](https://packagist.org/packages/silverstripe/versioned-snapshots)[![Total Downloads](https://camo.githubusercontent.com/0ddc582ea2a201ec824c7758e31b3524fe47d0edda6172130cf9d5c9235ba4c7/687474703a2f2f706f7365722e707567782e6f72672f73696c7665727374726970652f76657273696f6e65642d736e617073686f74732f646f776e6c6f616473)](https://packagist.org/packages/silverstripe/versioned-snapshots)[![Latest Unstable Version](https://camo.githubusercontent.com/9dfbf0f96717671d9dc2e693f1420ef1e8c8bae9e6fdd8432e9a258478714c16/687474703a2f2f706f7365722e707567782e6f72672f73696c7665727374726970652f76657273696f6e65642d736e617073686f74732f762f756e737461626c65)](https://packagist.org/packages/silverstripe/versioned-snapshots)[![License](https://camo.githubusercontent.com/5487b8e3d9b71cd04dc92f461b8d94c756ce4dee13ff6e4b341d43bb13b35317/687474703a2f2f706f7365722e707567782e6f72672f73696c7665727374726970652f76657273696f6e65642d736e617073686f74732f6c6963656e7365)](https://packagist.org/packages/silverstripe/versioned-snapshots)[![PHP Version Require](https://camo.githubusercontent.com/d44fafd7e27d44540946dacc885a14eea2cd235c78b9254597eac445a83de403/687474703a2f2f706f7365722e707567782e6f72672f73696c7665727374726970652f76657273696f6e65642d736e617073686f74732f726571756972652f706870)](https://packagist.org/packages/silverstripe/versioned-snapshots)

Overview
--------

[](#overview)

Enables snapshots for enhanced history and modification status for deeply nested ownership structures. It's solving an [important UX issue](https://github.com/silverstripe/silverstripe-versioned/issues/195) with versioning, which is particularly visible in [content blocks](https://github.com/dnadesign/silverstripe-elemental) implementations.

This module enables the data model for snapshots. To take full advantage of its core offering, you should install [silverstripe/versioned-snapshot-admin](https://github.com/silverstripe/silverstripe-versioned-snapshot-admin) to expose these snapshots through the "History" tab of the CMS.

WARNING: This module is experimental, and not considered stable.

Installation
------------

[](#installation)

```
$ composer require silverstripe/versioned-snapshots

```

You'll also need to run `dev/build`.

What does this do?
------------------

[](#what-does-this-do)

Imagine you have a content model that relies on an ownership structure, using the `$owns` setting.

```
BlockPage
  (has_many) Blocks
    (has_one) Gallery
       (many_many) Image

```

Ownership between each of those nodes affords publication of the entire graph through one commmand (or click of a button). But it is not apparent to the user what owned content, if any, will be published. If the Gallery is modified, `BlockPage` will not show a modified state.

This module aims to make these modification states and implicit edit history more transparent.

What does it *not* do?
----------------------

[](#what-does-it-not-do)

Currently, rolling back a record that owns other content is not supported and will produce unexpected results. Further, comparing owned changes between two versions of a parent is not supported.

Can I use this in my current project?
-------------------------------------

[](#can-i-use-this-in-my-current-project)

Yes, with few caveats:

- `many_many` relationships **must use "through" objects**. (implicit many\_many is not versionable)
- You will have to migrate all of your versioned content to snapshots (See [Migrating from versioned](#migrating-from-versioned))
- Some editing events may not be captured, particularly some provided by thirdparty modules. See ([Adding your own snapshot creator](#adding-your-own-snapshot-creator))
- Does not (yet) fully work with Postgres. Pull requests welcome!

API
---

[](#api)

While the `SnapshotPublishable` extension offers a large API surface, there are only a few primary methods that are relevant to the end user:

- `$myDataObject->hasOwnedModifications(): bool` returns true if the record owns records that have changes
- `$myDataObject->getPublishableObjects(): ArrayList`: returns a list of `DataObject` instances that will be published along with the owner.
- `$myDataObject->getActivityFeed(): ArrayList` Provides a collection of objects that can be rendered on a template to create a human-readable activity feed. Returns an array of `ActivityEntry` objects containing the following:
    - `Subject`: The `DataObject` record that instantiated the activity
    - `Action`: One of: `CREATED`, `MODIFIED`, `DELETED`, `ADDED`, or `REMOVED`.
    - `Owner`: Only defined in `many_many` reltionships. Provides information on what the record was linked to. Informs the `ADDED` and `REMOVED` actions.

Extensions
----------

[](#extensions)

The snapshot functionality is provided through the `SnapshotPublishable` extension, which is a drop-in replacement for `RecursivePublishable`. By default, this module will replace `RecursivePublishable`, which is added to all dataobjects by `silverstripe-versioned`, with this custom subclass.

How it works
------------

[](#how-it-works)

Snapshots are created with handlers registered to user events in the CMS triggered by the [`silverstripe/cms-events`](https://github.com/silverstripe/silverstripe-cms-events)module.

### Customising the snapshot messages

[](#customising-the-snapshot-messages)

By default, these events will trigger the message defined in the language file, e.g. `_t('SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.HANDLER_publish', 'Publish page')`. However, if you want to customise this message at the configuration level, simply override the message on the handler class.

```
SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler:
  messages:
    publish: 'My publish message'
```

In this case "publish" is the **action identifier** (the function that handles the form).

### Customising existing snapshot creators

[](#customising-existing-snapshot-creators)

All of the handlers are registered with injector, so the simplest way to customise them is to override their definitions in the configuration.

For instance, if you have something custom you with a snapshot when a page is saved:

```
use SilverStripe\Snapshots\Handler\Form\SaveHandler;
use SilverStripe\EventDispatcher\Event\EventContextInterface;
use SilverStripe\Snapshots\Snapshot;

class MySaveHandler extends SaveHandler
{
    protected function createSnapshot(EventContextInterface $context): ?Snapshot
    {
        //...
    }
}
```

```
SilverStripe\Core\Injector\Injector:
  SilverStripe\Snapshots\Handler\Form\SaveHandler:
    class: MyProject\MySaveHandler
```

### Adding your own snapshot creator

[](#adding-your-own-snapshot-creator)

If you have custom actions or form handlers you've added to the CMS, you might want to either ensure their tracked by the default snapshot creators, or maybe even build your own snapshot creator for them. In this case, you can use the declarative API on `Dispatcher` to subscribe to the events you need.

Let's say we have a form that submits to a function: `public function myFormHandler($data, $form)`.

```
SilverStripe\Core\Injector\Injector:
  SilverStripe\Snapshots\Dispatch\Dispatcher:
    properties:
      handlers:
        myForm:
          on:
            - 'formSubmitted.myFormHandler'
          handler: %$MyProject\Handlers\MyHandler
```

Notice that the event name is in the key of the configuration. This makes it possible for another layer of configuration to disable it. See below.

### Removing snapshot creators

[](#removing-snapshot-creators)

To remove an event from a handler, simply add it to the `off` array.

```
SilverStripe\Core\Injector\Injector:
  SilverStripe\Snapshots\Dispatch\Dispatcher:
    properties:
      handlers:
        myForm:
          off:
            - 'formSubmitted.myFormHandler'
```

### Procedurally adding event handlers

[](#procedurally-adding-event-handlers)

You can register a `EventHandlerLoader` implementation with `Dispatcher` to procedurally register and unregister events.

```
SilverStripe\Core\Injector\Injector:
  SilverStripe\Snapshots\Dispatch\Dispatcher:
    properties:
      loaders:
        myLoader: %$MyProject\MyEventLoader
```

```
use SilverStripe\Snapshots\Dispatch\DispatcherLoaderInterface;
use SilverStripe\Snapshots\Dispatch\Dispatcher;
use SilverStripe\Snapshots\Handler\Form\SaveHandler;

class MyEventLoader implements DispatcherLoaderInterface
{
    public function addToDispatcher(Dispatcher $dispatcher): void
    {
        $dispatcher->removeListenerByClassName('formSubmitted.save', SaveHandler::class);
    }
}
```

### Snapshot creation API

[](#snapshot-creation-api)

To cover all cases, this module allows you to invoke snapshot creation in any part of your code outside of normal action flow.

When you want to create a snapshot just call `createSnapshot` function like this:

```
Snapshot::singleton()->createSnapshot(DataObject $origin, array $extraObjects = []);
```

`$origin` is the object which should be matching the action, i.e. the action is changing the origin object.

`$extraObjects` is an array of extra dataobjects you want to be in the snapshot. Every call to `createSnapshot` implicitly includes the following records in addition to the origin:

- All of the records the origin is "owned" by, e.g. `BlockImage > BaseElement > ElementalArea > Page`
- All of the records the origin has *implicitly modified*. (See [Implicit modifications](#implicit-modifications))

When there is no "origin"
-------------------------

[](#when-there-is-no-origin)

Some modifications to your content aren't necessarily triggered by editing event to a specific entity. For these cases, you can use the `createSnapshotFromEvent` API.

```
Snapshot::singleton()->createSnapshotFromEvent('Description of event');
```

Examples of generic events include reordering the site tree, copying translations, importing content, and more. Think of it as a simple "git commit" message for your content. It creates a marker on your timeline that content editors can refer back to at some point in the future.

Implicit modifications
----------------------

[](#implicit-modifications)

Sometimes edits to the record that appears to be the "origin" are implicitly edits to other records. The most common case of this is adding related records. If a user makes a change to a `CheckboxSetField` that manages a `many_many` relation, for instance, the record that displays those checkboxes remains unchanged and does not merit a new version. The addition or removal of new related records, however, does merit a new snapshot as the ownership chain has been updated.

The `createSnapshot` API is aware of these kinds of modifications, and attempts to detect them using the `RelationDiffer` service. When a modification includes changes to relationships, `createSnapshot` will fallback to create a generic event that describes what changes happened, for instance: `'Added two categories'`.

This relation diffing is expensive to run on every save for every relationship, however, and therefore, you need to opt-in to it using the `$snapshot_relation_tracking` setting.

```
class Product extends DataObject
{
    private static $many_many = [
        'Categories' => Category::class,
    ];

    private static $snapshot_relation_tracking = ['Categories'];

}
```

Another common example of implicit modifications is the `ElementalEditor` field in [silverstripe-elemental](https://github.com/dnadesign/silverstripe-elemental) version 4.x. When the page is saved, it actually saves all the blocks in the editor, which are `has_many` relations. Because it is such a common use case, blocks are tracked in `snapshot_relation_tracking` by default, so that page saves will result in "Modified/added/deleted block" snapshots where appropriate.

Migrating from Versioned
------------------------

[](#migrating-from-versioned)

To migrate all your `_versions` tables to snapshots, use the `snapshot-migration` task:

```
$ vendor/bin/sake dev/tasks/snapshot-migration

```

Alternatively, this task is available as a [queued job](https://github.com/symbiote/silverstripe-queuedjobs).

The task should be fairly low-impact, as it only writes to the new (and presumably empty) snapshots tables. It should also perform at scale, since it doesn't do any processing of the records in PHP. The migration is pure SQL.

Thirdparty module support
-------------------------

[](#thirdparty-module-support)

Some common thirdparty modules are supported out of the box. The most notable is [silverstripe-elemental](https://github.com/dnadesign/silverstripe-elemental), which has several specific snapshot creators installed by default, including:

- Archive element
- Save individual element
- Create element (GraphQL query)
- Edit individual element
- Save all elements via page save
- Sort elements
- ModelAdmin and GridField CSV imports

As mentioned above, elements all receive `snapshot_relation_tracking` on their pages by default, as well.

Another module that is supported out of the box is [GridFieldExtensions](https://github.com/symbiote/silverstripe-gridfieldextensions). A handler is provided for its `GridFieldOrderableRows` component.

Localisation
------------

[](#localisation)

This module can be configured to work with the [Fluent](https://github.com/tractorcow-farm/silverstripe-fluent) module. Following the paradigm set by the Fluent version history, we do not allow any content inheritance when it comes to versioned history. Our `Snapshot` and `SnpashotItem` models represent a more detailed version history, so we need to apply the following configuration to comply with the Fluent paradigm:

```
SilverStripe\Snapshots\Snapshot:
    cms_localisation_required: 'exact'
    frontend_publish_required: 'exact'
    extensions:
        - TractorCow\Fluent\Extension\FluentExtension
    translate:
        - OriginHash

SilverStripe\Snapshots\SnapshotItem:
    cms_localisation_required: 'exact'
    frontend_publish_required: 'exact'
    extensions:
        - TractorCow\Fluent\Extension\FluentExtension
    translate:
        - ObjectHash
```

Upgrading to 1.x.x
------------------

[](#upgrading-to-1xx)

`1.x.x` release contains a couple of breaking changes. We provide upgrade path for both.

### Object version DB field rename

[](#object-version-db-field-rename)

DB field `Version` on `SnapshotItem` was renamed to `ObjectVersion` to prevent naming conflicts. Please follow the steps below to upgrade.

- run `composer update` to upgrade to the desired `1.x.x` version of this module
- run `dev/build flush=all`
- run `dev/tasks/migrate-object-version-task`, run via CLI

### Legacy Fluent setup

[](#legacy-fluent-setup)

This is relevant only for project which use [Fluent module](https://github.com/tractorcow-farm/silverstripe-fluent) and use localised snapshot models.

- run `composer update` to upgrade to the desired `1.x.x` version of this module
- review and update your Fluent configuration as per **Localisation** section of this readme
- run `dev/build flush=all`
- run `dev/tasks/migrate-fluent-object-hash-task`, run via CLI

### Recalculate hashes

[](#recalculate-hashes)

Object hashes may be out of date. It's recommended to update them otherwise pre-update history items may not show in the history viewer. Run `dev/tasks/recalculate-hashes-task`, run via CLI

This dev task supports Fluent out of the box,

Semantic versioning
-------------------

[](#semantic-versioning)

This library follows [Semver](http://semver.org). According to Semver, you will be able to upgrade to any minor or patch version of this library without any breaking changes to the public API. Semver also requires that we clearly define the public API for this library.

All methods, with `public` visibility, are part of the public API. All other methods are not part of the public API. Where possible, we'll try to keep `protected` methods backwards-compatible in minor/patch versions, but if you're overriding methods then please test your work before upgrading.

Reporting Issues
----------------

[](#reporting-issues)

Please [create an issue](http://github.com/silverstripe/silverstripe-versioned-snapshots/issues)for any bugs you've found, or features you're missing.

License
-------

[](#license)

This module is released under the [BSD 3-Clause License](LICENSE)

###  Health Score

45

—

FairBetter than 93% of packages

Maintenance32

Infrequent updates — may be unmaintained

Popularity36

Limited adoption so far

Community29

Small or concentrated contributor base

Maturity74

Established project with proven stability

 Bus Factor1

Top contributor holds 60% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~143 days

Recently: every ~0 days

Total

10

Last Release

425d ago

Major Versions

0.1.0 → 1.0.02023-08-30

1.x-dev → 2.0.02024-02-09

2.0.0 → 3.x-dev2025-03-20

PHP version history (4 changes)0.1.0PHP &gt;=7.1.0

1.0.0PHP ^7.4 || ^8

2.0.0PHP ^8.1

2.1.0PHP ^8.3

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/654636?v=4)[Aaron Carlino](/maintainers/unclecheese)[@unclecheese](https://github.com/unclecheese)

![](https://www.gravatar.com/avatar/b0cba8b534e20e6ab4fff555a97b237a18436ebca1446fc0b29c8a8b504038b9?d=identicon)[GuySartorelli](/maintainers/GuySartorelli)

![](https://avatars.githubusercontent.com/u/111025?v=4)[Ingo Schommer](/maintainers/chillu)[@chillu](https://github.com/chillu)

![](https://www.gravatar.com/avatar/a25bc04c5720a36869d5a39c6449dde7eb43e19b7c8e666d5f632d6a9ab440b1?d=identicon)[emteknetnz](/maintainers/emteknetnz)

![](https://www.gravatar.com/avatar/afbb3dcc9ef29c1a6eedd6addcae5fce9ab1271915a85a4c349301b71237368d?d=identicon)[silverstripe-machine01](/maintainers/silverstripe-machine01)

![](https://avatars.githubusercontent.com/u/1168676?v=4)[Maxime Rainville](/maintainers/maxime-rainville)[@maxime-rainville](https://github.com/maxime-rainville)

---

Top Contributors

[![mfendeksilverstripe](https://avatars.githubusercontent.com/u/26395487?v=4)](https://github.com/mfendeksilverstripe "mfendeksilverstripe (39 commits)")[![dnsl48](https://avatars.githubusercontent.com/u/9313746?v=4)](https://github.com/dnsl48 "dnsl48 (9 commits)")[![ScopeyNZ](https://avatars.githubusercontent.com/u/3260989?v=4)](https://github.com/ScopeyNZ "ScopeyNZ (5 commits)")[![chillu](https://avatars.githubusercontent.com/u/111025?v=4)](https://github.com/chillu "chillu (3 commits)")[![blueo](https://avatars.githubusercontent.com/u/948122?v=4)](https://github.com/blueo "blueo (2 commits)")[![chrispenny](https://avatars.githubusercontent.com/u/505788?v=4)](https://github.com/chrispenny "chrispenny (2 commits)")[![scott1702](https://avatars.githubusercontent.com/u/10215604?v=4)](https://github.com/scott1702 "scott1702 (2 commits)")[![satrun77](https://avatars.githubusercontent.com/u/166450?v=4)](https://github.com/satrun77 "satrun77 (1 commits)")[![ishannz](https://avatars.githubusercontent.com/u/20032948?v=4)](https://github.com/ishannz "ishannz (1 commits)")[![Jianbinzhu](https://avatars.githubusercontent.com/u/11606683?v=4)](https://github.com/Jianbinzhu "Jianbinzhu (1 commits)")

---

Tags

silverstripeversionedsnapshots

### Embed Badge

![Health badge](/badges/silverstripe-versioned-snapshots/health.svg)

```
[![Health](https://phpackages.com/badges/silverstripe-versioned-snapshots/health.svg)](https://phpackages.com/packages/silverstripe-versioned-snapshots)
```

###  Alternatives

[silverstripe/cms

The SilverStripe Content Management System

5163.4M1.3k](/packages/silverstripe-cms)[silverstripe/admin

SilverStripe admin interface

262.6M325](/packages/silverstripe-admin)[silverstripe/userforms

UserForms enables CMS users to create dynamic forms via a drag and drop interface and without getting involved in any PHP code

1321.0M72](/packages/silverstripe-userforms)[dnadesign/silverstripe-elemental

Elemental pagetype and collection of Elements

1151.0M255](/packages/dnadesign-silverstripe-elemental)[undefinedoffset/sortablegridfield

Adds drag and drop functionality to Silverstripe's GridField

941.2M50](/packages/undefinedoffset-sortablegridfield)[silverstripe/tagfield

Tag field for SilverStripe

571.2M45](/packages/silverstripe-tagfield)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
