Is it possible to have versioned many_many relations?

silverstripe

Solution

I've spent a lot of time looking into this and without fundamentally modifying `ManyManyList` (as it doesn't expose the necessary hooks through the extension system), there isn't many choices.

I am a dessert-first kind of person, how CAN we do it?

My only suggestion to accomplish this feat is essentially a many-to-many bridge object (ie. a separate entity joining `Page` and `Image`) via `$has_many` though it still requires quite a bit of modification.

This is partially discussed on the forum where a solution about subverting the actual relationship by storing the versioned items against the actual object rather than in a joining table. That would work but I think we can still do better than that.

I am personally leaning towards tying the version of the relationship to the `Page` itself and my partial solution below covers this. Read below the fold for more info trying this as an update to `ManyManyList`.

Something like this is a start:

class PageImageVersion extends DataObject
{
    private static $db = array(
        'Version' => 'Int'
    );

    private static $has_one = array(
        'Page' => 'Page',
        'Image' => 'Image'
    );
}

This contains our 2-way relationship plus we have our version number stored. You will want to specify the `getCMSFields` function to add the right fields required allowing you to relate it to an existing image or upload a new one. I am avoiding covering this as it should be relatively straight forward compared to the actual version handling part.

Now, we have a `has_many` on `Page` like so:

private static $has_many = array(
    'Images' => 'PageImageVersion' 
);

In my tests, I also added an extension for `Image` adding the matching `$has_many` onto it as well like so:

class ImageExtension extends DataExtension
{
    private static $has_many = array(
        'Pages' => 'PageImageVersion'
    );
}

Honestly, not sure if this is necessary beyond adding the `Pages` function on the `Image` side of the relationship. As far as I can see, it won't really matter for this particular usecase.

Unfortunately, because of this way of versioning, we can't use the standard way of calling the `Images`, we will need to be a bit creative. Something like this:

public function getVersionedImages($Version = null)
{
    if ($Version == null)
    {
        $Version = $this->Version;
    }
    else if ($Version < 0)
    {
        $Version = max($this->Version - $Version, 1);
    }

    return $this->Images()->filter(array('Version' => $Version));
}

When you call `getVersionedImages()`, it will return all images that have the `Version` set on it aligning with the version of the current page. Also supports getting previous versions via `getVersionedImages(-1)` for the last version or even gets images for a specific version of the page by passing any position number.

OK, so far so good. We now need to make sure that every page write we are getting a duplicate list of images for this new version of the page.

With an `onAfterWrite` function on `Page`, we can do this:

public function onAfterWrite()
{
    $lastVersionImages = $this->getVersionedImages(-1);
    foreach ($lastVersionImages as $image)
    {
        $duplicate = $image->duplicate(false);
        $duplicate->Version = $this->Version;
        $duplicate->write();
    }
}

For those playing at home, this is where things get a bit iffy relating to how restoring previous versions of `Page` would affect this.

Because we would be editing this in `GridField`, we will need to do a few things. First is make sure our code can handle the `Add New` function.

My idea is an `onAfterWrite` on the `PageImageVersion` object:

public function onAfterWrite()
{
    //Make sure the version is actually saved
    if ($this->Version == 0)
    {
        $this->Version = $this->Page()->Version;
        $this->write();
    }
}

To get your versioned items displaying in `GridField`, you would have it set up similar to this:

$gridFieldConfig = GridFieldConfig_RecordEditor::create();
$gridField = new GridField("Images", "Images", $this->getVersionedImages(), $gridFieldConfig);
$fields->addFieldToTab("Root.Images", $gridField);

You might want to link to images directly from the `GridField` via `GridFieldConfig_RelationEditor` however this is when things get sour.

Time for the veggies...

One of the big difficulties is `GridField`, for both linking and unlinking these entities. Using the standard `GridFieldDeleteAction` will directly update the relationship without the right version.

You will need to extend `GridFieldDeleteAction` and override the `handleAction` to write your `Page` object (to trigger another version), duplicate every version of our versioned image object for the last version while making it skip the one you don't want in the new version.

I'll admit, this last bit is just guesswork by me. From my understanding and debugging, it should work but simply there is a lot of fiddling to get it right.

Your extension of `GridFieldDeleteAction` then needs to be added to your specific `GridField`.

This would essentially be your last step away from making this solution work. Once you have the adding, removing, duplicating, version updating part down, it really is a matter of just using `getVersionedImages()` to get the right images.

Conclusion

Avoid. I get why you want to do this but I really don't see a clean way of being able to handle this without a decent sized update to how `many_many` relationships are handled in Silverstripe.

But I really want it as a `ManyManyList`!

The changes I see required for `ManyManyList` are having a 3-way key (Foreign Key, Local Key, Version Key) and the various methods for adding/removing/fetching etc updated.

If there were hooks in the `add` and `remove` functions, you might be able to sneak in the functionality as an extension (via Silverstripe's extension system) and add the needed data to the extra fields that `many_many` relationships allow.

While I could get this happening by extending `ManyManyList` directly and then forcing `ManyManyList` to be replaced with my custom class via `Object::useCustomClass`, it would be even more of a messy solution.

It is simply too long/complex for me to give a full answer for a pure `ManyManyList` solution at this stage (though I may get back to this later and give it a shot).

Disclaimer: I am not a Silverstripe Core dev, there may be a neater solution to this entire thing but I simply can't see how.

Problem

I already used versioning on `DataObjects` when they contain a lot of content, now I'm wondering if it's possible to apply versioning to a `many_many` relation? Assuming I have the following: ``` class Page extends SiteTree { private static $many_many = array( 'Images' => 'Image' ); } ``` Then the ORM will create a `Page_Images` table for me to store the relations. In order to have a versioned relation, more tables would be required (eg. `Page_Images_Live`). Is there any way to tell the ORM to create versioned relations? When looking at the above example with a `Page * – * Images` relation, I don't want the `Image` class to be versioned, but rather the relation. Eg. something like this: ``` Version Stage: --- PageA Images ( ImageA, ImageB, ImageC ) Version Live: --- PageA Images ( ImageA, ImageC, ImageD, ImageE ) ``` Is that even possible out of the box?

Original source