How to duplicate parent plus nested grid in MIGXdb

Using MIGXdb I have a number of objects with a parent/child relationship. The child grid is nested inside the parent and all works as expected - I can add parents and each can have multiple children.

However if I duplicate a parent its children are not duplicated.

Bruno outlined the process to achieve in the post linked below via the update-processor or custom save-function but not in enough detail that I understand how to implement this.
https://forums.modx.com/thread/90526/migxdb---duplicate-action-copy-item-plus-nested-migx#dis-post-496441

Can anyone help me achieve this please? Relevant schema and MIGX configs are below.

If relevant, my schema has 8x objects with parent/child relationship, so a generic solution may be more useful than one that needs 8x separate processors/functions.

Any help would be much appreciated.

Thanks

Schema

<?xml version="1.0" encoding="UTF-8"?>
<model package="venue" baseClass="xPDOObject" platform="mysql" defaultEngine="INNODB" version="1.1">
    <!--
        Class - VenueFaq
    -->
    <object class="VenueFaqGroup" table="venue__faq_group" extends="xPDOSimpleObject" >
        <!-- name and description for internal reference only, used to track what item is used for -->
        <field key="name" dbtype="varchar" precision="155" phptype="string" null="false" default="" index="index" />
        <field key="description" dbtype="varchar" precision="150" phptype="string" null="false" default="" index="index" />

        <!-- used by MIGX -->        
        <field key="deleted" dbtype="tinyint" precision="1" attributes="unsigned" phptype="integer" null="false" default="0" />
        <field key="published" dbtype="tinyint" precision="1" attributes="unsigned" phptype="integer" null="false" default="1" />
        <field key="pos" dbtype="int" precision="10" phptype="integer" null="false" default="0" />

        <!-- good practice for tracking -->
        <field key="createdon" dbtype="datetime" phptype="datetime" null="true"/>
        <field key="createdby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
        <field key="editedon" dbtype="datetime" phptype="datetime" null="true"/>
        <field key="editedby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />

        <composite alias="VenueFaqItem" class="VenueFaqItem" local="id" foreign="rel_faq_group_id" cardinality="many" owner="local" /> 
        
        <aggregate alias="CreatedBy" class="modUser" local="createdby" foreign="id" cardinality="one" owner="foreign"/>
        <aggregate alias="EditedBy" class="modUser" local="editedby" foreign="id" cardinality="one" owner="foreign"/>
    </object>

    <object class="VenueFaqItem" table="venue__faq_item" extends="xPDOSimpleObject">
        <!-- related core table id -->
        <field key="rel_faq_group_id" dbtype="int" precision="11" phptype="integer" null="false" default=""/>

        <field key="title" dbtype="varchar" precision="155" phptype="string" null="false" default="" />
        <field key="introtext" dbtype="varchar" precision="255" phptype="string" null="false" default="" />
        <field key="content"  dbtype="text" phptype="string" null="true" />

        <!-- used by MIGX -->        
        <field key="deleted" dbtype="tinyint" precision="1" attributes="unsigned" phptype="integer" null="false" default="0" />
        <field key="published" dbtype="tinyint" precision="1" attributes="unsigned" phptype="integer" null="false" default="1" />
        <field key="pos" dbtype="int" precision="10" phptype="integer" null="false" default="0" />

        <!-- good practice for tracking -->
        <field key="createdon" dbtype="datetime" phptype="datetime" null="true"/>
        <field key="createdby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
        <field key="editedon" dbtype="datetime" phptype="datetime" null="true"/>
        <field key="editedby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />

        <aggregate  alias="VenueFaqGroup" class="VenueFaqGroup" local="rel_faq_group_id" foreign="id" cardinality="one" owner="foreign" /> 
        
        <aggregate alias="CreatedBy" class="modUser" local="createdby" foreign="id" cardinality="one" owner="foreign"/>
        <aggregate alias="EditedBy" class="modUser" local="editedby" foreign="id" cardinality="one" owner="foreign"/>
    </object>
</model>

VenueFaqGroup

{
  "formtabs":[
    {
      "MIGX_id":20,
      "caption":"VenueFaqGroup",
      "print_before_tabs":"0",
      "fields":[
        {
          "MIGX_id":102,
          "field":"name",
          "caption":"Name",
          "description":"for internal reference",
          "description_is_code":"0",
          "inputTV":"",
          "inputTVtype":"",
          "validation":"",
          "configs":"",
          "restrictive_condition":"",
          "display":"",
          "sourceFrom":"config",
          "sources":"",
          "inputOptionValues":"",
          "default":"",
          "useDefaultIfEmpty":"0",
          "pos":1
        },
        {
          "MIGX_id":103,
          "field":"description",
          "caption":"Description",
          "description":"for internal notes",
          "description_is_code":"0",
          "inputTV":"",
          "inputTVtype":"textarea",
          "validation":"",
          "configs":"",
          "restrictive_condition":"",
          "display":"",
          "sourceFrom":"config",
          "sources":"",
          "inputOptionValues":"",
          "default":"",
          "useDefaultIfEmpty":"0",
          "pos":2
        },
        {
          "MIGX_id":104,
          "field":"VenueFaqItem",
          "caption":"VenueFaqItem",
          "description":"",
          "description_is_code":"0",
          "inputTV":"",
          "inputTVtype":"migxdb",
          "validation":"",
          "configs":"VenueFaqItem",
          "restrictive_condition":"",
          "display":"",
          "sourceFrom":"config",
          "sources":"",
          "inputOptionValues":"",
          "default":"",
          "useDefaultIfEmpty":"0",
          "pos":3
        }
      ],
      "pos":1
    }
  ],
  "contextmenus":"update||duplicate||remove",
  "actionbuttons":"addItem||bulk",
  "columnbuttons":"update||duplicate||publish||unpublish||remove",
  "filters":"",
  "extended":{
    "migx_add":"",
    "disable_add_item":"",
    "add_items_directly":1,
    "formcaption":"VenueFaqGroup form caption",
    "update_win_title":"VenueFaqGroup window title",
    "win_id":"VenueFaqGroup",
    "maxRecords":"",
    "addNewItemAt":"top",
    "media_source_id":"",
    "multiple_formtabs":"",
    "multiple_formtabs_label":"",
    "multiple_formtabs_field":"",
    "multiple_formtabs_optionstext":"",
    "multiple_formtabs_optionsvalue":"",
    "actionbuttonsperrow":4,
    "winbuttonslist":"",
    "extrahandlers":"this.handleColumnSwitch",
    "filtersperrow":4,
    "packageName":"venue",
    "classname":"VenueFaqGroup",
    "task":"",
    "getlistsort":"pos",
    "getlistsortdir":"asc",
    "sortconfig":[
      {
        "sortby":"name",
        "sortdir":"ASC"
      }
    ],
    "gridpagesize":"",
    "use_custom_prefix":"0",
    "prefix":"",
    "grid":"dragdrop",
    "gridload_mode":2,
    "check_resid":1,
    "check_resid_TV":"",
    "join_alias":"",
    "has_jointable":"no",
    "getlistwhere":"",
    "joins":"",
    "hooksnippets":"",
    "cmpmaincaption":"Website Components",
    "cmptabcaption":"FAQs",
    "cmptabdescription":"FAQ groups contain individual FAQs",
    "cmptabcontroller":"",
    "winbuttons":"",
    "onsubmitsuccess":"",
    "submitparams":""
  },
  "permissions":{
    "apiaccess":"",
    "view":"",
    "list":"",
    "save":"",
    "create":"",
    "remove":"",
    "delete":"",
    "publish":"",
    "unpublish":"",
    "viewdeleted":"",
    "viewunpublished":""
  },
  "fieldpermissions":"",
  "columns":[
    {
      "MIGX_id":2,
      "header":"id",
      "dataIndex":"id",
      "width":15,
      "sortable":true,
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":""
    },
    {
      "MIGX_id":3,
      "header":"name",
      "dataIndex":"name",
      "width":"",
      "sortable":true,
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":"this.textEditor"
    },
    {
      "MIGX_id":4,
      "header":"description",
      "dataIndex":"description",
      "width":"",
      "sortable":true,
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":"this.textEditor"
    },
    {
      "MIGX_id":5,
      "header":"published",
      "dataIndex":"published",
      "width":25,
      "sortable":true,
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"this.renderClickCrossTick",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":""
    },
    {
      "MIGX_id":6,
      "header":"pos",
      "dataIndex":"pos",
      "width":"",
      "sortable":true,
      "show_in_grid":"0",
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":""
    }
  ],
  "category":""
}

VenueFaqItem

{
  "formtabs":[
    {
      "MIGX_id":21,
      "caption":"FAQ caption",
      "print_before_tabs":"0",
      "fields":[
        {
          "MIGX_id":105,
          "field":"title",
          "caption":"title",
          "pos":1
        },
        {
          "MIGX_id":106,
          "field":"introtext",
          "caption":"introtext",
          "description":"",
          "description_is_code":"0",
          "inputTV":"",
          "inputTVtype":"textarea",
          "validation":"",
          "configs":"",
          "restrictive_condition":"",
          "display":"",
          "sourceFrom":"config",
          "sources":"",
          "inputOptionValues":"",
          "default":"",
          "useDefaultIfEmpty":"0",
          "pos":2
        },
        {
          "MIGX_id":108,
          "field":"content",
          "caption":"content",
          "description":"",
          "description_is_code":"0",
          "inputTV":"",
          "inputTVtype":"richtext",
          "validation":"",
          "configs":"",
          "restrictive_condition":"",
          "display":"",
          "sourceFrom":"config",
          "sources":"",
          "inputOptionValues":"",
          "default":"",
          "useDefaultIfEmpty":"0",
          "pos":3
        }
      ],
      "pos":1
    }
  ],
  "contextmenus":"update||duplicate||remove",
  "actionbuttons":"addItem",
  "columnbuttons":"",
  "filters":"",
  "extended":{
    "migx_add":"",
    "disable_add_item":"",
    "add_items_directly":1,
    "formcaption":"VenueFaqItem form caption",
    "update_win_title":"VenueFaqItem window title",
    "win_id":"VenueFaqItem",
    "maxRecords":"",
    "addNewItemAt":"top",
    "media_source_id":"",
    "multiple_formtabs":"",
    "multiple_formtabs_label":"",
    "multiple_formtabs_field":"",
    "multiple_formtabs_optionstext":"",
    "multiple_formtabs_optionsvalue":"",
    "actionbuttonsperrow":4,
    "winbuttonslist":"",
    "extrahandlers":"this.handleColumnSwitch||this.publishObject||this.unpublishObject",
    "filtersperrow":4,
    "packageName":"venue",
    "classname":"VenueFaqItem",
    "task":"",
    "getlistsort":"pos",
    "getlistsortdir":"asc",
    "sortconfig":"",
    "gridpagesize":"",
    "use_custom_prefix":"0",
    "prefix":"",
    "grid":"dragdrop",
    "gridload_mode":2,
    "check_resid":1,
    "check_resid_TV":"",
    "join_alias":"VenueFaqGroup",
    "has_jointable":"yes",
    "getlistwhere":"",
    "joins":"",
    "hooksnippets":"",
    "cmpmaincaption":"VenueReviewItem main caption",
    "cmptabcaption":"VenueReviewItem tab caption",
    "cmptabdescription":"VenueReviewItem tab description",
    "cmptabcontroller":"",
    "winbuttons":"",
    "onsubmitsuccess":"",
    "submitparams":""
  },
  "permissions":{
    "apiaccess":"",
    "view":"",
    "list":"",
    "save":"",
    "create":"",
    "remove":"",
    "delete":"",
    "publish":"",
    "unpublish":"",
    "viewdeleted":"",
    "viewunpublished":""
  },
  "fieldpermissions":"",
  "columns":[
    {
      "MIGX_id":6,
      "header":"id",
      "dataIndex":"id",
      "width":15,
      "sortable":true,
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":""
    },
    {
      "MIGX_id":2,
      "header":"title",
      "dataIndex":"title",
      "width":"",
      "sortable":true,
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":"this.textEditor"
    },
    {
      "MIGX_id":3,
      "header":"introtext",
      "dataIndex":"introtext",
      "width":"",
      "sortable":true,
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":"this.textEditor"
    },
    {
      "MIGX_id":5,
      "header":"published",
      "dataIndex":"published",
      "width":25,
      "sortable":true,
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"this.renderClickCrossTick",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":""
    }
  ],
  "category":""
}

To create a custom update processor, copy the file

core/components/migx/processors/mgr/default/update.php

to this path

core/components/venue/processors/mgr/default/update.php

(inside you’re custom package “venue”).

Then change the code in the copied file.


To copy the items from the parent, you could use code like this:

$original_id = $modx->getOption('original_id', $tempparams, ''); // ID of the object you duplicated
if ($scriptProperties['object_id'] == "new" && $original_id && $button == "duplicate" && $classname == "VenueFaqGroup"){
    $original_object = $xpdo->getObject($classname, $original_id); // Load the object you duplicated
    $original_items = $original_object->getMany("VenueFaqItem"); // Load the related items
    $new_items = [];
    foreach($original_items as $item){
        $new_item = $xpdo->newObject("VenueFaqItem");
        $new_item->fromArray($item->toArray()); // Copy the values of the old item to the new one
        // You may want to change the values for 'createdon', 'createdby', 'editedon' and 'editedby' here
        $new_items[] = $new_item;
    }
    $object->addMany($new_items);
    $object->save();
}

Add it near the end of the update processor, before the cache gets cleared:

Please test this code extensively before using it in production. It may not work correctly!

@halftrainedharry you are an absolute gent, thank you.

Will get a look at this as soon as I can and report back.

@halftrainedharry this works well.

I updated your example to duplicate child items across 6 specific classes by predefining the parent/child relationships in an array.

The only issue I found is minor and unrelated to your code. When duplicating a Parent Group that is unpublished, the duplicate becomes published. This is because my schema sets the default published value to 1 with

<field key="published" dbtype="tinyint" precision="1" attributes="unsigned" phptype="integer" null="false" default="1" />

I’ll need to go back and figure how to update the new_item values for createdby and editedby later.

Thanks again.

updated processor

/**
 * 
 * if duplicating a group class that has child items, duplicate the children as well
 * 
 * ref. https://community.modx.com/t/how-to-duplicate-parent-plus-nested-grid-in-migxdb/7141/2
 * 
 */

// Harry's example deals specifically with VenueFaqGroup class. updated below to allow multiple classes
// if ($scriptProperties['object_id'] == "new" && $original_id && $button == "duplicate" && $classname == "VenueFaqGroup") {

 // get the id of the duplicated parent object
$original_id = $modx->getOption('original_id', $tempparams, '');

// define array of classes with parent/child relationship where the child items should be duplicated
$arr_classmap = [];

$arr_classmap["VenueSuperheroGroup"] = "VenueSuperheroItem";
$arr_classmap["VenueVideoGroup"] = "VenueVideoItem";
$arr_classmap["VenueReviewGroup"] = "VenueReviewItem";
$arr_classmap["VenueGalleryGroup"] = "VenueGalleryItem";
$arr_classmap["VenueFaqGroup"] = "VenueFaqItem";
$arr_classmap["VenueCarouselGroup"] = "VenueCarouselItem";

// check if $classname exists as key in $arr_classmap meaning the child items should be duplicated
if ($scriptProperties['object_id'] == "new" && $original_id && $button == "duplicate" && isset($classname, $arr_classmap)) {

    /*
    // DEBUG: dump params to log
    $err_msg = "[DUPLICATE] original_id=$original_id, classname=$classname";
    $modx->log(modX::LOG_LEVEL_ERROR, $err_msg);
    */

    // duplicate the child items

    // load the object you duplicated
    $original_object = $xpdo->getObject($classname, $original_id);
    
    // load the related items (get the child item classname from arr_classmap)
    $original_items = $original_object->getMany($arr_classmap[$classname]);

    // define array to hold duplicate items
    $new_items = [];
    foreach($original_items as $item){
        $new_item = $xpdo->newObject($arr_classmap[$classname]);
        $new_item->fromArray($item->toArray()); // Copy the values of the old item to the new one
        // TODO: change the values for 'createdon', 'createdby', 'editedon' and 'editedby' here
            //$user = $modx->getUser();
            //createdby/editedby = $user->get('id');
        // NOTE: no need to update rel_faq_group_id, the id of the new group is automatically assigned
        $new_items[] = $new_item;
    }

    $object->addMany($new_items);
    $object->save();

}
1 Like

This topic was automatically closed 2 days after discussion ended and a solution was marked. New replies are no longer allowed. You can open a new topic by clicking the link icon below the original post or solution and selecting “+ New Topic”.