Возможно ли иметь версии с многочисленными отношениями?

Я уже использовал версию на DataObjects, когда они содержат много контента, теперь мне интересно, можно ли применить управление версиями к отношению many_many?

Предполагая, что у меня есть следующее:

class Page extends SiteTree
{
    private static $many_many = array(
        'Images' => 'Image' 
    );
}

Затем ORM создаст таблицу Page_Images для хранения отношений. Чтобы иметь отношение к версии, потребуется больше таблиц (например, Page_Images_Live).

Есть ли способ сообщить ORM о создании отношений с версией? Если посмотреть на приведенный выше пример с отношением Page * – * Images, я не хочу, чтобы класс Image был версией, а скорее отношением. Например. что-то вроде этого:

Version Stage:
---
    PageA
        Images ( ImageA, ImageB, ImageC )

Version Live:
---
    PageA
        Images ( ImageA, ImageC, ImageD, ImageE )

Возможно ли это из коробки?

Ответ 1

Я потратил много времени на это и без принципиальной модификации ManyManyList (так как он не раскрывает необходимые крючки через систему расширения), выбора не так много.

Я человек десертного типа, как мы можем это сделать?

Мое единственное предложение выполнить этот подвиг - это, по существу, мостовой объект "много ко многим" (т.е. отдельный объект, соединяющий Page и Image) через $has_many, хотя он по-прежнему требует довольно небольшой модификации.

Это частично обсуждается на форуме, где решение о подрыве фактической взаимосвязи, сохраняя элементы с версией в сравнении с реальным объектом, а не в соединение таблица. Это будет работать, но я думаю, что мы все еще можем сделать лучше.

Я лично склоняюсь к привязке версии отношения к Page, и мое частное решение ниже описывает это. Читайте ниже складки, чтобы узнать больше об этом как об обновлении до ManyManyList.

Что-то вроде этого - это начало:

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

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

Это содержит наши двухсторонние отношения, плюс мы сохранили наш номер версии. Вы хотите указать функцию getCMSFields, чтобы добавить нужные нужные поля, чтобы вы могли связать их с существующим изображением или загрузить новый. Я избегаю этого, так как он должен быть относительно прямым, по сравнению с фактической частью обработки.

Теперь мы имеем has_many на Page так:

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

В моих тестах я также добавил расширение для Image, добавляя на него сопоставление $has_many так же:

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

Честно говоря, не уверен, что это необходимо, кроме добавления Pagesфункции на стороне Image отношения. Насколько я могу судить, это не имеет особого значения для этого конкретного использования.

К сожалению, из-за этого способа управления версиями мы не можем использовать стандартный способ вызова Images, нам нужно быть немного творческим. Что-то вроде этого:

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));
}

Когда вы вызываете getVersionedImages(), он вернет все изображения, у которых Version установлен на нем, совпадающем с версией текущей страницы. Также поддерживает получение предыдущих версий с помощью getVersionedImages(-1) для последней версии или даже получение изображений для определенной версии страницы путем передачи любого номера позиции.

Хорошо, пока все хорошо. Теперь нам нужно убедиться, что каждая страница пишет, что мы получаем дублированный список изображений для этой новой версии страницы.

С помощью функции onAfterWrite на Page мы можем сделать это:

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

Для тех, кто играет дома, это то, где кое-что немного напоминает о том, как это повлияет на восстановление предыдущих версий Page.

Поскольку мы будем редактировать это в GridField, нам нужно будет сделать несколько вещей. Сначала убедитесь, что наш код может обрабатывать функцию Add New.

Моя идея onAfterWrite в объекте PageImageVersion:

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

Чтобы отобразить отображаемые вами элементы в GridField, вы должны настроить его так:

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

Вам может потребоваться ссылка на изображения непосредственно из GridField через GridFieldConfig_RelationEditor, но это происходит, когда все становится кислым.

Время для овощей...

Одна из больших трудностей - GridField, как для связывания, так и для развязывания этих объектов. Использование стандартного GridFieldDeleteAction будет напрямую обновлять отношения без правильной версии.

Вам нужно будет расширить GridFieldDeleteAction и переопределить handleAction для записи вашего объекта Page (для запуска другой версии), дублировать каждую версию нашего объекта с версией изображения для последней версии, в то же время пропуская ту, которую вы не хотите в новой версии.

Я признаю, что этот последний бит - это только догадки. Из моего понимания и отладки, он должен работать, но просто есть много попыток, чтобы понять это.

Ваше расширение GridFieldDeleteAction затем должно быть добавлено к вашему конкретному GridField.

Это будет ваш последний шаг от решения этого решения. После того, как вы добавите, удалите, дублируете, обновите часть версии, действительно нужно просто использовать getVersionedImages() для получения правильных изображений.

Заключение

Избегайте.. Я понимаю, почему вы хотите это сделать, но я действительно не вижу чистого способа справиться с этим без изменения размера приличного размера до того, как отношения many_many обрабатываются в SilverStripe.


Но я действительно хочу, чтобы это было как ManyManyList!

Изменения, которые я вижу для ManyManyList, имеют 3-позиционный ключ (внешний ключ, локальный ключ, ключ версии) и обновлены различные способы добавления/удаления/выборки и т.д.

Если в функциях add и remove есть крючки, вы можете прокрасться в функциональность как расширение (через систему расширения Silverstripe) и добавить необходимые данные в дополнительные поля, которые many_many отношения позволяют.

Пока я мог это сделать, расширив ManyManyList напрямую, а затем заставив ManyManyList заменить мой пользовательский класс через Object::useCustomClass, это было бы еще более беспорядочным решением.

Для меня просто слишком сложно/сложно дать полный ответ для чистого решения ManyManyList на этом этапе (хотя я могу вернуться к этому позже и дать ему шанс).


Отказ от ответственности: я не являюсь разработчиком Silverstripe Core, может быть более аккуратное решение для всего этого, но я просто не вижу, как это сделать.

Ответ 2

Вы можете определить второе отношение с суффиксом "_Live" и обновить его при публикации страницы. Примечание. Это решение хранит только две версии (live и stage).

Bellow - это моя реализация, которая автоматически определяет, является ли отношение многого числа версией или нет. Затем он обрабатывает публикацию и извлечение данных. Все, что необходимо, - определить одно дополнительное многозначное отношение с суффиксом _Live.

$page- > Images() возвращает элементы в соответствии с текущим этапом (stage/live).

class Page extends SiteTree
{
    private static $many_many = array(
        'Images' => 'Image',
        'Images_Live' => 'Image'
    );

    public function publish($fromStage, $toStage, $createNewVersion = false)
    {
        if ($toStage == 'Live')
        {
            $this->publishManyToManyComponents();
        }

        parent::publish($fromStage, $toStage, $createNewVersion);
    }

    protected function publishManyToManyComponents()
    {
        foreach (static::getVersionedManyManyComponentNames() as $component_name)
        {
            $this->publishManyToManyComponent($component_name);
        }
    }

    protected function publishManyToManyComponent($component_name)
    {
        $stage = $this->getManyManyComponents($component_name);
        $live = $this->getManyManyComponents("{$component_name}_Live");

        $live_table = $live->getJoinTable();
        $live_fk = $live->getForeignKey();
        $live_lk = $live->getLocalKey();

        $stage_table = $stage->getJoinTable();
        $stage_fk = $live->getForeignKey();
        $stage_lk = $live->getLocalKey();

        // update or add items from stage to live
        foreach ($stage as $item)
        {
            $live->add($item, $stage->getExtraData(null, $item->ID));
        }

        // delete remaining items from live table
        DB::query("DELETE l FROM $live_table AS l LEFT JOIN $stage_table AS s ON l.$live_fk = s.$stage_fk AND l.$live_lk = s.$stage_lk WHERE s.ID IS NULL");

        // update new items IDs in live table (IDs are incremental so the new records can only have higher IDs than items in ID => should not cause duplicate IDs)
        DB::query("UPDATE $live_table AS l INNER JOIN $stage_table AS s ON l.$live_fk = s.$stage_fk AND l.$live_lk = s.$stage_lk SET l.ID = s.ID WHERE l.ID != s.ID;");
    }

    public function manyManyComponent($component_name)
    {
        if (Versioned::current_stage() == 'Live' && static::isVersionedManyManyComponent($component_name))
        {
            return parent::manyManyComponent("{$component_name}_Live");
        }
        else
        {
            return parent::manyManyComponent($component_name);
        }
    }

    protected static function isVersionedManyManyComponent($component_name)
    {
        $many_many_components = (array) Config::inst()->get(static::class, 'many_many', Config::INHERITED);
        return isset($many_many_components[$component_name]) && isset($many_many_components["{$component_name}_Live"]);
    }

    protected static function getVersionedManyManyComponentNames()
    {
        $many_many_components = (array) Config::inst()->get(static::class, 'many_many', Config::INHERITED);

        foreach ($many_many_components as $component_name => $dummy)
        {
            $is_live = 0;

            $stage_component_name = preg_replace('/_Live$/', '', $component_name, -1, $is_live);

            if ($is_live > 0 && isset($many_many_components[$stage_component_name]))
            {
                yield $stage_component_name;
            }
        }
    }
}