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.
SortOrder
which would also change from Stage to Live). – RascalityGridField
changes to correctly unhook relations for that particular version (instead of all versions). – Penuche