Skip to content

plank/snapshots

Repository files navigation

PHP Version Support PHP Version Support GitHub Workflow Status

Laravel Snapshots

⚠️ Package is under active development. Wait for v1.0.0 for production use. ⚠️

Snapshots is a Laravel package that allows you to version the content of your app by replicating database tables and their content. Each snapshot represents a browesable version of your app's content at a specific point in time. By changing the active version of your app, you can view your app's content at a previous version.

The main goal of this package is for it to perform robust versioning of your content, but stay out of your way. You should be able to use it without having to change your existing codebase. It should be easy to install and configure, and it should be easy to use.

Table of Contents

 

Installation

You can install the package via composer:

composer require plank/snapshots

You can use the package's install command to complete the installation:

php artisan snapshots:install

Quick Start

Once the installation has completed, to begin using the package:

  1. Make all migrations for versioned content extend Plank\Snapshots\Migrations\SnapshotMigration instead of the framework's Migration class.
  2. Replace references to Illuminate\Database\Schema\Builder with $this->schema or Plank\Snapshots\Facades\SnapshotSchema in those migrations for the versioned content.
  3. Make all models representing versioned content implement Plank\Snapshots\Contracts\Versioned and use the Plank\Snapshots\Concerns\AsVersionedContent trait.
  4. Make all models that are not versioned, but have a relation to versioned content use the Plank\Snapshots\Concerns\InteractsWithVersionedContent trait.
  5. Create a middleware to set the active version of your app based on the request.

Middleware example:

<?php

namespace App\Http\Middleware;

use Closure;
use Plank\Snapshots\Contracts\ManagesVersions;

class SetActiveVersion
{
    public function __construct(
        protected ManagesVersions $versions
    ) {
    }

    public function handle($request, Closure $next)
    {
        $version = $request->route('version');

        if ($version = $this->versions->byNumber($version)) {
            $this->versions->setActive($version);
        }

        return $next($request);
    }
}

Now, whenever you create a new version, the SnapshotDatabase listener will handle the VersionCreated event and run all of the migrations for the versioned content. It will also copy the content from the previous version of the table into the new version of the table.

 

Configuration

The package's configuration file is located at config/snapshots.php. If you did not publish the config file during installation, you can publish the configuration file using the following command:

php artisan vendor:publish --provider="Plank\Snapshots\SnapshotsServiceProvider" --tag="config"

Version Model

The model option is the fully qualified class name of the model that will be used to store the versions of your app. The default value is Plank\Snapshots\Models\Version. Any model provided must implement the Plank\Snapshots\Contracts\Version interface.

Version Factory

The factory option is the fully qualified class name of the model factory that will be used to generate Version instances for testing and seeding your application. The default value is Plank\Snapshots\Factories\VersionFactory.

Repository

The repository option is the fully qualified class name of the repository that will be used to retrieve the versions of your app. The default value is Plank\Snapshots\Repository\VersionRepository. Any repository provided must implement the Plank\Snapshots\Contracts\ManagesVersions interface.

Auto Migration

The auto_migrate option determines whether or not the package will automatically create new tables for all of the versioned content when a new version model is created. The package provides the default implementation of Plank\Snapshots\Listeners\SnapshotDatabase, but you can provide your own implementation.

Auto Copy

The auto_copy option determines whether or not the package will automatically copy content to the newly versioned tables when a new version model is created.

The package provides the default implementation of Plank\Snapshots\Listeners\CopyTable, where the data is copied over at the database level.

The package also ships with Plank\Snapshots\Listeners\CopyModels, where the data is copied over at the model level. This is useful if you have custom logic in your models that needs to be run when the data is copied over.

You can also provide your own implementation by setting it in the configuration file.

 

Usage

Versions

Contract and Model

Snapshots are identified by and accessed through a Version model. This model is created by the package or can be overriden by the consumer by creating a class which implements the Plank\Contracts\Version contract, and specifying it as the model in the configuration file.

In applications that use this package, requests should generally specify an "active" Version. The active Version will alter the database tables which versioned content will be queried on.

Events
  • Plank\Events\VersionCreated
    • Fired after a new version model is created
    • Hooked on to by the package to run all the versioned migrations, but can be disabled by setting auto_migrate to false

Repository

The ManagesVersions interface is a minimal interface for a Version repository required for the migrator to function. The package provides a VersionRepository class which implements this interface, but it can be overriden by the consumer by creating a class which implements the Plank\Contracts\ManagesVersions contract, and specifying it as the repository in the configuration file.

The repository is responsible for querying existing versions and managing the active version. It is not used to create new versions, as that is out of scope for the package.

Migrations

SnapshotMigration

This package adds a SnapshotMigration class, to house the migrations for all of your versioned content. It allows the SnapshotMigrator to know which migrations need to be run accross all of the versions of your app.

To use it, simply make the Migration classes extend SnapshotMigration instead of the framework's Migration class.

<?php

use Plank\Snapshots\Facades\SnapshotSchema;
use Plank\Snapshots\Migrations\SnapshotBlueprint;
use Plank\Snapshots\Migrations\SnapshotMigration;

return new class extends SnapshotMigration
{
    public function up()
    {
        $this->schema->create('blocks', function (SnapshotBlueprint $table) {
            $table->id();
            $table->unsignedBigInteger('page_id');
            $table->string('name');
            $table->morphs('blockable');
            $table->timestamps();

            $table->foreign('page_id')->references('id')->onSnapshot('pages');
        });
    }

    public function down()
    {
        SnapshotSchema::dropIfExists('blocks');
    }
}

You will notice that in a SnapshotMigration you have $this->schema available which is the SnapshotSchemaBuilder instance. It is a wrapper around the framework's \Illuminate\Database\Schema\Builder class. You can also use the SnapshotSchema facade, however you should only use that inside a SnapshotMigration.

The SnapshotMigrations allow you to declare migrations in the way you are used to, but under the hood it will handle all of the versioning.

You will also notice the SnapshotBlueprint class. This blueprint type exists to allow you to define foreign keys to versioned content from versioned content using the ->onSnapshot() method.

Limitations
  1. Foreign keys for relations from unversioned content to versioned content can not be used. This is due to there being more than one version of the table, and the foreign key will not know which version to reference. Foreign keys from versioned content to unversioned content, and from versioned content to versioned content are still possible.

  2. It is important to note that pivot tables where at least one of the related models is versioned, should also be versioned. This is because the pivot table will need to be copied for each version of the related model.

  3. It is also important to note that if you are using a versioned custom Pivot model, you cannot relate unversioned content to unversioned content through the pivot. So be especially careful with what you are relating through your custom polymorphic pivot models.

 

SnapshotMigrator

This package will replace the framework's migrator with the SnapshotMigrator class. The migrator extends the framework's migrator with the sole purpose of ensuring the migrations for your versioned content are run for every version of your app.

For example, after running migrations in a traditional Laravel Application, you might have the following:

php artisan migrate

INFO  Preparing database.

Creating migration table ............................... 13ms DONE

INFO  Running migrations.

2023_09_25_000000_create_users_table ............................... 10ms DONE
2023_09_25_000001_create_roles_table ............................... 10ms DONE
2023_09_25_000002_create_pages_table ............................... 14ms DONE

However, in a Laravel Application using Snapshots, you might have the following after the initial migration:

php artisan migrate

INFO  Preparing database.

Creating migration table ............................... 13ms DONE

INFO  Running migrations.

2023_09_25_000000_create_users_table    ............................... 10ms DONE
2023_09_25_000001_create_roles_table    ............................... 10ms DONE
2023_09_25_000002_create_pages_table    ............................... 14ms DONE
2023_09_25_000002_create_versions_table ............................... 14ms DONE

When creating the first Version model – with auto_migrate set to true – assuming the Page content is versioned, the package will re-run your 2023_09_25_000002_create_pages_table migration for the new Version.

The following output is shown as an illustration. The migration is ran in the background, and not output.

php artisan migrate

INFO  Running migrations.

v1_0_0_2023_09_25_000000_create_pages_table    ............................... 10ms DONE

Finally, assume you add a new migration 2023_09_25_000003_add_slug_to_pages_table to add a slug column to the pages table, and you run the migrations on deploy.

php artisan migrate

INFO  Running migrations.

2023_09_25_000003_add_slug_to_pages_table         ............................... 10ms DONE
v1_0_0_2023_09_25_000003_add_slug_to_pages_table  ............................... 10ms DONE

The migration is applied to all versions of your content to achieve consistency across versions.

 

Models

Versioned Models

This package provides a Plank\Snapshots\Contracts\Versioned interface, and an AsVersionedContent trait. For models whose content should be versioned, have them implement the Versioned interface, and use the AsVersionedContent trait.

This trait ensures queries on the model's table are prefixed with the active version's prefix. It also overrides the pivotted relations to use the versioned pivot table.

Example:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Plank\Snapshots\Concerns\AsVersionedContent;
use Plank\Snapshots\Contracts\Versioned;

class Page extends Model implements Versioned
{
    use AsVersionedContent;
}

Unversioned Models

For any models that have an association to a versioned model, you can use the Plank\Snapshots\Concerns\InteractsWithVersionedContent trait. This trait ensures that when a versioned model is related through a pivot, the versioned pivot table is used.

Example:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Plank\Snapshots\Concerns\InteractsWithVersionedContent;

class User extends Model
{
    use InteractsWithVersionedContent;

    public function pages()
    {
        return $this->belongsToMany(Page::class);
    }
}

 

Contributing

Please see CONTRIBUTING for details.

 

Credits

 

License

The MIT License (MIT). Please see License File for more information.

 

Security Vulnerabilities

If you discover a security vulnerability within siren, please send an e-mail to security@plank.co. All security vulnerabilities will be promptly addressed.

 

Check Us Out!

 

Plank focuses on impactful solutions that deliver engaging experiences to our clients and their users. We're committed to innovation, inclusivity, and sustainability in the digital space. Learn more about our mission to improve the web.