Skip to content

Commit

Permalink
Additional fix for CloneCommand (CFM-127)
Browse files Browse the repository at this point in the history
  • Loading branch information
Benn Oshrin committed Dec 10, 2025
1 parent 84b9797 commit f0b5328
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 56 deletions.
6 changes: 6 additions & 0 deletions app/src/Command/CloneCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/src/Lib/Traits/ClonableTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 10 additions & 1 deletion app/src/Lib/Traits/PluggableModelTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
103 changes: 57 additions & 46 deletions app/src/Lib/Traits/TableMetaTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 10 additions & 7 deletions app/src/Lib/Util/TableUtilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion app/src/Model/Table/HistoryRecordsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down

0 comments on commit f0b5328

Please sign in to comment.