diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index 76450899b..a8f59a5e5 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -273,14 +273,10 @@ public function getCOID(): ?int { */ public function getCurrentTable(): \Cake\ORM\Table { - /** @var string $modelsName */ $modelsName = $this->getName(); + $plugin = $this->getPlugin(); - $alias = $this->getPlugin() !== null - ? $this->getPlugin() . '.' . $modelsName - : $modelsName; - - return $this->fetchTable($alias); + return $this->fetchTable(StringUtilities::getQualifiedName($plugin, $modelsName)); } diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 4a3326b36..d0a13334c 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -686,7 +686,7 @@ public function index() { $this->set('vv_permission_set', $this->RegistryAuth->calculatePermissionsForResultSet($resultSet)); // Default index view title is model name - [$title, , ] = StringUtilities::entityAndActionToTitle($resultSet, $modelsName, 'index'); + [$title, , ] = StringUtilities::entityAndActionToTitle(null, $modelsName, 'index'); $this->set('vv_title', $title); // Let the view render diff --git a/app/src/Controller/StandardEnrollerController.php b/app/src/Controller/StandardEnrollerController.php index c7f6e2124..b95dc8dcd 100644 --- a/app/src/Controller/StandardEnrollerController.php +++ b/app/src/Controller/StandardEnrollerController.php @@ -74,11 +74,45 @@ public function beforeRender(\Cake\Event\EventInterface $event) { if(!empty($link->value)) { $currentTable = $this->getCurrentTable(); - $efsTable = $currentTable->getAssociation('EnrollmentFlowSteps')->getTarget(); - - $this->set('vv_bc_parent_obj', $efsTable->get($link->value)); - $this->set('vv_bc_parent_displayfield', $efsTable->getDisplayField()); - $this->set('vv_bc_parent_primarykey', $efsTable->getPrimaryKey()); + // Not all enroller plugin tables have a direct EnrollmentFlowSteps association + if (method_exists($currentTable, 'hasAssociation') + && $currentTable->hasAssociation('EnrollmentFlowSteps')) { + + $efsTable = $currentTable->getAssociation('EnrollmentFlowSteps')->getTarget(); + + $this->set('vv_bc_parent_obj', $efsTable->get($link->value)); + $this->set('vv_bc_parent_displayfield', $efsTable->getDisplayField()); + $this->set('vv_bc_parent_primarykey', $efsTable->getPrimaryKey()); + } else { + // Two-hop case: foreign key from $link (e.g. attribute_collector_id) + // -> AttributeCollectors + // -> EnrollmentFlowSteps + // Useful for deeply nested enrollment configuration objects. + if (!empty($link->attr)) { + // Derive the table class name from the foreign key name + $fkClassName = StringUtilities::foreignKeyToClassName($link->attr); // e.g. 'AttributeCollectors' + + // Qualify with plugin if present + $tableAlias = !empty($link->plugin) + ? $link->plugin . '.' . $fkClassName // e.g. 'CoreEnroller.AttributeCollectors' + : $fkClassName; + + $attributeCollectorsTable = TableRegistry::getTableLocator()->get($tableAlias); + + // Load the intermediate object (AttributeCollector, in your case) + $collector = $attributeCollectorsTable->get((int)$link->value); + + // From the AttributeCollector, go up to the EnrollmentFlowStep + if (!empty($collector->enrollment_flow_step_id)) { + $efsTable = TableRegistry::getTableLocator()->get('EnrollmentFlowSteps'); + $step = $efsTable->get((int)$collector->enrollment_flow_step_id); + + $this->set('vv_bc_parent_obj', $step); + $this->set('vv_bc_parent_displayfield', $efsTable->getDisplayField()); + $this->set('vv_bc_parent_primarykey', $efsTable->getPrimaryKey()); + } + } + } } return parent::beforeRender($event); diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php index 0fda489e5..1f24cf1ca 100644 --- a/app/src/Lib/Util/StringUtilities.php +++ b/app/src/Lib/Util/StringUtilities.php @@ -204,6 +204,17 @@ public static function entityToClassName($entity): string { // $classPath will be something like App\Model\Entity\Name, but we want to return "Names" $classPath = get_class($entity); + return self::classPathClassName($classPath); + } + + /** + * Extracts and pluralizes the class name from a fully qualified class path. + * + * @param string $classPath Fully qualified class path (eg: App\Model\Entity\Name) + * @return string Pluralized class name (eg: Names) + * @since COmanage Registry v5.2.0 + */ + public static function classPathClassName(string $classPath): string { return Inflector::pluralize(substr($classPath, strrpos($classPath, '\\')+1)); } @@ -285,13 +296,15 @@ public static function entityAndActionToTitle($entity, [$plugin, $modelsName] = explode('.', $modelPath, 2); } - if($entity == null && !empty($plugin)) { - $count = $action == 'index' ? 99 : 1; - return [__d($domain, "controller.$modelsName", [$count]), '', '']; - } elseif($entity === null) { - $count = $action == 'index' ? 99 : 1; - return [__d($domain, "{$modelPath}.{$action}", [$count]), '', '']; + // Index view → use the controller plural form (token 99 convention) + if($action === 'index') { + if(!empty($plugin)) { + $domain = StringUtilities::pluginToTextDomain($plugin); + return [__d($domain, "controller.$modelsName", [99]), '', '']; + } + return [__d('controller', $modelsName, [99]), '', '']; } + // Base table and default message IDs for translation $linkTable = TableRegistry::getTableLocator()->get($modelPath); $msgId = "{$action}.a"; // eg: "edit.a" @@ -308,25 +321,20 @@ public static function entityAndActionToTitle($entity, // If the entity actually belongs to a different model than the provided $modelsName, // switch to that table and adjust the default message id pattern accordingly. // This is necessary for TAB oriented views - if(Inflector::singularize(self::entityToClassName($entity)) !== Inflector::singularize($modelsName)) { + if( + $entity !== null + && Inflector::singularize(self::entityToClassName($entity)) !== Inflector::singularize($modelsName) + ) { $linkTable = TableRegistry::getTableLocator()->get(self::entityToClassName($entity)); // If modelPath and action are equal, don’t concatenate (preserve legacy behavior) $msgId = $modelPath === $action ? $modelPath : "{$modelPath}.{$action}"; } - // 2) No action → default to the controller label for the model (singular) + // No action → default to the controller label for the model (singular) if($action === null) { return [__d('controller', $modelsName), '', '']; } - // 3) Index view → use the controller plural form (token 99 convention) - if($action === 'index') { - if(!empty($plugin)) { - return [__d($domain, "controller.$modelsName", [99]), '', '']; - } - return [__d('controller', $modelsName, [99]), '', '']; - } - // Add/Edit/View // The MVEA Models have an entityId. The one from the parent model. // We need to have a condition for this and exclude it. @@ -338,7 +346,7 @@ public static function entityAndActionToTitle($entity, $display = $entity->$field ?? null; } - // 6) Edit/View-like case for an existing entity with a usable display + // Edit/View-like case for an existing entity with a usable display // Title: translate with override key first; if not found, fall back to default key. // Super/Sub titles: set to the display (needed for External IDs in UI). if ( @@ -354,7 +362,7 @@ public static function entityAndActionToTitle($entity, return [$title, $supertitle, $subtitle]; } - // 7) Fallbacks: + // Fallbacks: // - New entities (no id), // - Add/Delete actions, // - Or we simply lack a display. @@ -372,7 +380,7 @@ public static function entityAndActionToTitle($entity, * @param string $domain Translation domain to use * @param string $overrideKey Primary translation key to try first * @param string $fallbackKey Fallback translation key if override not found - * @param string $value Value to substitute in translation + * @param string|int $value Value to substitute in translation * @return string Translated string using either override or fallback key * @since COmanage Registry v5.2.0 */ @@ -415,6 +423,22 @@ public static function foreignKeyToController(string $s): string { return Inflector::underscore(Inflector::pluralize(substr($s, 0, strlen($s)-3))); } + + /** + * Get the fully qualified name by combining plugin and name with a dot separator. + * + * @param string|null $plugin Plugin name, or null if no plugin + * @param string $name Base name to qualify + * @return string Qualified name in format "Plugin.Name" or just "Name" if no plugin + * @since COmanage Registry v5.2.0 + */ + public static function getQualifiedName(?string $plugin, string $name): string + { + return $plugin !== null && $plugin !== '' + ? $plugin . '.' . $name + : $name; + } + /** * Localize a controller name, accounting for plugins. * @@ -452,7 +476,7 @@ public static function qualifyModelPath(string $modelPath, ?string $plugin): str if (empty($plugin) || str_starts_with($modelPath, $plugin . '.')) { return $modelPath; } - return $plugin . '.' . $modelPath; + return self::getQualifiedName($plugin, $modelPath);; } /**