From f0b532857cad2644cab9e810153a07f0cbce88a4 Mon Sep 17 00:00:00 2001 From: Benn Oshrin Date: Wed, 10 Dec 2025 13:10:13 -0700 Subject: [PATCH] Additional fix for CloneCommand (CFM-127) --- app/src/Command/CloneCommand.php | 6 ++ app/src/Lib/Traits/ClonableTrait.php | 2 +- app/src/Lib/Traits/PluggableModelTrait.php | 11 ++- app/src/Lib/Traits/TableMetaTrait.php | 103 +++++++++++--------- app/src/Lib/Util/TableUtilities.php | 17 ++-- app/src/Model/Table/HistoryRecordsTable.php | 2 +- 6 files changed, 85 insertions(+), 56 deletions(-) diff --git a/app/src/Command/CloneCommand.php b/app/src/Command/CloneCommand.php index f47ef5880..73befe30d 100644 --- a/app/src/Command/CloneCommand.php +++ b/app/src/Command/CloneCommand.php @@ -126,6 +126,12 @@ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOption */ public function execute(Arguments $args, ConsoleIo $io) { + // In general we don't want fallback classes (CFM-405) but they're particularly + // painful here (in debugging errors), and we can safely turn them off in a + // command line context. + + TableRegistry::getTableLocator()->allowFallbackClass(false); + $this->io = $io; // By default, the target database is the same as the source (ie: cloning from one diff --git a/app/src/Lib/Traits/ClonableTrait.php b/app/src/Lib/Traits/ClonableTrait.php index bc01956cb..a453c67ff 100644 --- a/app/src/Lib/Traits/ClonableTrait.php +++ b/app/src/Lib/Traits/ClonableTrait.php @@ -126,7 +126,7 @@ public function prepareClone( $originalParent = $SourceTable->get($original->parent_id); // We're the Target Table, query using the UUID we just found - $cloneParent = $this->getByUuid($originalParent->uuid, $originalParent->co_id); + $cloneParent = $this->getByUuid($originalParent->uuid, $clone->co_id); // Update the foreign key $clone->parent_id = $cloneParent->id; diff --git a/app/src/Lib/Traits/PluggableModelTrait.php b/app/src/Lib/Traits/PluggableModelTrait.php index 31702e151..943e0073d 100644 --- a/app/src/Lib/Traits/PluggableModelTrait.php +++ b/app/src/Lib/Traits/PluggableModelTrait.php @@ -136,7 +136,16 @@ public function checkCloneDependencies( } }; - $fn($related, $targetDataSource, $pluginName); + // getCloneRelations() will return all possible relations for all plugins for the + // current model (eg if $original is ExternalIdentitySource, we'll get back an + // array of all EIS plugins), but we only want to instantiate related models + // for $plugin. + + $pluginModel = StringUtilities::pluginPlugin($original->plugin); + + if(!empty($related[$pluginModel])) { + $fn($related[$pluginModel], $targetDataSource, $pluginName); + } } } diff --git a/app/src/Lib/Traits/TableMetaTrait.php b/app/src/Lib/Traits/TableMetaTrait.php index 9c5a1327c..e94f0ffe6 100644 --- a/app/src/Lib/Traits/TableMetaTrait.php +++ b/app/src/Lib/Traits/TableMetaTrait.php @@ -108,63 +108,74 @@ public function filterMetadataForCopy( // we need to handle the related plugin data specially. foreach($related as $k => $v) { - if(is_int($k)) { - // $v is the model name (EnrollmentFlowSteps) - // $m is the lowercased model name (enrollment_flow_steps) - $m = Inflector::tableize($v); - // $m1 is the singular version (enrollment_flow_step) - $m1 = Inflector::singularize($m); - // $t is the Table for $v - if(!empty($entity->plugin) && StringUtilities::pluginModel($entity->plugin) == $v) { - // For pluggable models, get the plugin table from the entity configuration - $t = TableRegistry::getTableLocator()->get($entity->plugin); - } else { - if(!empty($entity->$m[0])) { - // hasMany Relation with at least one entity populated. Use the entity to get - // the appropriate table to make sure we handle plugins correctly. - $t = TableRegistry::getTableLocator()->get($entity->$m[0]->getSource()); + try { + if(is_int($k)) { + // $v is the model name (EnrollmentFlowSteps) + // $m is the lowercased model name (enrollment_flow_steps) + $m = Inflector::tableize($v); + // $m1 is the singular version (enrollment_flow_step) + $m1 = Inflector::singularize($m); + // $t is the Table for $v + if(!empty($entity->plugin) && StringUtilities::pluginModel($entity->plugin) == $v) { + // For pluggable models, get the plugin table from the entity configuration + $t = TableRegistry::getTableLocator()->get($entity->plugin); } else { - $t = TableRegistry::getTableLocator()->get($v); + if(!empty($entity->$m[0])) { + // hasMany Relation with at least one entity populated. Use the entity to get + // the appropriate table to make sure we handle plugins correctly. + $t = TableRegistry::getTableLocator()->get($entity->$m[0]->getSource()); + } else { + $t = TableRegistry::getTableLocator()->get($v); + } } - } - if(is_array($entity->$m)) { - // HasMany + if(is_array($entity->$m)) { + // HasMany - foreach($entity->$m as $s) { - $ret[$m][] = $this->filterMetadataForCopy($t, $s); + foreach($entity->$m as $s) { + $ret[$m][] = $this->filterMetadataForCopy($t, $s); + } + } elseif(!empty($entity->$m1)) { + // HasOne + + $ret[$m1] = $this->filterMetadataForCopy($t, $entity->$m1); + } + } elseif(is_array($v)) { + // $k is the model name (EnrollmentFlowSteps) and $v is an array of related models + // $m is the lowercased model name (enrollment_flow_steps) + $m = Inflector::tableize($k); + // $m1 is the singular version (enrollment_flow_step) + $m1 = Inflector::singularize($m); + // $t is the Table for $k + + if(!empty($entity->plugin) && StringUtilities::pluginModel($entity->plugin) == $k) { + // For pluggable models, get the plugin table from the entity configuration + $t = TableRegistry::getTableLocator()->get($entity->plugin); + } else { + $t = TableRegistry::getTableLocator()->get($k); } - } elseif(!empty($entity->$m1)) { - // HasOne - $ret[$m1] = $this->filterMetadataForCopy($t, $entity->$m1); - } - } elseif(is_array($v)) { - // $k is the model name (EnrollmentFlowSteps) and $v is an array of related models - // $m is the lowercased model name (enrollment_flow_steps) - $m = Inflector::tableize($k); - // $m1 is the singular version (enrollment_flow_step) - $m1 = Inflector::singularize($m); - // $t is the Table for $k - if(!empty($entity->plugin) && StringUtilities::pluginModel($entity->plugin) == $k) { - // For pluggable models, get the plugin table from the entity configuration - $t = TableRegistry::getTableLocator()->get($entity->plugin); - } else { - $t = TableRegistry::getTableLocator()->get($k); - } + if(is_array($entity->$m)) { + // HasMany - if(is_array($entity->$m)) { - // HasMany + foreach($entity->$m as $s) { + $ret[$m][] = $this->filterMetadataForCopy($t, $s, $v); + } + } elseif(!empty($entity->$m1)) { + // HasOne - foreach($entity->$m as $s) { - $ret[$m][] = $this->filterMetadataForCopy($t, $s, $v); + $ret[$m1] = $this->filterMetadataForCopy($t, $entity->$m1, $v); } - } elseif(!empty($entity->$m1)) { - // HasOne - - $ret[$m1] = $this->filterMetadataForCopy($t, $entity->$m1, $v); } } + catch(\Exception $e) { + if(empty($entity->plugin)) { + throw $e; + } + // else this is probably a relation for a plugin that isn't configured for this pluggable + // model (eg $k = ApiSources where $entity->plugin = FileConnector.FileSources), + // so we just ignore the exception + } } return $ret; diff --git a/app/src/Lib/Util/TableUtilities.php b/app/src/Lib/Util/TableUtilities.php index 6c831eab5..6e5d4084c 100644 --- a/app/src/Lib/Util/TableUtilities.php +++ b/app/src/Lib/Util/TableUtilities.php @@ -41,7 +41,7 @@ class TableUtilities { * will be the requested Table name (eg "People" for "People"). For any other connection, * the connection name will be CamelCased and prefixed to create the Table alias * (eg "RemotePeople" for "People"). For Plugins, the Plugin name will be removed and - * the Plugin model used as described (eg "RemoteWidgets" for "MyPlugin.Widgehs"). + * the Plugin model used as described (eg "RemoteWidgets" for "MyPlugin.Widgets"). * * @since COmanage Registry v5.2.0 * @param string $tableName Table name, in the usual format (including Plugin.Models) @@ -91,7 +91,7 @@ public static function getTableWithDataSource( $m = self::getTableFromRegistry($modelName, $mergedOptions); // Relabel associations with $prefix. Some will already be correctly set up, in particular - // dynamic plugin assocations created via PluggableModelTrait::setPluginRelations, so we + // dynamic plugin associations created via PluggableModelTrait::setPluginRelations, so we // check for and skip those. (We're actually doing something similar to that code, here.) $assns = $m->associations(); @@ -108,13 +108,16 @@ public static function getTableWithDataSource( $r = new \ReflectionClass($a); $aType = Inflector::variable($r->getShortName()); - // The (new) prefixed alias (eg: RemoteIdentifiers) + // The alias for the target as defined in the associations, eg "Identifiers" + // or "PipelineMatchTypes", prefixed by the datasource alias (eg "RemoteIdentifiers" + // or "RemotePipelineMatchTypes"). $targetAlias = $prefix . $target->getAlias(); - // The class name we are trying to create. We need to handle plugins here. + // The class name we are trying to instantiate. We need to handle plugins here. // If $pluginName is set, we'll assume HasMany and HasOne relations are within - // the same plugin. - $className = $target->getAlias(); + // the same plugin. This must be the underlying class name ("Identifiers" or "Types") + // so Cake can find it. + $className = Inflector::camelize($target->getTable()); if($pluginName && ($aType == 'hasMany' || $aType == 'hasOne')) { $className = $pluginName . "." . $className; @@ -135,7 +138,7 @@ public static function getTableWithDataSource( if(!$aTargetTable->hasAssociation($targetAlias)) { $m->$aType($targetAlias) - ->setClassName($target->getAlias()) + ->setClassName($className) ->setForeignKey(StringUtilities::tableToForeignKey($target)) ->setCascadeCallbacks(true) ->setTarget($aTargetTable); diff --git a/app/src/Model/Table/HistoryRecordsTable.php b/app/src/Model/Table/HistoryRecordsTable.php index 04b9ae401..5b5dd6885 100644 --- a/app/src/Model/Table/HistoryRecordsTable.php +++ b/app/src/Model/Table/HistoryRecordsTable.php @@ -61,7 +61,7 @@ public function initialize(array $config): void { $this->setTableType(\App\Lib\Enum\TableTypeEnum::Artifact); // Define associations - $this->belongsTo('ApiUser') + $this->belongsTo('ApiUsers') ->setForeignKey('actor_api_user_id') ->setProperty('actor_api_user'); $this->belongsTo('ActorPeople')